Repository: LykosAI/StabilityMatrix Branch: main Commit: 5d8a68d825fd Files: 1529 Total size: 7.7 MB Directory structure: gitextract_tguifjg2/ ├── .aiexclude ├── .backportrc.json ├── .config/ │ ├── .csharpierrc.json │ └── dotnet-tools.json ├── .csharpierrc.yaml ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-bug.yml │ │ ├── 2-bug-crash.yml │ │ ├── 3-bug-package.yml │ │ ├── 4-feature-request.yml │ │ └── config.yml │ ├── dependabot.yml │ └── workflows/ │ ├── backport.yml │ ├── build.yml │ ├── cla.yml │ ├── release.yml │ ├── stale.yml │ ├── test-ui.yml │ └── version-bump.yml ├── .gitignore ├── .husky/ │ └── task-runner.json ├── Avalonia.Gif/ │ ├── Avalonia.Gif.csproj │ ├── BgWorkerCommand.cs │ ├── BgWorkerState.cs │ ├── Decoding/ │ │ ├── BlockTypes.cs │ │ ├── ExtensionType.cs │ │ ├── FrameDisposal.cs │ │ ├── GifColor.cs │ │ ├── GifDecoder.cs │ │ ├── GifFrame.cs │ │ ├── GifHeader.cs │ │ ├── GifRect.cs │ │ ├── GifRepeatBehavior.cs │ │ ├── InvalidGifStreamException.cs │ │ └── LzwDecompressionException.cs │ ├── Extensions/ │ │ └── StreamExtensions.cs │ ├── GifImage.cs │ ├── GifInstance.cs │ ├── IGifInstance.cs │ ├── InvalidGifStreamException.cs │ └── WebpInstance.cs ├── Build/ │ ├── AppEntitlements.entitlements │ ├── EmbeddedEntitlements.entitlements │ ├── _utils.sh │ ├── build_macos_app.sh │ ├── codesign_embedded_macos.sh │ ├── codesign_macos.sh │ └── notarize_macos.sh ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ConditionalSymbols.props ├── Directory.Build.props ├── Directory.Packages.props ├── Jenkinsfile ├── LICENSE ├── NuGet.Config ├── README.md ├── Runtimes.Default.props ├── StabilityMatrix/ │ ├── App.xaml │ ├── App.xaml.cs │ ├── AppxManifest.xml │ ├── AssemblyInfo.cs │ ├── Assets/ │ │ ├── 7za - LICENSE.txt │ │ ├── Python310/ │ │ │ ├── LICENSE.txt │ │ │ ├── _asyncio.pyd │ │ │ ├── _bz2.pyd │ │ │ ├── _ctypes.pyd │ │ │ ├── _decimal.pyd │ │ │ ├── _elementtree.pyd │ │ │ ├── _hashlib.pyd │ │ │ ├── _lzma.pyd │ │ │ ├── _msi.pyd │ │ │ ├── _multiprocessing.pyd │ │ │ ├── _overlapped.pyd │ │ │ ├── _queue.pyd │ │ │ ├── _socket.pyd │ │ │ ├── _sqlite3.pyd │ │ │ ├── _ssl.pyd │ │ │ ├── _uuid.pyd │ │ │ ├── _zoneinfo.pyd │ │ │ ├── pyexpat.pyd │ │ │ ├── python.cat │ │ │ ├── python310._pth │ │ │ ├── select.pyd │ │ │ ├── unicodedata.pyd │ │ │ └── winsound.pyd │ │ ├── automatic_vladmandic.sm-package.yml │ │ ├── licenses.json │ │ ├── sm-package.schema.json │ │ └── venv/ │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── scripts/ │ │ ├── common/ │ │ │ ├── Activate.ps1 │ │ │ └── activate │ │ ├── nt/ │ │ │ ├── activate.bat │ │ │ └── deactivate.bat │ │ └── posix/ │ │ ├── activate.csh │ │ └── activate.fish │ ├── CheckpointBrowserPage.xaml │ ├── CheckpointBrowserPage.xaml.cs │ ├── CheckpointManagerPage.xaml │ ├── CheckpointManagerPage.xaml.cs │ ├── Controls/ │ │ ├── AppBrushes.cs │ │ ├── ProgressBarSmoother.cs │ │ ├── RefreshBadge.xaml │ │ └── RefreshBadge.xaml.cs │ ├── Converters/ │ │ ├── BoolNegationConverter.cs │ │ ├── BooleanToHiddenVisibleConverter.cs │ │ ├── IntDoubleConverter.cs │ │ ├── IsStringNullOrWhitespaceConverter.cs │ │ ├── LaunchOptionConverter.cs │ │ ├── LaunchOptionIntDoubleConverter.cs │ │ ├── NullToVisibilityConverter.cs │ │ ├── StringNullOrEmptyToVisibilityConverter.cs │ │ ├── UriToBitmapConverter.cs │ │ └── ValueConverterGroup.cs │ ├── DataDirectoryMigrationDialog.xaml │ ├── DataDirectoryMigrationDialog.xaml.cs │ ├── DesignData/ │ │ ├── MockCheckpointBrowserViewModel.cs │ │ ├── MockCheckpointFolder.cs │ │ ├── MockCheckpointManagerViewModel.cs │ │ ├── MockFirstLaunchSetupViewModel.cs │ │ ├── MockLaunchViewModel.cs │ │ └── MockModelVersionDialogViewModel.cs │ ├── ExceptionWindow.xaml │ ├── ExceptionWindow.xaml.cs │ ├── FirstLaunchSetupWindow.xaml │ ├── FirstLaunchSetupWindow.xaml.cs │ ├── Helper/ │ │ ├── AsyncDispatchTimer.cs │ │ ├── DialogFactory.cs │ │ ├── IDialogFactory.cs │ │ ├── ISnackbarService.cs │ │ ├── ScreenExtensions.cs │ │ └── SnackbarService.cs │ ├── InstallerWindow.xaml │ ├── InstallerWindow.xaml.cs │ ├── Interactions/ │ │ └── EventTriggerWithoutPropogation.cs │ ├── LaunchOptionsDialog.xaml │ ├── LaunchOptionsDialog.xaml.cs │ ├── LaunchPage.xaml │ ├── LaunchPage.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── Models/ │ │ ├── CheckpointFile.cs │ │ └── CheckpointFolder.cs │ ├── OneClickInstallDialog.xaml │ ├── OneClickInstallDialog.xaml.cs │ ├── Package.appxmanifest │ ├── PackageManagerPage.xaml │ ├── PackageManagerPage.xaml.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── SelectInstallLocationsDialog.xaml │ ├── SelectInstallLocationsDialog.xaml.cs │ ├── SelectModelVersionDialog.xaml │ ├── SelectModelVersionDialog.xaml.cs │ ├── Services/ │ │ ├── INotificationBarService.cs │ │ ├── InstallerWindowDialogService.cs │ │ ├── NotificationBarService.cs │ │ ├── PageContentDialogService.cs │ │ └── PageService.cs │ ├── SettingsPage.xaml │ ├── SettingsPage.xaml.cs │ ├── StabilityMatrix.csproj │ ├── Styles/ │ │ └── Styles.xaml │ ├── TextToImagePage.xaml │ ├── TextToImagePage.xaml.cs │ ├── UpdateWindow.xaml │ ├── UpdateWindow.xaml.cs │ ├── ViewModels/ │ │ ├── CheckpointBrowserCardViewModel.cs │ │ ├── CheckpointBrowserViewModel.cs │ │ ├── CheckpointManagerViewModel.cs │ │ ├── DataDirectoryMigrationViewModel.cs │ │ ├── ExceptionWindowViewModel.cs │ │ ├── FirstLaunchSetupViewModel.cs │ │ ├── InstallerViewModel.cs │ │ ├── LaunchOptionsDialogViewModel.cs │ │ ├── LaunchViewModel.cs │ │ ├── MainWindowViewModel.cs │ │ ├── OneClickInstallViewModel.cs │ │ ├── PackageManagerViewModel.cs │ │ ├── ProgressViewModel.cs │ │ ├── RefreshBadgeViewModel.cs │ │ ├── SelectInstallLocationsViewModel.cs │ │ ├── SelectModelVersionDialogViewModel.cs │ │ ├── SettingsViewModel.cs │ │ ├── SnackbarViewModel.cs │ │ ├── TextToImageViewModel.cs │ │ ├── UpdateWindowViewModel.cs │ │ └── WebLoginViewModel.cs │ ├── WebLoginDialog.xaml │ ├── WebLoginDialog.xaml.cs │ ├── app.manifest │ ├── appsettings.Development.json │ └── appsettings.json ├── StabilityMatrix.Analyzers.CodeFixes/ │ └── StabilityMatrix.Analyzers.CodeFixes.csproj ├── StabilityMatrix.Avalonia/ │ ├── Animations/ │ │ ├── BaseTransitionInfo.cs │ │ ├── BetterDrillInNavigationTransition.cs │ │ ├── BetterEntranceNavigationTransition.cs │ │ ├── BetterSlideNavigationTransition.cs │ │ └── ItemsRepeaterArrangeAnimation.cs │ ├── App.axaml │ ├── App.axaml.cs │ ├── Assets/ │ │ ├── AppIcon.icns │ │ ├── Fonts/ │ │ │ └── NotoSansJP/ │ │ │ └── OFL.txt │ │ ├── ImagePrompt.tmLanguage.json │ │ ├── ThemeMatrixDark.json │ │ ├── hf-packages.json │ │ ├── licenses.json │ │ ├── linux-x64/ │ │ │ ├── 7zzs │ │ │ └── 7zzs - LICENSE.txt │ │ ├── macos-arm64/ │ │ │ ├── 7zz │ │ │ └── 7zz - LICENSE.txt │ │ ├── markdown.css │ │ ├── sdprompt.xshd │ │ ├── sitecustomize.py │ │ └── win-x64/ │ │ ├── 7za - LICENSE.txt │ │ └── venv/ │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── scripts/ │ │ ├── common/ │ │ │ ├── Activate.ps1 │ │ │ └── activate │ │ ├── nt/ │ │ │ ├── activate.bat │ │ │ └── deactivate.bat │ │ └── posix/ │ │ ├── activate.csh │ │ └── activate.fish │ ├── Assets.cs │ ├── Behaviors/ │ │ ├── ConditionalToolTipBehavior.cs │ │ ├── ResizeBehavior.cs │ │ ├── TextEditorCompletionBehavior.cs │ │ ├── TextEditorToolTipBehavior.cs │ │ └── TextEditorWeightAdjustmentBehavior.cs │ ├── Collections/ │ │ └── SearchCollection.cs │ ├── Controls/ │ │ ├── AdvancedImageBox.axaml │ │ ├── AdvancedImageBox.axaml.cs │ │ ├── AdvancedImageBoxView.axaml │ │ ├── AdvancedImageBoxView.axaml.cs │ │ ├── AppWindowBase.cs │ │ ├── ApplicationSplashScreen.cs │ │ ├── AutoGrid.cs │ │ ├── BetterAdvancedImage.cs │ │ ├── BetterComboBox.cs │ │ ├── BetterContentDialog.cs │ │ ├── BetterContextDragBehavior.cs │ │ ├── BetterDownloadableComboBox.cs │ │ ├── BetterFlyout.cs │ │ ├── BetterImage.cs │ │ ├── BetterMarkdownScrollViewer.cs │ │ ├── Card.cs │ │ ├── CheckerboardBorder.cs │ │ ├── CodeCompletion/ │ │ │ ├── CompletionData.cs │ │ │ ├── CompletionIcons.cs │ │ │ ├── CompletionList.cs │ │ │ ├── CompletionListBox.cs │ │ │ ├── CompletionListThemes.axaml │ │ │ ├── CompletionWindow.axaml │ │ │ ├── CompletionWindow.axaml.cs │ │ │ ├── CompletionWindowBase.cs │ │ │ ├── ICompletionData.cs │ │ │ ├── InsertionRequestEventArgs.cs │ │ │ └── PopupWithCustomPosition.cs │ │ ├── ComfyUpscalerTemplateSelector.cs │ │ ├── DataTemplateSelector.cs │ │ ├── Dock/ │ │ │ └── DockUserControlBase.cs │ │ ├── DropTargetTemplatedControlBase.cs │ │ ├── DropTargetUserControlBase.cs │ │ ├── EditorCommands.cs │ │ ├── EditorFlyouts.axaml │ │ ├── FADownloadableComboBox.cs │ │ ├── FASymbolIconSource.cs │ │ ├── FrameCarousel.axaml │ │ ├── FrameCarousel.axaml.cs │ │ ├── GitVersionSelector.axaml │ │ ├── GitVersionSelector.axaml.cs │ │ ├── HybridModelTemplateSelector.cs │ │ ├── HyperlinkIconButton.cs │ │ ├── ImageLoaders.cs │ │ ├── Inference/ │ │ │ ├── BatchSizeCard.axaml │ │ │ ├── BatchSizeCard.axaml.cs │ │ │ ├── CfzCudnnToggleCard.axaml │ │ │ ├── CfzCudnnToggleCard.axaml.cs │ │ │ ├── ControlNetCard.axaml │ │ │ ├── ControlNetCard.axaml.cs │ │ │ ├── DiscreteModelSamplingCard.axaml │ │ │ ├── DiscreteModelSamplingCard.axaml.cs │ │ │ ├── ExtraNetworkCard.axaml │ │ │ ├── ExtraNetworkCard.axaml.cs │ │ │ ├── FaceDetailerCard.axaml │ │ │ ├── FaceDetailerCard.axaml.cs │ │ │ ├── FreeUCard.axaml │ │ │ ├── FreeUCard.axaml.cs │ │ │ ├── ImageFolderCard.axaml │ │ │ ├── ImageFolderCard.axaml.cs │ │ │ ├── ImageGalleryCard.axaml │ │ │ ├── ImageGalleryCard.axaml.cs │ │ │ ├── LayerDiffuseCard.axaml │ │ │ ├── LayerDiffuseCard.axaml.cs │ │ │ ├── ModelCard.axaml │ │ │ ├── ModelCard.axaml.cs │ │ │ ├── NrsCard.axaml │ │ │ ├── NrsCard.axaml.cs │ │ │ ├── PlasmaNoiseCard.axaml │ │ │ ├── PlasmaNoiseCard.axaml.cs │ │ │ ├── PromptCard.axaml │ │ │ ├── PromptCard.axaml.cs │ │ │ ├── PromptExpansionCard.axaml │ │ │ ├── PromptExpansionCard.axaml.cs │ │ │ ├── RescaleCfgCard.axaml │ │ │ ├── RescaleCfgCard.axaml.cs │ │ │ ├── SamplerCard.axaml │ │ │ ├── SamplerCard.axaml.cs │ │ │ ├── SeedCard.axaml │ │ │ ├── SeedCard.axaml.cs │ │ │ ├── SelectImageCard.axaml │ │ │ ├── SelectImageCard.axaml.cs │ │ │ ├── SharpenCard.axaml │ │ │ ├── SharpenCard.axaml.cs │ │ │ ├── StackCard.axaml │ │ │ ├── StackCard.axaml.cs │ │ │ ├── StackEditableCard.axaml │ │ │ ├── StackEditableCard.axaml.cs │ │ │ ├── StackExpander.axaml │ │ │ ├── StackExpander.axaml.cs │ │ │ ├── TiledVAECard.axaml │ │ │ ├── TiledVAECard.axaml.cs │ │ │ ├── UnetModelCard.axaml │ │ │ ├── UnetModelCard.axaml.cs │ │ │ ├── UpscalerCard.axaml │ │ │ ├── UpscalerCard.axaml.cs │ │ │ ├── WanModelCard.axaml │ │ │ └── WanModelCard.axaml.cs │ │ ├── LaunchOptionCardTemplateSelector.cs │ │ ├── LineDashFrame.cs │ │ ├── MarkdownViewer.axaml │ │ ├── MarkdownViewer.axaml.cs │ │ ├── Models/ │ │ │ ├── GitVersionSelectorVersionType.cs │ │ │ ├── PenPath.cs │ │ │ ├── PenPoint.cs │ │ │ └── SKLayer.cs │ │ ├── Paginator.axaml │ │ ├── Paginator.axaml.cs │ │ ├── Painting/ │ │ │ ├── PaintCanvas.axaml │ │ │ └── PaintCanvas.axaml.cs │ │ ├── ProgressRing.cs │ │ ├── PropertyGrid/ │ │ │ ├── BetterPropertyGrid.cs │ │ │ ├── PropertyGridCultureData.cs │ │ │ ├── PropertyGridLocalizationService.cs │ │ │ └── ToggleSwitchCellEditFactory.cs │ │ ├── RefreshBadge.axaml │ │ ├── RefreshBadge.axaml.cs │ │ ├── Scroll/ │ │ │ ├── BetterScrollContentPresenter.cs │ │ │ ├── BetterScrollViewer.axaml │ │ │ └── BetterScrollViewer.cs │ │ ├── SelectableImageCard/ │ │ │ ├── SelectableImageButton.axaml │ │ │ └── SelectableImageButton.cs │ │ ├── SettingsAccountLinkExpander.axaml │ │ ├── SettingsAccountLinkExpander.axaml.cs │ │ ├── SkiaCustomCanvas.axaml │ │ ├── SkiaCustomCanvas.axaml.cs │ │ ├── StarsRating.axaml │ │ ├── StarsRating.axaml.cs │ │ ├── TemplatedControlBase.cs │ │ ├── TextMarkers/ │ │ │ ├── TextMarker.cs │ │ │ ├── TextMarkerService.cs │ │ │ ├── TextMarkerValidationEventArgs.cs │ │ │ └── TextMarkerValidatorService.cs │ │ ├── TreeFileExplorer.axaml │ │ ├── TreeFileExplorer.axaml.cs │ │ ├── UserControlBase.cs │ │ ├── VendorLabs/ │ │ │ ├── AsyncImage/ │ │ │ │ ├── AsyncImageFailedEventArgs.cs │ │ │ │ ├── BetterAsyncImage.Events.cs │ │ │ │ ├── BetterAsyncImage.Properties.cs │ │ │ │ ├── BetterAsyncImage.cs │ │ │ │ └── BetterAsyncImageCacheProvider.cs │ │ │ ├── Cache/ │ │ │ │ ├── CacheBase.cs │ │ │ │ ├── CacheOptions.cs │ │ │ │ ├── FileCache.cs │ │ │ │ ├── IImageCache.cs │ │ │ │ ├── ImageCache.cs │ │ │ │ ├── InMemoryStorage.cs │ │ │ │ ├── InMemoryStorageItem.cs │ │ │ │ └── MemoryImageCache.cs │ │ │ ├── LICENSE │ │ │ └── Themes/ │ │ │ └── BetterAsyncImage.axaml │ │ ├── VideoGenerationSettingsCard.axaml │ │ ├── VideoGenerationSettingsCard.axaml.cs │ │ ├── VideoOutputSettingsCard.axaml │ │ └── VideoOutputSettingsCard.axaml.cs │ ├── Converters/ │ │ ├── BooleanChoiceMultiConverter.cs │ │ ├── CivitImageWidthConverter.cs │ │ ├── ComfyUpscalerConverter.cs │ │ ├── CultureInfoDisplayConverter.cs │ │ ├── CustomStringFormatConverter.cs │ │ ├── EnumAttributeConverter.cs │ │ ├── EnumAttributeConverters.cs │ │ ├── EnumStringConverter.cs │ │ ├── EnumToBooleanConverter.cs │ │ ├── EnumToIntConverter.cs │ │ ├── EnumToValuesConverter.cs │ │ ├── FileSizeConverters.cs │ │ ├── FileUriConverter.cs │ │ ├── FitSquarelyWithinAspectRatioConverter.cs │ │ ├── FuncCommandConverter.cs │ │ ├── IndexPlusOneConverter.cs │ │ ├── KiloFormatter.cs │ │ ├── KiloFormatterStringConverter.cs │ │ ├── LaunchOptionConverter.cs │ │ ├── LaunchOptionIntDoubleConverter.cs │ │ ├── MemoryBytesFormatter.cs │ │ ├── MultiplyConverter.cs │ │ ├── NullableDefaultNumericConverter.cs │ │ ├── NullableDefaultNumericConverters.cs │ │ ├── NumberFormatModeSampleConverter.cs │ │ ├── StringFormatConverters.cs │ │ ├── UriStringConverter.cs │ │ └── ValueConverterGroup.cs │ ├── DesignData/ │ │ ├── DesignData.cs │ │ ├── MockCompletionProvider.cs │ │ ├── MockDownloadProgressItemViewModel.cs │ │ ├── MockGitVersionProvider.cs │ │ ├── MockImageIndexService.cs │ │ ├── MockInferenceClientManager.cs │ │ ├── MockLaunchPageViewModel.cs │ │ ├── MockMetadataImportService.cs │ │ ├── MockModelIndexService.cs │ │ ├── MockPropertyGridObject.cs │ │ └── MockSettingsManager.cs │ ├── DialogHelper.cs │ ├── Extensions/ │ │ ├── AvaloniaEditExtensions.cs │ │ ├── BitmapExtensions.cs │ │ ├── ClipboardExtensions.cs │ │ ├── ComfyNodeBuilderExtensions.cs │ │ ├── DataObjectExtensions.cs │ │ ├── EnumExtensions.cs │ │ ├── InferenceProjectTypeExtensions.cs │ │ ├── NotificationLevelExtensions.cs │ │ ├── NotificationServiceExtensions.cs │ │ ├── RelayCommandExtensions.cs │ │ ├── ServiceManagerExtensions.cs │ │ ├── SkiaExtensions.cs │ │ ├── TextMateExtensions.cs │ │ └── VisualExtensions.cs │ ├── ExternalAnnotations/ │ │ ├── Microsoft.Extensions.Logging.Abstractions.xml │ │ └── System.Runtime.xml │ ├── FallbackRamCachedWebImageLoader.cs │ ├── Helpers/ │ │ ├── AttributeServiceInjector.Reflection.cs │ │ ├── AttributeServiceInjector.cs │ │ ├── ClipboardCommands.cs │ │ ├── ConsoleProcessRunner.cs │ │ ├── EnumHelpers.cs │ │ ├── IOCommands.cs │ │ ├── ImageProcessor.cs │ │ ├── ImageSearcher.cs │ │ ├── MarkdownSnippets.cs │ │ ├── PngDataHelper.cs │ │ ├── TagCsvParser.cs │ │ ├── TextEditorConfigs.cs │ │ ├── UnixPrerequisiteHelper.cs │ │ ├── UriHandler.cs │ │ ├── ViewModelSerializer.cs │ │ ├── Win32ClipboardFormat.cs │ │ ├── WindowsClipboard.cs │ │ ├── WindowsElevated.cs │ │ ├── WindowsPrerequisiteHelper.cs │ │ └── WindowsShortcuts.cs │ ├── Languages/ │ │ ├── Cultures.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.cs-CZ.resx │ │ ├── Resources.de.resx │ │ ├── Resources.es.resx │ │ ├── Resources.fr-FR.resx │ │ ├── Resources.it-it.resx │ │ ├── Resources.ja-JP.resx │ │ ├── Resources.ko-KR.resx │ │ ├── Resources.pt-BR.resx │ │ ├── Resources.pt-PT.resx │ │ ├── Resources.resx │ │ ├── Resources.ru-ru.resx │ │ ├── Resources.tr-TR.resx │ │ ├── Resources.uk-UA.resx │ │ ├── Resources.zh-Hans.resx │ │ └── Resources.zh-Hant.resx │ ├── Logging/ │ │ └── RichNLogTheme.cs │ ├── MarkupExtensions/ │ │ ├── EnumValuesExtension.cs │ │ └── TernaryExtension.cs │ ├── Models/ │ │ ├── AdvancedObservableList.cs │ │ ├── AppArgs.cs │ │ ├── AvaloniaResource.cs │ │ ├── CheckpointCategory.cs │ │ ├── CommandItem.cs │ │ ├── ContentDialogValueResult.cs │ │ ├── DirectionalNavigationEventArgs.cs │ │ ├── HuggingFace/ │ │ │ ├── HuggingFaceModelType.cs │ │ │ └── HuggingfaceItem.cs │ │ ├── IInfinitelyScroll.cs │ │ ├── IJsonLoadableState.cs │ │ ├── IParametersLoadableState.cs │ │ ├── IPersistentViewProvider.cs │ │ ├── IRemovableListItem.cs │ │ ├── ITemplateKey.cs │ │ ├── IconData.cs │ │ ├── ImageCacheProviders.cs │ │ ├── ImageSource.cs │ │ ├── ImageSourceTemplateType.cs │ │ ├── Inference/ │ │ │ ├── EditableModule.cs │ │ │ ├── FileNameFormat.cs │ │ │ ├── FileNameFormatPart.cs │ │ │ ├── FileNameFormatProvider.cs │ │ │ ├── FileNameFormatVar.cs │ │ │ ├── GenerateFlags.cs │ │ │ ├── GenerateOverrides.cs │ │ │ ├── IComfyStep.cs │ │ │ ├── IInputImageProvider.cs │ │ │ ├── IValidatableModule.cs │ │ │ ├── InferenceTextToImageModel.cs │ │ │ ├── LatentType.cs │ │ │ ├── LoadViewStateEventArgs.cs │ │ │ ├── ModuleApplyStepEventArgs.cs │ │ │ ├── NoiseType.cs │ │ │ ├── Prompt.cs │ │ │ ├── PromptAmplifierMode.cs │ │ │ ├── PromptCardModel.cs │ │ │ ├── SamplerCardModel.cs │ │ │ ├── SaveViewStateEventArgs.cs │ │ │ ├── SeedCardModel.cs │ │ │ ├── StackCardModel.cs │ │ │ ├── StackExpanderModel.cs │ │ │ ├── UpscalerCardModel.cs │ │ │ ├── VideoOutputMethod.cs │ │ │ └── ViewState.cs │ │ ├── InferenceProjectDocument.cs │ │ ├── ObservableDictionary.cs │ │ ├── OpenArtCustomNode.cs │ │ ├── OpenArtMetadata.cs │ │ ├── PackageManagerNavigationOptions.cs │ │ ├── PackageSteps/ │ │ │ └── UnpackSiteCustomizeStep.cs │ │ ├── PaintCanvasTool.cs │ │ ├── PythonPackageSpecifiersItem.cs │ │ ├── SelectableItem.cs │ │ ├── SharedState.cs │ │ ├── TagCompletion/ │ │ │ ├── CompletionProvider.cs │ │ │ ├── CompletionType.cs │ │ │ ├── EditorCompletionRequest.cs │ │ │ ├── ICompletionProvider.cs │ │ │ ├── ITokenizerProvider.cs │ │ │ ├── ModelCompletionData.cs │ │ │ ├── ModelTypeCompletionData.cs │ │ │ ├── TagCompletionData.cs │ │ │ ├── TagCsvEntry.cs │ │ │ ├── TagType.cs │ │ │ ├── TextCompletionRequest.cs │ │ │ └── TokenizerProvider.cs │ │ ├── TextEditorPreset.cs │ │ ├── TreeFileExplorer/ │ │ │ ├── TreeFileExplorerDirectory.cs │ │ │ ├── TreeFileExplorerFile.cs │ │ │ ├── TreeFileExplorerItem.cs │ │ │ ├── TreeFileExplorerOptions.cs │ │ │ └── TreeFileExplorerType.cs │ │ ├── TreeViewDirectory.cs │ │ ├── TypedNavigationEventArgs.cs │ │ ├── UpdateChannelCard.cs │ │ └── ViewModelState.cs │ ├── Program.cs │ ├── Services/ │ │ ├── AccountsService.cs │ │ ├── CivitBaseModelTypeService.cs │ │ ├── ConnectedServiceManager.cs │ │ ├── DiscordRichPresenceService.cs │ │ ├── IAccountsService.cs │ │ ├── ICivitBaseModelTypeService.cs │ │ ├── IConnectedServiceManager.cs │ │ ├── IDiscordRichPresenceService.cs │ │ ├── IInferenceClientManager.cs │ │ ├── IModelDownloadLinkHandler.cs │ │ ├── IModelImportService.cs │ │ ├── INavigationService.cs │ │ ├── INotificationService.cs │ │ ├── IServiceManager.cs │ │ ├── IServiceManagerScope.cs │ │ ├── InferenceClientManager.cs │ │ ├── ModelDownloadLinkHandler.cs │ │ ├── ModelImportService.cs │ │ ├── NavigationService.cs │ │ ├── NotificationService.cs │ │ ├── RunningPackageService.cs │ │ ├── ScopedServiceManager.cs │ │ ├── ServiceManager.cs │ │ ├── ServiceManagerScope.cs │ │ └── TabContext.cs │ ├── StabilityMatrix.Avalonia.csproj │ ├── StabilityMatrix.Avalonia.csproj.DotSettings │ ├── Styles/ │ │ ├── BorderStyles.axaml │ │ ├── ButtonStyles.axaml │ │ ├── Card.axaml │ │ ├── CommandBarButtonStyles.axaml │ │ ├── ContextMenuStyles.axaml │ │ ├── ControlThemes/ │ │ │ ├── BetterComboBoxStyles.axaml │ │ │ ├── ButtonStyles.Accelerator.axaml │ │ │ ├── HyperlinkIconButtonStyles.axaml │ │ │ ├── LabelStyles.Dark.axaml │ │ │ ├── LabelStyles.axaml │ │ │ ├── ListBoxStyles.axaml │ │ │ └── _index.axaml │ │ ├── DockStyles.axaml │ │ ├── FAComboBoxStyles.axaml │ │ ├── ListBoxStyles.axaml │ │ ├── Markdown/ │ │ │ ├── MarkdownStyleFluentAvalonia.axaml │ │ │ └── MarkdownStyleFluentAvalonia.axaml.cs │ │ ├── ProgressRing.axaml │ │ ├── SemiStyles.axaml │ │ ├── SemiStyles.axaml.cs │ │ ├── SplitButtonStyles.axaml │ │ ├── TextBoxStyles.axaml │ │ ├── ThemeColors.axaml │ │ ├── ThemeColors.cs │ │ ├── ThemeMaterials.axaml │ │ └── ToggleButtonStyles.axaml │ ├── ViewLocator.cs │ ├── ViewModels/ │ │ ├── Base/ │ │ │ ├── ConsoleProgressViewModel.cs │ │ │ ├── ContentDialogProgressViewModelBase.cs │ │ │ ├── ContentDialogViewModelBase.cs │ │ │ ├── DisposableLoadableViewModelBase.cs │ │ │ ├── DisposableViewModelBase.cs │ │ │ ├── InferenceGenerationViewModelBase.cs │ │ │ ├── InferenceTabViewModelBase.cs │ │ │ ├── LoadableViewModelBase.cs │ │ │ ├── PageViewModelBase.cs │ │ │ ├── PausableProgressItemViewModelBase.cs │ │ │ ├── ProgressItemViewModelBase.cs │ │ │ ├── ProgressViewModel.cs │ │ │ ├── SelectableViewModelBase.cs │ │ │ ├── TabViewModelBase.cs │ │ │ ├── TaskDialogViewModelBase.cs │ │ │ └── ViewModelBase.cs │ │ ├── CheckpointBrowser/ │ │ │ ├── CheckpointBrowserCardViewModel.cs │ │ │ ├── CivitAiBrowserViewModel.cs │ │ │ ├── CivitDetailsPageViewModel.cs │ │ │ ├── HuggingFacePageViewModel.cs │ │ │ ├── OpenModelDbBrowserCardViewModel.cs │ │ │ ├── OpenModelDbBrowserViewModel.Filters.cs │ │ │ └── OpenModelDbBrowserViewModel.cs │ │ ├── CheckpointBrowserViewModel.cs │ │ ├── CheckpointManager/ │ │ │ ├── BaseModelOptionViewModel.cs │ │ │ └── CheckpointFileViewModel.cs │ │ ├── CheckpointsPageViewModel.cs │ │ ├── ConsoleViewModel.cs │ │ ├── Controls/ │ │ │ ├── GitVersionSelectorViewModel.cs │ │ │ ├── PaintCanvasViewModel.Serializer.cs │ │ │ └── PaintCanvasViewModel.cs │ │ ├── Dialogs/ │ │ │ ├── AnalyticsOptInViewModel.cs │ │ │ ├── CivitFileDisplayViewModel.cs │ │ │ ├── CivitFileViewModel.cs │ │ │ ├── CivitImageViewModel.cs │ │ │ ├── ConfirmBulkDownloadDialogViewModel.cs │ │ │ ├── ConfirmDeleteDialogViewModel.cs │ │ │ ├── ConfirmPackageDeleteDialogViewModel.cs │ │ │ ├── DownloadResourceViewModel.cs │ │ │ ├── EnvVarsViewModel.cs │ │ │ ├── ExceptionViewModel.cs │ │ │ ├── ImageViewerViewModel.cs │ │ │ ├── InferenceConnectionHelpViewModel.cs │ │ │ ├── LaunchOptionsViewModel.cs │ │ │ ├── LykosLoginViewModel.cs │ │ │ ├── MaskEditorViewModel.cs │ │ │ ├── ModelMetadataEditorDialogViewModel.cs │ │ │ ├── ModelVersionViewModel.cs │ │ │ ├── NewOneClickInstallViewModel.cs │ │ │ ├── OAuthConnectViewModel.cs │ │ │ ├── OAuthDeviceAuthViewModel.cs │ │ │ ├── OAuthGoogleLoginViewModel.cs │ │ │ ├── OAuthLoginViewModel.cs │ │ │ ├── OneClickInstallViewModel.cs │ │ │ ├── OpenArtWorkflowViewModel.cs │ │ │ ├── OpenModelDbModelDetailsViewModel.cs │ │ │ ├── PackageImportViewModel.cs │ │ │ ├── PropertyGridViewModel.cs │ │ │ ├── PythonPackageSpecifiersViewModel.cs │ │ │ ├── PythonPackagesItemViewModel.cs │ │ │ ├── PythonPackagesViewModel.cs │ │ │ ├── RecommendedModelItemViewModel.cs │ │ │ ├── RecommendedModelsViewModel.cs │ │ │ ├── SafetensorMetadataViewModel.cs │ │ │ ├── SelectDataDirectoryViewModel.cs │ │ │ ├── SelectModelVersionViewModel.cs │ │ │ ├── SponsorshipPromptViewModel.cs │ │ │ └── UpdateViewModel.cs │ │ ├── FirstLaunchSetupViewModel.cs │ │ ├── HuggingFacePage/ │ │ │ ├── CategoryViewModel.cs │ │ │ └── HuggingfaceItemViewModel.cs │ │ ├── IDropTarget.cs │ │ ├── Inference/ │ │ │ ├── BatchSizeCardViewModel.cs │ │ │ ├── CfzCudnnToggleCardViewModel.cs │ │ │ ├── ControlNetCardViewModel.cs │ │ │ ├── DiscreteModelSamplingCardViewModel.cs │ │ │ ├── ExtraNetworkCardViewModel.cs │ │ │ ├── FaceDetailerViewModel.cs │ │ │ ├── FreeUCardViewModel.cs │ │ │ ├── IImageGalleryComponent.cs │ │ │ ├── ImageFolderCardItemViewModel.cs │ │ │ ├── ImageFolderCardViewModel.cs │ │ │ ├── ImageGalleryCardViewModel.cs │ │ │ ├── InferenceFluxTextToImageViewModel.cs │ │ │ ├── InferenceImageToImageViewModel.cs │ │ │ ├── InferenceImageToVideoViewModel.cs │ │ │ ├── InferenceImageUpscaleViewModel.cs │ │ │ ├── InferenceTextToImageViewModel.cs │ │ │ ├── InferenceWanImageToVideoViewModel.cs │ │ │ ├── InferenceWanTextToVideoViewModel.cs │ │ │ ├── LayerDiffuseCardViewModel.cs │ │ │ ├── ModelCardViewModel.cs │ │ │ ├── Modules/ │ │ │ │ ├── CfzCudnnToggleModule.cs │ │ │ │ ├── ControlNetModule.cs │ │ │ │ ├── DiscreteModelSamplingModule.cs │ │ │ │ ├── FaceDetailerModule.cs │ │ │ │ ├── FluxGuidanceModule.cs │ │ │ │ ├── FluxHiresFixModule.cs │ │ │ │ ├── FreeUModule.cs │ │ │ │ ├── HiresFixModule.cs │ │ │ │ ├── LayerDiffuseModule.cs │ │ │ │ ├── LoraModule.cs │ │ │ │ ├── ModuleBase.cs │ │ │ │ ├── NRSModule.cs │ │ │ │ ├── PlasmaNoiseModule.cs │ │ │ │ ├── PromptExpansionModule.cs │ │ │ │ ├── RescaleCfgModule.cs │ │ │ │ ├── SaveImageModule.cs │ │ │ │ ├── TiledVAEModule.cs │ │ │ │ └── UpscalerModule.cs │ │ │ ├── NrsCardViewModel.cs │ │ │ ├── PlasmaNoiseCardViewModel.cs │ │ │ ├── PromptCardViewModel.cs │ │ │ ├── PromptExpansionCardViewModel.cs │ │ │ ├── RescaleCfgCardViewModel.cs │ │ │ ├── SamplerCardViewModel.cs │ │ │ ├── SeedCardViewModel.cs │ │ │ ├── SelectImageCardViewModel.cs │ │ │ ├── SharpenCardViewModel.cs │ │ │ ├── StackCardViewModel.cs │ │ │ ├── StackEditableCardViewModel.cs │ │ │ ├── StackExpanderViewModel.cs │ │ │ ├── StackViewModelBase.cs │ │ │ ├── TiledVAECardViewModel.cs │ │ │ ├── UnetModelCardViewModel.cs │ │ │ ├── UpscalerCardViewModel.cs │ │ │ ├── Video/ │ │ │ │ ├── ImgToVidModelCardViewModel.cs │ │ │ │ ├── SvdImgToVidConditioningViewModel.cs │ │ │ │ └── VideoOutputSettingsCardViewModel.cs │ │ │ ├── WanModelCardViewModel.cs │ │ │ └── WanSamplerCardViewModel.cs │ │ ├── InferenceViewModel.cs │ │ ├── InstalledWorkflowsViewModel.cs │ │ ├── LaunchPageViewModel.cs │ │ ├── MainWindowViewModel.cs │ │ ├── OpenArtBrowserViewModel.cs │ │ ├── OutputsPage/ │ │ │ └── OutputImageViewModel.cs │ │ ├── OutputsPageViewModel.cs │ │ ├── PackageManager/ │ │ │ ├── MainPackageManagerViewModel.cs │ │ │ ├── PackageCardViewModel.cs │ │ │ ├── PackageExtensionBrowserViewModel.cs │ │ │ ├── PackageInstallBrowserViewModel.cs │ │ │ └── PackageInstallDetailViewModel.cs │ │ ├── PackageManagerViewModel.cs │ │ ├── Progress/ │ │ │ ├── DownloadProgressItemViewModel.cs │ │ │ ├── PackageInstallProgressItemViewModel.cs │ │ │ ├── ProgressItemViewModel.cs │ │ │ └── ProgressManagerViewModel.cs │ │ ├── RefreshBadgeViewModel.cs │ │ ├── RunningPackageViewModel.cs │ │ ├── Settings/ │ │ │ ├── AccountSettingsViewModel.cs │ │ │ ├── AnalyticsSettingsViewModel.cs │ │ │ ├── InferenceSettingsViewModel.cs │ │ │ ├── MainSettingsViewModel.cs │ │ │ ├── NotificationSettingsItem.cs │ │ │ ├── NotificationSettingsViewModel.cs │ │ │ └── UpdateSettingsViewModel.cs │ │ ├── SettingsViewModel.cs │ │ └── WorkflowsPageViewModel.cs │ ├── Views/ │ │ ├── CheckpointBrowserPage.axaml │ │ ├── CheckpointBrowserPage.axaml.cs │ │ ├── CheckpointsPage.axaml │ │ ├── CheckpointsPage.axaml.cs │ │ ├── CivitAiBrowserPage.axaml │ │ ├── CivitAiBrowserPage.axaml.cs │ │ ├── CivitDetailsPage.axaml │ │ ├── CivitDetailsPage.axaml.cs │ │ ├── ConsoleOutputPage.axaml │ │ ├── ConsoleOutputPage.axaml.cs │ │ ├── Dialogs/ │ │ │ ├── AnalyticsOptInDialog.axaml │ │ │ ├── AnalyticsOptInDialog.axaml.cs │ │ │ ├── ConfirmBulkDownloadDialog.axaml │ │ │ ├── ConfirmBulkDownloadDialog.axaml.cs │ │ │ ├── ConfirmDeleteDialog.axaml │ │ │ ├── ConfirmDeleteDialog.axaml.cs │ │ │ ├── ConfirmPackageDeleteDialog.axaml │ │ │ ├── ConfirmPackageDeleteDialog.axaml.cs │ │ │ ├── DownloadResourceDialog.axaml │ │ │ ├── DownloadResourceDialog.axaml.cs │ │ │ ├── EnvVarsDialog.axaml │ │ │ ├── EnvVarsDialog.axaml.cs │ │ │ ├── ExceptionDialog.axaml │ │ │ ├── ExceptionDialog.axaml.cs │ │ │ ├── ImageViewerDialog.axaml │ │ │ ├── ImageViewerDialog.axaml.cs │ │ │ ├── InferenceConnectionHelpDialog.axaml │ │ │ ├── InferenceConnectionHelpDialog.axaml.cs │ │ │ ├── LaunchOptionsDialog.axaml │ │ │ ├── LaunchOptionsDialog.axaml.cs │ │ │ ├── LykosLoginDialog.axaml │ │ │ ├── LykosLoginDialog.axaml.cs │ │ │ ├── MaskEditorDialog.axaml │ │ │ ├── MaskEditorDialog.axaml.cs │ │ │ ├── ModelMetadataEditorDialog.axaml │ │ │ ├── ModelMetadataEditorDialog.axaml.cs │ │ │ ├── NewOneClickInstallDialog.axaml │ │ │ ├── NewOneClickInstallDialog.axaml.cs │ │ │ ├── OAuthConnectDialog.axaml │ │ │ ├── OAuthConnectDialog.axaml.cs │ │ │ ├── OAuthDeviceAuthDialog.axaml │ │ │ ├── OAuthDeviceAuthDialog.axaml.cs │ │ │ ├── OAuthLoginDialog.axaml │ │ │ ├── OAuthLoginDialog.axaml.cs │ │ │ ├── OneClickInstallDialog.axaml │ │ │ ├── OneClickInstallDialog.axaml.cs │ │ │ ├── OpenArtWorkflowDialog.axaml │ │ │ ├── OpenArtWorkflowDialog.axaml.cs │ │ │ ├── OpenModelDbModelDetailsDialog.axaml │ │ │ ├── OpenModelDbModelDetailsDialog.axaml.cs │ │ │ ├── PackageImportDialog.axaml │ │ │ ├── PackageImportDialog.axaml.cs │ │ │ ├── PackageModificationDialog.axaml │ │ │ ├── PackageModificationDialog.axaml.cs │ │ │ ├── PropertyGridDialog.axaml │ │ │ ├── PropertyGridDialog.axaml.cs │ │ │ ├── PythonPackageSpecifiersDialog.axaml │ │ │ ├── PythonPackageSpecifiersDialog.axaml.cs │ │ │ ├── PythonPackagesDialog.axaml │ │ │ ├── PythonPackagesDialog.axaml.cs │ │ │ ├── RecommendedModelsDialog.axaml │ │ │ ├── RecommendedModelsDialog.axaml.cs │ │ │ ├── SafetensorMetadataDialog.axaml │ │ │ ├── SafetensorMetadataDialog.axaml.cs │ │ │ ├── SelectDataDirectoryDialog.axaml │ │ │ ├── SelectDataDirectoryDialog.axaml.cs │ │ │ ├── SelectModelVersionDialog.axaml │ │ │ ├── SelectModelVersionDialog.axaml.cs │ │ │ ├── SponsorshipPromptDialog.axaml │ │ │ ├── SponsorshipPromptDialog.axaml.cs │ │ │ ├── UpdateDialog.axaml │ │ │ └── UpdateDialog.axaml.cs │ │ ├── FirstLaunchSetupWindow.axaml │ │ ├── FirstLaunchSetupWindow.axaml.cs │ │ ├── HuggingFacePage.axaml │ │ ├── HuggingFacePage.axaml.cs │ │ ├── Inference/ │ │ │ ├── InferenceImageToImageView.axaml │ │ │ ├── InferenceImageToImageView.axaml.cs │ │ │ ├── InferenceImageToVideoView.axaml │ │ │ ├── InferenceImageToVideoView.axaml.cs │ │ │ ├── InferenceImageUpscaleView.axaml │ │ │ ├── InferenceImageUpscaleView.axaml.cs │ │ │ ├── InferenceTextToImageView.axaml │ │ │ ├── InferenceTextToImageView.axaml.cs │ │ │ ├── InferenceWanImageToVideoView.axaml │ │ │ ├── InferenceWanImageToVideoView.axaml.cs │ │ │ ├── InferenceWanTextToVideoView.axaml │ │ │ └── InferenceWanTextToVideoView.axaml.cs │ │ ├── InferencePage.axaml │ │ ├── InferencePage.axaml.cs │ │ ├── InstalledWorkflowsPage.axaml │ │ ├── InstalledWorkflowsPage.axaml.cs │ │ ├── LaunchPageView.axaml │ │ ├── LaunchPageView.axaml.cs │ │ ├── MainWindow.axaml │ │ ├── MainWindow.axaml.cs │ │ ├── OpenArtBrowserPage.axaml │ │ ├── OpenArtBrowserPage.axaml.cs │ │ ├── OpenModelDbBrowserPage.axaml │ │ ├── OpenModelDbBrowserPage.axaml.cs │ │ ├── OutputsPage.axaml │ │ ├── OutputsPage.axaml.cs │ │ ├── PackageManager/ │ │ │ ├── MainPackageManagerView.axaml │ │ │ ├── MainPackageManagerView.axaml.cs │ │ │ ├── PackageExtensionBrowserView.axaml │ │ │ ├── PackageExtensionBrowserView.axaml.cs │ │ │ ├── PackageInstallBrowserView.axaml │ │ │ ├── PackageInstallBrowserView.axaml.cs │ │ │ ├── PackageInstallDetailView.axaml │ │ │ └── PackageInstallDetailView.axaml.cs │ │ ├── PackageManagerPage.axaml │ │ ├── PackageManagerPage.axaml.cs │ │ ├── ProgressManagerPage.axaml │ │ ├── ProgressManagerPage.axaml.cs │ │ ├── Settings/ │ │ │ ├── AccountSettingsPage.axaml │ │ │ ├── AccountSettingsPage.axaml.cs │ │ │ ├── AnalyticsSettingsPage.axaml │ │ │ ├── AnalyticsSettingsPage.axaml.cs │ │ │ ├── InferenceSettingsPage.axaml │ │ │ ├── InferenceSettingsPage.axaml.cs │ │ │ ├── MainSettingsPage.axaml │ │ │ ├── MainSettingsPage.axaml.cs │ │ │ ├── NotificationSettingsPage.axaml │ │ │ ├── NotificationSettingsPage.axaml.cs │ │ │ ├── UpdateSettingsPage.axaml │ │ │ └── UpdateSettingsPage.axaml.cs │ │ ├── SettingsPage.axaml │ │ ├── SettingsPage.axaml.cs │ │ ├── WorkflowsPage.axaml │ │ └── WorkflowsPage.axaml.cs │ └── app.manifest ├── StabilityMatrix.Avalonia.Diagnostics/ │ ├── LogViewer/ │ │ ├── Controls/ │ │ │ ├── LogViewerControl.axaml │ │ │ └── LogViewerControl.axaml.cs │ │ ├── Converters/ │ │ │ ├── ChangeColorTypeConverter.cs │ │ │ └── EventIdConverter.cs │ │ ├── Core/ │ │ │ ├── Extensions/ │ │ │ │ └── LoggerExtensions.cs │ │ │ ├── Logging/ │ │ │ │ ├── DataStoreLoggerConfiguration.cs │ │ │ │ ├── ILogDataStore.cs │ │ │ │ ├── ILogDataStoreImpl.cs │ │ │ │ ├── LogDataStore.cs │ │ │ │ ├── LogEntryColor.cs │ │ │ │ └── LogModel.cs │ │ │ └── ViewModels/ │ │ │ ├── LogViewerControlViewModel.cs │ │ │ ├── ObservableObject.cs │ │ │ └── ViewModel.cs │ │ ├── DataStoreLoggerTarget.cs │ │ ├── Extensions/ │ │ │ └── ServicesExtension.cs │ │ ├── LICENSE │ │ ├── Logging/ │ │ │ └── LogDataStore.cs │ │ └── README.md │ ├── StabilityMatrix.Avalonia.Diagnostics.csproj │ ├── ViewModels/ │ │ └── LogWindowViewModel.cs │ └── Views/ │ ├── LogWindow.axaml │ └── LogWindow.axaml.cs ├── StabilityMatrix.Avalonia.pupnet.conf ├── StabilityMatrix.Core/ │ ├── Animation/ │ │ └── GifConverter.cs │ ├── Api/ │ │ ├── A3WebApiManager.cs │ │ ├── ApiFactory.cs │ │ ├── CivitCompatApiManager.cs │ │ ├── IA3WebApi.cs │ │ ├── IA3WebApiManager.cs │ │ ├── IApiFactory.cs │ │ ├── ICivitApi.cs │ │ ├── ICivitTRPCApi.cs │ │ ├── IComfyApi.cs │ │ ├── IHuggingFaceApi.cs │ │ ├── IInvokeAiApi.cs │ │ ├── ILykosAnalyticsApi.cs │ │ ├── ILykosAuthApiV1.cs │ │ ├── ILykosModelDiscoveryApi.cs │ │ ├── IOpenArtApi.cs │ │ ├── IOpenModelDbApi.cs │ │ ├── IPyPiApi.cs │ │ ├── ITokenProvider.cs │ │ ├── LykosAuthApi/ │ │ │ ├── .refitter │ │ │ ├── Generated/ │ │ │ │ └── Refitter.g.cs │ │ │ └── IRecommendedModelsApi.cs │ │ ├── LykosAuthTokenProvider.cs │ │ ├── OpenIdClientConstants.cs │ │ ├── PromptGen/ │ │ │ ├── .refitter │ │ │ └── Generated/ │ │ │ └── Refitter.g.cs │ │ └── TokenAuthHeaderHandler.cs │ ├── Attributes/ │ │ ├── BoolStringMemberAttribute.cs │ │ ├── ManagedServiceAttribute.cs │ │ ├── PreloadAttribute.cs │ │ ├── SingletonAttribute.cs │ │ ├── TransientAttribute.cs │ │ ├── TypedNodeOptionsAttribute.cs │ │ └── ViewAttribute.cs │ ├── Converters/ │ │ └── Json/ │ │ ├── AnalyticsRequestConverter.cs │ │ ├── DefaultUnknownEnumConverter.cs │ │ ├── LaunchOptionValueJsonConverter.cs │ │ ├── NodeConnectionBaseJsonConverter.cs │ │ ├── OneOfJsonConverter.cs │ │ ├── ParsableStringValueJsonConverter.cs │ │ ├── SKColorJsonConverter.cs │ │ ├── SemVersionJsonConverter.cs │ │ └── StringJsonConverter.cs │ ├── Database/ │ │ ├── CivitModelQueryCacheEntry.cs │ │ ├── ILiteDbContext.cs │ │ └── LiteDbContext.cs │ ├── Exceptions/ │ │ ├── AppException.cs │ │ ├── CivitDownloadDisabledException.cs │ │ ├── CivitLoginRequiredException.cs │ │ ├── ComfyNodeException.cs │ │ ├── EarlyAccessException.cs │ │ ├── FileExistsException.cs │ │ ├── HuggingFaceLoginRequiredException.cs │ │ ├── MissingPrerequisiteException.cs │ │ ├── ProcessException.cs │ │ ├── PromptError.cs │ │ ├── PromptSyntaxError.cs │ │ ├── PromptUnknownModelError.cs │ │ └── PromptValidationError.cs │ ├── Extensions/ │ │ ├── DictionaryExtensions.cs │ │ ├── DirectoryPathExtensions.cs │ │ ├── DynamicDataExtensions.cs │ │ ├── EnumAttributes.cs │ │ ├── EnumConversion.cs │ │ ├── EnumerableExtensions.cs │ │ ├── HashExtensions.cs │ │ ├── JsonObjectExtensions.cs │ │ ├── LiteDBExtensions.cs │ │ ├── NullableExtensions.cs │ │ ├── ObjectExtensions.cs │ │ ├── ProgressExtensions.cs │ │ ├── SemVersionExtensions.cs │ │ ├── ServiceProviderExtensions.cs │ │ ├── SizeExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── TypeExtensions.cs │ │ └── UriExtensions.cs │ ├── Git/ │ │ ├── CachedCommandGitVersionProvider.cs │ │ ├── CommandGitVersionProvider.cs │ │ └── IGitVersionProvider.cs │ ├── Helper/ │ │ ├── Analytics/ │ │ │ ├── AnalyticsHelper.cs │ │ │ └── IAnalyticsHelper.cs │ │ ├── ArchiveHelper.cs │ │ ├── Cache/ │ │ │ ├── GithubApiCache.cs │ │ │ ├── IGithubApiCache.cs │ │ │ ├── IPyPiCache.cs │ │ │ ├── LRUCache.cs │ │ │ └── PyPiCache.cs │ │ ├── CodeTimer.cs │ │ ├── Compat.cs │ │ ├── EnumerationOptionConstants.cs │ │ ├── EnvPathBuilder.cs │ │ ├── EventManager.cs │ │ ├── Factory/ │ │ │ ├── IPackageFactory.cs │ │ │ └── PackageFactory.cs │ │ ├── FileHash.cs │ │ ├── FileTransfers.cs │ │ ├── GenerationParametersConverter.cs │ │ ├── HardwareInfo/ │ │ │ ├── CpuInfo.cs │ │ │ ├── GpuInfo.cs │ │ │ ├── HardwareHelper.cs │ │ │ ├── MemoryInfo.cs │ │ │ ├── MemoryLevel.cs │ │ │ └── Win32MemoryStatusEx.cs │ │ ├── IPrerequisiteHelper.cs │ │ ├── ISharedFolders.cs │ │ ├── ImageMetadata.cs │ │ ├── LazyInstance.cs │ │ ├── MinimumDelay.cs │ │ ├── ModelCompatChecker.cs │ │ ├── ModelFinder.cs │ │ ├── MyTiffFile.cs │ │ ├── ObjectHash.cs │ │ ├── PlatformKind.cs │ │ ├── ProcessTracker.cs │ │ ├── PropertyComparer.cs │ │ ├── ReaderWriterLockAdvanced.cs │ │ ├── RemoteModels.cs │ │ ├── SharedFolders.cs │ │ ├── SharedFoldersConfigHelper.cs │ │ ├── SharedFoldersConfigOptions.cs │ │ ├── Size.cs │ │ ├── SystemInfo.cs │ │ ├── Utilities.cs │ │ └── Webp/ │ │ └── WebpReader.cs │ ├── Inference/ │ │ ├── ComfyClient.cs │ │ ├── ComfyProgressUpdateEventArgs.cs │ │ ├── ComfyTask.cs │ │ └── InferenceClientBase.cs │ ├── Models/ │ │ ├── Api/ │ │ │ ├── A3Options.cs │ │ │ ├── CivitAccountStatusUpdateEventArgs.cs │ │ │ ├── CivitBaseModelType.cs │ │ │ ├── CivitCommercialUse.cs │ │ │ ├── CivitCreator.cs │ │ │ ├── CivitFile.cs │ │ │ ├── CivitFileHashes.cs │ │ │ ├── CivitFileMetadata.cs │ │ │ ├── CivitFileType.cs │ │ │ ├── CivitImage.cs │ │ │ ├── CivitMetadata.cs │ │ │ ├── CivitMode.cs │ │ │ ├── CivitModel.cs │ │ │ ├── CivitModelFormat.cs │ │ │ ├── CivitModelFpType.cs │ │ │ ├── CivitModelSize.cs │ │ │ ├── CivitModelStats.cs │ │ │ ├── CivitModelType.cs │ │ │ ├── CivitModelVersion.cs │ │ │ ├── CivitModelVersionResponse.cs │ │ │ ├── CivitModelsRequest.cs │ │ │ ├── CivitModelsResponse.cs │ │ │ ├── CivitPeriod.cs │ │ │ ├── CivitSortMode.cs │ │ │ ├── CivitStats.cs │ │ │ ├── CivitTRPC/ │ │ │ │ ├── CivitApiTokens.cs │ │ │ │ ├── CivitGetUserByIdRequest.cs │ │ │ │ ├── CivitGetUserByIdResponse.cs │ │ │ │ ├── CivitImageGenerationDataResponse.cs │ │ │ │ ├── CivitUserAccountResponse.cs │ │ │ │ ├── CivitUserProfileRequest.cs │ │ │ │ ├── CivitUserProfileResponse.cs │ │ │ │ └── CivitUserToggleFavoriteModelRequest.cs │ │ │ ├── Comfy/ │ │ │ │ ├── ComfyAuxPreprocessor.cs │ │ │ │ ├── ComfyHistoryOutput.cs │ │ │ │ ├── ComfyHistoryResponse.cs │ │ │ │ ├── ComfyImage.cs │ │ │ │ ├── ComfyInputInfo.cs │ │ │ │ ├── ComfyObjectInfo.cs │ │ │ │ ├── ComfyPromptRequest.cs │ │ │ │ ├── ComfyPromptResponse.cs │ │ │ │ ├── ComfySampler.cs │ │ │ │ ├── ComfySamplerScheduler.cs │ │ │ │ ├── ComfyScheduler.cs │ │ │ │ ├── ComfyUploadImageResponse.cs │ │ │ │ ├── ComfyUpscaler.cs │ │ │ │ ├── ComfyUpscalerType.cs │ │ │ │ ├── ComfyWebSocketResponse.cs │ │ │ │ ├── ComfyWebSocketResponseType.cs │ │ │ │ ├── ComfyWebSocketResponseUnion.cs │ │ │ │ ├── NodeTypes/ │ │ │ │ │ ├── ConditioningConnections.cs │ │ │ │ │ ├── ModelConnections.cs │ │ │ │ │ ├── NodeConnectionBase.cs │ │ │ │ │ ├── NodeConnections.cs │ │ │ │ │ └── PrimaryNodeConnection.cs │ │ │ │ ├── Nodes/ │ │ │ │ │ ├── ComfyNode.cs │ │ │ │ │ ├── ComfyNodeBuilder.cs │ │ │ │ │ ├── ComfyTypedNodeBase.cs │ │ │ │ │ ├── IOutputNode.cs │ │ │ │ │ ├── NamedComfyNode.cs │ │ │ │ │ ├── NodeDictionary.cs │ │ │ │ │ └── RerouteNode.cs │ │ │ │ └── WebSocketData/ │ │ │ │ ├── ComfyStatus.cs │ │ │ │ ├── ComfyStatusExecInfo.cs │ │ │ │ ├── ComfyWebSocketExecutingData.cs │ │ │ │ ├── ComfyWebSocketExecutionErrorData.cs │ │ │ │ ├── ComfyWebSocketImageData.cs │ │ │ │ ├── ComfyWebSocketProgressData.cs │ │ │ │ └── ComfyWebSocketStatusData.cs │ │ │ ├── HuggingFace/ │ │ │ │ └── HuggingFaceUser.cs │ │ │ ├── HuggingFaceAccountStatusUpdateEventArgs.cs │ │ │ ├── ImageResponse.cs │ │ │ ├── Invoke/ │ │ │ │ ├── InstallModelRequest.cs │ │ │ │ ├── ModelInstallResult.cs │ │ │ │ └── ScanFolderResult.cs │ │ │ ├── Lykos/ │ │ │ │ ├── Analytics/ │ │ │ │ │ ├── AnalyticsRequest.cs │ │ │ │ │ ├── FirstTimeInstallAnalytics.cs │ │ │ │ │ ├── LaunchAnalyticsRequest.cs │ │ │ │ │ └── PackageInstallAnalyticsRequest.cs │ │ │ │ ├── GetDownloadResponse.cs │ │ │ │ ├── GetRecommendedModelsResponse.cs │ │ │ │ ├── GetUserResponse.cs │ │ │ │ ├── GoogleOAuthResponse.cs │ │ │ │ ├── LykosAccount.cs │ │ │ │ ├── LykosAccountStatusUpdateEventArgs.cs │ │ │ │ ├── LykosAccountV1Tokens.cs │ │ │ │ ├── LykosAccountV2Tokens.cs │ │ │ │ ├── LykosRole.cs │ │ │ │ ├── PostAccountRequest.cs │ │ │ │ ├── PostLoginRefreshRequest.cs │ │ │ │ ├── PostLoginRequest.cs │ │ │ │ └── RecommendedModelsV2Response.cs │ │ │ ├── OpenArt/ │ │ │ │ ├── NodesCount.cs │ │ │ │ ├── OpenArtCreator.cs │ │ │ │ ├── OpenArtDateTime.cs │ │ │ │ ├── OpenArtDownloadRequest.cs │ │ │ │ ├── OpenArtDownloadResponse.cs │ │ │ │ ├── OpenArtFeedRequest.cs │ │ │ │ ├── OpenArtSearchRequest.cs │ │ │ │ ├── OpenArtSearchResponse.cs │ │ │ │ ├── OpenArtSearchResult.cs │ │ │ │ ├── OpenArtStats.cs │ │ │ │ └── OpenArtThumbnail.cs │ │ │ ├── OpenModelsDb/ │ │ │ │ ├── OpenModelDbArchitecture.cs │ │ │ │ ├── OpenModelDbArchitecturesResponse.cs │ │ │ │ ├── OpenModelDbImage.cs │ │ │ │ ├── OpenModelDbKeyedModel.cs │ │ │ │ ├── OpenModelDbModel.cs │ │ │ │ ├── OpenModelDbModelsResponse.cs │ │ │ │ ├── OpenModelDbResource.cs │ │ │ │ ├── OpenModelDbTag.cs │ │ │ │ └── OpenModelDbTagsResponse.cs │ │ │ ├── ProgressRequest.cs │ │ │ ├── ProgressResponse.cs │ │ │ ├── Pypi/ │ │ │ │ ├── PyPiReleaseFile.cs │ │ │ │ └── PyPiResponse.cs │ │ │ └── TextToImageRequest.cs │ │ ├── Base/ │ │ │ └── StringValue.cs │ │ ├── CheckpointSortMode.cs │ │ ├── CheckpointSortOptions.cs │ │ ├── CivitPostDownloadContextAction.cs │ │ ├── CivitaiResource.cs │ │ ├── ComfyNodeMap.cs │ │ ├── Configs/ │ │ │ ├── ApiOptions.cs │ │ │ └── DebugOptions.cs │ │ ├── ConnectedModelInfo.cs │ │ ├── ConnectedModelSource.cs │ │ ├── CustomVersion.cs │ │ ├── Database/ │ │ │ ├── CivitBaseModelTypeCacheEntry.cs │ │ │ ├── GitCommit.cs │ │ │ ├── GithubCacheEntry.cs │ │ │ ├── InferenceProjectEntry.cs │ │ │ ├── LocalImageFile.cs │ │ │ ├── LocalImageFileType.cs │ │ │ ├── LocalModelFile.cs │ │ │ ├── LocalModelFolder.cs │ │ │ └── PyPiCacheEntry.cs │ │ ├── DimensionStringComparer.cs │ │ ├── DownloadPackageVersionOptions.cs │ │ ├── EnvVarKeyPair.cs │ │ ├── ExtraPackageCommand.cs │ │ ├── FDS/ │ │ │ ├── ComfyUiSelfStartSettings.cs │ │ │ └── StableSwarmSettings.cs │ │ ├── FileInterfaces/ │ │ │ ├── DirectoryPath.cs │ │ │ ├── FilePath.Fluent.cs │ │ │ ├── FilePath.cs │ │ │ ├── FileSystemPath.cs │ │ │ ├── IPathObject.cs │ │ │ └── TempDirectoryPath.cs │ │ ├── FileSizeType.cs │ │ ├── GenerationParameters.cs │ │ ├── GitVersion.cs │ │ ├── GlobalConfig.cs │ │ ├── GlobalEncryptedSerializer.cs │ │ ├── HybridModelFile.cs │ │ ├── HybridModelType.cs │ │ ├── IContextAction.cs │ │ ├── IDownloadableResource.cs │ │ ├── IHandleNavigation.cs │ │ ├── ISearchText.cs │ │ ├── IndexCollection.cs │ │ ├── Inference/ │ │ │ ├── InferenceProjectType.cs │ │ │ ├── LayerDiffuseMode.cs │ │ │ ├── ModelLoader.cs │ │ │ └── ModuleApplyStepTemporaryArgs.cs │ │ ├── InferenceDefaults.cs │ │ ├── InferenceRunCustomPromptEventArgs.cs │ │ ├── InstalledPackage.cs │ │ ├── InstalledPackageVersion.cs │ │ ├── LaunchOption.cs │ │ ├── LaunchOptionCard.cs │ │ ├── LaunchOptionDefinition.cs │ │ ├── LaunchOptionType.cs │ │ ├── LicenseInfo.cs │ │ ├── LoadState.cs │ │ ├── ModelPostDownloadContextAction.cs │ │ ├── ObservableHashSet.cs │ │ ├── OrderedValue.cs │ │ ├── PackageDifficulty.cs │ │ ├── PackageModification/ │ │ │ ├── ActionPackageStep.cs │ │ │ ├── AddInstalledPackageStep.cs │ │ │ ├── DownloadOpenArtWorkflowStep.cs │ │ │ ├── DownloadPackageVersionStep.cs │ │ │ ├── ICancellablePackageStep.cs │ │ │ ├── IPackageModificationRunner.cs │ │ │ ├── ImportModelsStep.cs │ │ │ ├── InstallExtensionStep.cs │ │ │ ├── InstallNunchakuStep.cs │ │ │ ├── InstallPackageStep.cs │ │ │ ├── InstallSageAttentionStep.cs │ │ │ ├── PackageModificationRunner.cs │ │ │ ├── PackageStep.cs │ │ │ ├── PipStep.cs │ │ │ ├── ProcessStep.cs │ │ │ ├── ScanMetadataStep.cs │ │ │ ├── SetPackageInstallingStep.cs │ │ │ ├── SetupModelFoldersStep.cs │ │ │ ├── SetupOutputSharingStep.cs │ │ │ ├── SetupPrerequisitesStep.cs │ │ │ ├── UninstallExtensionStep.cs │ │ │ ├── UpdateExtensionStep.cs │ │ │ └── UpdatePackageStep.cs │ │ ├── PackagePair.cs │ │ ├── PackagePrerequisite.cs │ │ ├── PackageType.cs │ │ ├── PackageVersion.cs │ │ ├── PackageVersionType.cs │ │ ├── Packages/ │ │ │ ├── A3WebUI.cs │ │ │ ├── AiToolkit.cs │ │ │ ├── BaseGitPackage.cs │ │ │ ├── BasePackage.cs │ │ │ ├── Cogstudio.cs │ │ │ ├── ComfyUI.cs │ │ │ ├── ComfyZluda.cs │ │ │ ├── Config/ │ │ │ │ ├── ConfigDefaultType.cs │ │ │ │ ├── ConfigFileType.cs │ │ │ │ ├── ConfigSharingOptions.cs │ │ │ │ ├── FdsConfigSharingStrategy.cs │ │ │ │ ├── IConfigSharingStrategy.cs │ │ │ │ ├── JsonConfigSharingStrategy.cs │ │ │ │ └── YamlConfigSharingStrategy.cs │ │ │ ├── DankDiffusion.cs │ │ │ ├── Extensions/ │ │ │ │ ├── A1111ExtensionManifest.cs │ │ │ │ ├── ComfyExtensionManifest.cs │ │ │ │ ├── ExtensionManifest.cs │ │ │ │ ├── ExtensionPack.cs │ │ │ │ ├── ExtensionSpecifier.cs │ │ │ │ ├── GitPackageExtensionManager.cs │ │ │ │ ├── IPackageExtensionManager.cs │ │ │ │ ├── InstalledPackageExtension.cs │ │ │ │ ├── PackageExtension.cs │ │ │ │ ├── PackageExtensionVersion.cs │ │ │ │ ├── SavedPackageExtension.cs │ │ │ │ └── VladExtensionItem.cs │ │ │ ├── FluxGym.cs │ │ │ ├── FocusControlNet.cs │ │ │ ├── Fooocus.cs │ │ │ ├── FooocusMre.cs │ │ │ ├── ForgeAmdGpu.cs │ │ │ ├── ForgeClassic.cs │ │ │ ├── ForgeNeo.cs │ │ │ ├── FramePack.cs │ │ │ ├── FramePackStudio.cs │ │ │ ├── IArgParsable.cs │ │ │ ├── InvokeAI.cs │ │ │ ├── KohyaSs.cs │ │ │ ├── Mashb1tFooocus.cs │ │ │ ├── OneTrainer.cs │ │ │ ├── Options/ │ │ │ │ ├── DownloadPackageOptions.cs │ │ │ │ ├── InstallPackageOptions.cs │ │ │ │ ├── PythonPackageOptions.cs │ │ │ │ ├── RunPackageOptions.cs │ │ │ │ └── UpdatePackageOptions.cs │ │ │ ├── PackageVersionOptions.cs │ │ │ ├── PackageVulnerability.cs │ │ │ ├── PipInstallConfig.cs │ │ │ ├── Reforge.cs │ │ │ ├── RuinedFooocus.cs │ │ │ ├── SDWebForge.cs │ │ │ ├── Sdfx.cs │ │ │ ├── SharedFolderLayout.cs │ │ │ ├── SharedFolderLayoutRule.cs │ │ │ ├── SimpleSDXL.cs │ │ │ ├── StableDiffusionDirectMl.cs │ │ │ ├── StableDiffusionUx.cs │ │ │ ├── StableSwarm.cs │ │ │ ├── UnknownPackage.cs │ │ │ ├── VladAutomatic.cs │ │ │ ├── VoltaML.cs │ │ │ └── Wan2GP.cs │ │ ├── Progress/ │ │ │ ├── ProgressItem.cs │ │ │ ├── ProgressReport.cs │ │ │ ├── ProgressState.cs │ │ │ └── ProgressType.cs │ │ ├── PromptSyntax/ │ │ │ ├── PromptNode.cs │ │ │ ├── PromptSyntaxBuilder.cs │ │ │ ├── PromptSyntaxTree.cs │ │ │ └── TextSpan.cs │ │ ├── RelayPropertyChangedEventArgs.cs │ │ ├── RemoteResource.cs │ │ ├── SafetensorMetadata.cs │ │ ├── Secrets.cs │ │ ├── Settings/ │ │ │ ├── AnalyticsSettings.cs │ │ │ ├── GlobalSettings.cs │ │ │ ├── HolidayMode.cs │ │ │ ├── LastDownloadLocationInfo.cs │ │ │ ├── LibrarySettings.cs │ │ │ ├── ModelSearchOptions.cs │ │ │ ├── NotificationKey.cs │ │ │ ├── NotificationLevel.cs │ │ │ ├── NotificationOption.cs │ │ │ ├── NumberFormatMode.cs │ │ │ ├── Settings.cs │ │ │ ├── SettingsTransaction.cs │ │ │ ├── Size.cs │ │ │ ├── TeachingTip.cs │ │ │ └── WindowSettings.cs │ │ ├── SharedFolderMethod.cs │ │ ├── SharedFolderType.cs │ │ ├── SharedOutputType.cs │ │ ├── StringValue.cs │ │ ├── TaskResult.cs │ │ ├── Tokens/ │ │ │ ├── PromptExtraNetwork.cs │ │ │ └── PromptExtraNetworkType.cs │ │ ├── TorchIndex.cs │ │ ├── TrackedDownload.cs │ │ ├── UnknownInstalledPackage.cs │ │ └── Update/ │ │ ├── UpdateChannel.cs │ │ ├── UpdateInfo.cs │ │ ├── UpdateManifest.cs │ │ ├── UpdatePlatforms.cs │ │ └── UpdateType.cs │ ├── Processes/ │ │ ├── AnsiCommand.cs │ │ ├── AnsiParser.cs │ │ ├── AnsiProcess.cs │ │ ├── ApcMessage.cs │ │ ├── ApcParser.cs │ │ ├── ApcType.cs │ │ ├── Argument.cs │ │ ├── AsyncStreamReader.cs │ │ ├── ProcessArgs.cs │ │ ├── ProcessArgsBuilder.cs │ │ ├── ProcessOutput.cs │ │ ├── ProcessResult.cs │ │ └── ProcessRunner.cs │ ├── Python/ │ │ ├── ArgParser.cs │ │ ├── IPyInstallationManager.cs │ │ ├── IPyRunner.cs │ │ ├── IPyVenvRunner.cs │ │ ├── IUvManager.cs │ │ ├── Interop/ │ │ │ └── PyIOStream.cs │ │ ├── MajorMinorVersion.cs │ │ ├── PipIndexResult.cs │ │ ├── PipInstallArgs.cs │ │ ├── PipPackageInfo.cs │ │ ├── PipPackageSpecifier.cs │ │ ├── PipPackageSpecifierOverride.cs │ │ ├── PipPackageSpecifierOverrideAction.cs │ │ ├── PipShowResult.cs │ │ ├── PyBaseInstall.cs │ │ ├── PyInstallation.cs │ │ ├── PyInstallationManager.cs │ │ ├── PyRunner.cs │ │ ├── PyVenvRunner.cs │ │ ├── PyVersion.cs │ │ ├── QueryTclTkLibraryResult.cs │ │ ├── UvInstallArgs.cs │ │ ├── UvManager.cs │ │ ├── UvPackageSpecifier.cs │ │ ├── UvPackageSpecifierOverride.cs │ │ ├── UvPackageSpecifierOverrideAction.cs │ │ ├── UvPythonInfo.cs │ │ ├── UvPythonListEntry.cs │ │ └── UvVenvRunner.cs │ ├── ReparsePoints/ │ │ ├── DeviceIoControlCode.cs │ │ ├── Junction.cs │ │ ├── ReparseDataBuffer.cs │ │ ├── Win32CreationDisposition.cs │ │ ├── Win32ErrorCode.cs │ │ ├── Win32FileAccess.cs │ │ ├── Win32FileAttribute.cs │ │ └── Win32FileShare.cs │ ├── Services/ │ │ ├── DownloadService.cs │ │ ├── IDownloadService.cs │ │ ├── IImageIndexService.cs │ │ ├── IMetadataImportService.cs │ │ ├── IModelIndexService.cs │ │ ├── IPipWheelService.cs │ │ ├── ISecretsManager.cs │ │ ├── ISettingsManager.cs │ │ ├── ITrackedDownloadService.cs │ │ ├── ImageIndexService.cs │ │ ├── MetadataImportService.cs │ │ ├── ModelIndexService.cs │ │ ├── OpenModelDbManager.cs │ │ ├── PipWheelService.cs │ │ ├── SecretsManager.cs │ │ ├── SettingsManager.cs │ │ └── TrackedDownloadService.cs │ ├── StabilityMatrix.Core.csproj │ ├── StabilityMatrix.Core.csproj.DotSettings │ ├── Updater/ │ │ ├── IUpdateHelper.cs │ │ ├── SignatureChecker.cs │ │ ├── UpdateHelper.cs │ │ └── UpdateStatusChangedEventArgs.cs │ └── Validators/ │ └── RequiresMatchAttribute.cs ├── StabilityMatrix.Native/ │ ├── NativeFileOperations.cs │ └── StabilityMatrix.Native.csproj ├── StabilityMatrix.Native.Abstractions/ │ ├── INativeRecycleBinProvider.cs │ ├── NativeFileOperationFlags.cs │ └── StabilityMatrix.Native.Abstractions.csproj ├── StabilityMatrix.Native.Windows/ │ ├── AssemblyInfo.cs │ ├── FileOperations/ │ │ └── FileOperationWrapper.cs │ ├── GlobalUsings.cs │ ├── Interop/ │ │ ├── ComReleaser.cs │ │ ├── FileOperationFlags.cs │ │ ├── FileOperationProgressSinkTcs.cs │ │ ├── IFileOperation.cs │ │ ├── IFileOperationProgressSink.cs │ │ ├── IShellItem.cs │ │ ├── IShellItemArray.cs │ │ └── SIGDN.cs │ ├── NativeRecycleBinProvider.cs │ └── StabilityMatrix.Native.Windows.csproj ├── StabilityMatrix.Native.macOS/ │ ├── AssemblyInfo.cs │ ├── NativeRecycleBinProvider.cs │ └── StabilityMatrix.Native.macOS.csproj ├── StabilityMatrix.Tests/ │ ├── Avalonia/ │ │ ├── CheckpointFileViewModelTests.cs │ │ ├── Converters/ │ │ │ └── NullableDefaultNumericConverterTests.cs │ │ ├── DesignDataTests.cs │ │ ├── FileNameFormatProviderTests.cs │ │ ├── FileNameFormatTests.cs │ │ ├── LoadableViewModelBaseTests.cs │ │ ├── PromptTests.cs │ │ └── UpdateViewModelTests.cs │ ├── Core/ │ │ ├── AnsiParserTests.cs │ │ ├── AsyncStreamReaderTests.cs │ │ ├── DefaultUnknownEnumConverterTests.cs │ │ ├── FileSystemPathTests.cs │ │ ├── GlobalEncryptedSerializerTests.cs │ │ ├── ModelIndexServiceTests.cs │ │ ├── PipInstallArgsTests.cs │ │ ├── PipShowResultsTests.cs │ │ └── ServiceProviderExtensionsTests.cs │ ├── Helper/ │ │ ├── EventManagerTests.cs │ │ ├── ImageProcessorTests.cs │ │ └── PackageFactoryTests.cs │ ├── Models/ │ │ ├── GenerationParametersTests.cs │ │ ├── InstalledPackageTests.cs │ │ ├── LocalModelFileTests.cs │ │ ├── Packages/ │ │ │ ├── PackageHelper.cs │ │ │ ├── PackageLinkTests.cs │ │ │ └── SharedFolderConfigHelperTests.cs │ │ ├── ProcessArgsTests.cs │ │ ├── SafetensorMetadataTests.cs │ │ └── SharedFoldersTests.cs │ ├── Native/ │ │ └── NativeRecycleBinProviderTests.cs │ ├── ReparsePoints/ │ │ └── JunctionTests.cs │ ├── StabilityMatrix.Tests.csproj │ ├── TempFiles.cs │ └── Usings.cs ├── StabilityMatrix.UITests/ │ ├── Attributes/ │ │ └── TestPriorityAttribute.cs │ ├── Extensions/ │ │ ├── VisualExtensions.cs │ │ └── WindowExtensions.cs │ ├── MainWindowTests.cs │ ├── ModelBrowser/ │ │ └── CivitAiBrowserTests.cs │ ├── ModuleInit.cs │ ├── PriorityOrderer.cs │ ├── Snapshots/ │ │ ├── MainWindowTests.MainWindowViewModel_ShouldOk.verified.txt │ │ └── MainWindowTests.MainWindow_ShouldOpen.verified.txt │ ├── StabilityMatrix.UITests.csproj │ ├── TempDirFixture.cs │ ├── TestAppBuilder.cs │ ├── TestBase.cs │ ├── Usings.cs │ ├── VerifyConfig.cs │ └── WaitHelper.cs ├── StabilityMatrix.sln ├── StabilityMatrix.sln.DotSettings ├── Tools/ │ ├── ConvertAttributes.csx │ └── add_resx_strings.py ├── analyzers/ │ ├── StabilityMatrix.Analyzers/ │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ ├── StabilityMatrix.Analyzers.csproj │ │ └── ViewModelControlConventionAnalyzer.cs │ └── StabilityMatrix.Analyzers.CodeFixes/ │ ├── ControlMustInheritBaseFixProvider.cs │ ├── DocumentEditorExtensions.cs │ ├── MissingViewAttributeFixProvider.cs │ └── StabilityMatrix.Analyzers.CodeFixes.csproj └── global.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .aiexclude ================================================ # Docs LICENSE CHANGELOG.md # Legacy StabilityMatrix/ # Tests *.verified.* # Misc projects StabilityMatrix.Native/ StabilityMatrix.Native.*/ StabilityMatrix.Avalonia.Diagnostics/ StabilityMatrix.Avalonia.Diagnostics/ StabilityMatrix.UITests/ # Vendored Avalonia.Gif/ # Configs *.editorconfig *.DotSettings # Assets *.svg StabilityMatrix.Avalonia/Assets/Fonts/ StabilityMatrix.Avalonia/Assets/linux-x64/ StabilityMatrix.Avalonia/Assets/macos-arm64/ StabilityMatrix.Avalonia/Assets/win-x64/ ================================================ FILE: .backportrc.json ================================================ { "sourceBranch": "dev", "targetBranch": "main", "mainline": 1, "fork": false, "targetPRLabels": ["backport"], "prTitle": "[{{sourceBranch}} to {{targetBranch}}] backport: {{sourcePullRequest.title}} ({{sourcePullRequest.number}})" } ================================================ FILE: .config/.csharpierrc.json ================================================ { "printWidth": 110, "preprocessorSymbolSets": ["", "DEBUG", "DEBUG,CODE_STYLE"] } ================================================ FILE: .config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "husky": { "version": "0.7.2", "commands": [ "husky" ], "rollForward": false }, "xamlstyler.console": { "version": "3.2404.2", "commands": [ "xstyler" ], "rollForward": false }, "csharpier": { "version": "1.0.1", "commands": [ "csharpier" ], "rollForward": false }, "refitter": { "version": "1.4.1", "commands": [ "refitter" ], "rollForward": false }, "dotnet-script": { "version": "1.6.0", "commands": [ "dotnet-script" ], "rollForward": false } } } ================================================ FILE: .csharpierrc.yaml ================================================ printWidth: 110 ================================================ FILE: .editorconfig ================================================ root = true [*.cs] max_line_length = 120 csharp_style_var_for_built_in_types = true:suggestion dotnet_sort_system_directives_first = true # ReSharper properties resharper_csharp_max_line_length = 120 resharper_place_field_attribute_on_same_line = false # dotnet code quality # noinspection EditorConfigKeyCorrectness dotnet_code_quality.ca1826.exclude_ordefault_methods = true # Microsoft .NET properties csharp_new_line_before_members_in_object_initializers = false csharp_preferred_modifier_order = public, private, protected, internal, file, static, new, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion csharp_style_prefer_utf8_string_literals = true:suggestion csharp_style_var_elsewhere = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion dotnet_naming_rule.private_constants_rule.import_to_resharper = True dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private) dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a dotnet_naming_rule.private_constants_rule.severity = warning dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = True dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private) dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c dotnet_naming_rule.private_instance_fields_rule.resharper_style = aaBb, _ + aaBb dotnet_naming_rule.private_instance_fields_rule.severity = warning dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style_1 dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private) dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df dotnet_naming_rule.private_static_fields_rule.severity = warning dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = True dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private) dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3 dotnet_naming_rule.private_static_readonly_rule.severity = warning dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols dotnet_naming_style.lower_camel_case_style.capitalization = camel_case dotnet_naming_style.lower_camel_case_style.required_prefix = _ dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field dotnet_naming_symbols.private_constants_symbols.required_modifiers = const dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds = constant_field dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field,readonly_field dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds = field dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = readonly,static dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_qualification_for_event = false:suggestion dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/FUNDING.yml ================================================ patreon: StabilityMatrix ko_fi: StabilityMatrix ================================================ FILE: .github/ISSUE_TEMPLATE/1-bug.yml ================================================ name: Bug report description: Submit a bug report labels: ["bug", "triage"] body: - type: markdown attributes: value: | **New to Stability Matrix?** For help or advice on using Stability Matrix, try one of the following options instead of opening a GitHub issue: - Asking on our [Discord server](https://link.lykos.ai/discord?ref=github-issue-template) - Creating a post on [Discussions](https://github.com/LykosAI/StabilityMatrix/discussions) This template is for reporting bugs experienced within the Stability Matrix app. If your issue is regarding Package behavior when running it, or when installing or updating a specific Package, please use the [Package issue template](https://github.com/LykosAI/StabilityMatrix/issues/new/choose) instead. Make sure to also search the [existing issues](https://github.com/LykosAI/StabilityMatrix/issues) to see if your issue has already been reported. - type: textarea id: what-happened attributes: label: What happened? description: Give a clear and concise description of what happened. Provide screenshots or videos of UI if necessary. Also tell us, what did you expect to happen? placeholder: | When dragging a model file into the ... page to import, I expected to see... Instead, I saw... validations: required: true - type: textarea id: how-to-reproduce attributes: label: Steps to reproduce description: Include a minimal step-by-step guide to reproduce the issue if possible. placeholder: | 1. Open Stability Matrix 2. Go to the ... page 3. Click on the ... button 4. Expected to see ... open, but instead ... - type: textarea id: app-logs attributes: label: Relevant logs description: Please copy and paste any relevant log output. (This will be automatically formatted, so no need for backticks.) render: shell - type: input id: version attributes: label: Version description: What version of Stability Matrix are you running? (Can be found at the bottom of the settings page) placeholder: ex. v2.11.0 validations: required: true - type: dropdown id: os-platform attributes: label: What Operating System are you using? options: - Windows - macOS - Linux - Other validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/2-bug-crash.yml ================================================ name: Crash report description: A crash of Stability Matrix, likely with the "An unexpected error occurred" dialog labels: ["bug", "crash", "triage"] body: - type: markdown attributes: value: | This template is for reporting crashes of Stability Matrix, likely with the "An unexpected error occurred" dialog. If you are experiencing a different issue, please use the [Bug Report or Package Issue templates](https://github.com/LykosAI/StabilityMatrix/issues/new/choose). - type: textarea id: what-happened attributes: label: What happened? description: Give a clear and concise description of what happened. Include some minimal steps to reproducible the issue if possible. placeholder: | 1. Open Stability Matrix 2. Go to the "..." page 3. Click on the "..." button 4. See the crash validations: required: true - type: textarea id: exception-details attributes: label: Exception Details description: Please click the "Copy Details" button on the crash dialog and paste the details exactly as formatted here. placeholder: | ## Exception OperationCanceledException: Example Message ### Sentry ID ``` bc7da9b2fcc3e3568ceb81a72f3a128d ``` ### Stack Trace ``` at StabilityMatrix.Avalonia.ViewModels.Settings.MainSettingsViewModel.DebugThrowException() in MainSettingsViewModel.cs:line 716 at CommunityToolkit.Mvvm.Input.RelayCommand.Execute(Object parameter) ... ``` - type: input id: version attributes: label: Version description: What version of Stability Matrix are you running? (Can be found at the bottom of the settings page) placeholder: ex. v2.11.0 validations: required: true - type: dropdown id: os-platform attributes: label: What Operating System are you using? options: - Windows - macOS - Linux - Other validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/3-bug-package.yml ================================================ name: Package issue description: Report an issue with installing, updating, or running a Package labels: ["bug", "area: package", "triage"] body: - type: markdown attributes: value: | **Experiencing an issue while running a Package?** Make sure to also search the GitHub issues of the Package, to see if your issue has already been reported and being worked on by upstream authors. - type: textarea id: package-details attributes: label: Package description: Provide the name of the Package you are experiencing issues with placeholder: ex. `ComfyUI` validations: required: true - type: dropdown id: package-issue-phase attributes: label: When did the issue occur? options: - Installing the Package - Updating the Package - Running the Package - Other validations: required: true - type: input id: hardware attributes: label: What GPU / hardware type are you using? description: Installed dependencies and Package features often depend on the GPU or hardware type you are using. placeholder: ex. Nvidia 2080 Super with CUDA, AMD Radeon VII, etc. validations: required: true - type: textarea id: what-happened attributes: label: What happened? description: Give a clear and concise description of what happened. Provide screenshots if necessary. Also tell us, what did you expect to happen? validations: required: true - type: textarea id: console-output attributes: label: Console output description: Please copy and paste any console output or error messages. For failed install or updates, locate the progress on the bottom left, open the dialog, and click on "More Details" to copy the full console output. placeholder: | ``` Unpacking... Successfully built lycoris_lora Installing collected packages: library, tomlkit, onnx, ml-dtypes, onnxruntime-gpu Running setup.py develop for library Attempting uninstall: onnx Found existing installation: onnx 1.14.1 ... ``` - type: input id: version attributes: label: Version description: What version of Stability Matrix are you running? (Can be found at the bottom of the settings page) placeholder: ex. v2.11.0 validations: required: true - type: dropdown id: os-platform attributes: label: What Operating System are you using? options: - Windows - macOS - Linux - Other validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/4-feature-request.yml ================================================ name: Feature or enhancement description: Submit a proposal for a new Stability Matrix feature or enhancement labels: ["enhancement"] body: - type: markdown attributes: value: | Consider first discussing your idea on our [Discord server](https://link.lykos.ai/discord?ref=github-issue-template) to get feedback from the developers and the community. - type: textarea id: proposal attributes: label: Proposal description: Explain your idea for a new feature or enhancement. Include any relevant details or links to resources like Package documentation. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Getting help url: https://link.lykos.ai/discord?ref=github-issue-template about: Ask questions about using Stability Matrix and get tips on using Packages on our Discord server ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "nuget" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/backport.yml ================================================ name: Automatic Backport on: pull_request: types: ["closed", "labeled"] jobs: backport: if: ${{ (github.event.pull_request.merged == true) && (contains(github.event.pull_request.labels.*.name, 'backport-to-main') == true) }} name: Backport PR runs-on: ubuntu-latest steps: # Get the merge target branch to decide mainline number # git cherry-pick mainline is 1 for merge to 'dev', else 2 - name: Get target branch run: echo "CP_MAINLINE=$(if [ '${{ github.event.pull_request.base.ref }}' == 'dev' ]; then echo 1; else echo 2; fi)" >> $GITHUB_ENV - name: Write json id: create-json uses: jsdaniell/create-json@v1.2.3 with: name: ".backportrc.json" json: | { "targetPRLabels": ["backport"], "mainline": ${{ env.CP_MAINLINE }}, "commitConflicts": "true", "prTitle": "[{{sourceBranch}} to {{targetBranch}}] backport: {{sourcePullRequest.title}} ({{sourcePullRequest.number}})" } - name: Backport Action uses: sorenlouv/backport-github-action@v9.5.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} auto_backport_label_prefix: backport-to- - name: Info log if: ${{ success() }} run: cat ~/.backport/backport.info.log - name: Debug log if: ${{ failure() }} run: cat ~/.backport/backport.debug.log ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: [ main ] pull_request: branches: [ main ] concurrency: group: build-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: if: github.repository == 'LykosAI/StabilityMatrix' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up .NET uses: actions/setup-dotnet@v3 with: dotnet-version: '9.0.x' - name: Install dependencies run: dotnet restore - name: Test run: dotnet test StabilityMatrix.Tests - name: Build run: > dotnet publish ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -o out -c Release -r linux-x64 --self-contained ================================================ FILE: .github/workflows/cla.yml ================================================ name: "CLA Assistant" on: issue_comment: types: [created] pull_request_target: types: [opened,closed,synchronize] # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings permissions: actions: write contents: write pull-requests: write statuses: write jobs: CLAAssistant: if: github.repository == 'LykosAI/StabilityMatrix' runs-on: ubuntu-latest steps: - name: "CLA Assistant" if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' uses: contributor-assistant/github-action@v2.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret # This token is required only if you have configured to store the signatures in a remote repository/organization PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_CONFIG_ACCESS_TOKEN }} with: path-to-signatures: 'signatures/version1/cla.json' path-to-document: 'https://lykos.ai/cla' # branch should not be protected branch: 'main' allowlist: ionite34,mohnjiles,bot* # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken remote-organization-name: LykosAI remote-repository-name: clabot-config custom-allsigned-prcomment: '**CLA Assistant bot** All Contributors have signed the CLA.' ================================================ FILE: .github/workflows/release.yml ================================================ name: Release permissions: contents: write on: workflow_dispatch: inputs: version: type: string required: true description: Version (Semver without leading v) sentry-release: type: boolean description: Make Sentry Release? default: true github-release: type: boolean description: Make GitHub Release? default: true github-release-draft: type: boolean description: Mark GitHub Release as Draft? default: false github-release-prerelease: type: boolean description: Mark GitHub Release as Prerelease? default: false auto-update-release: type: boolean description: Release auto-update? default: false auto-update-release-mode: type: choice description: Release auto-update mode options: - github url - upload to b2 auto-update-release-channel: type: choice description: Release auto-update channel options: - stable - preview - development test-release-artifacts: type: boolean description: "[Debug] Test release artifacts?" default: false jobs: release-linux: name: Release (linux-x64) env: platform-id: linux-x64 out-name: StabilityMatrix.AppImage runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: olegtarasov/get-tag@v2.1.2 if: github.event_name == 'release' id: tag_name with: tagRegex: "v(.*)" - name: Set Version from Tag if: github.event_name == 'release' run: | echo "Using tag ${{ env.GIT_TAG_NAME }}" echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $GITHUB_ENV - name: Set Version from manual input if: github.event_name == 'workflow_dispatch' run: | echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - name: Set up .NET 9 uses: actions/setup-dotnet@v3 with: # Net 8 needed for PupNet dotnet-version: | 8.0.x 9.0.x - name: Install PupNet run: | sudo apt-get -y install libfuse2 dotnet tool install -g KuiperZone.PupNet --version 1.8.0 - name: PupNet Build env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: pupnet -r linux-x64 -c Release --kind appimage --app-version $RELEASE_VERSION --clean -y - name: Post Build run: mv ./Release/linux-x64/StabilityMatrix.x86_64.AppImage ${{ env.out-name }} - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: StabilityMatrix-${{ env.platform-id }} path: ${{ env.out-name }} retention-days: 1 if-no-files-found: error - name: Create Sentry release if: ${{ github.event.inputs.sentry-release == 'true' }} uses: getsentry/action-release@v1 env: MAKE_SENTRY_RELEASE: ${{ secrets.SENTRY_PROJECT != '' }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} with: environment: production ignore_missing: true ignore_empty: true version: StabilityMatrix.Avalonia@${{ github.event.inputs.version }} release-windows: name: Release (win-x64) env: platform-id: win-x64 out-name: StabilityMatrix.exe runs-on: windows-latest steps: - uses: actions/checkout@v3 - uses: olegtarasov/get-tag@v2.1.2 if: github.event_name == 'release' id: tag_name with: tagRegex: "v(.*)" - name: Set Version from Tag if: github.event_name == 'release' run: | echo "Using tag ${{ env.GIT_TAG_NAME }}" echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $env:GITHUB_ENV - name: Set Version from manual input if: github.event_name == 'workflow_dispatch' run: | echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $env:GITHUB_ENV - name: Set up .NET 9 uses: actions/setup-dotnet@v3 with: dotnet-version: '9.0.x' - name: Install dependencies run: dotnet restore - name: .NET Publish env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: > dotnet publish ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -o out -c Release -r ${{ env.platform-id }} -p:Version=$env:RELEASE_VERSION -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=true -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} -p:SentryUploadSymbols=true -p:SentryUploadSources=true - name: Post Build run: mv ./out/StabilityMatrix.Avalonia.exe ./out/${{ env.out-name }} - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: StabilityMatrix-${{ env.platform-id }} path: ./out/${{ env.out-name }} retention-days: 1 if-no-files-found: error release-macos: name: Release (macos-arm64) env: platform-id: osx-arm64 app-name: "Stability Matrix.app" out-name: "StabilityMatrix-macos-arm64.dmg" runs-on: macos-14 steps: - uses: actions/checkout@v3 - uses: olegtarasov/get-tag@v2.1.2 if: github.event_name == 'release' id: tag_name with: tagRegex: "v(.*)" - name: Set Version from Tag if: github.event_name == 'release' run: | echo "Using tag ${{ env.GIT_TAG_NAME }}" echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $GITHUB_ENV - name: Set Version from manual input if: github.event_name == 'workflow_dispatch' run: | echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - name: Set up .NET 9 uses: actions/setup-dotnet@v3 with: dotnet-version: '9.0.x' - name: Install dependencies run: dotnet restore -p:PublishReadyToRun=true - name: Check Version run: echo $RELEASE_VERSION - name: .NET Msbuild (App) env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: > dotnet msbuild ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -t:BundleApp -p:UseAppHost=true -p:SelfContained=true -p:Configuration=Release -p:RuntimeIdentifier=${{ env.platform-id }} -p:Version=$RELEASE_VERSION -p:PublishDir=out -p:PublishReadyToRun=true -p:CFBundleShortVersionString=$RELEASE_VERSION -p:CFBundleName="Stability Matrix" -p:CFBundleDisplayName="Stability Matrix" -p:CFBundleVersion=$RELEASE_VERSION -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} -p:SentryUploadSymbols=true -p:SentryUploadSources=true - name: Post Build (App) run: mkdir -p signing && mv "./StabilityMatrix.Avalonia/out/Stability Matrix.app" "./signing/${{ env.app-name }}" - name: Codesign app bundle env: MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} run: ./Build/codesign_macos.sh "./signing/${{ env.app-name }}" - name: Notarize app bundle env: MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} run: ./Build/notarize_macos.sh "./signing/${{ env.app-name }}" - name: Zip Artifact (App) working-directory: signing run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-app.zip" "${{ env.app-name }}" - name: Upload Artifact (App) uses: actions/upload-artifact@v4 with: name: StabilityMatrix-${{ env.platform-id }}-app path: StabilityMatrix-${{ env.platform-id }}-app.zip retention-days: 1 if-no-files-found: error - uses: actions/setup-node@v4 with: node-version: '20.11.x' - name: Install dependencies for dmg creation run: brew install graphicsmagick imagemagick && npm install --global create-dmg - name: Create dmg working-directory: signing run: > create-dmg "${{ env.app-name }}" --overwrite --identity "${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}" - name: Rename dmg working-directory: signing run: mv "$(find . -type f -name "*.dmg")" "${{ env.out-name }}" - name: Zip Artifact (dmg) working-directory: signing run: zip -r -y "../StabilityMatrix-${{ env.platform-id }}-dmg.zip" "${{ env.out-name }}" - name: Upload Artifact (dmg) uses: actions/upload-artifact@v4 with: name: StabilityMatrix-${{ env.platform-id }}-dmg path: StabilityMatrix-${{ env.platform-id }}-dmg.zip retention-days: 1 if-no-files-found: error publish-release: name: Publish GitHub Release needs: [ release-linux, release-windows, release-macos ] if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.github-release == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Extract Release Notes id: release_notes run: | RELEASE_NOTES="$(awk -v version="${{ github.event.inputs.version }}" '/## v/{if(p) exit; if($0 ~ version) p=1}; p' CHANGELOG.md)" RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}" RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" echo "::set-output name=release_notes::$RELEASE_NOTES" # Downloads all previous artifacts to the current working directory - name: Download Artifacts uses: actions/download-artifact@v4 # Zip each build (except macos which is already dmg) - name: Zip Artifacts run: | cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip" - name: Create Github Release id: create_release uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | StabilityMatrix-win-x64.zip StabilityMatrix-linux-x64.zip StabilityMatrix-macos-arm64.dmg fail_on_unmatched_files: true tag_name: v${{ github.event.inputs.version }} body: ${{ steps.release_notes.outputs.release_notes }} draft: ${{ github.event.inputs.github-release-draft == 'true' }} prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }} test-artifacts: name: Test Release Artifacts needs: [ release-linux, release-windows, release-macos ] if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test-release-artifacts == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Extract Release Notes id: release_notes run: | RELEASE_NOTES="$(awk -v version="${{ github.event.inputs.version }}" '/## v/{if(p) exit; if($0 ~ version) p=1}; p' CHANGELOG.md)" RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}" RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" echo "::set-output name=release_notes::$RELEASE_NOTES" echo "Release Notes:" echo "$RELEASE_NOTES" # Downloads all previous artifacts to the current working directory - name: Download Artifacts uses: actions/download-artifact@v4 # Zip each build (except macos which is already dmg) - name: Zip Artifacts run: | cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip" # Check that the zips and CHANGELOG.md are in the current working directory - name: Check files run: | if [ ! -f StabilityMatrix-win-x64.zip ]; then echo "StabilityMatrix-win-x64.zip not found" exit 1 else echo "StabilityMatrix-win-x64.zip found" sha256sum StabilityMatrix-win-x64.zip fi if [ ! -f StabilityMatrix-linux-x64.zip ]; then echo "StabilityMatrix-linux-x64.zip not found" exit 1 else echo "StabilityMatrix-linux-x64.zip found" sha256sum StabilityMatrix-linux-x64.zip fi if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then echo "StabilityMatrix-macos-arm64.dmg not found" exit 1 else echo "StabilityMatrix-macos-arm64.dmg found" sha256sum StabilityMatrix-macos-arm64.dmg fi if [ ! -f CHANGELOG.md ]; then echo "CHANGELOG.md not found" exit 1 fi publish-auto-update-github: name: Publish Auto-Update Release (GitHub) needs: [ release-linux, release-windows, release-macos, publish-release ] if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'github url' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set Version from manual input run: | echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $env:GITHUB_ENV - uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Python Dependencies run: pip install stability-matrix-tools>=0.3.0 --upgrade - name: Publish Auto-Update Release env: SM_B2_API_ID: ${{ secrets.SM_B2_API_ID }} SM_B2_API_KEY: ${{ secrets.SM_B2_API_KEY }} SM_CF_CACHE_PURGE_TOKEN: ${{ secrets.SM_CF_CACHE_PURGE_TOKEN }} SM_CF_ZONE_ID: ${{ secrets.SM_CF_ZONE_ID }} SM_SIGNING_PRIVATE_KEY: ${{ secrets.SM_SIGNING_PRIVATE_KEY }} run: sm-tools updates publish-matrix-v3 -v ${{ github.event.inputs.version }} -y publish-auto-update-b2: name: Publish Auto-Update Release (B2) needs: [ release-linux, release-windows, release-macos ] if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.auto-update-release == 'true' && github.event.inputs.auto-update-release-mode == 'upload to b2' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set Version from manual input run: | echo "Using version ${{ github.event.inputs.version }}" echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $env:GITHUB_ENV # Downloads all previous artifacts to the current working directory - name: Download Artifacts uses: actions/download-artifact@v4 # Zip each build (except macos which is already dmg) - name: Zip Artifacts run: | cd StabilityMatrix-win-x64 && zip -r ../StabilityMatrix-win-x64.zip ./. && cd $OLDPWD cd StabilityMatrix-linux-x64 && zip -r ../StabilityMatrix-linux-x64.zip ./. && cd $OLDPWD unzip "StabilityMatrix-osx-arm64-dmg/StabilityMatrix-osx-arm64-dmg.zip" - uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Python Dependencies run: pip install stability-matrix-tools>=0.3.0 --upgrade # Check that the zips and CHANGELOG.md are in the current working directory - name: Check files run: | if [ ! -f StabilityMatrix-win-x64.zip ]; then echo "StabilityMatrix-win-x64.zip not found" exit 1 fi if [ ! -f StabilityMatrix-linux-x64.zip ]; then echo "StabilityMatrix-linux-x64.zip not found" exit 1 fi if [ ! -f StabilityMatrix-macos-arm64.dmg ]; then echo "StabilityMatrix-macos-arm64.dmg not found" exit 1 fi if [ ! -f CHANGELOG.md ]; then echo "CHANGELOG.md not found" exit 1 fi - name: Publish Auto-Update Release env: SM_B2_API_ID: ${{ secrets.SM_B2_API_ID }} SM_B2_API_KEY: ${{ secrets.SM_B2_API_KEY }} SM_CF_CACHE_PURGE_TOKEN: ${{ secrets.SM_CF_CACHE_PURGE_TOKEN }} SM_CF_ZONE_ID: ${{ secrets.SM_CF_ZONE_ID }} SM_SIGNING_PRIVATE_KEY: ${{ secrets.SM_SIGNING_PRIVATE_KEY }} run: > sm-tools updates publish-files-v3 -v ${{ github.event.inputs.version }} --channel ${{ github.event.inputs.auto-update-release-channel }} --changelog CHANGELOG.md --win-x64 StabilityMatrix-win-x64.zip --linux-x64 StabilityMatrix-linux-x64.zip --macos-arm64 StabilityMatrix-macos-arm64.dmg -y ================================================ FILE: .github/workflows/stale.yml ================================================ name: 'Close stale issues' permissions: issues: write pull-requests: write on: workflow_dispatch: schedule: - cron: '30 1 * * *' jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: stale-issue-message: 'This issue is pending because it has been awaiting a response for 30 days with no activity. Remove the pending label or comment, else this will be closed in 7 days.' close-issue-message: 'This issue was closed because it has been pending for 7 days with no activity.' only-labels: 'awaiting-feedback' stale-issue-label: 'pending' exempt-issue-labels: 'planned,milestone,work-in-progress,enhancement,crash' days-before-issue-stale: 30 days-before-issue-close: 7 days-before-pr-close: -1 days-before-pr-stale: -1 operations-per-run: 45 - uses: actions/stale@v9 with: stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove the stale label or comment, else this will be closed in 7 days.' close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity.' stale-issue-label: 'stale' exempt-issue-labels: 'planned,milestone,work-in-progress,enhancement,crash' days-before-issue-stale: 60 days-before-issue-close: 7 days-before-pr-close: -1 days-before-pr-stale: -1 operations-per-run: 45 ================================================ FILE: .github/workflows/test-ui.yml ================================================ name: UI Tests on: workflow_dispatch: concurrency: group: build-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: if: github.repository == 'LykosAI/StabilityMatrix' || github.event_name == 'workflow_dispatch' runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Set up .NET uses: actions/setup-dotnet@v3 with: dotnet-version: '9.0.x' - name: Install dependencies run: dotnet restore - name: Test run: dotnet test StabilityMatrix.UITests ================================================ FILE: .github/workflows/version-bump.yml ================================================ name: Version Bump on: workflow_dispatch: inputs: version_mask: type: string description: Version Bump Mask default: "0.0.1.0" required: false version_overwrite: type: string description: Version Overwrite Mask default: "*.*.*.*" required: false jobs: version-bump: name: Version Bump runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Setup .NET Core uses: actions/setup-dotnet@v2 with: dotnet-version: '8.0.x' - name: Bump versions uses: SiqiLu/dotnet-bump-version@2.0.0 with: version_files: "**/*.csproj" version_mask: ${{ github.event.inputs.version_mask }} version_overwrite: ${{ github.event.inputs.version_overwrite }} github_token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Rider .idea/ # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ [Oo]ut/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.tlog *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* !**/Models/Packages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio 6 auto-generated project file (contains which files were open etc.) *.vbp # Visual Studio 6 workspace and project file (working project files containing files to include in project) *.dsw *.dsp # Visual Studio 6 technical files *.ncb *.aps # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # Visual Studio History (VSHistory) files .vshistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd # VS Code files for those working on multiple tools .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace # Local History for Visual Studio Code .history/ # Windows Installer files from build outputs *.cab *.msi *.msix *.msm *.msp # JetBrains Rider *.sln.iml .husky/pre-commit ================================================ FILE: .husky/task-runner.json ================================================ { "tasks": [ { "name": "Run csharpier", "group": "pre-commit", "command": "dotnet", "args": [ "csharpier", "format", "${staged}" ], "include": [ "**/*.cs" ] }, { "name": "Run xamlstyler", "group": "pre-commit", "command": "dotnet", "args": [ "xstyler", "-f", "${staged}" ], "include": [ "**/*.axaml" ] }, { "name": "Run refitter for LykosAuthApi", "group": "generate-openapi", "command": "dotnet", "args": ["refitter", "--settings-file", "./StabilityMatrix.Core/Api/LykosAuthApi/.refitter"] }, { "name": "Run refitter for PromptGenApi", "group": "generate-promptgen-openapi", "command": "dotnet", "args": ["refitter", "--settings-file", "./StabilityMatrix.Core/Api/PromptGen/.refitter"] } ] } ================================================ FILE: Avalonia.Gif/Avalonia.Gif.csproj ================================================ latest true true true $(NoWarn);CS8765;CS8618;CS8625;CS0169 ================================================ FILE: Avalonia.Gif/BgWorkerCommand.cs ================================================ namespace Avalonia.Gif { internal enum BgWorkerCommand { Null, Play, Pause, Dispose } } ================================================ FILE: Avalonia.Gif/BgWorkerState.cs ================================================ namespace Avalonia.Gif { internal enum BgWorkerState { Null, Start, Running, Paused, Complete, Dispose } } ================================================ FILE: Avalonia.Gif/Decoding/BlockTypes.cs ================================================ namespace Avalonia.Gif.Decoding { internal enum BlockTypes { Empty = 0, Extension = 0x21, ImageDescriptor = 0x2C, Trailer = 0x3B, } } ================================================ FILE: Avalonia.Gif/Decoding/ExtensionType.cs ================================================ namespace Avalonia.Gif.Decoding { internal enum ExtensionType { GraphicsControl = 0xF9, Application = 0xFF } } ================================================ FILE: Avalonia.Gif/Decoding/FrameDisposal.cs ================================================ namespace Avalonia.Gif.Decoding { public enum FrameDisposal { Unknown = 0, Leave = 1, Background = 2, Restore = 3 } } ================================================ FILE: Avalonia.Gif/Decoding/GifColor.cs ================================================ using System.Runtime.InteropServices; namespace Avalonia.Gif { [StructLayout(LayoutKind.Explicit)] public readonly struct GifColor { [FieldOffset(3)] public readonly byte A; [FieldOffset(2)] public readonly byte R; [FieldOffset(1)] public readonly byte G; [FieldOffset(0)] public readonly byte B; /// /// A struct that represents a ARGB color and is aligned as /// a BGRA bytefield in memory. /// /// Red /// Green /// Blue /// Alpha public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue) { A = a; R = r; G = g; B = b; } } } ================================================ FILE: Avalonia.Gif/Decoding/GifDecoder.cs ================================================ // This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer // as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer) // Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) // Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved. // The rest of the source file is licensed under MIT License. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; using Avalonia; using Avalonia.Media.Imaging; using static Avalonia.Gif.Extensions.StreamExtensions; namespace Avalonia.Gif.Decoding { public sealed class GifDecoder : IDisposable { private static readonly ReadOnlyMemory G87AMagic = "GIF87a"u8.ToArray().AsMemory(); private static readonly ReadOnlyMemory G89AMagic = "GIF89a"u8.ToArray().AsMemory(); private static readonly ReadOnlyMemory NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory(); private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10); private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100); private static readonly GifColor TransparentColor = new(0, 0, 0, 0); private static readonly int MaxTempBuf = 768; private static readonly int MaxStackSize = 4096; private static readonly int MaxBits = 4097; private readonly Stream _fileStream; private readonly CancellationToken _currentCtsToken; private readonly bool _hasFrameBackups; private int _gctSize, _bgIndex, _prevFrame = -1, _backupFrame = -1; private bool _gctUsed; private GifRect _gifDimensions; // private ulong _globalColorTable; private readonly int _backBufferBytes; private GifColor[] _bitmapBackBuffer; private short[] _prefixBuf; private byte[] _suffixBuf; private byte[] _pixelStack; private byte[] _indexBuf; private byte[] _backupFrameIndexBuf; private volatile bool _hasNewFrame; public GifHeader Header { get; private set; } public readonly List Frames = new(); public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height); public GifDecoder(Stream fileStream, CancellationToken currentCtsToken) { _fileStream = fileStream; _currentCtsToken = currentCtsToken; ProcessHeaderData(); ProcessFrameData(); Header.IterationCount = Header.Iterations switch { -1 => new GifRepeatBehavior { Count = 1 }, 0 => new GifRepeatBehavior { LoopForever = true }, > 0 => new GifRepeatBehavior { Count = Header.Iterations }, _ => Header.IterationCount }; var pixelCount = _gifDimensions.TotalPixels; _hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore); _bitmapBackBuffer = new GifColor[pixelCount]; _indexBuf = new byte[pixelCount]; if (_hasFrameBackups) _backupFrameIndexBuf = new byte[pixelCount]; _prefixBuf = new short[MaxStackSize]; _suffixBuf = new byte[MaxStackSize]; _pixelStack = new byte[MaxStackSize + 1]; _backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor)); } public void Dispose() { Frames.Clear(); _bitmapBackBuffer = null; _prefixBuf = null; _suffixBuf = null; _pixelStack = null; _indexBuf = null; _backupFrameIndexBuf = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int PixCoord(int x, int y) => x + y * _gifDimensions.Width; static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) }; private void ClearImage() { Array.Fill(_bitmapBackBuffer, TransparentColor); //ClearArea(_gifDimensions); _prevFrame = -1; _backupFrame = -1; } public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false) { if (_currentCtsToken.IsCancellationRequested) return; if (fIndex < 0 | fIndex >= Frames.Count) return; if (_prevFrame == fIndex) return; if (fIndex == 0 || forceClear || fIndex < _prevFrame) ClearImage(); DisposePreviousFrame(); _prevFrame++; // render intermediate frame for (int idx = _prevFrame; idx < fIndex; ++idx) { var prevFrame = Frames[idx]; if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore) continue; if (prevFrame.FrameDisposalMethod == FrameDisposal.Background) { ClearArea(prevFrame.Dimensions); continue; } RenderFrameAt(idx, writeableBitmap); } RenderFrameAt(fIndex, writeableBitmap); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap) { var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); var curFrame = Frames[idx]; DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB); if (_hasFrameBackups & curFrame.ShouldBackup) { Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels); _backupFrame = idx; } DrawFrame(curFrame, _indexBuf); _prevFrame = idx; _hasNewFrame = true; using var lockedBitmap = writeableBitmap.Lock(); WriteBackBufToFb(lockedBitmap.Address); ArrayPool.Shared.Return(tmpB); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawFrame(GifFrame curFrame, Memory frameIndexSpan) { var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable; var cX = curFrame.Dimensions.X; var cY = curFrame.Dimensions.Y; var cH = curFrame.Dimensions.Height; var cW = curFrame.Dimensions.Width; var tC = curFrame.TransparentColorIndex; var hT = curFrame.HasTransparency; if (curFrame.IsInterlaced) { for (var i = 0; i < 4; i++) { var curPass = Pass[i]; var y = curPass.Start; while (y < cH) { DrawRow(y); y += curPass.Step; } } } else { for (var i = 0; i < cH; i++) DrawRow(i); } //for (var row = 0; row < cH; row++) void DrawRow(int row) { // Get the starting point of the current row on frame's index stream. var indexOffset = row * cW; // Get the target backbuffer offset from the frames coords. var targetOffset = PixCoord(cX, row + cY); var len = _bitmapBackBuffer.Length; for (var i = 0; i < cW; i++) { var indexColor = frameIndexSpan.Span[indexOffset + i]; if ( activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length ) return; if (!(hT & indexColor == tC)) _bitmapBackBuffer[targetOffset] = activeColorTable[indexColor]; targetOffset++; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DisposePreviousFrame() { if (_prevFrame == -1) return; var prevFrame = Frames[_prevFrame]; switch (prevFrame.FrameDisposalMethod) { case FrameDisposal.Background: ClearArea(prevFrame.Dimensions); break; case FrameDisposal.Restore: if (_hasFrameBackups && _backupFrame != -1) DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf); else ClearArea(prevFrame.Dimensions); break; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearArea(GifRect area) { for (var y = 0; y < area.Height; y++) { var targetOffset = PixCoord(area.X, y + area.Y); for (var x = 0; x < area.Width; x++) _bitmapBackBuffer[targetOffset + x] = TransparentColor; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span indexSpan, byte[] tempBuf) { _fileStream.Position = curFrame.LzwStreamPosition; var totalPixels = curFrame.Dimensions.TotalPixels; // Initialize GIF data stream decoder. var dataSize = curFrame.LzwMinCodeSize; var clear = 1 << dataSize; var endOfInformation = clear + 1; var available = clear + 2; var oldCode = -1; var codeSize = dataSize + 1; var codeMask = (1 << codeSize) - 1; for (var code = 0; code < clear; code++) { _prefixBuf[code] = 0; _suffixBuf[code] = (byte)code; } // Decode GIF pixel stream. int bits, first, top, pixelIndex; var datum = bits = first = top = pixelIndex = 0; while (pixelIndex < totalPixels) { var blockSize = _fileStream.ReadBlock(tempBuf); if (blockSize == 0) break; var blockPos = 0; while (blockPos < blockSize) { datum += tempBuf[blockPos] << bits; blockPos++; bits += 8; while (bits >= codeSize) { // Get the next code. var code = datum & codeMask; datum >>= codeSize; bits -= codeSize; // Interpret the code if (code == clear) { // Reset decoder. codeSize = dataSize + 1; codeMask = (1 << codeSize) - 1; available = clear + 2; oldCode = -1; continue; } // Check for explicit end-of-stream if (code == endOfInformation) return; if (oldCode == -1) { indexSpan[pixelIndex++] = _suffixBuf[code]; oldCode = code; first = code; continue; } var inCode = code; if (code >= available) { _pixelStack[top++] = (byte)first; code = oldCode; if (top == MaxBits) ThrowException(); } while (code >= clear) { if (code >= MaxBits || code == _prefixBuf[code]) ThrowException(); _pixelStack[top++] = _suffixBuf[code]; code = _prefixBuf[code]; if (top == MaxBits) ThrowException(); } first = _suffixBuf[code]; _pixelStack[top++] = (byte)first; // Add new code to the dictionary if (available < MaxStackSize) { _prefixBuf[available] = (short)oldCode; _suffixBuf[available] = (byte)first; available++; if ((available & codeMask) == 0 && available < MaxStackSize) { codeSize++; codeMask += available; } } oldCode = inCode; // Drain the pixel stack. do { indexSpan[pixelIndex++] = _pixelStack[--top]; } while (top > 0); } } } while (pixelIndex < totalPixels) indexSpan[pixelIndex++] = 0; // clear missing pixels void ThrowException() => throw new LzwDecompressionException(); } /// /// Directly copies the struct array to a bitmap IntPtr. /// private void WriteBackBufToFb(IntPtr targetPointer) { if (_currentCtsToken.IsCancellationRequested) return; if (!(_hasNewFrame & _bitmapBackBuffer != null)) return; unsafe { fixed (void* src = &_bitmapBackBuffer[0]) Buffer.MemoryCopy( src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes ); _hasNewFrame = false; } } /// /// Processes GIF Header. /// [MemberNotNull(nameof(Header))] private void ProcessHeaderData() { var str = _fileStream; var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); var tempBuf = tmpB.AsSpan(); var _ = str.Read(tmpB, 0, 6); if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span)) throw new InvalidGifStreamException("Not a GIF stream."); if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span))) throw new InvalidGifStreamException( "Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray()) ); ProcessScreenDescriptor(tmpB); Header = new GifHeader { Dimensions = _gifDimensions, HasGlobalColorTable = _gctUsed, // GlobalColorTableCacheID = _globalColorTable, GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize), GlobalColorTableSize = _gctSize, BackgroundColorIndex = _bgIndex, HeaderSize = _fileStream.Position }; ArrayPool.Shared.Return(tmpB); } /// /// Parses colors from file stream to target color table. /// private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors) { var nBytes = 3 * nColors; var target = new GifColor[nColors]; var n = stream.Read(rawBufSpan, 0, nBytes); if (n < nBytes) throw new InvalidOperationException("Wrong color table bytes."); int i = 0, j = 0; while (i < nColors) { var r = rawBufSpan[j++]; var g = rawBufSpan[j++]; var b = rawBufSpan[j++]; target[i++] = new GifColor(r, g, b); } return target; } /// /// Parses screen and other GIF descriptors. /// private void ProcessScreenDescriptor(byte[] tempBuf) { var width = _fileStream.ReadUShortS(tempBuf); var height = _fileStream.ReadUShortS(tempBuf); var packed = _fileStream.ReadByteS(tempBuf); _gctUsed = (packed & 0x80) != 0; _gctSize = 2 << (packed & 7); _bgIndex = _fileStream.ReadByteS(tempBuf); _gifDimensions = new GifRect(0, 0, width, height); _fileStream.Skip(1); } /// /// Parses all frame data. /// private void ProcessFrameData() { _fileStream.Position = Header.HeaderSize; var tempBuf = ArrayPool.Shared.Rent(MaxTempBuf); var terminate = false; var curFrame = 0; Frames.Add(new GifFrame()); do { var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf); switch (blockType) { case BlockTypes.Empty: break; case BlockTypes.Extension: ProcessExtensions(ref curFrame, tempBuf); break; case BlockTypes.ImageDescriptor: ProcessImageDescriptor(ref curFrame, tempBuf); _fileStream.SkipBlocks(tempBuf); break; case BlockTypes.Trailer: Frames.RemoveAt(Frames.Count - 1); terminate = true; break; default: _fileStream.SkipBlocks(tempBuf); break; } // Break the loop when the stream is not valid anymore. if (_fileStream.Position >= _fileStream.Length & terminate == false) throw new InvalidProgramException( "Reach the end of the filestream without trailer block." ); } while (!terminate); ArrayPool.Shared.Return(tempBuf); } /// /// Parses GIF Image Descriptor Block. /// private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf) { var str = _fileStream; var currentFrame = Frames[curFrame]; // Parse frame dimensions. var frameX = str.ReadUShortS(tempBuf); var frameY = str.ReadUShortS(tempBuf); var frameW = str.ReadUShortS(tempBuf); var frameH = str.ReadUShortS(tempBuf); frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX); frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY); currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH); // Unpack interlace and lct info. var packed = str.ReadByteS(tempBuf); currentFrame.IsInterlaced = (packed & 0x40) != 0; currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0; currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1); if (currentFrame.IsLocalColorTableUsed) currentFrame.LocalColorTable = ProcessColorTable( ref str, tempBuf, currentFrame.LocalColorTableSize ); currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf); currentFrame.LzwStreamPosition = str.Position; curFrame += 1; Frames.Add(new GifFrame()); } /// /// Parses GIF Extension Blocks. /// private void ProcessExtensions(ref int curFrame, byte[] tempBuf) { var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf); switch (extType) { case ExtensionType.GraphicsControl: _fileStream.ReadBlock(tempBuf); var currentFrame = Frames[curFrame]; var packed = tempBuf[0]; currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2); if ( currentFrame.FrameDisposalMethod != FrameDisposal.Restore && currentFrame.FrameDisposalMethod != FrameDisposal.Background ) currentFrame.ShouldBackup = true; currentFrame.HasTransparency = (packed & 1) != 0; currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10); if (currentFrame.FrameDelay <= FrameDelayThreshold) currentFrame.FrameDelay = FrameDelayDefault; currentFrame.TransparentColorIndex = tempBuf[3]; break; case ExtensionType.Application: var blockLen = _fileStream.ReadBlock(tempBuf); var _ = tempBuf.AsSpan(0, blockLen); var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length); if (blockHeader.SequenceEqual(NetscapeMagic.Span)) { var count = 1; while (count > 0) count = _fileStream.ReadBlock(tempBuf); var iterationCount = SpanToShort(tempBuf.AsSpan(1)); Header.Iterations = iterationCount; } else _fileStream.SkipBlocks(tempBuf); break; default: _fileStream.SkipBlocks(tempBuf); break; } } } } ================================================ FILE: Avalonia.Gif/Decoding/GifFrame.cs ================================================ using System; namespace Avalonia.Gif.Decoding { public class GifFrame { public bool HasTransparency, IsInterlaced, IsLocalColorTableUsed; public byte TransparentColorIndex; public int LzwMinCodeSize, LocalColorTableSize; public long LzwStreamPosition; public TimeSpan FrameDelay; public FrameDisposal FrameDisposalMethod; public bool ShouldBackup; public GifRect Dimensions; public GifColor[] LocalColorTable; } } ================================================ FILE: Avalonia.Gif/Decoding/GifHeader.cs ================================================ // Licensed under the MIT License. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. namespace Avalonia.Gif.Decoding { public class GifHeader { public bool HasGlobalColorTable; public int GlobalColorTableSize; public ulong GlobalColorTableCacheId; public int BackgroundColorIndex; public long HeaderSize; internal int Iterations = -1; public GifRepeatBehavior IterationCount; public GifRect Dimensions; private GifColor[] _globarColorTable; public GifColor[] GlobarColorTable; } } ================================================ FILE: Avalonia.Gif/Decoding/GifRect.cs ================================================ namespace Avalonia.Gif.Decoding { public readonly struct GifRect { public int X { get; } public int Y { get; } public int Width { get; } public int Height { get; } public int TotalPixels { get; } public GifRect(int x, int y, int width, int height) { X = x; Y = y; Width = width; Height = height; TotalPixels = width * height; } public static bool operator ==(GifRect a, GifRect b) { return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height; } public static bool operator !=(GifRect a, GifRect b) { return !(a == b); } public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; return this == (GifRect)obj; } public override int GetHashCode() { return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode(); } } } ================================================ FILE: Avalonia.Gif/Decoding/GifRepeatBehavior.cs ================================================ namespace Avalonia.Gif.Decoding { public class GifRepeatBehavior { public bool LoopForever { get; set; } public int? Count { get; set; } } } ================================================ FILE: Avalonia.Gif/Decoding/InvalidGifStreamException.cs ================================================ // Licensed under the MIT License. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. namespace Avalonia.Gif.Decoding { [Serializable] public class InvalidGifStreamException : Exception { public InvalidGifStreamException() { } public InvalidGifStreamException(string message) : base(message) { } public InvalidGifStreamException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: Avalonia.Gif/Decoding/LzwDecompressionException.cs ================================================ // Licensed under the MIT License. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. namespace Avalonia.Gif.Decoding { [Serializable] public class LzwDecompressionException : Exception { public LzwDecompressionException() { } public LzwDecompressionException(string message) : base(message) { } public LzwDecompressionException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: Avalonia.Gif/Extensions/StreamExtensions.cs ================================================ using System; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; namespace Avalonia.Gif.Extensions { [DebuggerStepThrough] internal static class StreamExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort SpanToShort(Span b) => (ushort)(b[0] | (b[1] << 8)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Skip(this Stream stream, long count) { stream.Position += count; } /// /// Read a Gif block from stream while advancing the position. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ReadBlock(this Stream stream, byte[] tempBuf) { stream.Read(tempBuf, 0, 1); var blockLength = (int)tempBuf[0]; if (blockLength > 0) stream.Read(tempBuf, 0, blockLength); // Guard against infinite loop. if (stream.Position >= stream.Length) throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); return blockLength; } /// /// Skips GIF blocks until it encounters an empty block. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SkipBlocks(this Stream stream, byte[] tempBuf) { int blockLength; do { stream.Read(tempBuf, 0, 1); blockLength = tempBuf[0]; stream.Position += blockLength; // Guard against infinite loop. if (stream.Position >= stream.Length) throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); } while (blockLength > 0); } /// /// Read a from stream by providing a temporary buffer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort ReadUShortS(this Stream stream, byte[] tempBuf) { stream.Read(tempBuf, 0, 2); return SpanToShort(tempBuf); } /// /// Read a from stream by providing a temporary buffer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte ReadByteS(this Stream stream, byte[] tempBuf) { stream.Read(tempBuf, 0, 1); var finalVal = tempBuf[0]; return finalVal; } } } ================================================ FILE: Avalonia.Gif/GifImage.cs ================================================ using System; using System.IO; using System.Numerics; using Avalonia; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.VisualTree; namespace Avalonia.Gif { public class GifImage : Control { public static readonly StyledProperty SourceUriRawProperty = AvaloniaProperty.Register< GifImage, string >("SourceUriRaw"); public static readonly StyledProperty SourceUriProperty = AvaloniaProperty.Register< GifImage, Uri >("SourceUri"); public static readonly StyledProperty SourceStreamProperty = AvaloniaProperty.Register< GifImage, Stream >("SourceStream"); public static readonly StyledProperty IterationCountProperty = AvaloniaProperty.Register("IterationCount", IterationCount.Infinite); private IGifInstance? _gifInstance; public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register("StretchDirection"); public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register< GifImage, Stretch >("Stretch"); private CompositionCustomVisual? _customVisual; private object? _initialSource = null; protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { switch (change.Property.Name) { case nameof(SourceUriRaw): case nameof(SourceUri): case nameof(SourceStream): SourceChanged(change); break; case nameof(Stretch): case nameof(StretchDirection): InvalidateArrange(); InvalidateMeasure(); Update(); break; case nameof(IterationCount): IterationCountChanged(change); break; case nameof(Bounds): Update(); break; } base.OnPropertyChanged(change); } public string SourceUriRaw { get => GetValue(SourceUriRawProperty); set => SetValue(SourceUriRawProperty, value); } public Uri SourceUri { get => GetValue(SourceUriProperty); set => SetValue(SourceUriProperty, value); } public Stream SourceStream { get => GetValue(SourceStreamProperty); set => SetValue(SourceStreamProperty, value); } public IterationCount IterationCount { get => GetValue(IterationCountProperty); set => SetValue(IterationCountProperty, value); } public StretchDirection StretchDirection { get => GetValue(StretchDirectionProperty); set => SetValue(StretchDirectionProperty, value); } public Stretch Stretch { get => GetValue(StretchProperty); set => SetValue(StretchProperty, value); } private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e) { var image = e.Sender as GifImage; if (image is null || e.NewValue is not IterationCount iterationCount) return; image.IterationCount = iterationCount; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { var compositor = ElementComposition.GetElementVisual(this)?.Compositor; if (compositor == null || _customVisual?.Compositor == compositor) return; _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); ElementComposition.SetElementChildVisual(this, _customVisual); _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); if (_initialSource is not null) { UpdateGifInstance(_initialSource); _initialSource = null; } Update(); base.OnAttachedToVisualTree(e); } private void Update() { if (_customVisual is null || _gifInstance is null) return; var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0; var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi); var viewPort = new Rect(Bounds.Size); var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); var scaledSize = sourceSize * scale; var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort); if (Stretch == Stretch.None) { _customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height); } else { _customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height); } _customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0); } private class CustomVisualHandler : CompositionCustomVisualHandler { private TimeSpan _animationElapsed; private TimeSpan? _lastServerTime; private IGifInstance? _currentInstance; private bool _running; public static readonly object StopMessage = new(), StartMessage = new(); public override void OnMessage(object message) { if (message == StartMessage) { _running = true; _lastServerTime = null; RegisterForNextAnimationFrameUpdate(); } else if (message == StopMessage) { _running = false; } else if (message is IGifInstance instance) { _currentInstance?.Dispose(); _currentInstance = instance; } } public override void OnAnimationFrameUpdate() { if (!_running) return; Invalidate(); RegisterForNextAnimationFrameUpdate(); } public override void OnRender(ImmediateDrawingContext drawingContext) { if (_running) { if (_lastServerTime.HasValue) _animationElapsed += (CompositionNow - _lastServerTime.Value); _lastServerTime = CompositionNow; } try { if (_currentInstance is null || _currentInstance.IsDisposed) return; var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed); if (bitmap is not null) { drawingContext.DrawBitmap( bitmap, new Rect(_currentInstance.GifPixelSize.ToSize(1)), GetRenderBounds() ); } } catch (Exception e) { Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString()); } } } /// /// Measures the control. /// /// The available size. /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { var result = new Size(); var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; if (_gifInstance != null) { result = Stretch.CalculateSize( availableSize, _gifInstance.GifPixelSize.ToSize(scaling), StretchDirection ); } return result; } /// protected override Size ArrangeOverride(Size finalSize) { if (_gifInstance is null) return new Size(); var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling); var result = Stretch.CalculateSize(finalSize, sourceSize); return result; } private void SourceChanged(AvaloniaPropertyChangedEventArgs e) { if ( e.NewValue is null || (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute)) ) { return; } if (_customVisual is null) { _initialSource = e.NewValue; return; } UpdateGifInstance(e.NewValue); InvalidateArrange(); InvalidateMeasure(); Update(); } private void UpdateGifInstance(object source) { _gifInstance?.Dispose(); try { _gifInstance = new WebpInstance(source); // _gifInstance = new GifInstance(source); _gifInstance.IterationCount = IterationCount; _customVisual?.SendHandlerMessage(_gifInstance); } catch (Exception e) { Logger.Sink?.Log(LogEventLevel.Warning, "GifImage Update Source ", this, e.ToString()); } } } } ================================================ FILE: Avalonia.Gif/GifInstance.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using Avalonia; using Avalonia.Animation; using Avalonia.Gif.Decoding; using Avalonia.Media.Imaging; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Gif { public class GifInstance : IGifInstance { public IterationCount IterationCount { get; set; } public bool AutoStart { get; private set; } = true; private readonly GifDecoder _gifDecoder; private readonly WriteableBitmap? _targetBitmap; private TimeSpan _totalTime; private readonly List _frameTimes; private uint _iterationCount; private int _currentFrameIndex; private readonly List _colorTableIdList; public CancellationTokenSource CurrentCts { get; } internal GifInstance(object newValue) : this( newValue switch { Stream s => s, Uri u => GetStreamFromUri(u), string str => GetStreamFromString(str), _ => throw new InvalidDataException("Unsupported source object") } ) { } public GifInstance(string uri) : this(GetStreamFromString(uri)) { } public GifInstance(Uri uri) : this(GetStreamFromUri(uri)) { } public GifInstance(Stream currentStream) { if (!currentStream.CanSeek) throw new InvalidDataException("The provided stream is not seekable."); if (!currentStream.CanRead) throw new InvalidOperationException("Can't read the stream provided."); currentStream.Seek(0, SeekOrigin.Begin); CurrentCts = new CancellationTokenSource(); _gifDecoder = new GifDecoder(currentStream, CurrentCts.Token); var pixSize = new PixelSize( _gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height ); // Different on os: https://github.com/mono/SkiaSharp/issues/1492#issuecomment-689015409 // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault var format = SKImageInfo.PlatformColorType switch { SKColorType.Bgra8888 => PixelFormat.Bgra8888, SKColorType.Rgba8888 => PixelFormat.Rgba8888, _ => throw new NotSupportedException( $"Unsupported color type: {SKImageInfo.PlatformColorType}" ) }; _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), format, AlphaFormat.Opaque); GifPixelSize = pixSize; _totalTime = TimeSpan.Zero; _frameTimes = _gifDecoder .Frames.Select(frame => { _totalTime = _totalTime.Add(frame.FrameDelay); return _totalTime; }) .ToList(); _gifDecoder.RenderFrame(0, _targetBitmap); } private static Stream GetStreamFromString(string str) { if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res)) { throw new InvalidCastException("The string provided can't be converted to URI."); } return GetStreamFromUri(res); } private static Stream GetStreamFromUri(Uri uri) { var uriString = uri.OriginalString.Trim(); if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares")) { return new FileStream(uriString, FileMode.Open, FileAccess.Read); } return AssetLoader.Open(uri); } public int GifFrameCount => _frameTimes.Count; public PixelSize GifPixelSize { get; } public void Dispose() { IsDisposed = true; CurrentCts.Cancel(); _targetBitmap?.Dispose(); } public bool IsDisposed { get; private set; } public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed) { if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value) { return null; } if (CurrentCts.IsCancellationRequested || _targetBitmap is null) { return null; } var elapsedTicks = stopwatchElapsed.Ticks; var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks); var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x); var currentFrame = _frameTimes.IndexOf(targetFrame); if (currentFrame == -1) currentFrame = 0; if (_currentFrameIndex == currentFrame) return _targetBitmap; _iterationCount = (uint)(elapsedTicks / _totalTime.Ticks); return ProcessFrameIndex(currentFrame); } internal WriteableBitmap ProcessFrameIndex(int frameIndex) { _gifDecoder.RenderFrame(frameIndex, _targetBitmap!); _currentFrameIndex = frameIndex; return _targetBitmap!; } } } ================================================ FILE: Avalonia.Gif/IGifInstance.cs ================================================ using Avalonia.Animation; using Avalonia.Media.Imaging; namespace Avalonia.Gif; public interface IGifInstance : IDisposable { IterationCount IterationCount { get; set; } bool AutoStart { get; } CancellationTokenSource CurrentCts { get; } int GifFrameCount { get; } PixelSize GifPixelSize { get; } bool IsDisposed { get; } WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed); } ================================================ FILE: Avalonia.Gif/InvalidGifStreamException.cs ================================================ namespace Avalonia.Gif { [Serializable] internal class InvalidGifStreamException : Exception { public InvalidGifStreamException() { } public InvalidGifStreamException(string message) : base(message) { } public InvalidGifStreamException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: Avalonia.Gif/WebpInstance.cs ================================================ using Avalonia.Animation; using Avalonia.Media.Imaging; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Gif; public class WebpInstance : IGifInstance { public IterationCount IterationCount { get; set; } public bool AutoStart { get; private set; } = true; private readonly WriteableBitmap? _targetBitmap; private TimeSpan _totalTime; private readonly List _frameTimes; private uint _iterationCount; private int _currentFrameIndex; private SKCodec? _codec; public CancellationTokenSource CurrentCts { get; } internal WebpInstance(object newValue) : this( newValue switch { Stream s => s, Uri u => GetStreamFromUri(u), string str => GetStreamFromString(str), _ => throw new InvalidDataException("Unsupported source object") } ) { } public WebpInstance(string uri) : this(GetStreamFromString(uri)) { } public WebpInstance(Uri uri) : this(GetStreamFromUri(uri)) { } public WebpInstance(Stream currentStream) { if (!currentStream.CanSeek) throw new InvalidDataException("The provided stream is not seekable."); if (!currentStream.CanRead) throw new InvalidOperationException("Can't read the stream provided."); currentStream.Seek(0, SeekOrigin.Begin); CurrentCts = new CancellationTokenSource(); var managedStream = new SKManagedStream(currentStream); _codec = SKCodec.Create(managedStream); var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height); // Different on os: https://github.com/mono/SkiaSharp/issues/1492#issuecomment-689015409 // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault var format = SKImageInfo.PlatformColorType switch { SKColorType.Bgra8888 => PixelFormat.Bgra8888, SKColorType.Rgba8888 => PixelFormat.Rgba8888, _ => throw new NotSupportedException($"Unsupported color type: {SKImageInfo.PlatformColorType}") }; _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), format, AlphaFormat.Opaque); GifPixelSize = pixSize; _totalTime = TimeSpan.Zero; _frameTimes = _codec .FrameInfo.Select(frame => { _totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration)); return _totalTime; }) .ToList(); RenderFrame(_codec, _targetBitmap, 0); } private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index) { codec.GetFrameInfo(index, out var frameInfo); var info = new SKImageInfo(codec.Info.Width, codec.Info.Height); var decodeInfo = info.WithAlphaType(frameInfo.AlphaType); using var frameBuffer = targetBitmap.Lock(); var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index)); if (result != SKCodecResult.Success) throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}."); } private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex) { codec.GetFrameInfo(index, out var frameInfo); var info = new SKImageInfo(codec.Info.Width, codec.Info.Height); var decodeInfo = info.WithAlphaType(frameInfo.AlphaType); using var frameBuffer = targetBitmap.Lock(); var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex)); if (result != SKCodecResult.Success) throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}."); } private static Stream GetStreamFromString(string str) { if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res)) { throw new InvalidCastException("The string provided can't be converted to URI."); } return GetStreamFromUri(res); } private static Stream GetStreamFromUri(Uri uri) { var uriString = uri.OriginalString.Trim(); if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares")) { // Local file using var fs = new FileStream(uriString, FileMode.Open, FileAccess.Read); // Copy to memory stream then return var memoryStream = new MemoryStream(); fs.CopyTo(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); return memoryStream; } // Internal Avalonia resources return AssetLoader.Open(uri); } public int GifFrameCount => _frameTimes.Count; public PixelSize GifPixelSize { get; } public void Dispose() { IsDisposed = true; CurrentCts.Cancel(); _targetBitmap?.Dispose(); _codec?.Dispose(); } public bool IsDisposed { get; private set; } public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed) { if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value) { return null; } if (CurrentCts.IsCancellationRequested || _targetBitmap is null) { return null; } var elapsedTicks = stopwatchElapsed.Ticks; var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks); var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x); var currentFrame = _frameTimes.IndexOf(targetFrame); if (currentFrame == -1) currentFrame = 0; if (_currentFrameIndex == currentFrame) return _targetBitmap; _iterationCount = (uint)(elapsedTicks / _totalTime.Ticks); return ProcessFrameIndex(currentFrame); } internal WriteableBitmap ProcessFrameIndex(int frameIndex) { if (_codec is null) throw new InvalidOperationException("The codec is null."); if (_targetBitmap is null) throw new InvalidOperationException("The target bitmap is null."); RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex); _currentFrameIndex = frameIndex; return _targetBitmap; } } ================================================ FILE: Build/AppEntitlements.entitlements ================================================ com.apple.security.cs.allow-jit ================================================ FILE: Build/EmbeddedEntitlements.entitlements ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation ================================================ FILE: Build/_utils.sh ================================================ #!/bin/bash print_hyperlink() { local url="$1" local text="$2" # macOS Terminal supports clickable links in the following format printf "\033]8;;%s\a%s\033]8;;\a" "$url" "$text" } ================================================ FILE: Build/build_macos_app.sh ================================================ #!/bin/bash output_dir="$(pwd)/out/osx-arm64/" app_name="Stability Matrix.app" . "./_utils.sh" > /dev/null 2>&1 || . "${BASH_SOURCE%/*}/_utils.sh" # Parse args while getopts v: flag do case "${flag}" in v) version=${OPTARG} ;; *) echo "Invalid option: -$OPTARG" >&2 exit 2 ;; esac done shift $((OPTIND - 1)) echo $"Passing extra args to msbuild: $@" set -e # Build the app dotnet \ msbuild \ StabilityMatrix.Avalonia \ -t:BundleApp \ -p:RuntimeIdentifier=osx-arm64 \ -p:UseAppHost=true \ -p:Configuration=Release \ -p:SelfContained=true \ -p:CFBundleName="Stability Matrix" \ -p:CFBundleDisplayName="Stability Matrix" \ -p:CFBundleVersion="$version" \ -p:CFBundleShortVersionString="$version" \ -p:PublishDir="${output_dir:?}/bin" \ "$@" target_plist_path="${output_dir:?}/bin/${app_name:?}/Contents/Info.plist" echo "> Checking Info.plist..." file "${target_plist_path:?}" plutil -lint "${target_plist_path:?}" echo "> Copying app to output..." # Delete existing file rm -rf "${output_dir:?}/${app_name:?}" # Copy the app out of bin cp -r "${output_dir:?}/bin/${app_name:?}" "${output_dir:?}/${app_name:?}" # Print output location echo "[App Build Completed]" print_hyperlink "file:///${output_dir:?}" "${output_dir:?}" print_hyperlink "file:///${output_dir:?}/${app_name:?}" "${app_name:?}" echo "" ================================================ FILE: Build/codesign_embedded_macos.sh ================================================ #!/bin/sh echo "Signing file: $1" # Setup keychain in CI if [ -n "$CI" ]; then # Turn our base64-encoded certificate back to a regular .p12 file echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12 # We need to create a new keychain, otherwise using the certificate will prompt # with a UI dialog asking for the certificate password, which we can't # use in a headless CI environment security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain fi # Sign all files PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P ) ENTITLEMENTS="$PARENT_PATH/EmbeddedEntitlements.entitlements" echo "Using entitlements file: $ENTITLEMENTS" # App if [ "$1" == "*.app" ]; then echo "[INFO] Signing app contents" find "$1/Contents/MacOS/"|while read fname; do if [[ -f $fname ]]; then echo "[INFO] Signing $fname" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" fi done echo "[INFO] Signing app file" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v # Directory elif [ -d "$1" ]; then echo "[INFO] Signing directory contents" find "$1"|while read fname; do if [[ -f $fname ]] && [[ ! $fname =~ /(*.(py|msg|enc))/ ]]; then echo "[INFO] Signing $fname" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" fi done # File elif [ -f "$1" ]; then echo "[INFO] Signing file" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v # Not matched else echo "[ERROR] Unknown file type" exit 1 fi ================================================ FILE: Build/codesign_macos.sh ================================================ #!/bin/sh echo "Signing file: $1" # Setup keychain in CI if [ -n "$CI" ]; then # Turn our base64-encoded certificate back to a regular .p12 file echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12 # We need to create a new keychain, otherwise using the certificate will prompt # with a UI dialog asking for the certificate password, which we can't # use in a headless CI environment security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain fi # Sign all files PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || return ; pwd -P ) ENTITLEMENTS="$PARENT_PATH/AppEntitlements.entitlements" echo "Using entitlements file: $ENTITLEMENTS" find "$1/Contents/MacOS/"|while read fname; do if [[ -f $fname ]]; then echo "[INFO] Signing $fname" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$fname" fi done echo "[INFO] Signing app file" codesign --force --timestamp -s "$MACOS_CERTIFICATE_NAME" --options=runtime --entitlements "$ENTITLEMENTS" "$1" -v ================================================ FILE: Build/notarize_macos.sh ================================================ #!/bin/sh echo "Notarizing file: $1" # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" xcrun notarytool store-credentials "notarytool-profile" \ --apple-id "$MACOS_NOTARIZATION_APPLE_ID" \ --team-id "$MACOS_NOTARIZATION_TEAM_ID" \ --password "$MACOS_NOTARIZATION_PWD" # We can't notarize an app bundle directly, but we need to compress it as an archive. # Therefore, we create a zip file containing our app bundle, so that we can send it to the # notarization service echo "Creating temp notarization archive" ditto -c -k --keepParent "$1" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if # you're curious echo "Notarize app" xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" xcrun stapler staple "$1" ================================================ FILE: CHANGELOG.md ================================================ # Changelog 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.15.6 ### Added - Added NVIDIA driver version warning when launching ComfyUI with CUDA 13.0 (cu130) and driver versions below 580.x - Added legacy Python warning when launching InvokeAI installations using Python 3.10.11 - Added Tiled VAE Decode to the Inference video workflows - thanks to @NeuralFault! ### Changed - Disabled update checking for legacy InvokeAI installations using Python 3.10.11 - Hide rating stars in the Civitai browser page if no rating is available - Updated uv to v0.9.30 - Updated PortableGit to v2.52.0.windows.1 - Updated Sage/Triton/Nunchaku installers to use GitHub API to fetch latest releases - Updated ComfyUI installations and updates to automatically install ComfyUI Manager - Updated gfx110X Windows ROCm nightly index - thanks to @NeuralFault! - Updated ComfyUI-Zluda install to more closely match the author's intended installation method - thanks to @NeuralFault! - Updated Forge Classic installs/updates to use the upstream install script for better version compatibility with torch/sage/triton/nunchaku - Backslashes can now be escaped in Inference prompts via `\\` ### Fixed - Fixed parsing of escape sequences in Inference such as `\\` - Fixed [#1546](https://github.com/LykosAI/StabilityMatrix/issues/1546), [#1541](https://github.com/LykosAI/StabilityMatrix/issues/1541) - "No module named 'pkg_resources'" error when installing Automatic1111/Forge/reForge packages - Fixed [#1545](https://github.com/LykosAI/StabilityMatrix/issues/1545), [#1518](https://github.com/LykosAI/StabilityMatrix/issues/1518), [#1513](https://github.com/LykosAI/StabilityMatrix/issues/1513), [#1488](https://github.com/LykosAI/StabilityMatrix/issues/1488) - Forge Neo update breaking things - Fixed [#1529](https://github.com/LykosAI/StabilityMatrix/issues/1529) - "Selected commit is null" error when installing packages and rate limited by GitHub - Fixed [#1525](https://github.com/LykosAI/StabilityMatrix/issues/1525) - Crash after downloading a model - Fixed [#1523](https://github.com/LykosAI/StabilityMatrix/issues/1523), [#1499](https://github.com/LykosAI/StabilityMatrix/issues/1499), [#1494](https://github.com/LykosAI/StabilityMatrix/issues/1494) - Automatic1111 using old stable diffusion repo - Fixed [#1505](https://github.com/LykosAI/StabilityMatrix/issues/1505) - incorrect port argument for Wan2GP - Possibly fix [#1502](https://github.com/LykosAI/StabilityMatrix/issues/1502) - English fonts not displaying correctly on Linux in Chinese environments - Fixed [#1476](https://github.com/LykosAI/StabilityMatrix/issues/1476) - Incorrect shared output folder for Forge Classic/Neo - Fixed [#1466](https://github.com/LykosAI/StabilityMatrix/issues/1466) - crash after moving portable install - Fixed [#1445](https://github.com/LykosAI/StabilityMatrix/issues/1445) - Linux app updates not actually updating - thanks to @NeuralFault! ### Supporters #### 🌟 Visionaries To our stellar Visionaries: **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, and **whudunit**! Your generosity keeps this project thriving and gives us the confidence to tackle the big challenges. Thank you for being the foundation that makes it all possible! #### 🚀 Pioneers Shoutout to our incredible Pioneer crew for keeping the momentum going! Thank you to: **Szir777**, **Noah M**, **[USA]TechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Adam**, **Droolguy**, **ACTUALLY_the_Real_Willem_Dafoe**, **takyamtom**, **robek**, **Ghislain G**, **Phil R**, **Tundra Everquill**, and a warm welcome to our newest Pioneers: **Andrew B**, **snotty**, **Miguel A**, **SinthCore**, and **Ahmed S**! ## v2.15.5 ### Added - Added new package - [Wan2GP](https://github.com/deepbeepmeep/Wan2GP) - Added [Stable Diffusion WebUI Forge - Neo](https://github.com/Haoming02/sd-webui-forge-classic/tree/neo) as a separate package for convenience - Added Tiled VAE decoding as an Inference addon thanks to @NeuralFault! - Added togglable `--uv` argument to the SD.Next launch options ### Changed - Moved the original Stable Diffusion WebUI Forge to the "Legacy" packages tab due to inactivity - Updated to cu130 torch index for ComfyUI installs with Nvidia GPUs - Consolidated and fixed AMD GPU architecture detection - Updated SageAttention installer to latest v2.2.0-windows.post4 version - Updated torch index for Forge-based UIs to rocm6.4 / cu128 depending on GPU - thanks to @NeuralFault! ### Fixed - Fixed [#1450](https://github.com/LykosAI/StabilityMatrix/issues/1450) - Older SD.Next not launching due to forced `--uv` argument - Fixed duplicate custom node installations when installing workflows from the Workflow Browser - thanks again to @NeuralFault! #### 🌟 Visionaries To our incredible Visionaries: **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, and **whudunit**! Your generosity drives this project forward and helps us turn ideas into reality. Thank you for being such a vital part of Stability Matrix! #### 🚀 Pioneers A massive thank you to our Pioneer crew! Your support keeps the lights on and the updates flowing. Shoutout to: **Szir777**, **Noah M**, **[USA]TechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Adam**, **Droolguy**, **ACTUALLY_the_Real_Willem_Dafoe**, **takyamtom**, **robek**, **Phil R**, **Tundra Everquill**, **TheTekknician**, and a warm welcome to our new Pioneers, **Benjamin M** and **Ghislain G**! ## v2.15.4 ### Changed - Updated Early Access indicators in the Civitai Details page to be more visible - Updated error message when attempting to download a website-generation-only model from Civitai - Updated nunchaku installer to 1.0.2 - Updated Package Import dialog to have Python version selector ### Fixed - Fixed [#1435](https://github.com/LykosAI/StabilityMatrix/issues/1435) - SwarmUI not launching due to missing dotnet - Fixed various install and Inference issues with ComfyUI-Zluda - big thanks to @neural_fault for the PRs! - Fixed sageattention version getting out of sync after torch updates in ComfyUI - Potentially fixed issue where uv-managed Python versions would not appear in the version selector ### Supporters #### 🌟 Visionaries Our heartfelt thanks to the driving force behind our progress, our Visionaries: **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, and **whudunit**! Your incredible support is the fuel that powers our development, allowing us to tackle bugs and push forward with confidence. #### 🚀 Pioneers A huge shoutout to our amazing Pioneers, who keep the momentum going strong! Thank you for being our trusted crew on this journey: **Szir777**, **Noah M**, **USATechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Tundra Everquill**, **Adam**, **Droolguy**, **Philip R.**, **ACTUALLY_the_Real_Willem_Dafoe**, **takyamtom**, and **robek**! ## v2.15.3 ### Changed - Updated fallback rocm index for InvokeAI to rocm6.3 - Updated SwarmUI to launch via the launch script for better compatibility ### Fixed - Fixed cuDNN frontend error on ComfyUI-Zluda startup (thanks @neural_fault!) - Maybe finally actually fixed threading issue with the Python Packages dialog search box for real this time? (may fix [#1392](https://github.com/LykosAI/StabilityMatrix/issues/1392)) - Fixed potential install failures when moving duplicate files into shared model folders (may fix [#1393](https://github.com/LykosAI/StabilityMatrix/issues/1393)) - Fixed potential threading issues with the Inference image gallery (may fix [#1408](https://github.com/LykosAI/StabilityMatrix/issues/1408)) - Fixed [#1424](https://github.com/LykosAI/StabilityMatrix/issues/1424) - Civitai account 401 error when connecting accounts, updated for new API changes ### Supporters #### 🌟 Visionaries Our deepest gratitude to our Visionaries for their foundational support: **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, and **whudunit**! Your commitment allows us to focus on the essential work of squashing bugs and improving stability, ensuring a rock-solid experience for everyone. #### 🚀 Pioneers A huge thank you to our incredible Pioneers for keeping the project on track! Your support is vital for these important refinement updates. Thank you to **Szir777**, **Noah M**, **USATechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Tundra Everquill**, **Adam**, **Droolguy**, **Philip R.**, **ACTUALLY_the_Real_Willem_Dafoe**, **takyamtom**, and a warm welcome to our newest Pioneer, **robek**! ## v2.15.2 ### Changed - Updated Avalonia to 11.3.7 ### Fixed - Fixed [#1409](https://github.com/LykosAI/StabilityMatrix/issues/1409) - incorrect triton version installed during FramePack install on non-Windows systems - Fixed [#1410](https://github.com/LykosAI/StabilityMatrix/issues/1410) - crash when resizing Civitai model details page - Fixed [#1417](https://github.com/LykosAI/StabilityMatrix/issues/1417), [#1419](https://github.com/LykosAI/StabilityMatrix/issues/1419) - unable to connect Inference to ComfyUI after updating to latest ComfyUI - Fixed missing dependencies for ComfyUI API nodes ### Supporters #### 🌟 Visionaries Huge thanks to our incredible Visionaries: **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, **Ibixat**, and **whudunit**! You’re the reason Stability Matrix keeps pushing forward. Your support lights the path and keeps the dream alive. #### 🚀 Pioneers Shoutout to our amazing Pioneer crew: **Szir777**, **Tigon**, **Noah M**, **USATechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Tundra Everquill**, **Adam**, **Droolguy**, **Philip R.**, **ACTUALLY_the_Real_Willem_Dafoe**, and **takyamtom**! You help keep the gears turning and the magic flowing. ## v2.15.1 ### Changed - Upgraded ComfyUI-Zluda and Stable Diffusion WebUI AMDGPU Forge packages to install and use HIP SDK 6.4.2 - Changed ComfyUI torch index from `cu129` back to `cu128` for better compatibility with custom nodes - Updated SageAttention installer to install v2.2.0-windows.post3 - Updated Nunchaku installer to install v1.0.1 - Updated Windows ROCm ComfyUI installs to use recommended environment variables by default ### Fixed - Fixed [#1372](https://github.com/LykosAI/StabilityMatrix/issues/1372), [#1399](https://github.com/LykosAI/StabilityMatrix/issues/1399) - LiteAsyncException upon starting Stability Matrix v2.15.0 - Fixed [#1391](https://github.com/LykosAI/StabilityMatrix/issues/1391) - "Failed to parse" error when upgrading pip packages with extra index url - Fixed [#1401](https://github.com/LykosAI/StabilityMatrix/issues/1401) - "Python was not found and/or failed to install" errors when path contains special characters - Fixed [#1403](https://github.com/LykosAI/StabilityMatrix/issues/1403) - Checkpoint Manager filters not being saved correctly - Fixed [#1411](https://github.com/LykosAI/StabilityMatrix/issues/1411) - SD.Next installs not using correct torch version - Fixed "cannot access local variable 'job' where it is not associated with a value" error when running jobs in AI Toolkit - Fixed Civitai browser not always returning at least 30 results when possible on initial search - Fixed model browser crashing when downloading a file with invalid characters in the name - Fixed model browser crashing when no author exists for a model ### Supporters #### 🌟 Visionaries To our guiding stars, the Visionaries! Thank you **Waterclouds**, **JungleDragon**, **bluepopsicle**, **Bob S**, **Ibixat**, and **whudunit**! While this release is focused on fixes and stability, your foundational support is what empowers us to build a reliable and robust platform for everyone. #### 🚀 Pioneers A huge round of applause for our fantastic Pioneers! Your steady support helps us smooth out the rough edges and deliver a better experience with every update. Our deepest thanks to: **Szir777**, **Tigon**, **Noah M**, **USATechDude**, **Thom**, **SeraphOfSalem**, **Desert Viber**, **Tundra Everquill**, **Adam**, and **Droolguy**. We're also thrilled to welcome our newest Pioneers to the crew: **Philip R.**, **ACTUALLY_the_Real_Willem_Dafoe**, and **takyamtom**! ## v2.15.0 ### Added - Added new package - [AI Toolkit](https://github.com/ostris/ai-toolkit/) - Added new package - [FramePack](https://github.com/lllyasviel/FramePack) - Added new package - [FramePack Studio](https://github.com/colinurbs/FramePack-Studio) - Added Python Version selector for all new package installs - Added the ability to rename packages - Added support for authenticated model downloads in the HuggingFace model browser. Visit Settings → Accounts to add your HuggingFace token. - Added support for dragging-and-dropping Civitai-generated images into Inference to load metadata - Added the ability to search by pasting an entire Civitai model URL into the search bar in the Civitai model browser - Added "Clear Pip Cache" and "Clear uv Cache" commands to the Settings -> Embedded Python section - Added settings to disable base models from appearing in the Checkpoint Manager and Civitai Model Browser base model selectors - Added Inference "Favorite Dimensions" quick selector - editable in Settings → Inference, or click the 💾 button inside the dropdown - Added setting for Inference dimension step change - the value the dimensions increase or decrease by when using the step buttons or scroll wheel in Inference - Added "Install Nunchaku" option to the ComfyUI Package Commands menu - Added "Select All" button to the Installed Extensions page - Added experimental ROCm pytorch install for ComfyUI (non-Zluda) on Windows - requires a compatible AMD GPU - Added base model type labels (SD1.5, SDXL, Flux, etc.) to Inference model selection boxes - Added UNET shared folder link for SD.Next - Added Manual Install button for installing Package extensions that aren't in the indexes - Added Next and Previous buttons to the Civitai details page to navigate between results - Added Negative Rejection Steering (NRS) by @reithan to Inference - Added Wan 2.2 models to the HuggingFace tab of the model browser - Added Tiled Encode/Decode options to FaceDetailer in Inference - Added Ukrainian translation thanks to @r0ddty! - Added Czech translation thanks to @PEKArt! ### Changed 🌟 Civitai Model Details: A Grand Reimagining! 🌟 - No more peering through a tiny window! Introducing a massive overhaul of the Civitai Model Details page, transforming it from a cramped dialog into a spacious, feature-rich hub for all your model exploration needs. - We've listened to your howls for more, and now you can dive deep into every aspect of your favorite models with unprecedented clarity and control: - Expansive View: The new full-page layout means all essential information, descriptions, and previews are laid out beautifully, banishing the old, restrictive dialog forever. - Rich Details at a Glance: Author, base model, last updated, SHA hashes, file name overrides/patterns – everything you need, perfectly organized and always accessible. - Overhauled Image Viewer: Enjoy a sleek, modern image viewer that includes Civitai metadata and supports zooming, panning, and full-screen viewing. No more squinting at tiny thumbnails! - Integrated Inference Options: For supported models, adjust sampler, scheduler, steps, CFG Scale, width, and height directly from the details page, streamlining your workflow like never before! ---- - Updated all Python version management, virtual environment creation, and pip installs to use `uv` for improved reliability, compatibility, and speed - You can now select release versions when installing ComfyUI - You can no longer select branches when installing InvokeAI - Updated InvokeAI install to use the intended install method (resolves [#1329](https://github.com/LykosAI/StabilityMatrix/issues/1329)) - Updated ComfyUI installs for AMD users on Linux to use the latest rocm6.3 torch index - Updated ComfyUI-Zluda installs to use the newer install-n method (fixes [#1347](https://github.com/LykosAI/StabilityMatrix/issues/1347)) - Removed disclaimer from reForge since the author is now active again - Updated git operations to better avoid conflicts - Updated Japanese translation - Civitai model browser image loading now uses dynamic resizing for better performance and a smoother scrolling experience - Undo ComfyUI process tracking changes for now due to causing more issues than it solved - Updated GPU parsing fallback on Linux systems to use the method provided by @irql-notlessorequal - New installs of ComfyUI, SD.Next, and InvokeAI will now use Python 3.12, unless otherwise specified in the Advanced Options during installation - New installs of all other packages will now use Python 3.10.18, unless otherwise specified in the Advanced Options during installation - Updated ComfyUI installs for AMD users on Linux to use the latest rocm6.4 torch index - Updated package delete confirmation dialog ### Fixed - Fixed an error when packages and other processes exit before process tracking on windows can initialize - Fixed "none" appearing in wildcard field when using Face Detailer in Inference - Fixed [#1254](https://github.com/LykosAI/StabilityMatrix/issues/1254) - Unable to scroll samplers in Inference - Fixed [#1294](https://github.com/LykosAI/StabilityMatrix/issues/1294) - Improper sorting of output folders in Output Browser - Fixed [#1300](https://github.com/LykosAI/StabilityMatrix/issues/1300) - Git errors when installing Extension Packs - Fixed [#1317](https://github.com/LykosAI/StabilityMatrix/issues/1317) - Inference missing GGUF text encoders - Fixed [#1324](https://github.com/LykosAI/StabilityMatrix/issues/1324) - Window height slightly increasing every launch - Fixed [#1357](https://github.com/LykosAI/StabilityMatrix/issues/1357) - Case insensitivity causing duplicate key exceptions on non-Windows systems - Fixed [#1360](https://github.com/LykosAI/StabilityMatrix/issues/1360) - A1111 install not using correct torch for 5000-series GPUs - Fixed [#1361](https://github.com/LykosAI/StabilityMatrix/issues/1361) - numpy and other Forge startup - Fixed [#1365](https://github.com/LykosAI/StabilityMatrix/issues/1365) - Output folder list not updating when Refresh button clicked ### Supporters #### 🌟 Visionaries To our incredible Visionaries, the architects of our ambition: Your profound support is the powerhouse behind this massive v2.15.0 release. You don't just light the path; you fuel the entire journey, allowing us to build bigger, move faster, and turn bold ideas into reality. Our deepest gratitude to: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, **whudunit**, and **TheTekknician**! We are immensely grateful for your trust and partnership in shaping the future of Stability Matrix. Thank you for everything! #### 🚀 Pioneers A heartfelt salute to our trailblazing Pioneers! Your consistent support helps us navigate the development landscape, ensuring we stay on the right track and can explore new frontiers. A huge thanks to: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, **Noah M**, **USATechDude**, **Thom**, **SeraphOfSalem**, and a special welcome to our newest Pioneers - **Desert Viber**, **Tundra Everquill**, **Adam**, and **Droolguy**! Thank you for being the vanguard of our community! ## v2.15.0-pre.2 ### Added - Added new package - [AI Toolkit](https://github.com/ostris/ai-toolkit/) - Added Manual Install button for installing Package extensions that aren't in the indexes - Added Next and Previous buttons to the Civitai details page to navigate between results - Added Negative Rejection Steering (NRS) by @reithan to Inference - Added Czech translation thanks to @PEKArt! - Added Wan 2.2 models to the HuggingFace tab of the model browser - Added Tiled Encode/Decode options to FaceDetailer in Inference ### Changed - Brought back the "size remaining after download" tooltip in the new Civitai details page - Updated ComfyUI installs for AMD users on Linux to use the latest rocm6.4 torch index - Updated package delete confirmation dialog ### Fixed - Fixed Inference custom step (e.g. HiresFix) Samplers potentially sharing state with other card UIs like model browser. - Fixed extension manager failing to install extensions due to incorrect clone directory - Fixed duplicate Python versions appearing in the Advanced Options when installing a package - Fixed an error when packages and other processes exit before process tracking on windows can initialize - Fixed "none" appearing in wildcard field when using Face Detailer in Inference - Fixed [#1254](https://github.com/LykosAI/StabilityMatrix/issues/1254) - Unable to scroll samplers in Inference - Fixed [#1294](https://github.com/LykosAI/StabilityMatrix/issues/1294) - Improper sorting of output folders in Output Browser - Fixed [#1300](https://github.com/LykosAI/StabilityMatrix/issues/1300) - Git errors when installing Extension Packs - Fixed [#1317](https://github.com/LykosAI/StabilityMatrix/issues/1317) - Inference missing GGUF text encoders - Fixed [#1324](https://github.com/LykosAI/StabilityMatrix/issues/1324) - Window height slightly increasing every launch - Fixed [#1360](https://github.com/LykosAI/StabilityMatrix/issues/1360) - A1111 install not using correct torch for 5000-series GPUs - Fixed [#1361](https://github.com/LykosAI/StabilityMatrix/issues/1361) - numpy and other Forge startup ### Supporters #### 🌟 Visionaries A huge thank-you to our incredible Visionary-tier supporters: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, **whudunit**, and **Akiro_Senkai**! Your continued support lights the way for Stability Matrix and helps us keep building features like these. We couldn’t do it without you. ## v2.15.0-pre.1 ### Added - Added settings to disable base models from appearing in the Checkpoint Manager and Civitai Model Browser base model selectors - Added Inference "Favorite Dimensions" quick selector - editable in Settings → Inference, or click the 💾 button inside the dropdown - Added setting for Inference dimension step change - the value the dimensions increase or decrease by when using the step buttons or scroll wheel in Inference - Added "Install Nunchaku" option to the ComfyUI Package Commands menu - Added "Select All" button to the Installed Extensions page - Added experimental ROCm pytorch install for ComfyUI (non-Zluda) on Windows - requires a compatible AMD GPU - Added base model type labels (SD1.5, SDXL, Flux, etc.) to Inference model selection boxes - Added UNET shared folder link for SD.Next - Added Ukrainian translation thanks to @r0ddty! ### Changed 🌟 Civitai Model Details: A Grand Reimagining! 🌟 - No more peering through a tiny window! Introducing a massive overhaul of the Civitai Model Details page, transforming it from a cramped dialog into a spacious, feature-rich hub for all your model exploration needs. - We've listened to your howls for more, and now you can dive deep into every aspect of your favorite models with unprecedented clarity and control: - Expansive View: The new full-page layout means all essential information, descriptions, and previews are laid out beautifully, banishing the old, restrictive dialog forever. - Rich Details at a Glance: Author, base model, last updated, SHA hashes, file name overrides/patterns – everything you need, perfectly organized and always accessible. - Overhauled Image Viewer: Enjoy a sleek, modern image viewer that includes Civitai metadata and supports zooming, panning, and full-screen viewing. No more squinting at tiny thumbnails! - Integrated Inference Options: For supported models, adjust sampler, scheduler, steps, CFG Scale, width, and height directly from the details page, streamlining your workflow like never before! ---- - You can now select release versions when installing ComfyUI - You can no longer select branches when installing InvokeAI - Updated InvokeAI install to use pinned torch index from release tag - Updated ComfyUI installs for AMD users on Linux to use the latest rocm6.3 torch index - Updated ComfyUI-Zluda installs to use the newer install-n method (fixes [#1347](https://github.com/LykosAI/StabilityMatrix/issues/1347)) - Updated uv to 0.8.4 - Removed disclaimer from reForge since the author is now active again - Updated git operations to better avoid conflicts - Updated Japanese translation - Undo ComfyUI process tracking changes for now due to causing more issues than it solved - Updated GPU parsing fallback on Linux systems to use the method provided by @irql-notlessorequal ### Fixed - Fixed Civitai-generated image parsing in Inference - Fixed some first-time setup crashes from missing prerequisites - Fixed one-click installer not using default preferred Python version - Fixed updating from old installs of InvokeAI using old frontend - Fixed [#1357](https://github.com/LykosAI/StabilityMatrix/issues/1357) - Case insensitivity causing duplicate key exceptions on non-Windows systems ### Supporters #### 🌟 Visionaries To our brilliant Visionary-tier Patrons: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, and **whudunit** — your support is the spark that keeps Stability Matrix blazing forward. Thanks to you, we can explore bolder features, tackle complex challenges, and keep making the impossible feel effortless. Thank you all so very much! 🚀 ## v2.15.0-dev.2 ### Added - Added new package - [FramePack](https://github.com/lllyasviel/FramePack) - Added new package - [FramePack Studio](https://github.com/colinurbs/FramePack-Studio) - Added support for authenticated model downloads in the HuggingFace model browser. Visit Settings → Accounts to add your HuggingFace token. - Added support for dragging-and-dropping Civitai-generated images into Inference to load metadata - Added the ability to search by pasting an entire Civitai model URL into the search bar in the Civitai model browser (when the Civitai API gets fixed) - Added "Clear Pip Cache" and "Clear uv Cache" commands to the Settings -> Embedded Python section ### Changed - Civitai model browser image loading now uses dynamic resizing for better performance and a smoother scrolling experience - Detailed notifications for Civitai model browser api errors - The main sidebar now remembers whether it was collapsed or expanded between restarts - Updated pre-selected download locations for certain model types in the Civitai model browser - Updated uv to 0.7.19 - Changed InvokeAI update process to no longer clone the repo ### Fixed - Fixed missing .NET 8 dependency for SwarmUI installs in certain cases - Fixed [#1291](https://github.com/LykosAI/StabilityMatrix/issues/1291) - Certain GPUs not being detected on Linux - Fixed [#1284](https://github.com/LykosAI/StabilityMatrix/issues/1284) - Output browser not ignoring InvokeAI thumbnails folders - Fixed [#1305](https://github.com/LykosAI/StabilityMatrix/issues/1305) - FluxGym installing incorrect packages for Blackwell GPUs - Fixed [#1316](https://github.com/LykosAI/StabilityMatrix/issues/1316) - Errors when installing Triton & SageAttention - Fixed "directory is not empty" error when updating packages with symlinks - Fixed missing base model types in the Checkpoint Manager & Civitai Model Browser ### Supporters #### 🌟 Visionaries A huge thank you to our amazing Visionary-tier Patrons: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, and our newest Visionary, **whudunit**! 🚀 Your generous support enables Stability Matrix to grow faster and tackle ambitious new ideas. You're truly making all the magic happen! ## v2.15.0-dev.1 ### Added - Added Python Version selector for all new package installs - Added the ability to rename packages ### Changed - Updated all Python version management, virtual environment creation, and pip installs to use `uv` for improved reliability, compatibility, and speed - The Civitai model browser Download Location selector will now remember the last location used based on the model type - New installs of ComfyUI, SD.Next, and InvokeAI will now use Python 3.12.10, unless otherwise specified in the Advanced Options during installation - New installs of all other packages will now use Python 3.10.17, unless otherwise specified in the Advanced Options during installation ### Supporters #### 🌟 Visionaries A massive thank you to our esteemed Visionary-tier Patrons: **Waterclouds**, **bluepopsicle**, **Bob S**, **Ibixat**, and **Corey T**! Your exceptional commitment propels Stability Matrix to new heights and allows us to push the boundaries of innovation. We're incredibly grateful for your foundational support! 🚀 ## v2.14.3 ### Added - Added the ability to search by pasting an entire Civitai model URL into the search bar in the Civitai model browser ### Changed - The main sidebar now remembers whether it was collapsed or expanded between restarts. - Inference is now able to load image metadata from Civitai generated images via drag & drop - Updated process tracking for ComfyUI to help mitigate restart issues when using Comfy Manager - Updated pre-selected download locations for certain model types in the Civitai model browser - Updated nodejs to v20.19.3 to support newer InvokeAI versions ### Fixed - Fixed missing .NET 8 dependency for SwarmUI installs in certain cases - Fixed ComfyUI-Zluda not being recognized as a valid Comfy install for the workflow browser - Fixed [#1291](https://github.com/LykosAI/StabilityMatrix/issues/1291) - Certain GPUs not being detected on Linux - Fixed [#1284](https://github.com/LykosAI/StabilityMatrix/issues/1284) - Output browser not ignoring InvokeAI thumbnails folders - Fixed [#1301](https://github.com/LykosAI/StabilityMatrix/issues/1301) - Error when installing kohya_ss - Fixed [#1305](https://github.com/LykosAI/StabilityMatrix/issues/1305) - FluxGym installing incorrect packages for Blackwell GPUs - Fixed [#1316](https://github.com/LykosAI/StabilityMatrix/issues/1316) - Errors when installing Triton & SageAttention - Fixed "directory is not empty" error when updating packages with symlinks - Fixed missing base model types in the Checkpoint Manager & Civitai Model Browser ### Supporters #### 🌟 Visionaries Big heartfelt thanks to our stellar Visionary-tier Patrons: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, and **whudunit**! 🌟 Your extraordinary generosity continues to fuel Stability Matrix’s journey toward innovation and excellence. We appreciate you immensely! #### 🚀 Pioneers Massive thanks to our fantastic Pioneer-tier Patrons: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, **Noah M**, **USATechDude**, **Thom**, and **SeraphOfSalem**! Your unwavering support keeps our community thriving and inspires us to push even further. You’re all awesome! ## v2.14.2 ### Changed - Changed Nvidia GPU detection to use compute capability level instead of the GPU name for certain feature gates / torch indexes ### Fixed - Fixed [#1266](https://github.com/LykosAI/StabilityMatrix/issues/1266) - crash when moving or deleting Lora models in the Checkpoint Manager - Fixed [#1268](https://github.com/LykosAI/StabilityMatrix/issues/1268) - wrong torch index used for Nvidia 1000-series GPUs and older - Fixed [#1269](https://github.com/LykosAI/StabilityMatrix/issues/1269), [#1257](https://github.com/LykosAI/StabilityMatrix/issues/1257), [#1234](https://github.com/LykosAI/StabilityMatrix/issues/1234) - "no such file or directory" errors when updating certain packages after folder migration - Fixed [#1274](https://github.com/LykosAI/StabilityMatrix/issues/1274), [#1276](https://github.com/LykosAI/StabilityMatrix/issues/1276) - incorrect torch installed when updating to InvokeAI v5.12+ - Fixed missing shared folder links for SwarmUI's diffusion_models and clip folders ### Supporters #### 🌟 Visionaries Our deepest gratitude to the brilliant Visionary-tier Patrons: **Waterclouds**, **bluepopsicle**, **Bob S**, **Ibixat**, and **Corey T**! Your incredible backing is instrumental in shaping the future of Stability Matrix and empowering us to deliver cutting-edge features. Thank you for believing in our vision! 🙏 #### 🚀 Pioneers A huge shout-out to our fantastic Pioneer-tier Patrons: **Mr. Unknown**, **tankfox**, **Szir777**, **Noah M**, **USATechDude**, **Thom**, **TheTekknician**, and **SeraphOfSalem**! Your consistent support and active engagement are vital to our community's growth and our ongoing development efforts. You truly make a difference! ✨ ## v2.14.1 ### Changed - Updated Inference Extra Networks (Lora / LyCORIS) base model filtering to consider SDXL variations (e.g., Noob AI / Illustrious) as compatible, unrecognized models or models with no base model will be considered compatible. - Changed hotkey for Inference prompt weight adjustment to (`⌘+Up`/`⌘+Down`) on macOS - Improved style consistency of Inference Prompt action buttons on top right - (Internal) Improved log console formatting & colorization for development ### Fixed - Fixed Inference hotkey weight adjustment multi-line behavior, now works as expected like the first line. - Fixed updates to versions with commit hash version parts not being recognized when the current version has no commit hash part. - Fixed Inference Extra Networks card not updating with newly added model files. - Fixed incorrect ROCmLibs being installed for RX 6800/6800XT users of Comfy-Zluda or AMDGPU-Forge - Fixed missing text when missing localized versions for Italian and Chinese languages - Fixed Python Packages dialog errors and potentially other issues due to concurrent OnLoaded events ### Supporters #### 🌟 Visionaries Big cheers to our incredible Visionary-tier Patrons: **bluepopsicle**, **Bob S**, **Ibixat**, **Waterclouds**, and **Corey T**! 🚀 Your amazing support lets us dream bigger and reach further every single month. Thanks for being the driving force behind Stability Matrix - we genuinely couldn't do it without you! #### 🚀 Pioneers Huge thanks to our fantastic Pioneer-tier Patrons: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, and **Noah M**! Special shoutout and welcome back to **TheTekknician**, and a warm welcome aboard to our newest Pioneers: **USATechDude**, **SeraphOfSalem**, and **Thom**! ✨ Your continued support keeps our community vibrant and pushes us to keep creating. You all rock! ## v2.14.0 ### Added #### New Packages & Integrations - Added new package - [Stable Diffusion WebUI AMDGPU Forge](https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu-forge) - Added new package - [Stable Diffusion WebUI Forge - Classic](https://github.com/Haoming02/sd-webui-forge-classic) - Added new Package Command (in the 3-dots menu) for installing Triton & SageAttention in ComfyUI #### Inference Features - Added Prompt Amplifier to Inference - click the magic wand 🪄 in the prompt editor to expand and enrich your ideas. Tailor the vibe with the ‘Feel’ selector and watch as your generations come to life with extra detail, coherence, and flair! - Added support for HiDream in Inference - see [ComfyUI Examples](https://comfyanonymous.github.io/ComfyUI_examples/hidream/) for more details - Added toggle for filtering Inference Extra Networks by base model - Added Rescale CFG addon to Inference - Added Swap Dimensions button between the width/height input in Inference - Added Ctrl+Tab/Ctrl+Shift+Tab shortcuts for navigating between Inference tabs - Added Align Your Steps scheduler to Inference - Added wildcards to Inference prompts, e.g. `{blue|green|red}` will randomly select one of the colors - Added Wan 2.1 Text to Video and Text to Image project types for Inference - Added new autocomplete tag source to Inference - [Danbooru/e621 merged tags](https://civitai.com/models/950325?modelVersionId=1419692) - Added Abstract Syntax Tree (AST) parsing for Inference prompts. This provides a more robust internal understanding of prompt structure, paving the way for future enhancements. - Added hotkey (`Ctrl+Up`/`Ctrl+Down`) in Inference prompt editors to adjust the weight emphasis of the token under the caret or the currently selected text. - This automatically wraps the token/selection in parentheses `()` if it's not already weighted. - It modifies existing weights within parentheses or adds weights if none exist (e.g. `(word:1.1)`). - Handles selection spanning multiple tokens intelligently. - Added Plasma Noise addon to Inference for text to image workflows #### Model Management & Discovery - Added Accelerated Model Discovery (Beta) (⚡ icon in Civitai Browser) for Insider+ supporters. Utilizes an optimized connection for dramatically faster, more responsive browsing of online model repositories. - Added OpenModelDB tab to the Model Browser - Added Wan 2.1 files to the HuggingFace model browser #### User Interface & Experience (UI/UX) - Added Undo/Redo commands to text editor context menus #### Internal / Developer Changes - (Internal) Introduced unified strategy pattern (`IConfigSharingStrategy`) to for handling different config file formats (JSON, YAML, FDS). - Added support for configuring nested paths in JSON and YAML files (e.g. `paths.models.vae`) via dot-notation in `SharedFolderLayoutRule.ConfigDocumentPaths`. - Packages can now use the `SharedFolderLayout` property to define a `ConfigFileType` and `ConfigSharingOptions` (like `RootKey`), without needing to implement custom configuration logic. ### Changed #### Inference Features - Improved the quality of Inference inpainting by upgrading the workflow behind the scenes. The workflow remains the same for you — just better results! - FaceDetailers in Inference will now inherit the primary sampler/scheduler/etc. by default. You can still manually set these by enabling the options via the ⚙️ button on the FaceDetailer card - Slightly rearranged the FaceDetailer card layout due to the above change - Inference "Extra Networks" selector now filters extra networks based on the selected base model - Merged Inference GGUF workflows into the UNet model loader option (no longer need to choose GGUF separately) #### Model Management & Discovery - Changed the names of some of the shared model folders to better reflect their contents - Improved Checkpoint Manager memory usage (thanks to @FireGeek for the profiling assistance!) - Performance optimizations for Checkpoint Manager (progress indicators now fully uses Compiled Bindings) #### Package Management & Compatibility - Upgraded HIP SDK installs to 6.2.4 for ComfyUI-Zluda and AMDGPU-Forge - Updated install for kohya_ss to support RTX 5000-series GPUs #### User Interface & Experience (UI/UX) - Improved window state handling - Updated some date strings to take into account the user's locale #### Localization - Updated Japanese, Brazilian Portuguese, Chinese, and Russian translations #### Internal / Developer Changes - (Internal) Upgraded FluentAvalonia to 2.3.0 - (Internal) Refactored configuration-based shared folder logic: Centralized handling into `SharedFoldersConfigHelper` and format-specific strategies, removing custom file I/O logic from individual package classes for improved consistency and maintainability. - Migrated packages ComfyUI (incl. Zluda), VladAutomatic (SD.Next), Sdfx, and StableSwarm to use the unified system for configuration and symlink based sharing. ### Fixed #### Installation, Compatibility & Core Functionality - Fixed RTX 5000-series GPU detection in certain cases - Fixed Package Updates and Change Version not using stored PyTorch index and instead using the default recommended index. - Fixed ComfyUI-Zluda not being recognized as an option for Inference or SwarmUI (for real this time) - Fixed errors from invalid pip specifiers in requirements files #### User Interface & Experience (UI/UX) - Fixed Image Viewer animation loader keeping file handles open, which resolves 2 different issues (OSes are fun): - (macOS) Fixed `FileNotFoundException` crash when navigating to Inference tab after deleting a Webp animation file previously opened in the Image Viewer Dialog. - (Windows) Fixed Webp animation files unable to be deleted without closing the app first. - Fixed Image Viewer `FileNotFoundException` on fetching image size, if navigating to a deleted image file. - (macOS) Fixed Webp / Gif animations RGB colors flipped. - Fixed window disappearing on macOS when the saved window size is very small - Fixed large white boxes appearing when tooltips are visible on macOS/Linux - Fixed package images sometimes showing as blank due to concurrent image caching. Requests to same image resources are now de-duplicated - Reduced memory usage from `ShowDisabledTooltipExtension` #### Inference & Workflows - Fixed some cases of missing custom nodes in SwarmUI - Fixed Inference ControlNet Preprocessors using incorrect resolution and increased maximum of smallest dimension to 16384 - Fixed Inference Extra Networks card not allowing for more than one model at a time #### Model Management & Discovery - Fixed missing base model options in the Metadata Editor - Fixed some crashes when using Accelerated Model Discovery ### Supporters #### Visionaries Our heartfelt gratitude goes out to our amazing Visionary-tier Patrons: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Akiro_Senkai**, and **Ibixat**! Your exceptional support is fundamental to the ongoing development and success of Stability Matrix. We are immensely grateful for your partnership and belief in the project! 🙏 #### Pioneers We also want to give a huge thank you to our dedicated Pioneer-tier Patrons: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, **NowFallenAngel**, **Al Gorithm**, and welcome to our newest Pioneer, **Noah M.**! Your consistent support and enthusiasm keep the momentum going. Thank you all for being such an important part of our community! ✨ ## v2.14.0-pre.2 ### Added - Added new package - [Stable Diffusion WebUI Forge - Classic](https://github.com/Haoming02/sd-webui-forge-classic) - Added Accelerated Model Discovery (Beta) (⚡ icon in Civitai Browser) for Insider+ supporters. Utilizes an optimized connection for dramatically faster, more responsive browsing of online model repositories. - Added Undo/Redo commands to text editor context menus - Added Prompt Amplifier to Inference - click the magic wand 🪄 in the prompt editor to expand and enrich your ideas. Tailor the vibe with the ‘Feel’ selector and watch as your generations come to life with extra detail, coherence, and flair! - (pre.2 re-release) Added support for HiDream in Inference - see [ComfyUI Examples](https://comfyanonymous.github.io/ComfyUI_examples/hidream/) for more details - (pre.2 re-release) Added toggle for filtering Inference Extra Networks by base model ### Changed - Updated install for kohya_ss to support RTX 5000-series GPUs - (pre.2 re-release) Merged Inference GGUF workflows into the UNet model loader option (no longer need to choose GGUF separately) - (pre.2 re-release) Updated some date strings to take into account the user's locale - (pre.2 re-release) Fixed some crashes when using Accelerated Model Discovery - (pre.2 re-release) Performance optimizations for Checkpoint Manager (progress indicators now fully uses Compiled Bindings) ### Fixed - Fixed Inference ControlNet Preprocessors using incorrect resolution and increased maximum of smallest dimension to 16384 - Fixed Triton/Sage install option showing for incompatible GPUs - Fixed errors from invalid pip specifiers in requirements files - Fixed package images sometimes showing as blank due to concurrent image caching. Requests to same image resources are now de-duplicated - (pre.2 re-release) Fixed Inference Extra Networks card not allowing for more than one model at a time - (pre.2 re-release) Reduced memory usage from `ShowDisabledTooltipExtension` ### Supporters #### Visionaries - Big shout-out to our Visionary-tier patrons: Waterclouds, Corey T, bluepopsicle, and Bob S! Your steadfast support keeps Stability Matrix moving forward, and we couldn’t do it without you. 🚀 Thank you! ## v2.14.0-pre.1 ### Added - Added new Package Command (in the 3-dots menu) for installing Triton & SageAttention in ComfyUI - Added Abstract Syntax Tree (AST) parsing for Inference prompts. This provides a more robust internal understanding of prompt structure, paving the way for future enhancements. - Added hotkey (`Ctrl+Up`/`Ctrl+Down`) in Inference prompt editors to adjust the weight emphasis of the token under the caret or the currently selected text. - This automatically wraps the token/selection in parentheses `()` if it's not already weighted. - It modifies existing weights within parentheses or adds weights if none exist (e.g. `(word:1.1)`). - Handles selection spanning multiple tokens intelligently. - Added Plasma Noise addon to Inference for text to image workflows - (Internal) Introduced unified strategy pattern (`IConfigSharingStrategy`) to for handling different config file formats (JSON, YAML, FDS). - Added support for configuring nested paths in JSON and YAML files (e.g. `paths.models.vae`) via dot-notation in `SharedFolderLayoutRule.ConfigDocumentPaths`. - Packages can now use the `SharedFolderLayout` property to define a `ConfigFileType` and `ConfigSharingOptions` (like `RootKey`), without needing to implement custom configuration logic. ### Changed - Changed the names of some of the shared model folders to better reflect their contents - Improved window state handling - Improved Checkpoint Manager memory usage (thanks to @FireGeek for the profiling assistance!) - Upgraded HIP SDK installs to 6.2.4 for ComfyUI-Zluda and AMDGPU-Forge - (Internal) Upgraded FluentAvalonia to 2.3.0 - (Internal) Refactored configuration-based shared folder logic: Centralized handling into `SharedFoldersConfigHelper` and format-specific strategies, removing custom file I/O logic from individual package classes for improved consistency and maintainability. - Migrated packages ComfyUI (incl. Zluda), VladAutomatic (SD.Next), Sdfx, and StableSwarm to use the unified system for configuration and symlink based sharing. ### Fixed - Fixed RTX 5000-series GPU detection in certain cases - Fixed Image Viewer animation loader keeping file handles open, which resolves 2 different issues (OSes are fun): - (macOS) Fixed `FileNotFoundException` crash when navigating to Inference tab after deleting a Webp animation file previously opened in the Image Viewer Dialog. - (Windows) Fixed Webp animation files unable to be deleted without closing the app first. - Fixed Image Viewer `FileNotFoundException` on fetching image size, if navigating to a deleted image file. - (macOS) Fixed Webp / Gif animations RGB colors flipped. - Fixed Package Updates and Change Version not using stored PyTorch index and instead using the default recommended index. - Fixed some cases of missing custom nodes in SwarmUI - Fixed window disappearing on macOS when the saved window size is very small - Fixed ComfyUI-Zluda not being recognized as an option for Inference or SwarmUI (for real this time) - Fixed missing base model options in the Metadata Editor - Fixed large white boxes appearing when tooltips are visible on macOS/Linux ### Supporters #### Visionaries - A special shout-out to our fantastic Visionary-tier Patreon supporters: Waterclouds, Corey T, and our newest Visionaries, bluepopsicle and Bob S! Your continued generosity powers the future of Stability Matrix—thank you so much! ## v2.14.0-dev.3 ### Added - Added Wan 2.1 Text to Video and Text to Image project types for Inference - Added Wan 2.1 files to the HuggingFace model browser - Added new package - [Stable Diffusion WebUI AMDGPU Forge](https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu-forge) - Added support for RTX 5000-series GPUs in ComfyUI, Forge, and reForge - Added "Rebuild .NET Project" command to SwarmUI installs - available via the 3-dots menu -> Package Commands -> Rebuild .NET Project - Added new autocomplete tag source to Inference - [Danbooru/e621 merged tags](https://civitai.com/models/950325?modelVersionId=1419692) ### Changed - Upgraded ComfyUI CUDA torch to 12.6 - Upgraded Lykos account connection to use OAuth 2.0 device flow - (Internal) Updated Avalonia to 11.2.5 ### Fixed - Fixed [#1128](https://github.com/LykosAI/StabilityMatrix/issues/1128) - overwriting models when downloading multiple with the same name - Fixed ROCm torch indexes for ComfyUI & Forge - Fixed model browser sometimes downloading to `ModelsLora` or `ModelsStableDiffusion` folders instead of the correct folder - Fixed incorrect Unet folder path for ComfyUI users on Linux/macOS - Fixed [#1157](https://github.com/LykosAI/StabilityMatrix/issues/1157) - crash when broken symlinks exist in model directories - Fixed [#1154](https://github.com/LykosAI/StabilityMatrix/issues/1154) - increased width for package name on the package cards - Fixed ComfyUI-Zluda not being recognized as an option for Inference or SwarmUI - Fixed SwarmUI showing Python options in the 3-dots menu - Fixed SD.Next install failures in certain cases when using Zluda ### Supporters #### Visionaries - Many thanks to our amazing Visionary-tier Patreon supporters, **Waterclouds**, **TheTekknician**, and **Corey T**! Your unwavering support is very much appreciated! ## v2.14.0-dev.2 ### Added - Added Align Your Steps scheduler to Inference - Added wildcards to Inference prompts, e.g. `{blue|green|red}` will randomly select one of the colors - Added Safetensor Metadata viewer to the Checkpoint Manager context menu - thanks to @genteure! ### Changed - Updated the Civitai Model Browser base model selector to match the new Checkpoint Manager filter UI - FaceDetailers in Inference will now inherit the primary sampler/scheduler/etc. by default. You can still manually set these by enabling the options via the ⚙️ button on the FaceDetailer card - Slightly rearranged the FaceDetailer card layout due to the above change - "Remove symbolic links on shutdown" option now also removes links from Output Sharing - Inference "Extra Networks" selector now filters extra networks based on the selected base model - Updated Japanese, Brazilian Portuguese, Chinese, and Russian translations ### Fixed - Fixed crash when dragging & dropping images in Inference (hopefully) - Fixed HiresFix Inference addon not inheriting sampler/scheduler properly - Fixed some plus (+) buttons getting cut off in the Inference UI - Fixed CFG Rescale addon interfering with refiner model in Inference - Fixed [#1083](https://github.com/LykosAI/StabilityMatrix/issues/1083) - "Show Nested Models" incorrectly displaying models from some non-nested folders - Fixed issue with InvokeAI model sharing when the host address is set to 0.0.0.0 - Fixed issue when parsing index URLs in Python Dependencies Override menu - Fixed ComfyUI-Zluda not respecting pip user overrides - Fixed issue with Checkpoint Manager not displaying any models - (dev.2 re-release) Fixed autocomplete not showing in certain cases when using wildcards - (dev.2 re-release) Fixed package restart button not working - (dev.2 re-release) Fixed [#1120](https://github.com/LykosAI/StabilityMatrix/issues/1120) - crash when right clicking in the console after restarting a package ### Supporters #### Visionaries - A huge thank you to our incredible Visionary-tier Patreon supporters, **Waterclouds**, **TheTekknician**, and our newest Visionary, **Corey**! Your generous support is greatly appreciated! ## v2.14.0-dev.1 ### Added - Added Rescale CFG addon to Inference - Added Swap Dimensions button between the width/height input in Inference - Added Ctrl+Tab/Ctrl+Shift+Tab shortcuts for navigating between Inference tabs - Added OpenModelDB tab to the Model Browser ### Changed - Improved the quality of Inference inpainting by upgrading the workflow behind the scenes. The workflow remains the same for you — just better results! - Redesigned the Checkpoint Manager Filter flyout to include more options and improve the layout - "Clear All" button will now remain at the top of the Downloads list regardless of scroll position - thanks to @Genteure! - Improved image metadata parsing - thanks to @Genteure! ### Fixed - Fixed Inference image selector card buttons taking up the whole height of the card - Fixed Inference mask editor failing to paint to the right-most edge on large images - Fixed Inference mask editor not showing the entire image in certain circumstances - Fixed an issue where certain sampler/scheduler combos would not get saved in image metadata - thanks to @yansigit! - Fixed [#1078](https://github.com/LykosAI/StabilityMatrix/issues/1078) - "Call from invalid thread" error after one-click install finishes - Fixed [#1080](https://github.com/LykosAI/StabilityMatrix/issues/1080) - Some models not displayed in Checkpoint Manager ### Supporters #### Visionaries - Many thanks to our incredible Visionary-tier Patreon supporters, **Waterclouds** and **TheTekknician**! Your support helps us continue to improve Stability Matrix! ## v2.13.4 ### Added - Added support for RTX 5000-series GPUs in ComfyUI, Forge, and reForge - Added "Rebuild .NET Project" command to SwarmUI installs - available via the 3-dots menu -> Package Commands -> Rebuild .NET Project ### Changed - Upgraded ComfyUI CUDA torch to 12.6 - Upgraded Lykos account connection to use OAuth 2.0 device flow - (Internal) Updated Avalonia to 11.2.5 ### Fixed - Fixed [#1128](https://github.com/LykosAI/StabilityMatrix/issues/1128) - overwriting models when downloading multiple with the same name - Fixed ROCm torch indexes for ComfyUI & Forge - Fixed model browser sometimes downloading to `ModelsLora` or `ModelsStableDiffusion` folders instead of the correct folder - Fixed incorrect Unet folder path for ComfyUI users on Linux/macOS - Fixed [#1157](https://github.com/LykosAI/StabilityMatrix/issues/1157) - crash when broken symlinks exist in model directories - Fixed [#1154](https://github.com/LykosAI/StabilityMatrix/issues/1154) - increased width for package name on the package cards - Fixed ComfyUI-Zluda not being recognized as an option for Inference or SwarmUI - Fixed SwarmUI showing Python options in the 3-dots menu - Fixed SD.Next install failures in certain cases when using Zluda ### Supporters #### Visionaries - Huge thanks to our amazing Visionary-tier Patrons, **Waterclouds** and **Corey T**! We're truly grateful for your continued generosity and support! #### Pioneers - Special appreciation to our fantastic Pioneer-tier Patrons: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, **NowFallenAngel**, and our newest addition, **Al Gorithm**! Thank you all for your incredible commitment and ongoing encouragement! ## v2.13.3 ### Changed - "Remove symbolic links on shutdown" option now also removes links from Output Sharing ### Fixed - Fixed [#1083](https://github.com/LykosAI/StabilityMatrix/issues/1083) - "Show Nested Models" incorrectly displaying models from some non-nested folders - Fixed [#1120](https://github.com/LykosAI/StabilityMatrix/issues/1120) - crash when right clicking in the console after restarting a package - Fixed issue with InvokeAI model sharing when the host address is set to 0.0.0.0 - Fixed issue when parsing index URLs in Python Dependencies Override menu - Fixed issue where models were filtered incorrectly in the Checkpoint Manager - Fixed ComfyUI-Zluda not using the user-defined pip overrides ### Supporters #### Visionaries - A heartfelt thank you to our incredible Visionary-tier Patrons, **Waterclouds**, **TheTekknician**, and **Corey**! Your unwavering support means the world to us! #### Pioneers - A big shoutout to our outstanding Pioneer-tier Patrons, **tankfox**, **Mr. Unknown**, **Szir777**, and **NowFallenAngel**! We deeply appreciate your ongoing support and dedication! ## v2.13.2 ### Changed - Removed SimpleSDXL due to security concerns - thanks to @iwr-redmond for the detailed report. For more information please visit https://github.com/LykosAI/StabilityMatrix/security/advisories. ### Supporters #### Visionaries - Many thanks to our amazing Visionary-tier Patrons, **Waterclouds** and **TheTekknician**! Your support is greatly appreciated! #### Pioneers - Shoutout to our Pioneer-tier Patrons, **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, and **NowFallenAngel**! Thank you for your continued support! ## v2.13.1 ### Changed - Redesigned the Checkpoint Manager Filter flyout to include more options and improve the layout - "Clear All" button will now remain at the top of the Downloads list regardless of scroll position - thanks to @Genteure! - Improved image metadata parsing - thanks to @Genteure! ### Fixed - Fixed [#1078](https://github.com/LykosAI/StabilityMatrix/issues/1078) - "Call from invalid thread" error after one-click install finishes - Fixed [#1080](https://github.com/LykosAI/StabilityMatrix/issues/1080) - Some models not displayed in Checkpoint Manager - Fixed Inference image selector card buttons taking up the whole height of the card - Fixed Inference mask editor failing to paint to the right-most edge on large images - Fixed Inference mask editor not showing the entire image in certain circumstances - Fixed crash when dragging & dropping images in Inference (hopefully) - Fixed an issue where certain sampler/scheduler combos would not get saved in image metadata - thanks to @yansigit! ### Supporters #### Visionaries - A heartfelt thank you to our exceptional Visionary-tier Patreon backers, **Waterclouds** and **TheTekknician**! We truly appreciate your steadfast support! #### Pioneers - We are also very grateful to our wonderful Pioneer-tier Patreon supporters, **tankfox**, **Mr Unknown**, **Szir777**, **Tigon**, and **NowFallenAngel**! Your support means a lot to us! ## v2.13.0 ### Added - Added new package - [ComfyUI-Zluda](https://github.com/patientx/ComfyUI-Zluda) - for AMD GPU users on Windows - Added file sizes to the Checkpoint Manager tab - Added the Discrete Model Sampling addon for Inference samplers, allows selecting different sampling methods, such as v_prediction, lcm, or x0, and optionally adjusts the model’s noise reduction strategy with the zero-shot noise ratio (ZSNR) toggle. - Added Default GPU override in Settings -> System Settings -> Default GPU - Added new "Copy" menu to the Inference gallery context menu, allowing you to copy generation parameters as well as the image - Added "StableDiffusion" folder as an option when downloading Flux models in the CivitAI model browser - Added support for SD3.5 in Inference - Added CLIP_G to HuggingFace model browser - Added search bar to the Installed Workflows tab - Added "Search with Google" and "Search with ChatGPT" to the package console output & install progress console output context menus - Added "Date Created" and "Date Last Modified" sorting options to the Checkpoints tab - Added a new "Extension Packs" section to the extension manager, allowing you to create packs for easier installation of multiple extensions at once - Added "Search by Creator" command to Civitai browser context menu - Added Beta scheduler to the scheduler selector in Inference - Added zipping of log files and "Show Log in Explorer" button on exceptions dialog for easier support - Added max concurrent downloads option & download queueing for most downloads - Added the ability to change the Models directory separately from the rest of the Data directory. This can be set in `Settings > Select new Models Folder` - Added InvokeAI model sharing option ### Changed - Improved Packages Page grid layout to dynamically stretch to fill available space - Text Encoder / CLIP selection in Inference is now enabled via the cogwheel ⚙️ button next to the model selector - Updated Civitai model descriptions to properly render the interactive elements - Adjusted the Branch/Release toggle during package install flow to be a little more obvious - Updated the Dock library used for Inference - fixes some weirdness with resizing / rearranging panels - New file format and key derivation for protecting locally encrypted secrets (i.e. Civit / Lykos accounts) that is no longer dependent on the OS Version. This should prevent system updates from clearing account logins. - (Internal) Updated to .NET 9 Runtime and Avalonia 11.2.2 for performance improvements, lower memory usage, and bug fixes - Updated pytorch index to `rocm6.2` for AMD users of ComfyUI on Linux ### Fixed - Fixed text alignment issues in the Downloads tab for certain long names / progress infos - Improved startup performance and resource usage with optimizations to hardware lookups. Moved reflection usages in dependency injection to source generation. - Fixed an issue with ComfyUI-Impact-Subpack not being installed when using FaceDetailer in Inference - Fixed GGUF models not showing in Inference without the GGUF extension installed (this means it will now properly prompt you to install the extension as well) ### Supporters #### Visionaries - We're extremely grateful to our incredible Visionary-tier Patreon supporters, **Waterclouds** and **TheTekknician**! Thank you very much for your unwavering support! #### Pioneers - Many thanks to our amazing Pioneer-tier Patreon supporters, **tankfox**, **Mr Unknown**, **Szir777**, and our newest Pioneer, **NowFallenAngel**! Your generous support is very much appreciated! ## v2.13.0-pre.2 ### Added - Added new package - [ComfyUI-Zluda](https://github.com/patientx/ComfyUI-Zluda) - for AMD GPU users on Windows - Added "StableDiffusion" folder as an option when downloading Flux models in the CivitAI model browser ### Changed - Updated pytorch index to `rocm6.2` for AMD users of ComfyUI on Linux ### Supporters #### Visionaries - Big shoutout to our incredible Visionary-tier Patreon supporter, **Waterclouds**! We're also delighted to introduce our newest Visionary-tier Patreon supporter, **TheTekknician**! Thank you both for your generous support! ## v2.13.0-pre.1 ### Added - Added new package - [CogVideo](https://github.com/THUDM/CogVideo) - many thanks to @NullDev for the contribution! - Added file sizes to the Checkpoint Manager tab - Added more formatting options for Inference output filenames - thanks to @yansigit! - Added the Discrete Model Sampling addon for Inference samplers, allows selecting different sampling methods, such as v_prediction, lcm, or x0, and optionally adjusts the model’s noise reduction strategy with the zero-shot noise ratio (ZSNR) toggle. - Added Default GPU override in Settings -> System Settings -> Default GPU - Added the ability to copy more generation parameters from the Inference gallery context menu ### Changed - Improved Packages Page grid layout to dynamically stretch to fill available space - New file format and key derivation for protecting locally encrypted secrets (i.e. Civit / Lykos accounts) that is no longer dependent on the OS Version. This should prevent system updates from clearing account logins. - (Internal) Updated to .NET 9 Runtime and Avalonia 11.2.2 for performance improvements, lower memory usage, and bug fixes ### Fixed - Improved startup performance and resource usage with optimizations to hardware lookups. Moved reflection usages in dependency injection to source generation. - Fixed a typo in the Japanese translation - thanks to @mattyatea! - Fixed missing package thumbnails due to moved or inaccessible urls - Fixed an issue with ComfyUI-Impact-Subpack not being installed when using FaceDetailer in Inference - Fixed GGUF models not showing in Inference without the GGUF extension installed (this means it will now properly prompt you to install the extension as well) ### Supporters #### Visionaries - Huge thank you to our incredible Visionary-tier Patreon supporter, **Waterclouds**! Your unwavering support is very much appreciated! ## v2.13.0-dev.3 ### Added - Added support for SD3.5 in Inference - Added CLIP_G to HuggingFace model browser - Added search bar to the Installed Workflows tab - Added "Search with Google" and "Search with ChatGPT" to the package console output & install progress console output context menus - Added "Date Created" and "Date Last Modified" sorting options to the Checkpoints tab ### Changed - Text Encoder / CLIP selection in Inference is now enabled via the cogwheel ⚙️ button next to the model selector - Added more base model types to the CivitAI Model Browser & Checkpoint Manager - Model browser base model types are now loaded dynamically from CivitAI, reducing the need for updates to add new types - Updated Civitai model descriptions to properly render the interactive elements - Updated Russian translations thanks to @vanja-san - Updated Simplified Chinese translations thanks to @QL-boy - (Internal) Updated to Avalonia 11.2.0 ### Fixed - Fixed some instances of Civitai model browser not loading new results - Fixed "Unsupported Torch Version: Cuda" errors when installing a1111 - Fixed crash when clicking "Remind me Later" on the update dialog - Fixed some cases of crashing when GitHub API rate limits are exceeded - Fixed Git missing from env vars when running SwarmUI ### Supporters #### Visionaries - Big shoutout to our amazing Visionary-tier Patreon supporter, **Waterclouds**! We are very grateful for your continued support! ## v2.13.0-dev.2 ### Added - Added new package - [SimpleSDXL](https://github.com/metercai/SimpleSDXL) - many thanks to @NullDev for the contribution! - Added new package - [FluxGym](https://github.com/cocktailpeanut/fluxgym) - many thanks to @NullDev for the contribution! - Added a new "Extension Packs" section to the extension manager, allowing you to create packs for easier installation of multiple extensions at once - Added "Search by Creator" command to Civitai browser context menu - Added Beta scheduler to the scheduler selector in Inference - Added zipping of log files and "Show Log in Explorer" button on exceptions dialog for easier support - Added max concurrent downloads option & download queueing for most downloads ### Changed - (Internal) Updated to Avalonia 11.1.4 - Adjusted the Branch/Release toggle during package install flow to be a little more obvious - Updated the Dock library used for Inference - fixes some weirdness with resizing / rearranging panels ### Fixed - Fixed ComfyUI NF4 extension not installing properly when prompted in Inference - Fixed [#932](https://github.com/LykosAI/StabilityMatrix/issues/932), [#935](https://github.com/LykosAI/StabilityMatrix/issues/935), [#939](https://github.com/LykosAI/StabilityMatrix/issues/939) - InvokeAI failing to update - Fixed repeated nested folders being created in `Models/StableDiffusion` when using Forge in Symlink mode in certain conditions. Existing folders will be repaired to their original structure on launch. - Fixed minimize button not working on macOS - Fixed InvokeAI model sharing spamming the console with "This may take awhile" in certain conditions - Fixed text alignment issues in the Downloads tab for certain long names / progress infos ### Supporters #### Visionaries - A big thank you to our amazing Visionary-tier Patreon supporter, **Waterclouds**! Your continued support is invaluable! ## v2.13.0-dev.1 ### Added - Added the ability to change the Models directory separately from the rest of the Data directory. This can be set in `Settings > Select new Models Folder` - Added "Copy" menu to the Inference gallery context menu, allowing you to copy the image or the seed (other params coming soon™️) - Added InvokeAI model sharing option ### Supporters #### Visionaries - A heartfelt thank you to our incredible Visionary-tier Patreon supporter, **Waterclouds**! Your ongoing support means a lot to us, and we’re grateful to have you with us on this journey! ## v2.12.4 ### Added - Added new package - [CogVideo](https://github.com/THUDM/CogVideo) - many thanks to @NullDev for the contribution! - Added more formatting options for Inference output filenames - thanks to @yansigit! ### Changed - Model browser base model types are now loaded dynamically from CivitAI, reducing the need for updates to add new types ### Fixed - Fixed crash when clicking "Remind me Later" on the update dialog - Fixed some cases of crashing when GitHub API rate limits are exceeded - Fixed Git missing from env vars when running SwarmUI - Fixed missing package thumbnails due to moved or inaccessible urls - Fixed an issue with updating FluxGym in certain cases - thanks to @NullDev! - Fixed a typo in the Japanese translation - thanks to @mattyatea! ### Supporters #### Visionaries - A huge thank you to our dedicated Visionary-tier Patreon supporter, **Waterclouds**! We’re thrilled to have your ongoing support! #### Pioneers - Shoutout to our great Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, **Szir777**, and our newest Pioneer, **Tigon**!. Your continued support is greatly appreciated! ## v2.12.3 ### Added - Added new package - [SimpleSDXL](https://github.com/metercai/SimpleSDXL) - many thanks to @NullDev for the contribution! - Added new package - [FluxGym](https://github.com/cocktailpeanut/fluxgym) - many thanks to @NullDev for the contribution! - Added more base model types to the CivitAI Model Browser & Checkpoint Manager ### Changed - Updated Russian translations thanks to @vanja-san - Updated Simplified Chinese translations thanks to @QL-boy ### Fixed - Fixed some cases of FileTransferExists error when running re/Forge or Automatic1111 - Fixed update check not happening on startup for some users - Fixed error when installing Automatic1111 on macOS - Fixed some instances of Civitai model browser not loading new results - Fixed "Unsupported Torch Version: Cuda" errors when installing a1111 ### Supporters #### Visionaries - A huge shout-out to our dedicated Visionary-tier Patreon supporter, **Waterclouds**! Your unwavering support is greatly appreciated! #### Pioneers - We'd also like to express our gratitude to our amazing Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**! Your ongoing support means a great deal! ## v2.12.2 ### Added - Added Beta scheduler to the scheduler selector in Inference ### Changed - (Internal) Updated to Avalonia 11.1.4 ### Fixed - Fixed ComfyUI NF4 extension not installing properly when prompted in Inference - Fixed [#932](https://github.com/LykosAI/StabilityMatrix/issues/932), [#935](https://github.com/LykosAI/StabilityMatrix/issues/935), [#939](https://github.com/LykosAI/StabilityMatrix/issues/939) - InvokeAI failing to update - Fixed repeated nested folders being created in `Models/StableDiffusion` when using Forge in Symlink mode in certain conditions. Existing folders will be repaired to their original structure on launch. - Fixed minimize button not working on macOS ### Supporters #### Visionaries - We extend our heartfelt appreciation to our dedicated Visionary-tier Patreon supporter, **Waterclouds**. Your ongoing support is invaluable! #### Pioneers - We’d also like to thank our great Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**. Your continuous support means a lot! ## v2.12.1 ### Fixed - Fixed [#916](https://github.com/LykosAI/StabilityMatrix/issues/916) - InvokeAI failing to install/update on macOS - Fixed [#914](https://github.com/LykosAI/StabilityMatrix/issues/914) - Unable to use escaped colon `:` character in Inference prompts - Fixed [#908](https://github.com/LykosAI/StabilityMatrix/issues/908) - Forge unable to use models from "unet" shared folder - Fixed [#902](https://github.com/LykosAI/StabilityMatrix/issues/902) - Images from shared outputs folder not displaying properly in Stable Diffusion WebUI-UX - Fixed [#898](https://github.com/LykosAI/StabilityMatrix/issues/898) - Incorrect launch options for RuinedFooocus - Fixed index url parsing in Python Packages window causing some packages to not have versions available - Fixed a crash when switching between Model Sharing options for certain packages ### Supporters #### Visionaries - A sincere thank you to our valued Visionary-tier Patreon supporter, **Waterclouds**. Your continued support is truly appreciated, and we’re grateful to have you with us on this journey. #### Pioneers - We’d also like to extend our gratitude to our Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**. Your ongoing support means a great deal to us! ## v2.12.0 ### Added #### New Packages - [Fooocus - mashb1t's 1-Up Edition](https://github.com/mashb1t/Fooocus) by mashb1t - [Stable Diffusion WebUI reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge/) by Panchovix #### Inference - Added type-to-search for the Inference model selectors. Start typing while the dropdown is open to navigate the list. - Added "Model Loader" option to Inference, for loading UNet/GGUF/NF4 models (e.g. Flux) - Added support for the FP8 version of Flux in the default Model Loader via the "Use Flux Guidance" Sampler Addon - Added trigger words to the Inference Extra Networks (Lora/Lyco) selector for quick copy & paste - Image viewer context menus now have 2 options: `Copy (Ctrl+C)` which now always copies the image as a file, and `Copy as Bitmap (Shift+Ctrl+C)` (Available on Windows) which copies to the clipboard as native bitmap. This changes the previous single `Copy` button behavior that would first attempt a native bitmap copy on Windows when available, and fall back to a file copy if not. - Added Face Detailer module to Inference #### Package Manager - Added Python dependencies override table to package installation options, where the default pip packages may be overriden for a package's install and updates. This can be changed later or added to existing packages through `Package Menu > Python Dependencies Override` - Added "Change Version" option to the package card overflow menu, allowing you to downgrade or upgrade a package to a specific version or commit ([#701](https://github.com/LykosAI/StabilityMatrix/issues/701), [#857](https://github.com/LykosAI/StabilityMatrix/issues/857)) - Added "Disable Update Check" option to the package card overflow menu, allowing you to disable update checks for a specific package - Added Custom commit option in the Advanced Options for package installs ([#670](https://github.com/LykosAI/StabilityMatrix/issues/670), [#839](https://github.com/LykosAI/StabilityMatrix/issues/839), [#842](https://github.com/LykosAI/StabilityMatrix/issues/842)) - Added macOS support for Fooocus & related forks - Added Intel OneAPI XPU backend (IPEX) option for SD.Next #### Checkpoint Manager - Added new Metadata Editor (accessible via the right-click menu), allowing you to create or edit metadata for models - Added "New Directory" and "Delete" options to the context menu of the tree view. - Added new toggle for drag & drop - when enabled, all selected models will now move together with the dragged model - Added "File Size" sorting option - Added "Hide Empty Categories" toggle - Added "Select All" button to the InfoBar (shown when at least one model is selected) - Added "Show NSFW Images" toggle #### Model Browser - Added "Hide Installed Models" toggle to the CivitAI Model Browser - Added toggle to hide "Early Access" models in the CivitAI Model Browser - Added ultralytics models to HuggingFace model browser #### Other - Added "Sign in with Google" option for connecting your Lykos Account on the Account Settings page - Added zoom sliders for Outputs, Checkpoints, and Model Browser pages - Added Settings option "Console: History Size" to adjust the number of lines stored in the console history when running packages. Defaults to 9001 lines. - Added optional anonymous usage reporting for gauging popularity of package installs and features. You will be asked whether you want to enable this feature on launch, and can change your choice at any time in `Settings > System > Analytics` - Added "Run Command" option in Settings for running a command with the embedded Python or Git executables - Added "Enable Long Paths" option for Git to the Settings page - Added "System Settings > Enable Long Paths" option to enable NTFS long paths on Windows - Added Korean translations thanks to maakcode! - (Windows, Linux) Added Vulkan rendering support using launch argument `--vulkan`. (On Windows, the default WinUI composition renderer is likely still preferrable. Linux users are encouraged to try the new renderer to see if it improves performance and responsiveness.) ### Changed - Optimized image loading across the app, with loading speed now up to 4x faster for local images, and up to 17x faster for remote images - Image loading in the Outputs page now uses native memory management for ~2x less peak memory usage, and will release memory more quickly when switching away from the Outputs page or scrolling images out of view - Improved animation fluidity of image rendering while scrolling quickly across large collections (e.g. Outputs, Model Browser) - ComfyUI will no longer be pinned to torch 2.1.2 for nvidia users on Windows ([#861](https://github.com/LykosAI/StabilityMatrix/issues/861)) - Model browser download progress no longer covers the entire card for the entire duration of the download - Updated torch index to `rocm6.1` for AMD users of ComfyUI - Show better error message for early access model downloads - Updated torch version for a1111 on mac - Checkpoints tab now shows "image hidden" for images that are hidden by the NSFW filter - OAuth-type connection errors in Account Settings now show a more detailed error message - The "Download Failed" message for model downloads is now persistent until dismissed - Separated the Generate button from the prompt control in Inference so it can be moved like other controls - Updated translations for Turkish and Russian - (Internal) Updated Avalonia to 11.1.3 - Includes major rendering and performance optimizations, animation refinements, improved IME / text selection, and improvements for window sizing / z-order / multi-monitor DPI scaling. ([avaloniaui.net/blog/avalonia-11-1-a-quantum-leap-in-cross-platform-ui-development](https://avaloniaui.net/blog/avalonia-11-1-a-quantum-leap-in-cross-platform-ui-development)) - (Internal) Updated SkiaSharp (Rendering Backend) to 3.0.0-preview.4.1, potentially fixes issues with window rendering artifacts on some machines. - (Internal) Updated other dependencies for security and bug fixes. ### Fixed - Fixed [#888](https://github.com/LykosAI/StabilityMatrix/issues/888) - error updating kohya_ss due to long paths - Fixed some ScrollViewers changing scroll position when focus changes - Fixed CivitAI Model Browser sometimes incorrectly showing "No models found" before toggling "Show NSFW" or "Hide Installed" filters - Fixed SwarmUI settings being overwritten on launch - Fixed issue where some Inference-generated images would be saved with the bottom missing - Fixed [#851](https://github.com/LykosAI/StabilityMatrix/issues/851) - missing fbgemm.dll errors when using latest torch with certain packages - Fixed issue where ApproxVAE models would show up in the VAE folder - Fixed [#878](https://github.com/LykosAI/StabilityMatrix/issues/878) - Checkpoints tab will no longer try to browse directories it can't access - Fixed crash when opening Settings page when refreshing CivitAI account status results in an error - Fixed [#814](https://github.com/LykosAI/StabilityMatrix/issues/814), [#875](https://github.com/LykosAI/StabilityMatrix/issues/875) - Error when installing RuinedFooocus - LORAs are now sorted by model name properly in the Extra Networks dropdown - (macOS) Fixed OAuth connection prompts in Account Settings not automatically updating status after connection. Custom URL schemes are now also supported on macOS builds. ### Supporters #### Visionaries - A heartfelt thank you to our Visionary-tier patron, **Waterclouds**! We greatly appreciate your continued support! #### Pioneers - A special shoutout to our Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**! Your unwavering support means a great deal! ## v2.12.0-pre.3 ### Added - Added Python dependencies override table to package installation options, where the default pip packages may be overriden for a package's install and updates. This can be changed later or added to existing packages through `Package Menu > Python Dependencies Override` - Added optional anonymous usage reporting for gauging popularity of package installs and features. You will be asked whether you want to enable this feature on launch, and can change your choice at any time in `Settings > System > Analytics` - Added Korean translations thanks to maakcode! ### Changed - Show better error message for early access model downloads - Updated torch version for a1111 on mac - Checkpoints tab now shows "image hidden" for images that are hidden by the NSFW filter - Updated translations for Turkish and Russian ### Fixed - Fixed issue where some Inference-generated images would be saved with the bottom missing - Fixed CivitAI Browser page scroll refresh not ordering models correctly - Fixed missing fbgemm.dll errors when using latest torch with certain packages - Fixed issue where ApproxVAE models would show up in the VAE folder - Fixed [#878](https://github.com/LykosAI/StabilityMatrix/issues/878) - Checkpoints tab will no longer try to browse directories it can't access - Fixed crash when opening Settings page when refreshing CivitAI account status results in an error ### Supporters #### Visionaries - A huge thank you to our Visionary-tier Patreon supporter, **Waterclouds**! We appreciate your continued support, and are grateful to have you on this journey with us! ## v2.12.0-pre.2 ### Added - Added "Show NSFW Images" toggle to the Checkpoints page - Added "Model Loader" option to Inference, for loading UNet/GGUF/NF4 models (e.g. Flux) - Added type-to-search for the Inference model selectors. Start typing while the dropdown is open to navigate the list. - Added "Sign in with Google" option for connecting your Lykos Account on the Account Settings page ### Changed - Updated Brazilian Portuguese translations thanks to thiagojramos - Merged the "Flux Text to Image" workflow back into the main Text to Image workflow ### Fixed - Fixed CivitAI Model Browser sometimes incorrectly showing "No models found" before toggling "Show NSFW" or "Hide Installed" filters - Fixed Automatic1111 & related packages not including the gradio-allowed-path argument for the shared output folder - Fixed SwarmUI settings being overwritten on launch - Fixed Forge output folder links pointing to the incorrect folder - LORAs are now sorted by model name properly in the Extra Networks dropdown - Fixed errors when downloading models with invalid characters in the file name ## v2.12.0-pre.1 ### Added - Added "Hide Installed Models" toggle to the CivitAI Model Browser ### Changed - ComfyUI will no longer be pinned to torch 2.1.2 for nvidia users on Windows - Model browser download progress no longer covers the entire card for the entire duration of the download - Updated torch index to `rocm6.0` for AMD users of ComfyUI - (Internal) Updated to Avalonia 11.1.2 - OAuth-type connection errors in Account Settings now show a more detailed error message ### Fixed - Fixed Inference not connecting with "Could not connect to backend - JSON value could not be converted" error with API changes from newer ComfyUI versions - (macOS) Fixed OAuth connection prompts in Account Settings not automatically updating status after connection. Custom URL schemes are now also supported on macOS builds. ## v2.12.0-dev.3 ### Added - Added Settings option "Console: History Size" to adjust the number of lines stored in the console history when running packages. Defaults to 9001 lines. #### Inference - Added new project type, "Flux Text to Image", a Flux-native workflow for text-to-image projects - Added support for the FP8 version of Flux in the regular Text to Image and Image to Image workflows via the "Use Flux Guidance" Sampler Addon #### Model Browser - Added AuraFlow & Flux base model types to the CivitAI model browser - Added CLIP/Text Encoders section to HuggingFace model browser #### Checkpoint Manager - Added new Metadata Editor (accessible via the right-click menu), allowing you to create or edit metadata for models - Added "New Directory" and "Delete" options to the context menu of the tree view. - Added new toggle for drag & drop - when enabled, all selected models will now move together with the dragged model - Added "File Size" sorting option - Added "Hide Empty Categories" toggle - Added "Select All" button to the InfoBar (shown when at least one model is selected) - Added "unet" shared model folder for ComfyUI ### Changed - Optimized image loading across the app, with loading speed now up to 4x faster for local images, and up to 17x faster for remote images - Image loading in the Outputs page now uses native memory management for ~2x less peak memory usage, and will release memory more quickly when switching away from the Outputs page or scrolling images out of view - Improved animation fluidity of image rendering while scrolling quickly across large collections (e.g. Outputs, Model Browser) - The "Download Failed" message for model downloads is now persistent until dismissed - Separated the Generate button from the prompt control in Inference so it can be moved like other controls ### Fixed - Fixed "The version of the native libSkiaSharp library (88.1) is incompatible with this version of SkiaSharp." error for Linux users - Fixed download links for IPAdapters in the HuggingFace model browser - Fixed potential memory leak of transient controls (Inference Prompt and Output Image Viewer) not being garbage collected due to event subscriptions - Fixed Batch Count seeds not being recorded properly in Inference projects and image metadata ### Supporters #### Visionaries - A heartfelt thank you to our Visionary-tier Patreon supporter, **Scopp Mcdee**! We truly appreciate your continued support! ## v2.12.0-dev.2 ### Added - Added Face Detailer module to Inference - Added ultralytics models to HuggingFace model browser - Added DoRA category to CivitAI model browser - Added macOS support for Fooocus & related forks - (Windows, Linux) Added Vulkan rendering support using launch argument `--vulkan`. (On Windows, the default WinUI composition renderer is likely still preferrable. Linux users are encouraged to try the new renderer to see if it improves performance and responsiveness.) ### Changed - (Internal) Updated Avalonia to 11.1.1 - Includes major rendering and performance optimizations, animation refinements, improved IME / text selection, and improvements for window sizing / z-order / multi-monitor DPI scaling. ([avaloniaui.net/blog/avalonia-11-1-a-quantum-leap-in-cross-platform-ui-development](https://avaloniaui.net/blog/avalonia-11-1-a-quantum-leap-in-cross-platform-ui-development)) - (Internal) Updated SkiaSharp (Rendering Backend) to 3.0.0-preview.4.1, potentially fixes issues with window rendering artifacts on some machines. - (Internal) Updated other dependencies for security and bug fixes. ### Fixed - Fixed some ScrollViewers changing scroll position when focus changes - Fixed [#782](https://github.com/LykosAI/StabilityMatrix/issues/782) - conflict error when launching new versions of Forge - Fixed incorrect torch versions being installed for InvokeAI ### Supporters #### Visionaries - A huge thank you goes out to our esteemed Visionary-tier Patreon backers: **Scopp Mcdee**, **Waterclouds**, and **Akiro_Senkai**. Your kind support means the world! ## v2.12.0-dev.1 ### Added - Added new package: [Fooocus - mashb1t's 1-Up Edition](https://github.com/mashb1t/Fooocus) by mashb1t - Added new package: [Stable Diffusion WebUI reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge/) by Panchovix - Image viewer context menus now have 2 options: `Copy (Ctrl+C)` which now always copies the image as a file, and `Copy as Bitmap (Shift+Ctrl+C)` (Available on Windows) which copies to the clipboard as native bitmap. This changes the previous single `Copy` button behavior that would first attempt a native bitmap copy on Windows when available, and fall back to a file copy if not. - Added "Change Version" option to the package card overflow menu, allowing you to downgrade or upgrade a package to a specific version or commit - Added "Disable Update Check" option to the package card overflow menu, allowing you to disable update checks for a specific package - Added "Run Command" option in Settings for running a command with the embedded Python or Git executables - Added Intel OneAPI XPU backend (IPEX) option for SD.Next ### Supporters #### Visionaries - Shoutout to our Visionary-tier Patreon supporters, **Scopp Mcdee**, **Waterclouds**, and our newest Visionary, **Akiro_Senkai**! Many thanks for your generous support! ## v2.11.8 ### Added - Added Flux & AuraFlow types to CivitAI Browser - Added unet folder links for ComfyUI thanks to jeremydk - Added CLIP folder links for Forge ### Changed - Updated Brazilian Portuguese translations thanks to thiagojramos ### Fixed - Fixed [#840](https://github.com/LykosAI/StabilityMatrix/issues/840) - CivitAI model browser not loading search results - Fixed SwarmUI settings being overwritten on launch - Fixed [#832](https://github.com/LykosAI/StabilityMatrix/issues/832) [#847](https://github.com/LykosAI/StabilityMatrix/issues/847) - Forge output folder links pointing to the incorrect folder - Fixed errors when downloading models with invalid characters in the file name - Fixed error when installing RuinedFooocus on nvidia GPUs ### Supporters #### Pioneers - A big shoutout to our Pioneer-tier patrons: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**! We deeply appreciate your ongoing support! ## v2.11.7 ### Changed - Forge will use the recommended pytorch version 2.3.1 the next time it is updated - InvokeAI users with AMD GPUs on Linux will be upgraded to the rocm5.6 version of pytorch the next time it is updated ### Fixed - Fixed Inference not connecting with "Could not connect to backend - JSON value could not be converted" error with API changes from newer ComfyUI versions ### Supporters #### Pioneers - Shoutout to our Pioneer-tier supporters on Patreon: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**! Thanks for all of your continued support! ## v2.11.6 ### Fixed - Fixed incorrect IPAdapter download links in the HuggingFace model browser - Fixed potential memory leak of transient controls (Inference Prompt and Output Image Viewer) not being garbage collected due to event subscriptions - Fixed Batch Count seeds not being recorded properly in Inference projects and image metadata - Fixed [#795](https://github.com/LykosAI/StabilityMatrix/issues/795) - SwarmUI launch args not working properly - Fixed [#745](https://github.com/LykosAI/StabilityMatrix/issues/745) - not passing Environment Variables to SwarmUI ### Supporters #### Visionaries - Shoutout to our Visionary-tier Patreon supporter, **Scopp Mcdee**! Huge thanks for your continued support! #### Pioneers - Many thanks to our Pioneer-tier supporters on Patreon: **tankfox**, **tanangular**, **Mr. Unknown**, and **Szir777**! Your continued support is greatly appreciated! ## v2.11.5 ### Added - Added DoRA category to CivitAI model browser ### Fixed - Fixed `TaskCanceledException` when adding CivitAI Api key or searching for models when the API takes too long to respond. Retry and timeout behavior has been improved. - Fixed [#782](https://github.com/LykosAI/StabilityMatrix/issues/782) - conflict error when launching new versions of Forge - Fixed incorrect torch versions being installed for InvokeAI - Fixed `ArgumentOutOfRangeException` with the Python Packages dialog ItemSourceView when interacting too quickly after loading. ### Supporters #### Visionaries - Shoutout to our Visionary-tier Patreon supporters, **Scopp Mcdee**, **Waterclouds**, and our newest Visionary, **Akiro_Senkai**! Many thanks for your generous support! #### Pioneers - Many thanks to our Pioneer-tier supporters on Patreon, **tankfox**, **tanangular**, and our newest Pioneers, **Mr. Unknown** and **Szir777**! Your support is greatly appreciated! ## v2.11.4 ### Changed - Base Python install will now use `setuptools==69.5.1` for compatibility with `torchsde`. Individual Packages can upgrade as required. - Improved formatting of "Copy Details" action on the Unexpected Error dialog - (Debug) Logging verbosity for classes can now be configured with environment variables (`Logging__LogLevel__`). ### Fixed - Fixed ComfyUI slower generation speed with new torch versions not including flash attention for windows, pinned `torch==2.1.2` for ComfyUI on Windows CUDA - Fixed [#719](https://github.com/LykosAI/StabilityMatrix/issues/719) - Fix comments in Inference prompt not being ignored - Fixed TaskCanceledException when Inference prompts finish before the delayed progress handler (250ms) ### Supporters #### Visionaries - Huge thanks to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your support helps us continue to improve Stability Matrix! #### Pioneers - Thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support is greatly appreciated! ## v2.11.3 ### Changed - Base Python install will now use `pip>=23.3.2,<24.1` for compatibility with `torchsde`.Individual Packages can upgrade as required. - Added default `PIP_DISABLE_PIP_VERSION_CHECK=1` environment variable to suppress notices about pip version checks. - As with other default environment variables, this can be overridden by setting your own value in `Settings > Environment Variables [Edit]`. ### Fixed - Fooocus Package - Added `pip>=23.3.2,<24.1` specifier before install, fixes potential install errors due to deprecated requirement spec used by `torchsde`. - Fixed error when launching SwarmUI when installed to a path with spaces - Fixed issue where model folders were being created too late in certain cases - Fixed [#683](https://github.com/LykosAI/StabilityMatrix/issues/683) - Model indexing causing LiteDB errors after upgrading from older versions due to updated enum values ### Supporters #### Visionaries - Huge thanks to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your support helps us continue to improve Stability Matrix! #### Pioneers - Thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support is greatly appreciated! ## v2.11.2 ### Changed - StableSwarmUI installs will be migrated to SwarmUI by mcmonkeyprojects the next time the package is updated - Note: As of 2024/06/21 StableSwarmUI will no longer be maintained under Stability AI. The original developer will be maintaining an independent version of this project ### Fixed - Fixed [#700](https://github.com/LykosAI/StabilityMatrix/issues/700) - `cannot import 'packaging'` error for Forge ### Supporters #### Visionaries - Huge thanks to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your support helps us continue to improve Stability Matrix! #### Pioneers - Thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support is greatly appreciated! ## v2.11.1 ### Added - Added Rename option back to the Checkpoints page ### Changed - Unobserved Task Exceptions across the app will now show a toast notification to aid in debugging - Updated SD.Next Package details and thumbnail - [#697](https://github.com/LykosAI/StabilityMatrix/pull/697) ### Fixed - Fixed [#689](https://github.com/LykosAI/StabilityMatrix/issues/689) - New ComfyUI installs encountering launch error due to torch 2.0.0 update, added pinned `numpy==1.26.4` to install and update. - Fixed Inference image mask editor's 'Load Mask' not able to load image files - Fixed Fooocus ControlNet default config shared folder mode not taking effect - Fixed tkinter python libraries not working on macOS with 'Can't find a usable init.tcl' error ### Supporters #### Visionaries - Shoutout to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your generous support is appreciated and helps us continue to make Stability Matrix better for everyone! #### Pioneers - A big thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support helps us continue to improve Stability Matrix! ## v2.11.0 ### Added #### Packages - Added new package: [SDFX](https://github.com/sdfxai/sdfx/) by sdfxai - Added ZLUDA option for SD.Next - Added more launch options for Forge - [#618](https://github.com/LykosAI/StabilityMatrix/issues/618) - Added search bar to the Python Packages dialog #### Inference - Added Inpainting support for Image To Image projects using the new image mask canvas editor - Added alternate Lora / LyCORIS drop-down model selection, can be toggled via the model settings button. Allows choosing both CLIP and Model Weights. The existing prompt-based `` method is still available. - Added optional Recycle Bin mode when deleting images in the Inference image browser, can be disabled in settings (Currently available on Windows and macOS) #### Model Browsers - Added PixArt, SDXL Hyper, and SD3 options to the CivitAI Model Browser - Added XL ControlNets section to HuggingFace model browser - Added download speed indicator to model downloads in the Downloads tab #### Output Browser - Added support for indexing and displaying jpg/jpeg & gif images (in additional to png and webp / animated webp), with metadata parsing and search for compatible formats #### Settings - Added setting for locale specific or invariant number formatting - Added setting for toggling model browser auto-search on load - Added option in Settings to choose whether to Copy or Move files when dragging and dropping files into the Checkpoint Manager - Added folder shortcuts in Settings for opening common app and system folders, such as Data Directory and Logs #### Translations - Added Brazilian Portuguese language option, thanks to jbostroski for the translation! ### Changed - Maximized state is now stored on exit and restored on launch - Drag & drop imports now move files by default instead of copying - Clicking outside the Select Model Version dialog will now close it - Changed Package card buttons to better indicate that they are buttons - Log file storage has been moved from `%AppData%/StabilityMatrix` to a subfolder: `%AppData%/StabilityMatrix/Logs` - Archived log files now have an increased rolling limit of 9 files, from 2 files previously. Their file names will now be in the format `app.{yyyy-MM-dd HH_mm_ss}.log`. The current session log file remains named `app.log`. - Updated image controls on Recommended Models dialog to match the rest of the app - Improved app shutdown clean-up process reliability and speed - Improved ProcessTracker speed and clean-up safety for faster subprocess and package launching performance - Updated HuggingFace page so the command bar stays fixed at the top - Revamped Checkpoints page now shows available model updates and has better drag & drop functionality - Revamped file deletion confirmation dialog with affected file paths display and recycle bin / permanent delete options (Checkpoint and Output Browsers) (Currently available on Windows and macOS) ### Fixed - Fixed crash when parsing invalid generated images in Output Browser and Inference image viewer, errors will be logged instead and the image will be skipped - Fixed missing progress text during package updates - (Windows) Fixed "Open in Explorer" buttons across the app not opening the correct path on ReFS partitions - (macOS, Linux) Fixed Subprocesses of packages sometimes not being closed when the app is closed - Fixed Inference tabs sometimes not being restored from previous sessions - Fixed multiple log files being archived in a single session, and losing some log entries - Fixed error when installing certain packages with comments in the requirements file - Fixed error when deleting Inference browser images in a nested project path with recycle bin mode - Fixed extra text in positive prompt when loading image parameters in Inference with empty negative prompt value - Fixed NullReferenceException that sometimes occurred when closing Inference tabs with images due to Avalonia.Bitmap.Size accessor issue - Fixed [#598](https://github.com/LykosAI/StabilityMatrix/issues/598) - program not exiting after printing help or version text - Fixed [#630](https://github.com/LykosAI/StabilityMatrix/issues/630) - InvokeAI update hangs forever waiting for input - Fixed issue where the "installed" state on HuggingFace model browser was not always correct - Fixed model folders not being created on startup ### Supporters #### Visionaries - Shoutout to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your generous support is appreciated and helps us continue to make Stability Matrix better for everyone! #### Pioneers - A big thank you to our Pioneer-tier supporters on Patreon, **tankfox** and **tanangular**! Your support helps us continue to improve Stability Matrix! ## v2.11.0-pre.2 ### Added - Added folder shortcuts in Settings for opening common app and system folders, such as Data Directory and Logs. ### Changed - Log file storage have been moved from `%AppData%/StabilityMatrix` to a subfolder: `%AppData%/StabilityMatrix/Logs` - Archived log files now have an increased rolling limit of 9 files, from 2 files previously. Their file names will now be in the format `app.{yyyy-MM-dd HH_mm_ss}.log`. The current session log file remains named `app.log`. - Updated image controls on Recommended Models dialog to match the rest of the app - Improved app shutdown clean-up process reliability and speed - Improved ProcessTracker speed and clean-up safety for faster subprocess and package launching performance ### Fixed - Fixed crash when parsing invalid generated images in Output Browser and Inference image viewer, errors will be logged instead and the image will be skipped - Fixed issue where blue and red color channels were swapped in the mask editor dialog - Fixed missing progress text during package updates - Fixed "Git and Node.js are required" error during SDFX install - (Windows) Fixed "Open in Explorer" buttons across the app not opening the correct path on ReFS partitions - (Windows) Fixed Sdfx electron window not closing when stopping the package - (macOS, Linux) Fixed Subprocesses of packages sometimes not being closed when the app is closed - Fixed Inference tabs sometimes not being restored from previous sessions - Fixed multiple log files being archived in a single session, and losing some log entries - Fixed error when installing certain packages with comments in the requirements file - Fixed some more missing progress texts during various activities ### Supporters #### Visionaries - A heartfelt thank you to our Visionary-tier Patreon supporters, **Scopp Mcdee** and **Waterclouds**! Your generous contributions enable us to keep enhancing Stability Matrix! ## v2.11.0-pre.1 ### Added - Added new package: [SDFX](https://github.com/sdfxai/sdfx/) by sdfxai - Added "Show Nested Models" toggle for new Checkpoints page, allowing users to show or hide models in subfolders of the selected folder - Added ZLUDA option for SD.Next - Added PixArt & SDXL Hyper options to the Civitai model browser - Added release date to model update notification card on the Checkpoints page - Added option in Settings to choose whether to Copy or Move files when dragging and dropping files into the Checkpoint Manager - Added more launch options for Forge - [#618](https://github.com/LykosAI/StabilityMatrix/issues/618) #### Inference - Added Inpainting support for Image To Image projects using the new image mask canvas editor ### Changed - Maximized state is now stored on exit and restored on launch - Clicking outside the Select Model Version dialog will now close it - Changed Package card buttons to better indicate that they are buttons ### Fixed - Fixed error when deleting Inference browser images in a nested project path with recycle bin mode - Fixed extra text in positive prompt when loading image parameters in Inference with empty negative prompt value - Fixed NullReferenceException that sometimes occured when closing Inference tabs with images due to Avalonia.Bitmap.Size accessor issue - Fixed package installs not showing any progress messages - Fixed crash when viewing model details for Unknown model types in the Checkpoint Manager - Fixed [#598](https://github.com/LykosAI/StabilityMatrix/issues/598) - program not exiting after printing help or version text - Fixed [#630](https://github.com/LykosAI/StabilityMatrix/issues/630) - InvokeAI update hangs forever waiting for input ### Supporters #### Visionaries - Many thanks to our Visionary-tier supporters on Patreon, **Scopp Mcdee** and **Waterclouds**! Your generous support helps us continue to improve Stability Matrix! ## v2.11.0-dev.3 ### Added - Added download speed indicator to model downloads in the Downloads tab - Added XL ControlNets section to HuggingFace model browser - Added toggle in Settings for model browser auto-search on load - Added optional Recycle Bin mode when deleting images in the Inference image browser, can be disabled in settings (Currently on Windows only) ### Changed - Revamped Checkpoints page now shows available model updates and has better drag & drop functionality - Updated HuggingFace page so the command bar stays fixed at the top - Revamped file deletion confirmation dialog with affected file paths display and recycle bin / permanent delete options (Checkpoint and Output Browsers) (Currently on Windows only) ### Fixed - Fixed issue where the "installed" state on HuggingFace model browser was not always correct ### Supporters #### Visionaries - Special shoutout to our first two Visionaries on Patreon, **Scopp Mcdee** and **Waterclouds**! Thank you for your generous support! ## v2.11.0-dev.2 ### Added - Added Brazilian Portuguese language option, thanks to jbostroski for the translation! - Added setting for locale specific or invariant number formatting - Added support for jpg/jpeg & gif images in the Output Browser ### Changed - Centered OpenArt browser cards ### Fixed - Fixed MPS install on macOS for ComfyUI, A1111, SDWebUI Forge, and SDWebUI UX causing torch to be upgraded to dev nightly versions and causing incompatibilities with dependencies. - Fixed "Auto Scroll to End" not working in some scenarios - Fixed "Auto Scroll to End" toggle button not scrolling to the end when toggled on - Fixed/reverted output folder name changes for Automatic1111 - Fixed xformers being uninstalled with every ComfyUI update - Fixed Inference Lora menu strength resetting to default if out of slider range (0 to 1) - Fixed missing progress text during package installs ## v2.11.0-dev.1 ### Added - Added search bar to the Python Packages dialog #### Inference - Alternate Lora / LyCORIS drop-down model selection, can be toggled via the model settings button. The existing prompt-based Lora / LyCORIS method is still available. ### Fixed - Fixed crash when failing to parse Python package details ## v2.10.3 ### Changed - Centered OpenArt browser cards ### Fixed - Fixed MPS install on macOS for ComfyUI, A1111, SDWebUI Forge, and SDWebUI UX causing torch to be upgraded to dev nightly versions and causing incompatibilities with dependencies. - Fixed crash when failing to parse Python package details - Fixed "Auto Scroll to End" not working in some scenarios - Fixed "Auto Scroll to End" toggle button not scrolling to the end when toggled on - Fixed/reverted output folder name changes for Automatic1111 - Fixed xformers being uninstalled with every ComfyUI update - Fixed missing progress text during package installs ## v2.10.2 ### Changed - Updated translations for Spanish and Turkish ### Fixed - Fixed more crashes when loading invalid connected model info files - Fixed pip installs not parsing comments properly - Fixed crash when sending input to a process that isn't running - Fixed breadcrumb on console page showing incorrect running package name - Fixed [#576](https://github.com/LykosAI/StabilityMatrix/issues/576) - drag & drop crashes on macOS & Linux - Fixed [#594](https://github.com/LykosAI/StabilityMatrix/issues/594) - missing thumbnails in Inference model selector - Fixed [#600](https://github.com/LykosAI/StabilityMatrix/issues/600) - kohya_ss v24+ not launching - Downgraded Avalonia back to 11.0.9 to fix [#589](https://github.com/LykosAI/StabilityMatrix/issues/589) and possibly other rendering issues ## v2.10.1 ### Added - Added SVD Shared Model & Output Folders for Forge (fixes [#580](https://github.com/LykosAI/StabilityMatrix/issues/580)) ### Changed - Improved error message when logging in with a Lykos account fails due to incorrect email or password - Model Browser & Workflow Browser now auto-load when first navigating to those pages - Removed update confirmation dialog, instead showing the new version in the update button tooltip ### Fixed - Fixed package launch not working when environment variable `SETUPTOOLS_USE_DISTUTILS` is set due to conflict with a default environment variable. User environment variables will now correctly override any default environment variables. - Fixed "No refresh token found" error when failing to login with Lykos account in some cases - Fixed blank entries appearing in the Categories dropdown on the Checkpoints page - Fixed crash when loading invalid connected model info files - Fixed [#585](https://github.com/LykosAI/StabilityMatrix/issues/585) - Crash when drag & drop source and destination are the same - Fixed [#584](https://github.com/LykosAI/StabilityMatrix/issues/584) - `--launch-package` argument not working - Fixed [#581](https://github.com/LykosAI/StabilityMatrix/issues/581) - Inference teaching tip showing more often than it should - Fixed [#578](https://github.com/LykosAI/StabilityMatrix/issues/578) - "python setup.py egg_info did not run successfully" failure when installing Auto1111 or SDWebUI Forge - Fixed [#574](https://github.com/LykosAI/StabilityMatrix/issues/574) - local images not showing on macOS or Linux ## v2.10.0 ### Added - Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting. - Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image. - Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too. - Added Layer Diffuse sampler addon to Inference, allows generating foreground with transparency with SD1.5 and SDXL. - Added support for deep links from the new Stability Matrix Chrome extension - Added OpenArt.AI workflow browser for ComfyUI workflows - Added more metadata to the image dialog info flyout - Added Output Sharing toggle in Advanced Options during install flow ### Changed - Revamped the Packages page to enable running multiple packages at the same time - Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector - Model download location selector now searches all subfolders - Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option. - Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading - Updated translations for French, Spanish, and Turkish - Changed to a new image control for pages with many images - (Internal) Updated to Avalonia 11.0.10 ### Fixed - Fixed [#559](https://github.com/LykosAI/StabilityMatrix/issues/559) - "Unable to load bitmap from provided data" error in Checkpoints page - Fixed [#522](https://github.com/LykosAI/StabilityMatrix/issues/522) - Incorrect output directory path for latest Auto1111 - Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update - Fixed Civitai model browser error when sorting by Installed with more than 100 installed models - Fixed CLIP Install errors due to setuptools distutils conflict, added default environment variable setting `SETUPTOOLS_USE_DISTUTILS=stdlib` - Fixed progress bars not displaying properly during package installs & updates - Fixed ComfyUI extension updates not running install.py / updating requirements.txt - Improved performance when deleting many images from the Outputs page - Fixed ComfyUI torch downgrading to 2.1.2 when updating - Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked - Fixed model download location options for VAEs in the CivitAI Model Browser ### Removed - Removed the main Launch page, as it is no longer needed with the new Packages page ## v2.10.0-pre.2 ### Added - Added more metadata to the image dialog info flyout - Added Restart button to console page ### Changed - Model download location selector now searches all subfolders ### Fixed - Fixed Civitai model browser not showing images when "Show NSFW" is disabled - Fixed crash when Installed Workflows page is opened with no Workflows folder - Fixed progress bars not displaying properly during package installs & updates - Fixed ComfyUI extension updates not running install.py / updating requirements.txt ## v2.10.0-pre.1 ### Added - Added OpenArt.AI workflow browser for ComfyUI workflows - Added Output Sharing toggle in Advanced Options during install flow ### Changed - Changed to a new image control for pages with many images - Removed Symlink option for InvokeAI due to changes with InvokeAI v4.0+ - Output sharing is now enabled by default for new installations - (Internal) Updated to Avalonia 11.0.10 ### Fixed - Improved performance when deleting many images from the Outputs page - Fixed ComfyUI torch downgrading to 2.1.2 when updating - Fixed [#529](https://github.com/LykosAI/StabilityMatrix/issues/529) - OneTrainer requesting input during update - Fixed "Could not find entry point for InvokeAI" error on InvokeAI v4.0+ ## v2.10.0-dev.3 ### Added - Added support for deep links from the new Stability Matrix Chrome extension ### Changed - Due to changes on the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser - Due to the above API changes, new pages are now loaded via "infinite scrolling" ### Fixed - Fixed Inference HiresFix module "Inherit Primary Sampler Addons" setting not effectively disabling when unchecked - Fixed model download location options for VAEs in the CivitAI Model Browser - Fixed crash on startup when library directory is not set - Fixed One-Click install progress dialog not disappearing after completion - Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area - Fixed no packages being available for one-click install on PCs without a GPU - Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page - Fixed missing ratings on some models in the CivitAI Model Browser - Fixed missing favorite count in the CivitAI Model Browser - Fixed recommended models not showing all SDXL models ## v2.10.0-dev.2 ### Added - Added Reference-Only mode for Inference ControlNet, used for guiding the sampler with an image without a pretrained model. Part of the latent and attention layers will be connected to the reference image, similar to Image to Image or Inpainting. ### Changed - Inference Primary Sampler Addons (i.e. ControlNet, FreeU) are now inherited by Hires Fix Samplers, this can be overriden from the Hires Fix module's settings menu by disabling the "Inherit Primary Sampler Addons" option. - Revisited the way images are loaded on the outputs page, with improvements to loading speed & not freezing the UI while loading ### Fixed - Fixed Outputs page not remembering where the user last was in the TreeView in certain circumstances - Fixed Inference extension upgrades not being added to missing extensions list for prompted install - Fixed "The Open Web UI button has moved" teaching tip spam ## v2.10.0-dev.1 ### Added - Inference ControlNet module now supports over 42 preprocessors, a new button next to the preprocessors dropdown allows previewing the output of the selected preprocessor on the image. - Added resolution selection for Inference ControlNet module, this controls preprocessor resolution too. ### Changed - Revamped the Packages page to enable running multiple packages at the same time - Changed the Outputs Page to use a TreeView for the directory selection instead of a dropdown selector ### Removed - Removed the main Launch page, as it is no longer needed with the new Packages page ## v2.9.3 ### Changed - Removed Symlink option for InvokeAI to prevent InvokeAI from moving models into its own directories (will be replaced with a Config option in a future update) ### Fixed - Fixed images not appearing in Civitai Model Browser when "Show NSFW" was disabled - Fixed [#556](https://github.com/LykosAI/StabilityMatrix/issues/556) - "Could not find entry point for InvokeAI" error ## v2.9.2 ### Changed - Due to changes with the CivitAI API, you can no longer select a specific page in the CivitAI Model Browser - Due to the above API changes, new pages are now loaded via "infinite scrolling" ### Fixed - Fixed models not being removed from the installed models cache when deleting them from the Checkpoints page - Fixed model download location options for VAEs in the CivitAI Model Browser - Fixed One-Click install progress dialog not disappearing after completion - Fixed ComfyUI with Inference pop-up during one-click install appearing below the visible scroll area - Fixed no packages being available for one-click install on PCs without a GPU ## v2.9.1 ### Added - Fixed [#498](https://github.com/LykosAI/StabilityMatrix/issues/498) Added "Pony" category to CivitAI Model Browser ### Changed - Changed package deletion warning dialog to require additional confirmation ### Fixed - Fixed [#502](https://github.com/LykosAI/StabilityMatrix/issues/502) - missing launch options for Forge - Fixed [#500](https://github.com/LykosAI/StabilityMatrix/issues/500) - missing output images in Forge when using output sharing - Fixed [#490](https://github.com/LykosAI/StabilityMatrix/issues/490) - `mpmath has no attribute 'rational'` error on macOS - Fixed [#510](https://github.com/ionite34/StabilityMatrix/pull/564/files) - kohya_ss packages with v23.0.x failing to install due to missing 'packaging' dependency - Fixed incorrect progress text when deleting a checkpoint from the Checkpoints page - Fixed incorrect icon colors on macOS ## v2.9.0 ### Added - Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI - Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel - Added extension management for SD.Next and Stable Diffusion WebUI-UX - Added the ability to choose where CivitAI model downloads are saved - Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`) - Added more Base Model search options to the CivitAI Model Browser - Added Stable Cascade to the HuggingFace Model Browser #### Inference - Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2) - Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file. - Added copy image support on linux and macOS for Inference outputs viewer menu ### Changed - Updated translations for German, Spanish, French, Japanese, Portuguese, and Turkish - (Internal) Updated to Avalonia 11.0.9 ### Fixed - Fixed StableSwarmUI not installing properly on macOS - Fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS - Fixed [#335](https://github.com/LykosAI/StabilityMatrix/issues/335) Update hanging indefinitely after git step for Auto1111 and SDWebUI Forge - Fixed Inference output viewer menu "Copy" not copying image - Fixed image viewer dialog arrow key navigation not working - Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins - Fixed unknown model types not showing on checkpoints page (thanks Jerry!) - Improved error handling for Inference Select Image hash calculation in case file is being written to while being read ## v2.9.0-pre.2 ### Added - Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`) - Added more Base Model search options to the CivitAI Model Browser - Added Stable Cascade to the HuggingFace Model Browser ### Changed - (Internal) Updated to Avalonia 11.0.9 ### Fixed - Fixed image viewer dialog arrow key navigation not working - Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins ## v2.9.0-pre.1 ### Added - Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2) - Added copy image support on linux and macOS for Inference outputs viewer menu ### Fixed - Fixed StableSwarmUI not installing properly on macOS - Fixed output sharing for Stable Diffusion WebUI Forge - Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS - Fixed default command line args for SDWebUI Forge on macOS - Fixed output paths and output sharing for SDWebUI Forge - Maybe fixed update hanging for Auto1111 and SDWebUI Forge - Fixed Inference output viewer menu "Copy" not copying image ## v2.9.0-dev.2 ### Added #### Inference - Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file. ### Fixed - Fixed icon sizes of Inference Addons and Steps buttons ## v2.9.0-dev.1 ### Added - Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI - Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel - Added extension management for SD.Next and Stable Diffusion WebUI-UX - Added the ability to choose where CivitAI model downloads are saved ## v2.8.4 ### Fixed - Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS ## v2.8.3 ### Fixed - Fixed user tokens read error causing failed downloads - Failed downloads will now log error messages - Fixed [#458](https://github.com/LykosAI/StabilityMatrix/issues/458) - Save Intermediate Image not working - Fixed [#453](https://github.com/LykosAI/StabilityMatrix/issues/453) - Update Fooocus `--output-directory` argument to `--output-path` ## v2.8.2 ### Added - Added missing GFPGAN link to Automatic1111 packages ### Fixed - Fixed Inference Image to Image Denoise setting becoming hidden after changing schedulers - Fixed Inference ControlNet models showing as downloadable even when they are already installed - Fixed Inference Sampler Addon conditioning not applying (i.e. ControlNet) - Fixed extension modification dialog not showing any progress messages ## v2.8.1 ### Fixed - Fixed model links not working in RuinedFooocus for new installations - Fixed incorrect nodejs download link on Linux (thanks to slogonomo for the fix) - Fixed failing InvokeAI install on macOS due to missing nodejs - Increased timeout on Recommended Models call to prevent potential timeout errors on slow connections - Fixed SynchronizationLockException when saving settings - Improved error messages with process output for 7z extraction errors - Fixed missing tkinter dependency for OneTrainer on Windows - Fixed auto-update on macOS not starting new version from an issue in starting .app bundles with arguments - Fixed [#436](https://github.com/LykosAI/StabilityMatrix/issues/436) - Crash on invalid json files during checkpoint indexing ## v2.8.0 ### Added - Added Image to Video project type - Added CLIP Skip setting to inference, toggleable from the model settings button - Added image and model details in model selection boxes - Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer) - Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation) - Currently available on Windows and Linux, macOS support is pending - Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and Automatic1111. - Added new launch argument options for Fooocus - Added "Config" Shared Model Folder option for Fooocus - Added Recommended Models dialog after one-click installer - Added "Copy Details" button to Unexpected Error dialog - Added German language option, thanks to Mario da Graca for the translation - Added Portuguese language options, thanks to nextosai for the translation - Added base model filter to Checkpoints page - Added "Compatible Images" category when selecting images for Inference projects - Added "Find in Model Browser" option to the right-click menu on the Checkpoints page - Added `--use-directml` launch argument for SDWebUI DirectML fork - Added release builds for macOS (Apple Silicon) - Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision - Added Delete button to the CivitAI Model Browser details dialog - Added "Copy Link to Clipboard" for connected models in the Checkpoints page - Added support for webp files to the Output Browser - Added "Send to Image to Image" and "Send to Image to Video" options to the context menu ### Changed - New package installation flow - Changed one-click installer to match the new package installation style - Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation. - Search box on Checkpoints page now searches tags and trigger words - Changed the Close button on the package install dialog to "Hide" - Functionality remains the same, just a name change - Updated translations for the following languages: - Spanish - French - Japanese - Turkish - Inference file name patterns with directory separator characters will now have the subdirectories created automatically - Changed how settings file is written to disk to reduce potential data loss risk - (Internal) Updated to Avalonia 11.0.7 ### Fixed - Fixed error when ControlNet module image paths are not found, even if the module is disabled - Fixed error when finding metadata for archived models - Fixed error when extensions folder is missing - Fixed crash when model was not selected in Inference - Fixed Fooocus Config shared folder mode overwriting unknown config keys - Fixed potential SD.Next update issues by moving to shared update process - Fixed crash on startup when Outputs page failed to load categories properly - Fixed image gallery arrow key navigation requiring clicking before responding - Fixed crash when loading extensions list with no internet connection - Fixed crash when invalid launch arguments are passed - Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible ## v2.8.0-pre.5 ### Fixed - Fixed error when ControlNet module image paths are not found, even if the module is disabled - Fixed error when finding metadata for archived models - Fixed error when extensions folder is missing - Fixed error when webp files have incorrect metadata - Fixed crash when model was not selected in Inference - Fixed Fooocus Config shared folder mode overwriting unknown config keys ## v2.8.0-pre.4 ### Added - Added Recommended Models dialog after one-click installer - Added native desktop push notifications for some events (i.e. Downloads, Package installs, Inference generation) - Currently available on Windows and Linux, macOS support is pending - Added settings options for notifications - Added new launch argument options for Fooocus - Added Automatic1111 & Stable Diffusion WebUI-UX to the compatible macOS packages ### Changed - Changed one-click installer to match the new package installation style - Automatic1111 packages will now use PyTorch v2.1.2. Upgrade will occur during the next package update or upon fresh installation. - Updated French translation with the latest changes ### Fixed - Fixed [#413](https://github.com/LykosAI/StabilityMatrix/issues/413) - Environment Variables are editable again - Fixed potential SD.Next update issues by moving to shared update process - Fixed Invoke install trying to use system nodejs - Fixed crash on startup when Outputs page failed to load categories properly ## v2.8.0-pre.3 ### Added - Added "Config" Shared Model Folder option for Fooocus - Added "Copy Details" button to Unexpected Error dialog ### Changed - (Internal) Updated to Avalonia 11.0.7 - Changed the Close button on the package install dialog to "Hide" - Functionality remains the same, just a name change - Updated French translation (thanks Greg!) ### Fixed - Webp static images can now be shown alongside existing webp animation support - Fixed image gallery arrow key navigation requiring clicking before responding - Fixed crash when loading extensions list with no internet connection - Fixed crash when invalid launch arguments are passed - Fixed "must give at least one requirement to install" error when installing extensions with empty requirements.txt ## v2.8.0-pre.2 ### Added - Added German language option, thanks to Mario da Graca for the translation - Added Portuguese language options, thanks to nextosai for the translation ### Changed - Updated translations for the following languages: - Spanish - French - Japanese - Turkish ### Fixed - Fixed Auto-update failing to start new version on Windows and Linux when path contains spaces - Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI - Install button will now be properly disabled when the duplicate warning is shown ## v2.8.0-pre.1 ### Added - Added Package Extensions (Plugins) management - accessible from the Packages' 3-dot menu. Currently supports ComfyUI and A1111. - Added base model filter to Checkpoints page - Search box on Checkpoints page now searches tags and trigger words - Added "Compatible Images" category when selecting images for Inference projects - Added "Find in Model Browser" option to the right-click menu on the Checkpoints page ### Changed - Removed "Failed to load image" notification when loading some images on the Checkpoints page - Installed models will no longer be selectable on the Hugging Face tab of the model browser ### Fixed - Inference file name patterns with directory separator characters will now have the subdirectories created automatically - Fixed missing up/downgrade buttons on the Python Packages dialog when the version was not semver compatible - Automatic1111 package installs will now install the missing `jsonmerge` package ## v2.8.0-dev.4 ### Added - Auto-update support for macOS - New package installation flow - Added `--use-directml` launch argument for SDWebUI DirectML fork ### Changed - Changed default Period to "AllTime" in the Model Browser ### Fixed - Fixed SDTurboScheduler's missing denoise parameter ## v2.8.0-dev.3 ### Added - Added release builds for macOS (Apple Silicon) - Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer) - Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision - Added Delete button to the CivitAI Model Browser details dialog - Added "Copy Link to Clipboard" for connected models in the Checkpoints page ### Changed - Python Packages install dialog now allows entering multiple arguments or option flags ### Fixed - Fixed environment variables grid not being editable related to [Avalonia #13843](https://github.com/AvaloniaUI/Avalonia/issues/13843) ## v2.8.0-dev.2 ### Added #### Inference - Added Image to Video project type #### Output Browser - Added support for webp files - Added "Send to Image to Image" and "Send to Image to Video" options to the context menu ### Changed - Changed how settings file is written to disk to reduce potential data loss risk ## v2.8.0-dev.1 ### Added #### Inference - Added image and model details in model selection boxes - Added CLIP Skip setting, toggleable from the model settings button ## v2.7.9 ### Fixed - Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI ## v2.7.8 ### Changed - Python Packages install dialog now allows entering multiple arguments or option flags ### Fixed - Fixed InvokeAI Package dependency versions ([#395](https://github.com/LykosAI/StabilityMatrix/pull/395)) ## v2.7.7 ### Added - Added `--use-directml` launch argument for SDWebUI DirectML fork ### Changed - Model Browser downloads will no longer be disabled if the free drive space is unavailable - Default Linux installation folder changed to prevent issues with hidden folders - Changed default Period to "AllTime" in the Model Browser ### Fixed - Fixed error where Environment Variables were not editable - Fixed SDTurboScheduler's missing denoise parameter ## v2.7.6 ### Added - Added SDXL Turbo and Stable Video Diffusion to the Hugging Face tab ### Changed - ControlNet model selector will now show the parent directory of a model when relevant ### Fixed - Fixed Python Packages dialog crash due to pip commands including warnings - Fixed Base Model downloads from the Hugging Face tab downloading to the wrong folder - Fixed InvokeAI `! [rejected] v3.4.0post2 -> v3.4.0post2 (would clobber existing tag)` error on updating to the latest version - Fixed settings not saving in some scenarios, such as when the `settings.json` file existed but was empty ## v2.7.5 ### Fixed - Fixed Python Packages manager crash when pip list returns warnings in json - Fixed slowdown when loading PNGs with large amounts of metadata - Fixed crash when scanning directories for missing metadata ## v2.7.4 ### Changed - Improved low disk space handling ### Fixed - Fixed denoise strength in Inference Text to Image - Fixed PathTooLongException for IPAdapter folders when using ComfyUI in Symlink mode - Fixed configs and symlinks not being cleaned up when switched to the opposite mode - Fixed model indexing stopping when encountering paths longer than 1021 bytes in length - Fixed repeated nested folders being created in `Models/ControlNet` when using ComfyUI in Symlink mode. Existing folders will be repaired to their original structure on launch. ## v2.7.3 ### Added - Added missing IPAdapter and CLIP Vision folder links for ComfyUI ### Fixed - Fixed UnicodeDecodeError when using extra_model_paths.yaml in ComfyUI on certain locales - Fixed SDXL CLIP Vision model directory name conflict - Fixed [#334](https://github.com/LykosAI/StabilityMatrix/issues/334) - Win32Exception if Settings are opened ## v2.7.2 ### Changed - Changed Symlink shared folder link targets for Automatic1111 and ComfyUI. From `ControlNet -> models/controlnet` to `ControlNet -> models/controlnet/ControlNet` and `T2IAdapter -> models/controlnet/T2IAdapter`. - Changed FreeU defaults to match recommended SD1.5 defaults - Changed default denoise strength from 1.0 to 0.7 ### Fixed - Fixed ControlNet / T2IAdapter shared folder links for Automatic1111 conflicting with each other - Fixed URIScheme registration errors on Linux - Fixed RuinedFooocus missing output folder on startup - Fixed incorrect Fooocus VRAM launch arguments ## v2.7.1 ### Added - Added Turkish UI language option, thanks to Progesor for the translation ### Fixed - Fixed Inference Image to Image projects missing denoise strength setting ## v2.7.0 ### Added #### General - New package: [RuinedFooocus](https://github.com/runew0lf/RuinedFooocus) - Added an X button to all search fields to instantly clear them (Esc key also works) - Added System Information section to Settings #### Inference - Added Image to Image project type - Added Modular custom steps - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline. - Added Sampler addons - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes. - Added SD Turbo Scheduler - Added display names for new samplers ("Heun++ 2", "DDPM", "LCM") - Added Ctrl+Enter as a shortcut for the Generate Image button #### Accounts Settings Subpage - Lykos Account sign-up and login - currently for Patreon OAuth connections but GitHub requests caching and settings sync are planned - Supporters can now connect your Patreon accounts, then head to the Updates page to choose to receive auto-updates from the Dev or Preview channels - CivitAI Account login with API key - enables downloading models from the Browser page that require CivitAI logins, more integrations like liking and commenting are also planned #### Updates Settings Subpage - Toggle auto-update notifications and manually check for updates - Choose between Stable, Preview, and Dev update channels #### Inference Settings Subpage - Moved Inference settings to subpage - Updated with more localized labels #### Outputs Page - Added Refresh button to update gallery from file system changes #### Checkpoints Page - Added the ability to drag & drop checkpoints between different folders - Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available) - Added trigger words on checkpoint card and tooltip - Added "Find Connected Metadata" options for root-level and file-level scans - Added "Update Existing Metadata" button #### Model Browser - Added Hugging Face tab to the Model Browser - Added additional base model filter options for CivitAI ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other") - Added the ability to type in a specific page number in the CivitAI Model Browser - Right clicking anywhere on the model card will open the same menu as the three-dots button - New model downloads will save trigger words in metadata, if available - Model author username and avatar display, with clickable link to their profile ### Changed #### General - Model Browser page has been redesigned, featuring more information like rating and download counts - Model Browser navigation has improved animations on hover and compact number formatting - Updated Outputs Page button and menu layout - Rearranged Add Package dialog slightly to accommodate longer package list - Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders - Model Browser now split into "CivitAI" and "Hugging Face" tabs #### Inference - Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected - Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix ### Fixed - Fixed Outputs page reverting back to Shared Output Folder every time the page is reloaded - Potentially fixed updates sometimes clearing settings or launching in the wrong directory - Improved startup time and window load time after exiting dialogs - Fixed control character decoding that caused some progress bars to show as `\u2588` - Fixed Python `rich` package's progress bars not showing in console - Optimized ProgressRing animation bindings to reduce CPU usage - Improved safety checks in custom control rendering to reduce potential graphical artifacts - Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111) - Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters - Fixed crash when loading an empty settings file - Improve Settings save and load performance with .NET 8 Source Generating Serialization - Fixed ApplicationException during database shutdown - InvokeAI model links for T2I/IpAdapters now point to the correct folders - Added extra checks to help prevent settings resetting in certain scenarios - Fixed Refiner model enabled state not saving to Inference project files - Fixed NullReference error labels when clearing the Inference batch size settings, now shows improved message with minimum and maximum value constraints ## v2.7.0-pre.4 ### Added #### Inference - Added Image to Image project type - Added Modular custom steps - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline. - Added Sampler addons - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes. - Added SD Turbo Scheduler - Added display names for new samplers ("Heun++ 2", "DDPM", "LCM") #### Model Browser - Added additional base model filter options ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other") ### Changed #### Inference - Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected - Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix ### Fixed - Fixed Refiner model enabled state not saving to Inference project files ## v2.7.0-pre.3 ### Added - Added "Find Connected Metadata" options for root-level and file-level scans to the Checkpoints page - Added "Update Existing Metadata" button to the Checkpoints page - Added Hugging Face tab to the Model Browser - Added the ability to type in a specific page number in the CivitAI Model Browser ### Changed - Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders - Model Browser now split into "CivitAI" and "Hugging Face" tabs ### Fixed - InvokeAI model links for T2I/IpAdapters now point to the correct folders - Added extra checks to help prevent settings resetting in certain scenarios ## v2.7.0-pre.2 ### Added - Added System Information section to Settings ### Changed - Moved Inference Settings to subpage ### Fixed - Fixed crash when loading an empty settings file - Improve Settings save and load performance with .NET 8 Source Generating Serialization - Fixed ApplicationException during database shutdown ## v2.7.0-pre.1 ### Fixed - Fixed control character decoding that caused some progress bars to show as `\u2588` - Fixed Python `rich` package's progress bars not showing in console - Optimized ProgressRing animation bindings to reduce CPU usage - Improved safety checks in custom control rendering to reduce potential graphical artifacts - Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111) ## v2.7.0-dev.4 ### Fixed - Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters ## v2.7.0-dev.3 ### Added - New package: [RuinedFooocus](https://github.com/runew0lf/RuinedFooocus) #### Model Browser - Right clicking anywhere on the model card will open the same menu as the three-dots button - New model downloads will save trigger words in metadata, if available - Model author username and avatar display, with clickable link to their profile #### Checkpoints Page - Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available) - Added trigger words on checkpoint card and tooltip ### Changed #### Model Browser - Improved number formatting with K/M suffixes for download and favorite counts - Animated zoom effect on hovering over model images #### Checkpoints Page - Rearranged top row layout to use CommandBar ### Fixed - Improved startup time and window load time after exiting dialogs ## v2.7.0-dev.2 ### Added #### General - Added an X button to all search fields to instantly clear them (Esc key also works) #### Outputs Page - Added Refresh button to update gallery from file system changes #### Checkpoints Page - Added the ability to drag & drop checkpoints between different folders ### Changed #### Outputs Page - Updated button and menu layout #### Packages Page - Rearranged Add Package dialog slightly to accommodate longer package list ### Fixed - Fixed InvalidOperation errors when signing into accounts shortly after signing out, while the previous account update is still running - Fixed Outputs page reverting back to Shared Output Folder every time the page is reloaded - Potentially fixed updates sometimes clearing settings or launching in the wrong directory ## v2.7.0-dev.1 ### Added - Accounts Settings Subpage - Lykos Account sign-up and login - currently for Patreon OAuth connections but GitHub requests caching and settings sync are planned - Supporters can now connect your Patreon accounts, then head to the Updates page to choose to receive auto-updates from the Dev or Preview channels - CivitAI Account login with API key - enables downloading models from the Browser page that require CivitAI logins, more integrations like liking and commenting are also planned - Updates Settings Subpage - Toggle auto-update notifications and manually check for updates - Choose between Stable, Preview, and Dev update channels ### Changed - Model Browser page has been redesigned, featuring more information like rating and download counts ## v2.6.7 ### Fixed - Fixed prerequisite install not unpacking due to improperly formatted 7z argument (Caused the "python310._pth FileNotFoundException") - Fixed [#301](https://github.com/LykosAI/StabilityMatrix/issues/301) - Package updates failing silently because of a PortableGit error ## v2.6.6 ### Fixed - Fixed [#297](https://github.com/LykosAI/StabilityMatrix/issues/297) - Model browser LiteAsyncException occuring when fetching entries with unrecognized values from enum name changes ## v2.6.5 ### Fixed - Fixed error when receiving unknown model format values from the Model Browser - Fixed process errors when installing or updating Pip packages using the Python packages dialog ## v2.6.4 ### Fixed - Fixed errors preventing Model Browser from finding results with certain search queries ## v2.6.3 ### Fixed - Fixed InvalidOperationException during prerequisite installs on certain platforms where process name and duration reporting are not supported ## v2.6.2 ### Changed - Backend changes for auto-update schema v3, supporting customizable release channels and faster downloads with zip compression ### Fixed - Better error reporting including outputs for git subprocess errors during package install / update - Fixed `'accelerate' is not recognized as an internal or external command` error when starting training in kohya_ss - Fixed some instances of `ModuleNotFoundError: No module named 'bitsandbytes.cuda_setup.paths'` error when using 8-bit optimizers in kohya_ss - Fixed errors preventing Inference outputs from loading in the img2img tabs of other packages ## v2.6.1 ### Changed - NVIDIA GPU users will be updated to use CUDA 12.1 for the InvokeAI package for a slight performance improvement - Update will occur the next time the package is updated, or on a fresh install - Note: CUDA 12.1 is only available on Maxwell (GTX 900 series) and newer GPUs ### Fixed - Reduced the amount of calls to GitHub to help prevent rate limiting - Fixed rate limit crash on startup preventing app from starting ## v2.6.0 ### Added - Added **Output Sharing** option for all packages in the three-dots menu on the Packages page - This will link the package's output folders to the relevant subfolders in the "Outputs" directory - When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled - Added **Outputs page** for viewing generated images from any package, or the shared output folder - Added [Stable Diffusion WebUI/UX](https://github.com/anapnoe/stable-diffusion-webui-ux) package - Added [Stable Diffusion WebUI-DirectML](https://github.com/lshqqytiger/stable-diffusion-webui-directml) package - Added [kohya_ss](https://github.com/bmaltais/kohya_ss) package - Added [Fooocus-ControlNet-SDXL](https://github.com/fenneishi/Fooocus-ControlNet-SDXL) package - Added GPU compatibility badges to the installers - Added filtering of "incompatible" packages (ones that do not support your GPU) to all installers - This can be overridden by checking the new "Show All Packages" checkbox - Added more launch options for Fooocus, such as the `--preset` option - Added Ctrl+ScrollWheel to change image size in the inference output gallery and new Outputs page - Added "No Images Found" placeholder for non-connected models on the Checkpoints tab - Added "Open on GitHub" option to the three-dots menu on the Packages page ### Changed - If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page - Changed all package installs & updates to use git commands instead of downloading zip files - The One-Click Installer now uses the new progress dialog with console - NVIDIA GPU users will be updated to use CUDA 12.1 for ComfyUI & Fooocus packages for a slight performance improvement - Update will occur the next time the package is updated, or on a fresh install - Note: CUDA 12.1 is only available on Maxwell (GTX 900 series) and newer GPUs - Improved Model Browser download stability with automatic retries for download errors - Optimized page navigation and syntax formatting configurations to improve startup time ### Fixed - Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer - Fixed Inference popup Install button not working on One-Click Installer - Fixed Inference Prompt Completion window sometimes not showing while typing - Fixed "Show Model Images" toggle on Checkpoints page sometimes displaying cut-off model images - Fixed missing httpx package during Automatic1111 install - Fixed some instances of localized text being cut off from controls being too small ## v2.5.7 ### Fixed - Fixed error `got an unexpected keyword argument 'socket_options'` on fresh installs of Automatic1111 Stable Diffusion WebUI due to missing httpx dependency specification from gradio ## v2.5.6 ### Added - Added Russian UI language option, thanks to aolko for the translation ## v2.5.5 ### Added - Added Spanish UI language options, thanks to Carlos Baena and Lautaroturina for the translations - Manual input prompt popup on package input requests besides Y/n confirmations - Added `--disable-gpu` launch argument to disable hardware accelerated rendering ### Fixed - Fixed infinite progress wheel when package uninstall fails ## v2.5.4 ### Fixed - Fixed [#208](https://github.com/LykosAI/StabilityMatrix/issues/208) - error when installing xformers ## v2.5.3 ### Added - Added French UI language option, thanks to eephyne for the translation ### Fixed - Fixed Automatic 1111 missing dependencies on startup by no longer enabling `--skip-install` by default. ## v2.5.2 ### Added - Right click Inference Batch options to enable selecting a "Batch Index". This can be used to reproduce a specific image from a batch generation. The field will be automatically populated in metadata of individual images from a batch generation. - The index is 1-based, so the first image in a batch is index 1, and the last image is the batch size. - Currently this generates different individual images for batches using Ancestral samplers, due to an upstream ComfyUI issue with noise masking. Looking into fixing this. - Inference Batches option now is implemented, previously the setting had no effect ### Changed - Default upscale factor for Inference is now 2x instead of 1x ### Fixed - Fixed batch combined image grids not showing metadata and not being importable - Fixed "Call from invalid thread" errors that sometimes occured during update notifications ## v2.5.1 ### Added - `--skip-install` default launch argument for Automatic1111 Package ### Fixed - Fixed Prompt weights showing syntax error in locales where decimal separator is not a period ## v2.5.0 ### Added - Added Inference, a built-in native Stable Diffusion interface, powered by ComfyUI - Added option to change the Shared Folder method for packages using the three-dots menu on the Packages page - Added the ability to Favorite models in the Model Browser - Added "Favorites" sort option to the Model Browser - Added notification flyout for new available updates. Dismiss to hide until the next update version. - Added Italian UI language options, thanks to Marco Capelli for the translations ### Changed - Model Browser page size is now 20 instead of 14 - Update changelog now only shows the difference between the current version and the latest version ### Fixed - Fixed [#141](https://github.com/LykosAI/StabilityMatrix/issues/141) - Search not working when sorting by Installed on Model Browser - Fixed SD.Next not showing "Open Web UI" button when finished loading - Fixed model index startup errors when `./Models` contains unknown custom folder names - Fixed ストップ button being cut off in Japanese translation - Fixed update progress freezing in some cases - Fixed light theme being default in first time setup window - Fixed shared folder links not recreating fully when partially missing ## v2.4.6 ### Added - LDSR / ADetailer shared folder links for Automatic1111 Package ### Changed - Made Dark Mode background slightly lighter ## v2.4.5 ### Fixed - Fixed "Library Dir not set" error on launch ## v2.4.4 ### Added - Added button to toggle automatic scrolling of console output ### Fixed - Fixed [#130](https://github.com/LykosAI/StabilityMatrix/issues/130) ComfyUI extra_model_paths.yaml file being overwritten on each launch - Fixed some package updates not showing any console output - Fixed auto-close of update dialog when package update is complete ## v2.4.3 ### Added - Added "--no-download-sd-model" launch argument option for Stable Diffusion Web UI - Added Chinese (Simplified) and Chinese (Traditional) UI language options, thanks to jimlovewine for the translations ### Changed - Package updates now use the new progress dialog with console output ### Fixed - Updated Japanese translation for some terms ## v2.4.2 ### Added - Added Japanese UI language option, thanks to kgmkm_mkgm for the translation - Language selection available in Settings, and defaults to system language if supported ## v2.4.1 ### Fixed - Fixed deleting checkpoints not updating the visual grid until the page is refreshed - Fixed updates sometimes freezing on "Installing Requirements" step ## v2.4.0 ### Added - New installable Package - [Fooocus-MRE](https://github.com/MoonRide303/Fooocus-MRE) - Added toggle to show connected model images in the Checkpoints tab - Added "Find Connected Metadata" option to the context menu of Checkpoint Folders in the Checkpoints tab to connect models that don't have any metadata ### Changed - Revamped package installer - Added "advanced options" section for commit, shared folder method, and pytorch options - Can be run in the background - Shows progress in the Downloads tab - Even more performance improvements for loading and searching the Checkpoints page ### Fixed - Fixed [#97](https://github.com/LykosAI/StabilityMatrix/issues/97) - Codeformer folder should now get linked correctly - Fixed [#106](https://github.com/LykosAI/StabilityMatrix/issues/106) - ComfyUI should now install correctly on Windows machines with an AMD GPU using DirectML - Fixed [#107](https://github.com/LykosAI/StabilityMatrix/issues/107) - Added `--autolaunch` option to SD.Next - Fixed [#110](https://github.com/LykosAI/StabilityMatrix/issues/110) - Model Browser should properly navigate to the next page of Installed models - Installed tag on model browser should now show for connected models imported via drag & drop ## v2.3.4 ### Fixed - Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103) ## v2.3.3 ### Fixed - Fixed GPU recognition for Nvidia Tesla GPUs - Fixed checkpoint file index extension identification with some path names - Fixed issue where config file may be overwritten during Automatic1111 package updates - Fixed "Directory Not Found" error on startup when previously selected Data directory does not exist - Fixed [#83](https://github.com/LykosAI/StabilityMatrix/issues/83) - Display of packages with long names in the Package Manager - Fixed [#64](https://github.com/LykosAI/StabilityMatrix/issues/64) - Package install error if venv already exists ## v2.3.2 ### Added - Added warning for exFAT / FAT32 drives when selecting a data directory ### Fixed - Automatic1111 and ComfyUI should now install the correct version of pytorch for AMD GPUs - Fixed "Call from invalid thread" exceptions preventing download completion notifications from showing - Fixed model preview image downloading with incorrect name ### Changed - Redesigned "Select Model Version" dialog to include model description and all preview images ## v2.3.1 ### Fixed - Fixed Auto update not appearing in some regions due to date formatting issues - Local package import now migrates venvs and existing models ## v2.3.0 ### Added - New installable Package - [Fooocus](https://github.com/lllyasviel/Fooocus) - Added "Select New Data Directory" button to Settings - Added "Skip to First/Last Page" buttons to the Model Browser - Added VAE as a checkpoint category in the Model Browser - Pause/Resume/Cancel buttons on downloads popup. Paused downloads persists and may be resumed after restarting the app - Unknown Package installs in the Package directory will now show up with a button to import them ### Fixed - Fixed issue where model version wouldn't be selected in the "All Versions" section of the Model Browser - Improved Checkpoints page indexing performance - Fixed issue where Checkpoints page may not show all checkpoints after clearing search filter - Fixed issue where Checkpoints page may show incorrect checkpoints for the given filter after changing pages - Fixed issue where Open Web UI button would try to load 0.0.0.0 addresses - Fixed Dictionary error when launch arguments saved with duplicate arguments - Fixed Launch arguments search not working ### Changed - Changed update method for SD.Next to use the built-in upgrade functionality - Model Browser navigation buttons are no longer disabled while changing pages ## v2.2.1 ### Fixed - Fixed SD.Next shared folders config not working with new config format, reverted to Junctions / Symlinks ## v2.2.1 ### Fixed - Fixed SD.Next shared folders config not working with new config format, reverted to Junctions / Symlinks ## v2.2.0 ### Added - Added option to search by Base Model in the Model Browser - Animated page transitions ### Fixed - Fixed [#59](https://github.com/LykosAI/StabilityMatrix/issues/61) - `GIT` environment variable is now set for the embedded portable git on Windows as A1111 uses it instead of default `PATH` resolution - Fixed embedded Python install check on Linux when an incompatible windows DLL is in the Python install directory - Fixed "ObjectDisposed" database errors that sometimes appeared when closing the app ### Changed - Revamped Package Manager UI - InvokeAI installations can now use checkpoints from the Shared Models folder ## v2.1.2 ### Changed - SD.Next install now uses ROCm PyTorch backend on Linux AMD GPU machines for better performance over DirectML ## v2.1.1 ### Added - Discord Rich Presence support can now be enabled in Settings ### Fixed - Launch Page selected package now persists in settings ## v2.1.0 ### Added - New installable Package - [VoltaML](https://github.com/VoltaML/voltaML-fast-stable-diffusion) - New installable Package - [InvokeAI](https://github.com/invoke-ai/InvokeAI) - Launch button can now support alternate commands / modes - currently only for InvokeAI > ![](https://github.com/LykosAI/StabilityMatrix/assets/13956642/16a8ffdd-a3cb-4f4f-acc5-c062d3ade363) - Settings option to set global environment variables for Packages > ![](https://github.com/LykosAI/StabilityMatrix/assets/13956642/d577918e-82bb-46d4-9a3a-9b5318d3d4d8) ### Changed - Compatible packages (ComfyUI, Vlad/SD.Next) now use config files / launch args instead of symbolic links for shared model folder redirect ### Fixed - Fixed [#48](https://github.com/LykosAI/StabilityMatrix/issues/48) - model folders not showing in UI when they were empty - Updater now shows correct current version without trailing `.0` - Fixed program sometimes starting off-screen on multi-monitor setups - Fixed console input box transparency - Fixed [#52](https://github.com/LykosAI/StabilityMatrix/issues/52) - A1111 default approx-vae model download errors by switching default preview method to TAESD - Fixes [#50](https://github.com/LykosAI/StabilityMatrix/issues/50) - model browser crash when no model versions exist - Fixed [#31](https://github.com/LykosAI/StabilityMatrix/issues/31) - missing ControlNet link to Shared Models Folder for SD.Next - Fixed [#49](https://github.com/LykosAI/StabilityMatrix/issues/49) - download progress disappearing when changing pages in Model Browser ## v2.0.4 ### Fixed - Fixed Model Browser downloading files without extensions ## v2.0.3 ### Added - (Windows) New settings option to add Stability Matrix to the start menu - (Windows) Improved background "Mica" effect on Windows 11, should be smoother with less banding artifacts ### Fixed - Fixed model categories sometimes not showing if they are empty - Improved model download hash verification performance - Fixed some text wrapping visuals on expanded model version dialog on model browser - Added cancel button for create folder dialog - One click first time installer now defaults to using the "Package" name instead of the display name ("stable-diffusion-webui" instead of "Stable Diffusion WebUI") for the install folder name - probably safer against upstream issues on folder names with spaces. ## v2.0.2 ### Fixed - (Linux) Updater now sets correct execute permissions - Image loading (i.e. Checkpoints File preview thumbnail) now has a notification for unsupported local image formats instead of crashing - Fix unable to start app issues on some machines and dropdowns showing wrong categories - disabled assembly trimming ## v2.0.1 ### Added - Fully rewritten using Avalonia for improved UI and cross-platform support, our biggest update so far, with over 18,000 lines of code. - Release support for Windows and Linux, with macOS coming soon - Model Browser now indicates models that are already downloaded / need updates - Checkpoints Manager now supports filtering/searching - One-click installer now suggests all 3 WebUI packages for selection - Hardware compatibility and GPU detection is now more accurate - Download Indicator on the nav menu for ongoing downloads and progress; supports multiple concurrent model downloads - Improved console with syntax highlighting, and provisional ANSI rendering for progress bars and advanced graphics - Input can now be sent to the running package process using the top-right keyboard button on the Launch page. Package input requests for a (y/n) response will now have an interactive popup. ### Fixed - Fixed crash on exit - Fixed updating from versions prior to 2.x.x - Fixed page duplication memory leak that caused increased memory usage when switching between pages - Package page launch button will now navigate and launch the package, instead of just navigating to launch page ================================================ FILE: CONTRIBUTING.md ================================================ # Building ## Running & Debug - If building using managed IDEs like Rider or Visual Studio, ensure that a valid `--runtime ...` argument is being passed to `dotnet`, or `RuntimeIdentifier=...` is set for calling `msbuild`. This is required for runtime-specific resources to be included in the build. Stability Matrix currently supports building for the `win-x64`, `linux-x64` and `osx-arm64` runtimes. - You can also build the `StabilityMatrix.Avalonia` project using `dotnet`: ```bash dotnet build ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -r win-x64 -c Debug ``` - Note that on Windows, the `net8.0-windows10.0.17763.0` framework is used, build outputs will be in `StabilityMatrix.Avalonia/bin/Debug/net8.0-windows10.0.17763.0/win-x64`. On other platforms the `net8.0` framework is used. ## Building to single file for release (Replace `$RELEASE_VERSION` with a non v-prefixed semver version number, e.g. `2.10.0`, `2.11.0-dev.1`, etc.) ### Windows ```bash dotnet publish ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -r win-x64 -c Release -p:Version=$env:RELEASE_VERSION -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=true ``` ### macOS The `output_dir` environment variable can be specified or defaults to `./out/osx-arm64/` ```bash ./Build/build_macos_app.sh -v $RELEASE_VERSION ``` ### Linux ```bash sudo apt-get -y install libfuse2 dotnet tool install -g KuiperZone.PupNet pupnet -r linux-x64 -c Release --kind appimage --app-version $RELEASE_VERSION --clean ``` # Scripts ## Install Husky.Net & Pre-commit hooks - Building the `StabilityMatrix.Avalonia` project once should also install Husky.Net, or run the following command: ```bash dotnet tool restore && dotnet husky install ``` ## Adding Husky pre-commit hooks ```bash dotnet husky install ``` ## Generated OpenApi clients - Refitter is used to generate some OpenApi clients. New clients should be added to `./.husky/task-runner.json`. - To regenerate clients, run the following command: ```bash dotnet husky run -g generate-openapi ``` # Style Guidelines These are just guidelines, mostly following the official C# style guidelines, except in a few cases. We might not adhere to these 100% ourselves, but lets try our best :) ## Naming conventions #### Pascal Case - Use pascal casing ("PascalCasing") when naming a `class`, `record`, `struct`, or `public` members of types, such as fields, properties, methods, and local functions. - When naming an `interface`, use pascal casing in addition to prefixing the name with the letter `I` to clearly indicate to consumers that it's an `interface`. #### Camel Case - Use camel casing ("camelCasing") when naming `private` or `internal` fields. - **Do not** prefix them with an underscore `_` ## `using` Directives - Please do not check in code with unused using statements. ## File-scoped Namespaces - Always use file-scoped namespaces. For example: ```csharp using System; namespace X.Y.Z; class Foo { } ``` ## Implicitly typed local variables - Use implicit typing (`var`) for local variables when the type of the variable is obvious from the right side of the assignment, or when the precise type is not important. ## Optional Curly Braces - Only omit curly braces from `if` statements if the statement immediately following is a `return`. For example, the following snippet is acceptable: ```csharp if (alreadyAteBreakfast) return; ``` Otherwise, it must be wrapped in curly braces, like so: ```csharp if (alreadyAteLunch) { mealsEaten++; } ``` ## Project Structure - Try to follow our existing structure, such as putting model classes in the `Models\` directory, ViewModels in `ViewModels\`, etc. - Static classes with only extension methods should be in `Extensions\` - Mock data for XAML Designer should go in `DesignData\` - The `Helper\` and `Services\` folder don't really have guidelines, use your best judgment - XAML & JSON converters should go in the `Converters\` and `Converters\Json\` directories respectively - Refit interfaces should go in the `Api\` folder ================================================ FILE: ConditionalSymbols.props ================================================ false CodeGeneration false $(DefineConstants);REGISTER_SERVICE_USAGES $(DefineConstants);SM_LOG_WINDOW $(DefineConstants);REGISTER_SERVICE_REFLECTION;REGISTER_SERVICE_USAGES ================================================ FILE: Directory.Build.props ================================================  net9.0 preview enable enable true true CS0108 11.3.7 $(NoWarn);AVLN3001 $(NoWarn);CsWinRT1028 ================================================ FILE: Directory.Packages.props ================================================ ================================================ FILE: Jenkinsfile ================================================ node("Diligence") { def repoName = "StabilityMatrix" def author = "ionite34" def version = "" stage('Clean') { deleteDir() } stage('Checkout') { git branch: env.BRANCH_NAME, credentialsId: 'Ionite', url: "https://github.com/${author}/${repoName}.git" } try { stage('Test') { sh "dotnet test StabilityMatrix.Tests" } if (env.BRANCH_NAME == 'main') { stage('Set Version') { script { if (env.TAG_NAME) { version = env.TAG_NAME.replaceFirst(/^v/, '') } else { version = VersionNumber projectStartDate: '2023-06-21', versionNumberString: '${BUILDS_ALL_TIME}', worstResultForIncrement: 'SUCCESS' } } } stage('Publish Windows') { sh "dotnet publish ./StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj -c Release -o out -r win-x64 -p:PublishSingleFile=true -p:VersionPrefix=2.0.0 -p:VersionSuffix=${version} -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableWindowsTargeting=true" } stage('Publish Linux') { sh "rm -rf StabilityMatrix.Avalonia/bin/*" sh "rm -rf StabilityMatrix.Avalonia/obj/*" sh "/home/jenkins/.dotnet/tools/pupnet --runtime linux-x64 --kind appimage --app-version ${version} --clean -y" } } } finally { stage('Cleanup') { cleanWs() } } } ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: NuGet.Config ================================================  ================================================ FILE: README.md ================================================ # Stability Matrix [![Build](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml/badge.svg)](https://github.com/LykosAI/StabilityMatrix/actions/workflows/build.yml) [![Discord Server](https://img.shields.io/discord/1115555685476868168?logo=discord&logoColor=white&label=Discord%20Server)](https://discord.com/invite/TUrgfECxHz) [![Latest Stable](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Stable&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release] [![Latest Preview](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.lykos.ai%2Fupdate-v3.json&query=%24.updates.preview%5B%22win-x64%22%5D.version&prefix=v&label=Latest%20Preview&color=b57400&cacheSeconds=60&link=https%3A%2F%2Flykos.ai%2Fdownloads)](https://lykos.ai/downloads) [![Latest Dev](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcdn.lykos.ai%2Fupdate-v3.json&query=%24.updates.development%5B%22win-x64%22%5D.version&prefix=v&label=Latest%20Dev&color=880c21&cacheSeconds=60&link=https%3A%2F%2Flykos.ai%2Fdownloads)](https://lykos.ai/downloads) [release]: https://github.com/LykosAI/StabilityMatrix/releases/latest [download-win-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-win-x64.zip [download-linux-appimage-x64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-linux-x64.zip [download-linux-aur-x64]: https://aur.archlinux.org/packages/stabilitymatrix [download-macos-arm64]: https://github.com/LykosAI/StabilityMatrix/releases/latest/download/StabilityMatrix-macos-arm64.dmg [auto1111]: https://github.com/AUTOMATIC1111/stable-diffusion-webui [auto1111-directml]: https://github.com/lshqqytiger/stable-diffusion-webui-directml [webui-ux]: https://github.com/anapnoe/stable-diffusion-webui-ux [comfy]: https://github.com/comfyanonymous/ComfyUI [sdnext]: https://github.com/vladmandic/automatic [voltaml]: https://github.com/VoltaML/voltaML-fast-stable-diffusion [invokeai]: https://github.com/invoke-ai/InvokeAI [fooocus]: https://github.com/lllyasviel/Fooocus [fooocus-mre]: https://github.com/MoonRide303/Fooocus-MRE [ruined-fooocus]: https://github.com/runew0lf/RuinedFooocus [fooocus-controlnet]: https://github.com/fenneishi/Fooocus-ControlNet-SDXL [kohya-ss]: https://github.com/bmaltais/kohya_ss [onetrainer]: https://github.com/Nerogar/OneTrainer [forge]: https://github.com/lllyasviel/stable-diffusion-webui-forge [stable-swarm]: https://github.com/Stability-AI/StableSwarmUI [sdfx]: https://github.com/sdfxai/sdfx [fooocus-mashb1t]: https://github.com/mashb1t/Fooocus [reforge]: https://github.com/Panchovix/stable-diffusion-webui-reForge [simplesdxl]: https://github.com/metercai/SimpleSDXL/ [fluxgym]: https://github.com/cocktailpeanut/fluxgym [cogvideo]: https://github.com/THUDM/CogVideo [cogstudio]: https://github.com/pinokiofactory/cogstudio [amdforge]: https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu-forge [civitai]: https://civitai.com/ [huggingface]: https://huggingface.co/ ![Header image for Stability Matrix, Multi-Platform Package Manager and Inference UI for Stable Diffusion](https://cdn.lykos.ai/static/sm-banner-rounded.webp) [![Windows](https://img.shields.io/badge/Windows%2010,%2011-%230079d5.svg?style=for-the-badge&logo=Windows%2011&logoColor=white)][download-win-x64] [![Linux (AppImage)](https://img.shields.io/badge/Linux%20(AppImage)-FCC624?style=for-the-badge&logo=linux&logoColor=black)][download-linux-appimage-x64] [![Arch Linux (AUR)](https://img.shields.io/badge/Arch%20Linux%20(AUR)-1793D1?style=for-the-badge&logo=archlinux&logoColor=white)][download-linux-aur-x64] [![macOS](https://img.shields.io/badge/mac%20os%20%28apple%20silicon%29-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)][download-macos-arm64] Multi-Platform Package Manager and Inference UI for Stable Diffusion ### 🖱️ One click install and update for Stable Diffusion Web UI Packages - Supports: - [Stable Diffusion WebUI reForge][reforge], [Stable Diffusion WebUI Forge][forge], [Stable Diffusion WebUI AMDGPU Forge][amdforge] [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext] - [Fooocus][fooocus], [Fooocus MRE][fooocus-mre], [Fooocus ControlNet SDXL][fooocus-controlnet], [Ruined Fooocus][ruined-fooocus], [Fooocus - mashb1t's 1-Up Edition][fooocus-mashb1t], [SimpleSDXL][simplesdxl] - [ComfyUI][comfy] - [StableSwarmUI][stable-swarm] - [VoltaML][voltaml] - [InvokeAI][invokeai] - [SDFX][sdfx] - [Kohya's GUI][kohya-ss] - [OneTrainer][onetrainer] - [FluxGym][fluxgym] - [CogVideo][cogvideo] via [CogStudio][cogstudio] - Manage plugins / extensions for supported packages ([Automatic1111][auto1111], [Comfy UI][comfy], [SD Web UI-UX][webui-ux], and [SD.Next][sdnext]) - Easily install or update Python dependencies for each package - Embedded Git and Python dependencies, with no need for either to be globally installed - Fully portable - move Stability Matrix's Data Directory to a new drive or computer at any time ### ✨ Inference - A Reimagined Interface for Stable Diffusion, Built-In to Stability Matrix - Powerful auto-completion and syntax highlighting using a formal language grammar - Workspaces open in tabs that save and load from `.smproj` project files ![](https://cdn.lykos.ai/static/sm-banner-inference-rounded.webp) - Customizable dockable and float panels - Generated images contain Inference Project, ComfyUI Nodes, and A1111-compatible metadata - Drag and drop gallery images or files to load states

### 🚀 Launcher with syntax highlighted terminal emulator, routed GUI input prompts - Launch arguments editor with predefined or custom options for each Package install - Configurable Environment Variables

### 🗃️ Checkpoint Manager, configured to be shared by all Package installs - Option to find CivitAI metadata and preview thumbnails for new local imports ### ☁️ Model Browser to import from [CivitAI][civitai] and [HuggingFace][huggingface] - Automatically imports to the associated model folder depending on the model type - Downloads relevant metadata files and preview image - Pause and resume downloads, even after closing the app

### Shared model directory for all your packages - Import local models by simple drag and drop - Option to automatically find CivitAI metadata and preview thumbnails for new local imports

- Find connected metadata for existing models

## Localization Stability Matrix is now available in the following languages, thanks to our community contributors: - 🇺🇸 English - 🇯🇵 日本語 - kgmkm_mkgm - 🇨🇳 中文(简体,繁体) - jimlovewine - 🇮🇹 Italiano - Marco Capelli - 🇫🇷 Français - eephyne - Greg - 🇪🇸 Español - Carlos Baena - Lautaroturina - 🇷🇺 Русский - aolko - den1251 - vanja-san - 🇹🇷 Türkçe - Progesor - 🇩🇪 Deutsch - Mario da Graca - 🇵🇹 Português - nextosai - 🇧🇷 Português (Brasil) - jbostroski - thiagojramos - 🇰🇷 한국어 - maakcode - 🇺🇦 Українська - rodtty - 🇨🇿 Čeština - PEKArt! If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project. ## Disclaimers All trademarks, logos, and brand names are the property of their respective owners. All company, product and service names used in this document and licensed applications are for identification purposes only. Use of these names, trademarks, and brands does not imply endorsement. Please note that we do not have any involvement in cryptocurrencies. Any accounts you see claiming otherwise are scams. Please be careful. The only official source of information for Lykos AI is https://lykos.ai or our [Discord Server](https://discord.com/invite/TUrgfECxHz). ## License This repository maintains the latest source code release for Stability Matrix, and is licensed under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html). Binaries and executable releases are licensed under the [End User License Agreement](https://lykos.ai/license). ================================================ FILE: Runtimes.Default.props ================================================ win-x64;linux-x64;osx-x64;osx-arm64 win-x64 linux-x64 osx-arm64 net9.0-windows10.0.17763.0 ================================================ FILE: StabilityMatrix/App.xaml ================================================ ================================================ FILE: StabilityMatrix/App.xaml.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Windows; using AsyncAwaitBestPractices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using NLog; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; using Octokit; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Extensions.Http; using Polly.Timeout; using Refit; using Sentry; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using StabilityMatrix.Helper; using StabilityMatrix.Services; using StabilityMatrix.ViewModels; using Wpf.Ui.Contracts; using Wpf.Ui.Services; using Application = System.Windows.Application; using ISnackbarService = StabilityMatrix.Helper.ISnackbarService; using LogLevel = Microsoft.Extensions.Logging.LogLevel; using SnackbarService = StabilityMatrix.Helper.SnackbarService; namespace StabilityMatrix { /// /// Interaction logic for App.xaml /// public partial class App : Application { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private ServiceProvider? serviceProvider; // ReSharper disable once MemberCanBePrivate.Global public static bool IsSentryEnabled => !Debugger.IsAttached || Environment .GetEnvironmentVariable("DEBUG_SENTRY")?.ToLowerInvariant() == "true"; // ReSharper disable once MemberCanBePrivate.Global public static bool IsExceptionWindowEnabled => !Debugger.IsAttached || Environment .GetEnvironmentVariable("DEBUG_EXCEPTION_WINDOW")?.ToLowerInvariant() == "true"; public static IConfiguration Config { get; set; } = null!; private readonly LoggingConfiguration logConfig; public App() { Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; Config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true) .Build(); // This needs to be done before OnStartup // Or Sentry will not be initialized correctly ConfigureErrorHandling(); // Setup logging logConfig = ConfigureLogging(); } private void ConfigureErrorHandling() { if (IsSentryEnabled) { SentrySdk.Init(o => { o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; o.StackTraceMode = StackTraceMode.Enhanced; o.TracesSampleRate = 1.0; o.IsGlobalModeEnabled = true; // Enables Sentry's "Release Health" feature. o.AutoSessionTracking = true; // 1.0 to capture 100% of transactions for performance monitoring. o.TracesSampleRate = 1.0; #if DEBUG o.Environment = "Development"; #endif }); } if (IsSentryEnabled || IsExceptionWindowEnabled) { DispatcherUnhandledException += App_DispatcherUnhandledException; } } private static LoggingConfiguration ConfigureLogging() { var logConfig = new LoggingConfiguration(); var fileTarget = new FileTarget("logfile") { ArchiveOldFileOnStartup = true, FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveNumbering = ArchiveNumberingMode.Rolling, MaxArchiveFiles = 2 }; var debugTarget = new DebuggerTarget("debugger") { Layout = "${message}" }; logConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, fileTarget); logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, debugTarget); NLog.LogManager.Configuration = logConfig; // Add Sentry to NLog if enabled if (IsSentryEnabled) { logConfig.AddSentry(o => { o.InitializeSdk = false; o.Layout = "${message}"; o.IncludeEventDataOnBreadcrumbs = true; o.BreadcrumbLayout = "${logger}: ${message}"; // Debug and higher are stored as breadcrumbs (default is Info) o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; // Error and higher is sent as event (default is Error) o.MinimumEventLevel = NLog.LogLevel.Error; }); } return logConfig; } private void App_OnStartup(object sender, StartupEventArgs e) { if (AppDomain.CurrentDomain.BaseDirectory.EndsWith("Update\\")) { var delays = Backoff.DecorrelatedJitterBackoffV2( TimeSpan.FromMilliseconds(150), retryCount: 3); foreach (var dlay in delays) { try { File.Copy( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "StabilityMatrix.exe"), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "StabilityMatrix.exe"), true); Process.Start(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "StabilityMatrix.exe")); Current.Shutdown(); } catch (Exception) { Thread.Sleep(dlay); } } return; } var updateDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Update"); if (Directory.Exists(updateDir)) { try { Directory.Delete(updateDir, true); } catch (Exception exception) { Logger.Error(exception, "Failed to delete update file"); } } var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.Configure(Config.GetSection(nameof(DebugOptions))); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddTransient(_ => { var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); var githubApiKey = Config["GithubApiKey"]; if (string.IsNullOrWhiteSpace(githubApiKey)) return client; client.Credentials = new Credentials(githubApiKey); return client; }); serviceCollection.AddSingleton(); // Database serviceCollection.AddSingleton(); // Caches serviceCollection.AddMemoryCache(); serviceCollection.AddSingleton(); // Configure Refit and Polly var defaultSystemTextJsonSettings = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); defaultSystemTextJsonSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var defaultRefitSettings = new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(defaultSystemTextJsonSettings), }; // HTTP Policies var retryStatusCodes = new[] { HttpStatusCode.RequestTimeout, // 408 HttpStatusCode.InternalServerError, // 500 HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 }; var delay = Backoff .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(80), retryCount: 5); var retryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync(delay); // Shorter timeout for local requests var localTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(3)); var localDelay = Backoff .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(50), retryCount: 3); var localRetryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync(localDelay, onRetryAsync: (x, y) => { Debug.WriteLine("Retrying local request..."); return Task.CompletedTask; }); // named client for update serviceCollection.AddHttpClient("UpdateClient") .AddPolicyHandler(retryPolicy); // Add Refit clients serviceCollection.AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); c.Timeout = TimeSpan.FromSeconds(15); }) .AddPolicyHandler(retryPolicy); // Add Refit client managers serviceCollection.AddHttpClient("A3Client") .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); serviceCollection.AddSingleton(services => new A3WebApiManager(services.GetRequiredService(), services.GetRequiredService()) { RefitSettings = defaultRefitSettings, }); // Add logging serviceCollection.AddLogging(builder => { builder.ClearProviders(); builder.AddFilter("Microsoft.Extensions.Http", LogLevel.Warning) .AddFilter("Microsoft", LogLevel.Warning) .AddFilter("System", LogLevel.Warning); builder.SetMinimumLevel(LogLevel.Debug); builder.AddNLog(logConfig); }); // Remove HTTPClientFactory logging serviceCollection.RemoveAll(); // Default error handling for 'SafeFireAndForget' SafeFireAndForgetExtensions.Initialize(); SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => { Logger?.Warn(ex, "Background Task failed: {ExceptionMessage}", ex.Message); }); serviceProvider = serviceCollection.BuildServiceProvider(); var settingsManager = serviceProvider.GetRequiredService(); // First time setup if needed if (!settingsManager.IsEulaAccepted()) { var setupWindow = serviceProvider.GetRequiredService(); if (setupWindow.ShowDialog() ?? false) { settingsManager.SetEulaAccepted(); } else { Current.Shutdown(); return; } } var window = serviceProvider.GetRequiredService(); window.Show(); } private void App_OnExit(object sender, ExitEventArgs e) { serviceProvider?.GetRequiredService().OnShutdown(); var settingsManager = serviceProvider?.GetRequiredService(); // Skip remaining steps if no library is set if (!(settingsManager?.TryFindLibrary() ?? false)) return; // If RemoveFolderLinksOnShutdown is set, delete all package junctions if (settingsManager.Settings.RemoveFolderLinksOnShutdown) { var sharedFolders = serviceProvider?.GetRequiredService(); sharedFolders?.RemoveLinksForAllPackages(); } // Dispose of database serviceProvider?.GetRequiredService().Dispose(); } [DoesNotReturn] private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { if (SentrySdk.IsEnabled) { SentrySdk.CaptureException(e.Exception); } var logger = serviceProvider?.GetRequiredService>(); logger?.LogCritical(e.Exception, "Unhandled Exception: {ExceptionMessage}", e.Exception.Message); if (IsExceptionWindowEnabled) { var vm = new ExceptionWindowViewModel { Exception = e.Exception }; var exceptionWindow = new ExceptionWindow { DataContext = vm }; if (MainWindow?.IsActive ?? false) { exceptionWindow.Owner = MainWindow; } exceptionWindow.ShowDialog(); } e.Handled = true; Current.Shutdown(1); Environment.Exit(1); } } } ================================================ FILE: StabilityMatrix/AppxManifest.xml ================================================  Stability Matrix Stability Matrix LLC None Assets\StoreLogo.png ================================================ FILE: StabilityMatrix/AssemblyInfo.cs ================================================ using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located //(used if a resource is not found in the page, // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located //(used if a resource is not found in the page, // app, or any theme specific resource dictionaries) )] ================================================ FILE: StabilityMatrix/Assets/7za - LICENSE.txt ================================================ 7-Zip Extra 18.01 ----------------- 7-Zip Extra is package of extra modules of 7-Zip. 7-Zip Copyright (C) 1999-2018 Igor Pavlov. 7-Zip is free software. Read License.txt for more information about license. Source code of binaries can be found at: http://www.7-zip.org/ 7-Zip Extra ~~~~~~~~~~~ License for use and distribution ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Copyright (C) 1999-2018 Igor Pavlov. 7-Zip Extra files are under the GNU LGPL license. Notes: You can use 7-Zip Extra on any computer, including a computer in a commercial organization. You don't need to register or pay for 7-Zip. GNU LGPL information -------------------- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You can receive a copy of the GNU Lesser General Public License from http://www.gnu.org/ ================================================ FILE: StabilityMatrix/Assets/Python310/LICENSE.txt ================================================ A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. In May 2000, Guido and the Python core development team moved to BeOpen.com to form the BeOpen PythonLabs team. In October of the same year, the PythonLabs team moved to Digital Creations, which became Zope Corporation. In 2001, the Python Software Foundation (PSF, see https://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation was a sponsoring member of the PSF. All Python releases are Open Source (see https://opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. Release Derived Year Owner GPL- from compatible? (1) 0.9.0 thru 1.2 1991-1995 CWI yes 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes 1.6 1.5.2 2000 CNRI no 2.0 1.6 2000 BeOpen.com no 1.6.1 1.6 2001 CNRI yes (2) 2.1 2.0+1.6.1 2001 PSF no 2.0.1 2.0+1.6.1 2001 PSF yes 2.1.1 2.1+2.0.1 2001 PSF yes 2.1.2 2.1.1 2002 PSF yes 2.1.3 2.1.2 2002 PSF yes 2.2 and above 2.1.1 2001-now PSF yes Footnotes: (1) GPL-compatible doesn't mean that we're distributing Python under the GPL. All Python licenses, unlike the GPL, let you distribute a modified version without making your changes open source. The GPL-compatible licenses make it possible to combine Python with other software that is released under the GPL; the others don't. (2) According to Richard Stallman, 1.6.1 is not GPL-compatible, because its license has a choice of law clause. According to CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 is "not incompatible" with the GPL. Thanks to the many outside volunteers who have worked under Guido's direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== Python software and documentation are licensed under the Python Software Foundation License Version 2. Starting with Python 3.8.6, examples, recipes, and other code in the documentation are dual licensed under the PSF License Version 2 and the Zero-Clause BSD license. Some software incorporated into Python is under different licenses. The licenses are listed with code falling under that license. PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 ------------------------------------------- BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 --------------------------------------- 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6.1 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6.1 alone or in any derivative version, provided, however, that CNRI's License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 1995-2001 Corporation for National Research Initiatives; All Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 is made available subject to the terms and conditions in CNRI's License Agreement. This Agreement together with Python 1.6.1 may be located on the internet using the following unique, persistent identifier (known as a handle): 1895.22/1013. This Agreement may also be obtained from a proxy server on the internet using the following URL: http://hdl.handle.net/1895.22/1013". 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6.1 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 1.6.1. 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. This License Agreement shall be governed by the federal intellectual property law of the United States, including without limitation the federal copyright law, and, to the extent such U.S. federal law does not apply, by the law of the Commonwealth of Virginia, excluding Virginia's conflict of law provisions. Notwithstanding the foregoing, with regard to derivative works based on Python 1.6.1 that incorporate non-separable material that was previously distributed under the GNU General Public License (GPL), the law of the Commonwealth of Virginia shall govern this License Agreement only as to issues arising under or with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and conditions of this License Agreement. ACCEPT CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 -------------------------------------------------- Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION ---------------------------------------------------------------------- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Additional Conditions for this Windows binary build --------------------------------------------------- This program is linked with and uses Microsoft Distributable Code, copyrighted by Microsoft Corporation. The Microsoft Distributable Code is embedded in each .exe, .dll and .pyd file as a result of running the code through a linker. If you further distribute programs that include the Microsoft Distributable Code, you must comply with the restrictions on distribution specified by Microsoft. In particular, you must require distributors and external end users to agree to terms that protect the Microsoft Distributable Code at least as much as Microsoft's own requirements for the Distributable Code. See Microsoft's documentation (included in its developer tools and on its website at microsoft.com) for specific details. Redistribution of the Windows binary build of the Python interpreter complies with this agreement, provided that you do not: - alter any copyright, trademark or patent notice in Microsoft's Distributable Code; - use Microsoft's trademarks in your programs' names or in a way that suggests your programs come from or are endorsed by Microsoft; - distribute Microsoft's Distributable Code to run on a platform other than Microsoft operating systems, run-time technologies or application platforms; or - include Microsoft Distributable Code in malicious, deceptive or unlawful programs. These restrictions apply only to the Microsoft Distributable Code as defined above, not to Python itself or any programs running on the Python interpreter. The redistribution of the Python interpreter and libraries is governed by the Python Software License included with this file, or by other licenses as marked. -------------------------------------------------------------------------- This program, "bzip2", the associated library "libbzip2", and all documentation, are copyright (C) 1996-2019 Julian R Seward. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Julian Seward, jseward@acm.org bzip2/libbzip2 version 1.0.8 of 13 July 2019 -------------------------------------------------------------------------- LICENSE ISSUES ============== The OpenSSL toolkit stays under a double license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ libffi - Copyright (c) 1996-2014 Anthony Green, Red Hat, Inc and others. See source files for details. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ``Software''), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This software is copyrighted by the Regents of the University of California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState Corporation and other parties. The following terms apply to all files associated with the software unless explicitly disclaimed in individual files. The authors hereby grant permission to use, copy, modify, distribute, and license this software and its documentation for any purpose, provided that existing copyright notices are retained in all copies and that this notice is included verbatim in any distributions. No written agreement, license, or royalty fee is required for any of the authorized uses. Modifications to this software may be copyrighted by their authors and need not follow the licensing terms described here, provided that the new terms are clearly indicated on the first page of each file where they apply. IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. GOVERNMENT USE: If you are acquiring this software on behalf of the U.S. government, the Government shall have only "Restricted Rights" in the software and related documentation as defined in the Federal Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you are acquiring the software on behalf of the Department of Defense, the software shall be classified as "Commercial Computer Software" and the Government shall have only "Restricted Rights" as defined in Clause 252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the authors grant the U.S. Government and others acting in its behalf permission to use and distribute the software in accordance with the terms specified in this license. This software is copyrighted by the Regents of the University of California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState Corporation, Apple Inc. and other parties. The following terms apply to all files associated with the software unless explicitly disclaimed in individual files. The authors hereby grant permission to use, copy, modify, distribute, and license this software and its documentation for any purpose, provided that existing copyright notices are retained in all copies and that this notice is included verbatim in any distributions. No written agreement, license, or royalty fee is required for any of the authorized uses. Modifications to this software may be copyrighted by their authors and need not follow the licensing terms described here, provided that the new terms are clearly indicated on the first page of each file where they apply. IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. GOVERNMENT USE: If you are acquiring this software on behalf of the U.S. government, the Government shall have only "Restricted Rights" in the software and related documentation as defined in the Federal Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you are acquiring the software on behalf of the Department of Defense, the software shall be classified as "Commercial Computer Software" and the Government shall have only "Restricted Rights" as defined in Clause 252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the authors grant the U.S. Government and others acting in its behalf permission to use and distribute the software in accordance with the terms specified in this license. Copyright (c) 1993-1999 Ioi Kim Lam. Copyright (c) 2000-2001 Tix Project Group. Copyright (c) 2004 ActiveState This software is copyrighted by the above entities and other parties. The following terms apply to all files associated with the software unless explicitly disclaimed in individual files. The authors hereby grant permission to use, copy, modify, distribute, and license this software and its documentation for any purpose, provided that existing copyright notices are retained in all copies and that this notice is included verbatim in any distributions. No written agreement, license, or royalty fee is required for any of the authorized uses. Modifications to this software may be copyrighted by their authors and need not follow the licensing terms described here, provided that the new terms are clearly indicated on the first page of each file where they apply. IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. GOVERNMENT USE: If you are acquiring this software on behalf of the U.S. government, the Government shall have only "Restricted Rights" in the software and related documentation as defined in the Federal Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you are acquiring the software on behalf of the Department of Defense, the software shall be classified as "Commercial Computer Software" and the Government shall have only "Restricted Rights" as defined in Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the authors grant the U.S. Government and others acting in its behalf permission to use and distribute the software in accordance with the terms specified in this license. ---------------------------------------------------------------------- Parts of this software are based on the Tcl/Tk software copyrighted by the Regents of the University of California, Sun Microsystems, Inc., and other parties. The original license terms of the Tcl/Tk software distribution is included in the file docs/license.tcltk. Parts of this software are based on the HTML Library software copyrighted by Sun Microsystems, Inc. The original license terms of the HTML Library software distribution is included in the file docs/license.html_lib. ================================================ FILE: StabilityMatrix/Assets/Python310/python310._pth ================================================ python310.zip . # Uncomment to run site.main() automatically import site ================================================ FILE: StabilityMatrix/Assets/automatic_vladmandic.sm-package.yml ================================================ name: automatic display-name: SD.Next Web UI author: vladmandic download: steps: - uses: git with: args: clone ${git_repo_url} ${install_dir} - uses: git with: args: checkout ${version_commit_sha} install: steps: - name: Install PyTorch (CUDA) uses: venv-run if: system.has_nvidia_gpu with: args: - pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 - pip install xformers - name: Install PyTorch (DirectML) uses: venv-run if: not system.has_nvidia_gpu and system.has_amd_gpu with: args: pip install torch-directml - name: Install Requirements uses: venv-run with: args: pip install -r requirements.txt ================================================ FILE: StabilityMatrix/Assets/licenses.json ================================================ [{"PackageName":"7-Zip.CommandLine","PackageVersion":"18.1.0","PackageUrl":"http://www.7-zip.org/","Copyright":"7-Zip Copyright (C) 1999-2018 Igor Pavlov.","Authors":["Igor Pavlov"],"Description":"7-Zip is a file archiver with a high compression ratio.\r\n\r\n7za.exe is a standalone console version of 7-Zip with reduced formats support.\r\n\r\n7za.exe features:\r\n\r\n- High compression ratio in 7z format\r\n- Supported formats:\r\n - Packing / unpacking: 7z, xz, ZIP, GZIP, BZIP2 and TAR \r\n - Unpacking only: Z, lzma, CAB.\r\n- Highest compression ratio for ZIP and GZIP formats.\r\n- Fast compression and decompression\r\n- Strong AES-256 encryption in 7z and ZIP formats.","LicenseUrl":"http://www.7-zip.org/license.txt","LicenseType":""},{"PackageName":"CommunityToolkit.Mvvm","PackageVersion":"8.2.0","PackageUrl":"https://github.com/CommunityToolkit/dotnet","Copyright":"(c) .NET Foundation and Contributors. All rights reserved.","Authors":["Microsoft"],"Description":"This package includes a .NET MVVM library with helpers such as:\r\n - ObservableObject: a base class for objects implementing the INotifyPropertyChanged interface.\r\n - ObservableRecipient: a base class for observable objects with support for the IMessenger service.\r\n - ObservableValidator: a base class for objects implementing the INotifyDataErrorInfo interface.\r\n - RelayCommand: a simple delegate command implementing the ICommand interface.\r\n - AsyncRelayCommand: a delegate command supporting asynchronous operations and cancellation.\r\n - WeakReferenceMessenger: a messaging system to exchange messages through different loosely-coupled objects.\r\n - StrongReferenceMessenger: a high-performance messaging system that trades weak references for speed.\r\n - Ioc: a helper class to configure dependency injection service containers.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"FuzzySharp","PackageVersion":"2.0.2","PackageUrl":"https://github.com/JakeBayer/FuzzySharp","Copyright":"","Authors":["Jacob Bayer"],"Description":"Fuzzy string matcher based on FuzzyWuzzy algorithm from SeatGeek","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"Markdown.Xaml","PackageVersion":"1.0.0","PackageUrl":"https://github.com/theunrepentantgeek/Markdown.XAML","Copyright":"Copyright (c) 2010 Bevan Arps","Authors":["Bevan Arps"],"Description":"Markdown XAML processor","LicenseUrl":"https://github.com/theunrepentantgeek/Markdown.XAML/blob/master/License.txt","LicenseType":""},{"PackageName":"NCode.ReparsePoints","PackageVersion":"1.0.2","PackageUrl":"https://github.com/NCodeGroup/NCode.ReparsePoints","Copyright":"Copyright © 2015 NCode Group","Authors":["NCode Group"],"Description":"This library provides an API to create and inspect win32 file and folder reparse points such as hard links, junctions (aka soft links), and symbolic links.","LicenseUrl":"https://raw.githubusercontent.com/NCodeGroup/NCode.ReparsePoints/master/LICENSE.txt","LicenseType":""},{"PackageName":"NLog","PackageVersion":"5.1.4","PackageUrl":"https://nlog-project.org/","Copyright":"Copyright (c) 2004-2023 NLog Project - https://nlog-project.org/","Authors":["Jarek Kowalski","Kim Christensen","Julian Verdurmen"],"Description":"NLog is a logging platform for .NET with rich log routing and management capabilities.\r\nNLog supports traditional logging, structured logging and the combination of both.\r\n\r\nSupported platforms:\r\n\r\n- .NET 5, 6 and 7\r\n- .NET Core 1, 2 and 3\r\n- .NET Standard 1.3+ and 2.0+\r\n- .NET Framework 3.5 - 4.8\r\n- Xamarin Android + iOS (.NET Standard)\r\n- Mono 4\r\n\r\nFor ASP.NET Core, check: https://www.nuget.org/packages/NLog.Web.AspNetCore","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause"},{"PackageName":"NLog.Extensions.Logging","PackageVersion":"5.2.3","PackageUrl":"https://github.com/NLog/NLog.Extensions.Logging","Copyright":"","Authors":["Microsoft","Julian Verdurmen"],"Description":"NLog LoggerProvider for Microsoft.Extensions.Logging for logging in .NET Standard libraries and .NET Core applications.\r\n\r\nFor ASP.NET Core, check: https://www.nuget.org/packages/NLog.Web.AspNetCore","LicenseUrl":"https://licenses.nuget.org/BSD-2-Clause","LicenseType":"BSD-2-Clause"},{"PackageName":"Octokit","PackageVersion":"6.0.0","PackageUrl":"https://github.com/octokit/octokit.net","Copyright":"Copyright GitHub 2017","Authors":["GitHub"],"Description":"An async-based GitHub API client library for .NET and .NET Core","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"Ookii.Dialogs.Wpf","PackageVersion":"5.0.1","PackageUrl":"https://github.com/ookii-dialogs/ookii-dialogs-wpf","Copyright":"Copyright (c) 2009-2021 Ookii Dialogs Contributors","Authors":["Ookii Dialogs Contributors"],"Description":"Ookii.Dialogs.Wpf is a class library for WPF applications providing several common dialogs. Included are classes for task dialogs, credential dialogs, progress dialogs, and common file dialogs.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause"},{"PackageName":"Polly","PackageVersion":"7.2.3","PackageUrl":"https://github.com/App-vNext/Polly","Copyright":"Copyright (c) 2022, App vNext","Authors":["Michael Wolfenden"," App vNext"],"Description":"Polly is a library that allows developers to express resilience and transient fault handling policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause"},{"PackageName":"Polly.Contrib.WaitAndRetry","PackageVersion":"1.1.1","PackageUrl":"https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry","Copyright":"Copyright (c) 2020, App vNext and contributors","Authors":["Grant Dickinson"," App vNext"],"Description":"Polly.Contrib.WaitAndRetry is an extension library for Polly containing helper methods for a variety of wait-and-retry strategies.","LicenseUrl":"https://licenses.nuget.org/BSD-3-Clause","LicenseType":"BSD-3-Clause"},{"PackageName":"pythonnet","PackageVersion":"3.0.1","PackageUrl":"https://pythonnet.github.io/","Copyright":"","Authors":["Python.Runtime"],"Description":"Python and CLR (.NET and Mono) cross-platform language interop","LicenseUrl":"https://www.nuget.org/packages/pythonnet/3.0.1/License","LicenseType":"LICENSE"},{"PackageName":"Refit","PackageVersion":"6.3.2","PackageUrl":"https://github.com/reactiveui/refit","Copyright":"","Authors":[".NET Foundation and Contributors"],"Description":"The automatic type-safe REST library for Xamarin and .NET","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"Refit.HttpClientFactory","PackageVersion":"6.3.2","PackageUrl":"https://github.com/reactiveui/refit","Copyright":"","Authors":[".NET Foundation and Contributors"],"Description":"Refit HTTP Client Factory Extensions","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"SharpCompress","PackageVersion":"0.33.0","PackageUrl":"https://github.com/adamhathcock/sharpcompress","Copyright":"Copyright (c) 2014 Adam Hathcock","Authors":["Adam Hathcock"],"Description":"SharpCompress is a compression library for NET Standard 2.0/2.1/NET 6.0/NET 7.0 that can unrar, decompress 7zip, decompress xz, zip/unzip, tar/untar lzip/unlzip, bzip2/unbzip2 and gzip/ungzip with forward-only reading and file random access APIs. Write support for zip/tar/bzip2/gzip is implemented.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"},{"PackageName":"WPF-UI","PackageVersion":"3.0.0-preview.2","PackageUrl":"https://github.com/lepoco/wpfui","Copyright":"Copyright (C) 2021-2022 Leszek Pomianowski and WPF UI Contributors","Authors":["lepo.co"],"Description":"A simple way to make your application written in WPF keep up with modern design trends. Library changes the base elements like Page, ToggleButton or List, and also includes additional controls like Navigation, NumberBox, Dialog or Snackbar.","LicenseUrl":"https://licenses.nuget.org/MIT","LicenseType":"MIT"}] ================================================ FILE: StabilityMatrix/Assets/sm-package.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "name": { "type": "string" }, "display-name": { "type": "string" }, "author": { "type": "string" }, "download": { "$ref": "#/definitions/steps" }, "install": { "$ref": "#/definitions/steps" } }, "required": ["name", "display-name", "author", "download", "install"], "definitions": { "steps": { "type": "object", "properties": { "steps": { "type": "array", "items": { "$ref": "#/definitions/step" } } }, "required": ["steps"] }, "step": { "type": "object", "properties": { "name": { "type": "string" }, "uses": { "enum": ["venv", "venv-run", "git"] }, "if": { "type": "string" }, "with": {} }, "required": ["uses", "with"], "allOf": [ { "if": { "properties": { "uses": { "const": "venv" } } }, "then": { "properties": { "with": { "$ref": "#/definitions/with-path" } } } }, { "if": { "properties": { "uses": { "const": "venv-run" } } }, "then": { "properties": { "with": { "$ref": "#/definitions/with-args" } } } }, { "if": { "properties": { "uses": { "const": "git" } } }, "then": { "properties": { "with": { "$ref": "#/definitions/with-args" } } } } ] }, "with-path": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"], "additionalProperties": false }, "with-args": { "type": "object", "properties": { "args": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] } }, "required": ["args"], "additionalProperties": false } } } ================================================ FILE: StabilityMatrix/Assets/venv/__init__.py ================================================ """ Virtual environment (venv) package for Python. Based on PEP 405. Copyright (C) 2011-2014 Vinay Sajip. Licensed to the PSF under a contributor agreement. """ import logging import os import shutil import subprocess import sys import sysconfig import types CORE_VENV_DEPS = ('pip', 'setuptools') logger = logging.getLogger(__name__) class EnvBuilder: """ This class exists to allow virtual environment creation to be customized. The constructor parameters determine the builder's behaviour when called upon to create a virtual environment. By default, the builder makes the system (global) site-packages dir *un*available to the created environment. If invoked using the Python -m option, the default is to use copying on Windows platforms but symlinks elsewhere. If instantiated some other way, the default is to *not* use symlinks. :param system_site_packages: If True, the system (global) site-packages dir is available to created environments. :param clear: If True, delete the contents of the environment directory if it already exists, before environment creation. :param symlinks: If True, attempt to symlink rather than copy files into virtual environment. :param upgrade: If True, upgrade an existing virtual environment. :param with_pip: If True, ensure pip is installed in the virtual environment :param prompt: Alternative terminal prefix for the environment. :param upgrade_deps: Update the base venv modules to the latest on PyPI """ def __init__(self, system_site_packages=False, clear=False, symlinks=False, upgrade=False, with_pip=False, prompt=None, upgrade_deps=False): self.system_site_packages = system_site_packages self.clear = clear self.symlinks = symlinks self.upgrade = upgrade self.with_pip = with_pip if prompt == '.': # see bpo-38901 prompt = os.path.basename(os.getcwd()) self.prompt = prompt self.upgrade_deps = upgrade_deps def create(self, env_dir): """ Create a virtual environment in a directory. :param env_dir: The target directory to create an environment in. """ env_dir = os.path.abspath(env_dir) context = self.ensure_directories(env_dir) # See issue 24875. We need system_site_packages to be False # until after pip is installed. true_system_site_packages = self.system_site_packages self.system_site_packages = False self.create_configuration(context) self.setup_python(context) if self.with_pip: self._setup_pip(context) if not self.upgrade: self.setup_scripts(context) self.post_setup(context) if true_system_site_packages: # We had set it to False before, now # restore it and rewrite the configuration self.system_site_packages = True self.create_configuration(context) if self.upgrade_deps: self.upgrade_dependencies(context) def clear_directory(self, path): for fn in os.listdir(path): fn = os.path.join(path, fn) if os.path.islink(fn) or os.path.isfile(fn): os.remove(fn) elif os.path.isdir(fn): shutil.rmtree(fn) def ensure_directories(self, env_dir): """ Create the directories for the environment. Returns a context object which holds paths in the environment, for use by subsequent logic. """ def create_if_needed(d): if not os.path.exists(d): os.makedirs(d) elif os.path.islink(d) or os.path.isfile(d): raise ValueError('Unable to create directory %r' % d) if os.path.exists(env_dir) and self.clear: self.clear_directory(env_dir) context = types.SimpleNamespace() context.env_dir = env_dir context.env_name = os.path.split(env_dir)[1] prompt = self.prompt if self.prompt is not None else context.env_name context.prompt = '(%s) ' % prompt create_if_needed(env_dir) executable = sys._base_executable if not executable: # see gh-96861 raise ValueError('Unable to determine path to the running ' 'Python interpreter. Provide an explicit path or ' 'check that your PATH environment variable is ' 'correctly set.') dirname, exename = os.path.split(os.path.abspath(executable)) context.executable = executable context.python_dir = dirname context.python_exe = exename if sys.platform == 'win32': binname = 'Scripts' incpath = 'Include' libpath = os.path.join(env_dir, 'Lib', 'site-packages') else: binname = 'bin' incpath = 'include' libpath = os.path.join(env_dir, 'lib', 'python%d.%d' % sys.version_info[:2], 'site-packages') context.inc_path = path = os.path.join(env_dir, incpath) create_if_needed(path) create_if_needed(libpath) # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX if ((sys.maxsize > 2**32) and (os.name == 'posix') and (sys.platform != 'darwin')): link_path = os.path.join(env_dir, 'lib64') if not os.path.exists(link_path): # Issue #21643 os.symlink('lib', link_path) context.bin_path = binpath = os.path.join(env_dir, binname) context.bin_name = binname context.env_exe = os.path.join(binpath, exename) create_if_needed(binpath) # Assign and update the command to use when launching the newly created # environment, in case it isn't simply the executable script (e.g. bpo-45337) context.env_exec_cmd = context.env_exe if sys.platform == 'win32': # bpo-45337: Fix up env_exec_cmd to account for file system redirections. # Some redirects only apply to CreateFile and not CreateProcess real_env_exe = os.path.realpath(context.env_exe) if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe): logger.warning('Actual environment location may have moved due to ' 'redirects, links or junctions.\n' ' Requested location: "%s"\n' ' Actual location: "%s"', context.env_exe, real_env_exe) context.env_exec_cmd = real_env_exe return context def create_configuration(self, context): """ Create a configuration file indicating where the environment's Python was copied from, and whether the system site-packages should be made available in the environment. :param context: The information for the environment creation request being processed. """ context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg') with open(path, 'w', encoding='utf-8') as f: f.write('home = %s\n' % context.python_dir) if self.system_site_packages: incl = 'true' else: incl = 'false' f.write('include-system-site-packages = %s\n' % incl) f.write('version = %d.%d.%d\n' % sys.version_info[:3]) if self.prompt is not None: f.write(f'prompt = {self.prompt!r}\n') if os.name != 'nt': def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): """ Try symlinking a file, and if that fails, fall back to copying. """ force_copy = not self.symlinks if not force_copy: try: if not os.path.islink(dst): # can't link to itself! if relative_symlinks_ok: assert os.path.dirname(src) == os.path.dirname(dst) os.symlink(os.path.basename(src), dst) else: os.symlink(src, dst) except Exception: # may need to use a more specific exception logger.warning('Unable to symlink %r to %r', src, dst) force_copy = True if force_copy: shutil.copyfile(src, dst) else: def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): """ Try symlinking a file, and if that fails, fall back to copying. """ bad_src = os.path.lexists(src) and not os.path.exists(src) if self.symlinks and not bad_src and not os.path.islink(dst): try: if relative_symlinks_ok: assert os.path.dirname(src) == os.path.dirname(dst) os.symlink(os.path.basename(src), dst) else: os.symlink(src, dst) return except Exception: # may need to use a more specific exception logger.warning('Unable to symlink %r to %r', src, dst) # On Windows, we rewrite symlinks to our base python.exe into # copies of venvlauncher.exe basename, ext = os.path.splitext(os.path.basename(src)) srcfn = os.path.join(os.path.dirname(__file__), "scripts", "nt", basename + ext) # Builds or venv's from builds need to remap source file # locations, as we do not put them into Lib/venv/scripts if sysconfig.is_python_build(True) or not os.path.isfile(srcfn): if basename.endswith('_d'): ext = '_d' + ext basename = basename[:-2] if basename == 'python': basename = 'venvlauncher' elif basename == 'pythonw': basename = 'venvwlauncher' src = os.path.join(os.path.dirname(src), basename + ext) else: src = srcfn if not os.path.exists(src): if not bad_src: logger.warning('Unable to copy %r', src) return shutil.copyfile(src, dst) def setup_python(self, context): """ Set up a Python executable in the environment. :param context: The information for the environment creation request being processed. """ binpath = context.bin_path path = context.env_exe copier = self.symlink_or_copy dirname = context.python_dir if os.name != 'nt': copier(context.executable, path) if not os.path.islink(path): os.chmod(path, 0o755) for suffix in ('python', 'python3', f'python3.{sys.version_info[1]}'): path = os.path.join(binpath, suffix) if not os.path.exists(path): # Issue 18807: make copies if # symlinks are not wanted copier(context.env_exe, path, relative_symlinks_ok=True) if not os.path.islink(path): os.chmod(path, 0o755) else: if self.symlinks: # For symlinking, we need a complete copy of the root directory # If symlinks fail, you'll get unnecessary copies of files, but # we assume that if you've opted into symlinks on Windows then # you know what you're doing. suffixes = [ f for f in os.listdir(dirname) if os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll') ] if sysconfig.is_python_build(True): suffixes = [ f for f in suffixes if os.path.normcase(f).startswith(('python', 'vcruntime')) ] else: suffixes = {'python.exe', 'python_d.exe', 'pythonw.exe', 'pythonw_d.exe'} base_exe = os.path.basename(context.env_exe) suffixes.add(base_exe) for suffix in suffixes: src = os.path.join(dirname, suffix) if os.path.lexists(src): copier(src, os.path.join(binpath, suffix)) if sysconfig.is_python_build(True): # copy init.tcl for root, dirs, files in os.walk(context.python_dir): if 'init.tcl' in files: tcldir = os.path.basename(root) tcldir = os.path.join(context.env_dir, 'Lib', tcldir) if not os.path.exists(tcldir): os.makedirs(tcldir) src = os.path.join(root, 'init.tcl') dst = os.path.join(tcldir, 'init.tcl') shutil.copyfile(src, dst) break def _call_new_python(self, context, *py_args, **kwargs): """Executes the newly created Python using safe-ish options""" # gh-98251: We do not want to just use '-I' because that masks # legitimate user preferences (such as not writing bytecode). All we # really need is to ensure that the path variables do not overrule # normal venv handling. args = [context.env_exec_cmd, *py_args] kwargs['env'] = env = os.environ.copy() env['VIRTUAL_ENV'] = context.env_dir env.pop('PYTHONHOME', None) env.pop('PYTHONPATH', None) kwargs['cwd'] = context.env_dir kwargs['executable'] = context.env_exec_cmd subprocess.check_output(args, **kwargs) def _setup_pip(self, context): """Installs or upgrades pip in a virtual environment""" self._call_new_python(context, '-m', 'ensurepip', '--upgrade', '--default-pip', stderr=subprocess.STDOUT) def setup_scripts(self, context): """ Set up scripts into the created environment from a directory. This method installs the default scripts into the environment being created. You can prevent the default installation by overriding this method if you really need to, or if you need to specify a different location for the scripts to install. By default, the 'scripts' directory in the venv package is used as the source of scripts to install. """ path = os.path.abspath(os.path.dirname(__file__)) path = os.path.join(path, 'scripts') self.install_scripts(context, path) def post_setup(self, context): """ Hook for post-setup modification of the venv. Subclasses may install additional packages or scripts here, add activation shell scripts, etc. :param context: The information for the environment creation request being processed. """ pass def replace_variables(self, text, context): """ Replace variable placeholders in script text with context-specific variables. Return the text passed in , but with variables replaced. :param text: The text in which to replace placeholder variables. :param context: The information for the environment creation request being processed. """ text = text.replace('__VENV_DIR__', context.env_dir) text = text.replace('__VENV_NAME__', context.env_name) text = text.replace('__VENV_PROMPT__', context.prompt) text = text.replace('__VENV_BIN_NAME__', context.bin_name) text = text.replace('__VENV_PYTHON__', context.env_exe) return text def install_scripts(self, context, path): """ Install scripts into the created environment from a directory. :param context: The information for the environment creation request being processed. :param path: Absolute pathname of a directory containing script. Scripts in the 'common' subdirectory of this directory, and those in the directory named for the platform being run on, are installed in the created environment. Placeholder variables are replaced with environment- specific values. """ binpath = context.bin_path plen = len(path) for root, dirs, files in os.walk(path): if root == path: # at top-level, remove irrelevant dirs for d in dirs[:]: if d not in ('common', os.name): dirs.remove(d) continue # ignore files in top level for f in files: if (os.name == 'nt' and f.startswith('python') and f.endswith(('.exe', '.pdb'))): continue srcfile = os.path.join(root, f) suffix = root[plen:].split(os.sep)[2:] if not suffix: dstdir = binpath else: dstdir = os.path.join(binpath, *suffix) if not os.path.exists(dstdir): os.makedirs(dstdir) dstfile = os.path.join(dstdir, f) with open(srcfile, 'rb') as f: data = f.read() if not srcfile.endswith(('.exe', '.pdb')): try: data = data.decode('utf-8') data = self.replace_variables(data, context) data = data.encode('utf-8') except UnicodeError as e: data = None logger.warning('unable to copy script %r, ' 'may be binary: %s', srcfile, e) if data is not None: with open(dstfile, 'wb') as f: f.write(data) shutil.copymode(srcfile, dstfile) def upgrade_dependencies(self, context): logger.debug( f'Upgrading {CORE_VENV_DEPS} packages in {context.bin_path}' ) self._call_new_python(context, '-m', 'pip', 'install', '--upgrade', *CORE_VENV_DEPS) def create(env_dir, system_site_packages=False, clear=False, symlinks=False, with_pip=False, prompt=None, upgrade_deps=False): """Create a virtual environment in a directory.""" builder = EnvBuilder(system_site_packages=system_site_packages, clear=clear, symlinks=symlinks, with_pip=with_pip, prompt=prompt, upgrade_deps=upgrade_deps) builder.create(env_dir) def main(args=None): compatible = True if sys.version_info < (3, 3): compatible = False elif not hasattr(sys, 'base_prefix'): compatible = False if not compatible: raise ValueError('This script is only for use with Python >= 3.3') else: import argparse parser = argparse.ArgumentParser(prog=__name__, description='Creates virtual Python ' 'environments in one or ' 'more target ' 'directories.', epilog='Once an environment has been ' 'created, you may wish to ' 'activate it, e.g. by ' 'sourcing an activate script ' 'in its bin directory.') parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', help='A directory to create the environment in.') parser.add_argument('--system-site-packages', default=False, action='store_true', dest='system_site', help='Give the virtual environment access to the ' 'system site-packages dir.') if os.name == 'nt': use_symlinks = False else: use_symlinks = True group = parser.add_mutually_exclusive_group() group.add_argument('--symlinks', default=use_symlinks, action='store_true', dest='symlinks', help='Try to use symlinks rather than copies, ' 'when symlinks are not the default for ' 'the platform.') group.add_argument('--copies', default=not use_symlinks, action='store_false', dest='symlinks', help='Try to use copies rather than symlinks, ' 'even when symlinks are the default for ' 'the platform.') parser.add_argument('--clear', default=False, action='store_true', dest='clear', help='Delete the contents of the ' 'environment directory if it ' 'already exists, before ' 'environment creation.') parser.add_argument('--upgrade', default=False, action='store_true', dest='upgrade', help='Upgrade the environment ' 'directory to use this version ' 'of Python, assuming Python ' 'has been upgraded in-place.') parser.add_argument('--without-pip', dest='with_pip', default=True, action='store_false', help='Skips installing or upgrading pip in the ' 'virtual environment (pip is bootstrapped ' 'by default)') parser.add_argument('--prompt', help='Provides an alternative prompt prefix for ' 'this environment.') parser.add_argument('--upgrade-deps', default=False, action='store_true', dest='upgrade_deps', help='Upgrade core dependencies: {} to the latest ' 'version in PyPI'.format( ' '.join(CORE_VENV_DEPS))) options = parser.parse_args(args) if options.upgrade and options.clear: raise ValueError('you cannot supply --upgrade and --clear together.') builder = EnvBuilder(system_site_packages=options.system_site, clear=options.clear, symlinks=options.symlinks, upgrade=options.upgrade, with_pip=options.with_pip, prompt=options.prompt, upgrade_deps=options.upgrade_deps) for d in options.dirs: builder.create(d) if __name__ == '__main__': rc = 1 try: main() rc = 0 except Exception as e: print('Error: %s' % e, file=sys.stderr) sys.exit(rc) ================================================ FILE: StabilityMatrix/Assets/venv/__main__.py ================================================ import sys from . import main rc = 1 try: main() rc = 0 except Exception as e: print('Error: %s' % e, file=sys.stderr) sys.exit(rc) ================================================ FILE: StabilityMatrix/Assets/venv/scripts/common/Activate.ps1 ================================================ <# .Synopsis Activate a Python virtual environment for the current PowerShell session. .Description Pushes the python executable for a virtual environment to the front of the $Env:PATH environment variable and sets the prompt to signify that you are in a Python virtual environment. Makes use of the command line switches as well as the `pyvenv.cfg` file values present in the virtual environment. .Parameter VenvDir Path to the directory that contains the virtual environment to activate. The default value for this is the parent of the directory that the Activate.ps1 script is located within. .Parameter Prompt The prompt prefix to display when this virtual environment is activated. By default, this prompt is the name of the virtual environment folder (VenvDir) surrounded by parentheses and followed by a single space (ie. '(.venv) '). .Example Activate.ps1 Activates the Python virtual environment that contains the Activate.ps1 script. .Example Activate.ps1 -Verbose Activates the Python virtual environment that contains the Activate.ps1 script, and shows extra information about the activation as it executes. .Example Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv Activates the Python virtual environment located in the specified location. .Example Activate.ps1 -Prompt "MyPython" Activates the Python virtual environment that contains the Activate.ps1 script, and prefixes the current prompt with the specified string (surrounded in parentheses) while the virtual environment is active. .Notes On Windows, it may be required to enable this Activate.ps1 script by setting the execution policy for the user. You can do this by issuing the following PowerShell command: PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser For more information on Execution Policies: https://go.microsoft.com/fwlink/?LinkID=135170 #> Param( [Parameter(Mandatory = $false)] [String] $VenvDir, [Parameter(Mandatory = $false)] [String] $Prompt ) <# Function declarations --------------------------------------------------- #> <# .Synopsis Remove all shell session elements added by the Activate script, including the addition of the virtual environment's Python executable from the beginning of the PATH variable. .Parameter NonDestructive If present, do not remove this function from the global namespace for the session. #> function global:deactivate ([switch]$NonDestructive) { # Revert to original values # The prior prompt: if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT } # The prior PYTHONHOME: if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME } # The prior PATH: if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH Remove-Item -Path Env:_OLD_VIRTUAL_PATH } # Just remove the VIRTUAL_ENV altogether: if (Test-Path -Path Env:VIRTUAL_ENV) { Remove-Item -Path env:VIRTUAL_ENV } # Just remove VIRTUAL_ENV_PROMPT altogether. if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { Remove-Item -Path env:VIRTUAL_ENV_PROMPT } # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force } # Leave deactivate function in the global namespace if requested: if (-not $NonDestructive) { Remove-Item -Path function:deactivate } } <# .Description Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the given folder, and returns them in a map. For each line in the pyvenv.cfg file, if that line can be parsed into exactly two strings separated by `=` (with any amount of whitespace surrounding the =) then it is considered a `key = value` line. The left hand string is the key, the right hand is the value. If the value starts with a `'` or a `"` then the first and last character is stripped from the value before being captured. .Parameter ConfigDir Path to the directory that contains the `pyvenv.cfg` file. #> function Get-PyVenvConfig( [String] $ConfigDir ) { Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue # An empty map will be returned if no config file is found. $pyvenvConfig = @{ } if ($pyvenvConfigPath) { Write-Verbose "File exists, parse `key = value` lines" $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath $pyvenvConfigContent | ForEach-Object { $keyval = $PSItem -split "\s*=\s*", 2 if ($keyval[0] -and $keyval[1]) { $val = $keyval[1] # Remove extraneous quotations around a string value. if ("'""".Contains($val.Substring(0, 1))) { $val = $val.Substring(1, $val.Length - 2) } $pyvenvConfig[$keyval[0]] = $val Write-Verbose "Adding Key: '$($keyval[0])'='$val'" } } } return $pyvenvConfig } <# Begin Activate script --------------------------------------------------- #> # Determine the containing directory of this script $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition $VenvExecDir = Get-Item -Path $VenvExecPath Write-Verbose "Activation script is located in path: '$VenvExecPath'" Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" # Set values required in priority: CmdLine, ConfigFile, Default # First, get the location of the virtual environment, it might not be # VenvExecDir if specified on the command line. if ($VenvDir) { Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" } else { Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") Write-Verbose "VenvDir=$VenvDir" } # Next, read the `pyvenv.cfg` file to determine any required value such # as `prompt`. $pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir # Next, set the prompt from the command line, or the config file, or # just use the name of the virtual environment folder. if ($Prompt) { Write-Verbose "Prompt specified as argument, using '$Prompt'" } else { Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" if ($pyvenvCfg -and $pyvenvCfg['prompt']) { Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" $Prompt = $pyvenvCfg['prompt']; } else { Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" $Prompt = Split-Path -Path $venvDir -Leaf } } Write-Verbose "Prompt = '$Prompt'" Write-Verbose "VenvDir='$VenvDir'" # Deactivate any currently active virtual environment, but leave the # deactivate function in place. deactivate -nondestructive # Now set the environment variable VIRTUAL_ENV, used by many tools to determine # that there is an activated venv. $env:VIRTUAL_ENV = $VenvDir if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { Write-Verbose "Setting prompt to '$Prompt'" # Set the prompt to include the env name # Make sure _OLD_VIRTUAL_PROMPT is global function global:_OLD_VIRTUAL_PROMPT { "" } Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt function global:prompt { Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " _OLD_VIRTUAL_PROMPT } $env:VIRTUAL_ENV_PROMPT = $Prompt } # Clear PYTHONHOME if (Test-Path -Path Env:PYTHONHOME) { Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME Remove-Item -Path Env:PYTHONHOME } # Add the venv to the PATH Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH $Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" ================================================ FILE: StabilityMatrix/Assets/venv/scripts/common/activate ================================================ # This file must be used with "source bin/activate" *from bash* # you cannot run it directly deactivate () { # reset old environment variables if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then PATH="${_OLD_VIRTUAL_PATH:-}" export PATH unset _OLD_VIRTUAL_PATH fi if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" export PYTHONHOME unset _OLD_VIRTUAL_PYTHONHOME fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then hash -r 2> /dev/null fi if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then PS1="${_OLD_VIRTUAL_PS1:-}" export PS1 unset _OLD_VIRTUAL_PS1 fi unset VIRTUAL_ENV unset VIRTUAL_ENV_PROMPT if [ ! "${1:-}" = "nondestructive" ] ; then # Self destruct! unset -f deactivate fi } # unset irrelevant variables deactivate nondestructive VIRTUAL_ENV="__VENV_DIR__" export VIRTUAL_ENV _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" export PATH # unset PYTHONHOME if set # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) # could use `if (set -u; : $PYTHONHOME) ;` in bash if [ -n "${PYTHONHOME:-}" ] ; then _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" unset PYTHONHOME fi if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then _OLD_VIRTUAL_PS1="${PS1:-}" PS1="__VENV_PROMPT__${PS1:-}" export PS1 VIRTUAL_ENV_PROMPT="__VENV_PROMPT__" export VIRTUAL_ENV_PROMPT fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then hash -r 2> /dev/null fi ================================================ FILE: StabilityMatrix/Assets/venv/scripts/nt/activate.bat ================================================ @echo off rem This file is UTF-8 encoded, so we need to update the current code page while executing it for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do ( set _OLD_CODEPAGE=%%a ) if defined _OLD_CODEPAGE ( "%SystemRoot%\System32\chcp.com" 65001 > nul ) set VIRTUAL_ENV=__VENV_DIR__ if not defined PROMPT set PROMPT=$P$G if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT% if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME% set _OLD_VIRTUAL_PROMPT=%PROMPT% set PROMPT=__VENV_PROMPT__%PROMPT% if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME% set PYTHONHOME= if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH% if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH% set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH% set VIRTUAL_ENV_PROMPT=__VENV_PROMPT__ :END if defined _OLD_CODEPAGE ( "%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul set _OLD_CODEPAGE= ) ================================================ FILE: StabilityMatrix/Assets/venv/scripts/nt/deactivate.bat ================================================ @echo off if defined _OLD_VIRTUAL_PROMPT ( set "PROMPT=%_OLD_VIRTUAL_PROMPT%" ) set _OLD_VIRTUAL_PROMPT= if defined _OLD_VIRTUAL_PYTHONHOME ( set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%" set _OLD_VIRTUAL_PYTHONHOME= ) if defined _OLD_VIRTUAL_PATH ( set "PATH=%_OLD_VIRTUAL_PATH%" ) set _OLD_VIRTUAL_PATH= set VIRTUAL_ENV= set VIRTUAL_ENV_PROMPT= :END ================================================ FILE: StabilityMatrix/Assets/venv/scripts/posix/activate.csh ================================================ # This file must be used with "source bin/activate.csh" *from csh*. # You cannot run it directly. # Created by Davide Di Blasi . # Ported to Python 3.3 venv by Andrew Svetlov alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' # Unset irrelevant variables. deactivate nondestructive setenv VIRTUAL_ENV "__VENV_DIR__" set _OLD_VIRTUAL_PATH="$PATH" setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH" set _OLD_VIRTUAL_PROMPT="$prompt" if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then set prompt = "__VENV_PROMPT__$prompt" setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" endif alias pydoc python -m pydoc rehash ================================================ FILE: StabilityMatrix/Assets/venv/scripts/posix/activate.fish ================================================ # This file must be used with "source /bin/activate.fish" *from fish* # (https://fishshell.com/); you cannot run it directly. function deactivate -d "Exit virtual environment and return to normal shell environment" # reset old environment variables if test -n "$_OLD_VIRTUAL_PATH" set -gx PATH $_OLD_VIRTUAL_PATH set -e _OLD_VIRTUAL_PATH end if test -n "$_OLD_VIRTUAL_PYTHONHOME" set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME set -e _OLD_VIRTUAL_PYTHONHOME end if test -n "$_OLD_FISH_PROMPT_OVERRIDE" set -e _OLD_FISH_PROMPT_OVERRIDE # prevents error when using nested fish instances (Issue #93858) if functions -q _old_fish_prompt functions -e fish_prompt functions -c _old_fish_prompt fish_prompt functions -e _old_fish_prompt end end set -e VIRTUAL_ENV set -e VIRTUAL_ENV_PROMPT if test "$argv[1]" != "nondestructive" # Self-destruct! functions -e deactivate end end # Unset irrelevant variables. deactivate nondestructive set -gx VIRTUAL_ENV "__VENV_DIR__" set -gx _OLD_VIRTUAL_PATH $PATH set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH # Unset PYTHONHOME if set. if set -q PYTHONHOME set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME set -e PYTHONHOME end if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" # fish uses a function instead of an env var to generate the prompt. # Save the current fish_prompt function as the function _old_fish_prompt. functions -c fish_prompt _old_fish_prompt # With the original prompt function renamed, we can override with our own. function fish_prompt # Save the return status of the last command. set -l old_status $status # Output the venv prompt; color taken from the blue of the Python logo. printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal) # Restore the return status of the previous command. echo "exit $old_status" | . # Output the original/"old" prompt. _old_fish_prompt end set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__" end ================================================ FILE: StabilityMatrix/CheckpointBrowserPage.xaml ================================================  ================================================ FILE: StabilityMatrix/CheckpointBrowserPage.xaml.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using StabilityMatrix.ViewModels; namespace StabilityMatrix; public partial class CheckpointBrowserPage : Page { public CheckpointBrowserPage(CheckpointBrowserViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } private void VirtualizingGridView_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Handled) return; e.Handled = true; var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = MouseWheelEvent, Source = sender }; if (((Control)sender).Parent is UIElement parent) { parent.RaiseEvent(eventArg); } } private void FrameworkElement_OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { e.Handled = true; } private void CheckpointBrowserPage_OnLoaded(object sender, RoutedEventArgs e) { (DataContext as CheckpointBrowserViewModel)?.OnLoaded(); } } ================================================ FILE: StabilityMatrix/CheckpointManagerPage.xaml ================================================  ================================================ FILE: StabilityMatrix/CheckpointManagerPage.xaml.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using StabilityMatrix.ViewModels; namespace StabilityMatrix; public partial class CheckpointManagerPage : Page { private readonly CheckpointManagerViewModel viewModel; public CheckpointManagerPage(CheckpointManagerViewModel viewModel) { this.viewModel = viewModel; InitializeComponent(); DataContext = viewModel; } private async void CheckpointManagerPage_OnLoaded(object sender, RoutedEventArgs e) { await viewModel.OnLoaded(); } /// /// Bubbles the mouse wheel event up to the parent. /// /// /// private void VirtualizingGridView_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (e.Handled) return; e.Handled = true; var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = MouseWheelEvent, Source = sender }; if (((Control)sender).Parent is UIElement parent) { parent.RaiseEvent(eventArg); } } } ================================================ FILE: StabilityMatrix/Controls/AppBrushes.cs ================================================ using System.Windows.Media; namespace StabilityMatrix.Controls; public static class AppBrushes { public static readonly SolidColorBrush SuccessGreen = FromHex("#4caf50")!; public static readonly SolidColorBrush FailedRed = FromHex("#f44336")!; public static readonly SolidColorBrush WarningYellow = FromHex("#ffeb3b")!; private static SolidColorBrush? FromHex(string hex) { return new BrushConverter().ConvertFrom(hex) as SolidColorBrush; } } ================================================ FILE: StabilityMatrix/Controls/ProgressBarSmoother.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media.Animation; using System; namespace StabilityMatrix.Controls; public class ProgressBarSmoother { public static double GetSmoothValue(DependencyObject obj) { return (double)obj.GetValue(SmoothValueProperty); } public static void SetSmoothValue(DependencyObject obj, double value) { obj.SetValue(SmoothValueProperty, value); } public static readonly DependencyProperty SmoothValueProperty = DependencyProperty.RegisterAttached("SmoothValue", typeof(double), typeof(ProgressBarSmoother), new PropertyMetadata(0.0, Changing)); private static void Changing(DependencyObject d, DependencyPropertyChangedEventArgs e) { var anim = new DoubleAnimation((double)e.OldValue, (double)e.NewValue, TimeSpan.FromMilliseconds(150)); (d as ProgressBar)?.BeginAnimation(RangeBase.ValueProperty, anim, HandoffBehavior.Compose); } } ================================================ FILE: StabilityMatrix/Controls/RefreshBadge.xaml ================================================  ================================================ FILE: StabilityMatrix/Controls/RefreshBadge.xaml.cs ================================================ using System.Windows.Controls; namespace StabilityMatrix.Controls; public partial class RefreshBadge : UserControl { public RefreshBadge() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix/Converters/BoolNegationConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace StabilityMatrix.Converters; [ValueConversion(typeof(bool), typeof(bool))] public class BoolNegationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is bool?) { var boolVal = value as bool?; return !boolVal ?? false; } return false; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return Convert(value, targetType, parameter, culture); } } ================================================ FILE: StabilityMatrix/Converters/BooleanToHiddenVisibleConverter.cs ================================================ using System; using System.Globalization; using System.Windows; using System.Windows.Data; namespace StabilityMatrix.Converters; public class BooleanToHiddenVisibleConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var bValue = false; if (value is bool b) { bValue = b; } else if (value is bool) { var tmp = (bool?) value; bValue = tmp.Value; } return bValue ? Visibility.Visible : Visibility.Hidden; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (value is Visibility visibility) { return visibility == Visibility.Visible; } return false; } } ================================================ FILE: StabilityMatrix/Converters/IntDoubleConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace StabilityMatrix.Converters; public class IntDoubleConverter : IValueConverter { // Convert from int to double public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(double?)) { if (value == null) { return null; } return System.Convert.ToDouble(value); } throw new ArgumentException($"Unsupported type {targetType}"); } // Convert from double to int (floor) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(int?)) { return System.Convert.ToInt32(value); } throw new ArgumentException($"Unsupported type {targetType}"); } } ================================================ FILE: StabilityMatrix/Converters/IsStringNullOrWhitespaceConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace StabilityMatrix.Converters; [ValueConversion(typeof(string), typeof(bool))] public class IsStringNullOrWhitespaceConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is string strValue) { return string.IsNullOrWhiteSpace(strValue); } throw new InvalidOperationException("Cannot convert non-string value"); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } ================================================ FILE: StabilityMatrix/Converters/LaunchOptionConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace StabilityMatrix.Converters; public class LaunchOptionConverter : IValueConverter { public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(string)) { return value?.ToString() ?? ""; } if (targetType == typeof(bool?)) { return bool.TryParse(value?.ToString(), out var boolValue) && boolValue; } if (targetType == typeof(double?)) { if (value == null) { return null; } return double.TryParse(value?.ToString(), out var doubleValue) ? doubleValue : 0; } if (targetType == typeof(int?)) { if (value == null) { return null; } return int.TryParse(value?.ToString(), out var intValue) ? intValue : 0; } throw new ArgumentException("Unsupported type"); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } ================================================ FILE: StabilityMatrix/Converters/LaunchOptionIntDoubleConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; namespace StabilityMatrix.Converters; public class LaunchOptionIntDoubleConverter : IValueConverter { // Convert from int to double public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(double?)) { if (value == null) { return null; } return System.Convert.ToDouble(value); } throw new ArgumentException($"Unsupported type {targetType}"); } // Convert from double to object int (floor) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(int?) || targetType == typeof(object)) { return System.Convert.ToInt32(value); } throw new ArgumentException($"Unsupported type {targetType}"); } } ================================================ FILE: StabilityMatrix/Converters/NullToVisibilityConverter.cs ================================================ using System; using System.Globalization; using System.Windows; using System.Windows.Data; namespace StabilityMatrix.Converters; public class NullToVisibilityConverter : IValueConverter { public object Convert(object? value, Type targetType, object parameter, CultureInfo culture) { return value == null ? Visibility.Collapsed : Visibility.Visible; } public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } ================================================ FILE: StabilityMatrix/Converters/StringNullOrEmptyToVisibilityConverter.cs ================================================ using System; using System.Windows; using System.Windows.Data; namespace StabilityMatrix.Converters; public class StringNullOrEmptyToVisibilityConverter : IValueConverter { public object Convert(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return string.IsNullOrEmpty(value as string) ? Visibility.Collapsed : Visibility.Visible; } public object? ConvertBack(object? value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return null; } } ================================================ FILE: StabilityMatrix/Converters/UriToBitmapConverter.cs ================================================ using System; using System.Globalization; using System.Windows.Data; using System.Windows.Media.Imaging; namespace StabilityMatrix.Converters; public class UriToBitmapConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is Uri uri) { return new BitmapImage(uri); } if (value is string uriString) { return new BitmapImage(new Uri(uriString)); } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } ================================================ FILE: StabilityMatrix/Converters/ValueConverterGroup.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Windows.Data; namespace StabilityMatrix.Converters; public class ValueConverterGroup : List, IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameter, culture)); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } ================================================ FILE: StabilityMatrix/DataDirectoryMigrationDialog.xaml ================================================  ================================================ FILE: StabilityMatrix/ExceptionWindow.xaml.cs ================================================ using System.Windows; using Wpf.Ui.Controls.Window; namespace StabilityMatrix; public partial class ExceptionWindow : FluentWindow { public ExceptionWindow() { InitializeComponent(); } private void ExceptionWindow_OnLoaded(object sender, RoutedEventArgs e) { System.Media.SystemSounds.Hand.Play(); } } ================================================ FILE: StabilityMatrix/FirstLaunchSetupWindow.xaml ================================================  I have read and agree to the License Agreement. ================================================ FILE: StabilityMatrix/FirstLaunchSetupWindow.xaml.cs ================================================ using System.Windows; using StabilityMatrix.ViewModels; using Wpf.Ui.Controls.Window; namespace StabilityMatrix; public sealed partial class FirstLaunchSetupWindow : FluentWindow { public FirstLaunchSetupWindow(FirstLaunchSetupViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } private void QuitButton_OnClick(object sender, RoutedEventArgs e) { DialogResult = false; Close(); } private void ContinueButton_OnClick(object sender, RoutedEventArgs e) { DialogResult = true; Hide(); } private void FirstLaunchSetupWindow_OnLoaded(object sender, RoutedEventArgs e) { (DataContext as FirstLaunchSetupViewModel)!.OnLoaded(); } } ================================================ FILE: StabilityMatrix/Helper/AsyncDispatchTimer.cs ================================================ using System; using System.Diagnostics; using System.Threading.Tasks; using System.Windows.Threading; namespace StabilityMatrix.Helper; public class AsyncDispatcherTimer : DispatcherTimer { public AsyncDispatcherTimer() { Tick += AsyncDispatcherTimer_Tick; } private async void AsyncDispatcherTimer_Tick(object? sender, EventArgs e) { if (TickTask == null) { // no task to run return; } if (IsRunning && !IsReentrant) { // previous task hasn't completed return; } try { IsRunning = true; await TickTask.Invoke(); } catch (Exception) { Debug.WriteLine("Task Failed"); throw; } finally { // allow it to run again IsRunning = false; } } public bool IsReentrant { get; set; } public bool IsRunning { get; private set; } public Func? TickTask { get; set; } } ================================================ FILE: StabilityMatrix/Helper/DialogFactory.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; using StabilityMatrix.Services; using StabilityMatrix.ViewModels; using Wpf.Ui.Contracts; using Wpf.Ui.Controls; using Wpf.Ui.Controls.ContentDialogControl; using TextBox = Wpf.Ui.Controls.TextBox; namespace StabilityMatrix.Helper; public class DialogFactory : IDialogFactory { private readonly IContentDialogService contentDialogService; private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel; private readonly InstallerViewModel installerViewModel; private readonly OneClickInstallViewModel oneClickInstallViewModel; private readonly SelectInstallLocationsViewModel selectInstallLocationsViewModel; private readonly DataDirectoryMigrationViewModel dataDirectoryMigrationViewModel; private readonly WebLoginViewModel webLoginViewModel; private readonly InstallerWindowDialogService installerWindowDialogService; private readonly ISettingsManager settingsManager; public DialogFactory(IContentDialogService contentDialogService, LaunchOptionsDialogViewModel launchOptionsDialogViewModel, ISettingsManager settingsManager, InstallerViewModel installerViewModel, OneClickInstallViewModel oneClickInstallViewModel, SelectInstallLocationsViewModel selectInstallLocationsViewModel, DataDirectoryMigrationViewModel dataDirectoryMigrationViewModel, InstallerWindowDialogService installerWindowDialogService, WebLoginViewModel webLoginViewModel) { this.contentDialogService = contentDialogService; this.launchOptionsDialogViewModel = launchOptionsDialogViewModel; this.installerViewModel = installerViewModel; this.oneClickInstallViewModel = oneClickInstallViewModel; this.selectInstallLocationsViewModel = selectInstallLocationsViewModel; this.dataDirectoryMigrationViewModel = dataDirectoryMigrationViewModel; this.webLoginViewModel = webLoginViewModel; this.installerWindowDialogService = installerWindowDialogService; this.settingsManager = settingsManager; } public LaunchOptionsDialog CreateLaunchOptionsDialog(IEnumerable definitions, InstalledPackage installedPackage) { // Load user settings var userLaunchArgs = settingsManager.GetLaunchArgs(installedPackage.Id); launchOptionsDialogViewModel.Initialize(definitions, userLaunchArgs); return new LaunchOptionsDialog(contentDialogService, launchOptionsDialogViewModel); } /// /// Creates a dialog that allows the user to enter text for each field name. /// Return a list of strings that correspond to the field names. /// If cancel is pressed, return null. /// List of (fieldName, placeholder) /// public async Task?> ShowTextEntryDialog(string title, IEnumerable<(string, string)> fields, string closeButtonText = "Cancel", string saveButtonText = "Save") { var dialog = contentDialogService.CreateDialog(); dialog.Title = title; dialog.PrimaryButtonAppearance = ControlAppearance.Primary; dialog.CloseButtonText = closeButtonText; dialog.PrimaryButtonText = saveButtonText; dialog.IsPrimaryButtonEnabled = true; var textBoxes = new List(); var stackPanel = new StackPanel(); dialog.Content = stackPanel; foreach (var (fieldName, fieldPlaceholder) in fields) { var textBox = new TextBox { PlaceholderText = fieldPlaceholder, PlaceholderEnabled = true, MinWidth = 200, }; textBoxes.Add(textBox); stackPanel.Children.Add(new Card { Content = new StackPanel { Children = { new TextBlock { Text = fieldName, Margin = new Thickness(0, 0, 0, 4) }, textBox } }, Margin = new Thickness(16) }); } var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { return textBoxes.Select(x => x.Text).ToList(); } return null; } /// /// Creates and shows a confirmation dialog. /// Return true if the user clicks the primary button. /// public async Task ShowConfirmationDialog(string title, string message, string closeButtonText = "Cancel", string primaryButtonText = "Confirm") { var dialog = contentDialogService.CreateDialog(); dialog.Title = title; dialog.PrimaryButtonAppearance = ControlAppearance.Primary; dialog.CloseButtonText = closeButtonText; dialog.PrimaryButtonText = primaryButtonText; dialog.IsPrimaryButtonEnabled = true; dialog.Content = new TextBlock { Text = message, Margin = new Thickness(16) }; var result = await dialog.ShowAsync(); return result == ContentDialogResult.Primary; } public OneClickInstallDialog CreateOneClickInstallDialog() { return new OneClickInstallDialog(contentDialogService, oneClickInstallViewModel); } public InstallerWindow CreateInstallerWindow() { return new InstallerWindow(installerViewModel, installerWindowDialogService); } public SelectInstallLocationsDialog CreateInstallLocationsDialog() { var dialog = new SelectInstallLocationsDialog(contentDialogService, selectInstallLocationsViewModel) { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false }; return dialog; } public DataDirectoryMigrationDialog CreateDataDirectoryMigrationDialog() { var dialog = new DataDirectoryMigrationDialog(contentDialogService, dataDirectoryMigrationViewModel) { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false }; return dialog; } public WebLoginDialog CreateWebLoginDialog() { return new WebLoginDialog(contentDialogService, webLoginViewModel) { CloseButtonText = "Cancel", }; } public SelectModelVersionDialog CreateSelectModelVersionDialog(CivitModel model) { return new SelectModelVersionDialog(contentDialogService, new SelectModelVersionDialogViewModel(model, settingsManager)) { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false }; } } ================================================ FILE: StabilityMatrix/Helper/IDialogFactory.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Helper; public interface IDialogFactory { LaunchOptionsDialog CreateLaunchOptionsDialog(IEnumerable definitions, InstalledPackage installedPackage); /// /// Creates a dialog that allows the user to enter text for each field name. /// Return a list of strings that correspond to the field names. /// If cancel is pressed, return null. /// List of (fieldName, placeholder) /// Task?> ShowTextEntryDialog(string title, IEnumerable<(string, string)> fields, string closeButtonText = "Cancel", string saveButtonText = "Save"); /// /// Creates and shows a confirmation dialog. /// Return true if the user clicks the primary button. /// Task ShowConfirmationDialog(string title, string message, string closeButtonText = "Cancel", string primaryButtonText = "Confirm"); OneClickInstallDialog CreateOneClickInstallDialog(); InstallerWindow CreateInstallerWindow(); SelectInstallLocationsDialog CreateInstallLocationsDialog(); DataDirectoryMigrationDialog CreateDataDirectoryMigrationDialog(); WebLoginDialog CreateWebLoginDialog(); SelectModelVersionDialog CreateSelectModelVersionDialog(CivitModel model); } ================================================ FILE: StabilityMatrix/Helper/ISnackbarService.cs ================================================ using System; using System.Threading.Tasks; using StabilityMatrix.Core.Models; using Wpf.Ui.Common; using Wpf.Ui.Controls; namespace StabilityMatrix.Helper; public interface ISnackbarService { /// /// Default timeout for snackbar messages. /// public TimeSpan DefaultTimeout { get; } /// /// Shows a generic error snackbar with the given message. /// /// The title to show in the snackbar. /// The message to show /// The appearance of the snackbar. /// The icon to show in the snackbar. /// Snackbar timeout, defaults to class DefaultTimeout public Task ShowSnackbarAsync( string title, string message, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null); /// /// Attempt to run the given task, showing a generic error snackbar if it fails. /// /// The task to run. /// The title to show in the snackbar. /// The message to show, default to exception.Message /// The appearance of the snackbar. /// The icon to show in the snackbar. /// Snackbar timeout, defaults to class DefaultTimeout public Task> TryAsync( Task task, string title = "Error", string? message = null, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null); /// /// Attempt to run the given void task, showing a generic error snackbar if it fails. /// Return a TaskResult with true if the task succeeded, false if it failed. /// /// The task to run. /// The title to show in the snackbar. /// The message to show, default to exception.Message /// The appearance of the snackbar. /// The icon to show in the snackbar. /// Snackbar timeout, defaults to class DefaultTimeout Task> TryAsync( Task task, string title = "Error", string? message = null, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null); } ================================================ FILE: StabilityMatrix/Helper/ScreenExtensions.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; namespace StabilityMatrix.Helper; public static class ScreenExtensions { public const string User32 = "user32.dll"; public const string Shcore = "Shcore.dll"; public static void GetDpi(this System.Windows.Forms.Screen screen, DpiType dpiType, out uint dpiX, out uint dpiY) { var pnt = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1); var mon = MonitorFromPoint(pnt, 2 /*MONITOR_DEFAULTTONEAREST*/); GetDpiForMonitor(mon, dpiType, out dpiX, out dpiY); } public static double GetScalingForPoint(System.Drawing.Point aPoint) { var mon = MonitorFromPoint(aPoint, 2 /*MONITOR_DEFAULTTONEAREST*/); uint dpiX, dpiY; GetDpiForMonitor(mon, DpiType.Effective, out dpiX, out dpiY); return (double) dpiX / 96.0; } [DllImport(User32)] private static extern IntPtr MonitorFromPoint([In] System.Drawing.Point pt, [In] uint dwFlags); [DllImport(Shcore)] private static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out uint dpiX, [Out] out uint dpiY); [DllImport(User32, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl); [DllImport(User32, CharSet = CharSet.Auto, SetLastError = true)] [ResourceExposure(ResourceScope.None)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WindowPlacement lpwndpl); public enum DpiType { Effective = 0, Angular = 1, Raw = 2, } public static WindowPlacement GetPlacement(IntPtr hWnd) { var placement = new WindowPlacement(); placement.length = Marshal.SizeOf(placement); GetWindowPlacement(hWnd, ref placement); return placement; } public static bool SetPlacement(IntPtr hWnd, WindowPlacement aPlacement) { var erg = SetWindowPlacement(hWnd, ref aPlacement); return erg; } [StructLayout(LayoutKind.Sequential)] public struct Pointstruct { public int x; public int y; public Pointstruct(int x, int y) { this.x = x; this.y = y; } } [StructLayout(LayoutKind.Sequential)] public struct Rect { public int left; public int top; public int right; public int bottom; public Rect(int left, int top, int right, int bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } public Rect(System.Windows.Rect r) { left = (int) r.Left; top = (int) r.Top; right = (int) r.Right; bottom = (int) r.Bottom; } public static Rect FromXywh(int x, int y, int width, int height) { return new Rect(x, y, x + width, y + height); } public System.Windows.Size Size => new(right - left, bottom - top); } [StructLayout(LayoutKind.Sequential)] public struct WindowPlacement { public int length; public uint flags; public uint showCmd; public Pointstruct ptMinPosition; public Pointstruct ptMaxPosition; public Rect rcNormalPosition; public override string ToString() { var structBytes = RawSerialize(this); return Convert.ToBase64String(structBytes); } public void ReadFromBase64String(string aB64) { var b64 = Convert.FromBase64String(aB64); var newWp = ReadStruct(b64, 0); length = newWp.length; flags = newWp.flags; showCmd = newWp.showCmd; ptMinPosition.x = newWp.ptMinPosition.x; ptMinPosition.y = newWp.ptMinPosition.y; ptMaxPosition.x = newWp.ptMaxPosition.x; ptMaxPosition.y = newWp.ptMaxPosition.y; rcNormalPosition.left = newWp.rcNormalPosition.left; rcNormalPosition.top = newWp.rcNormalPosition.top; rcNormalPosition.right = newWp.rcNormalPosition.right; rcNormalPosition.bottom = newWp.rcNormalPosition.bottom; } public static T ReadStruct(byte[] aSrcBuffer, int aOffset) { var buffer = new byte[Marshal.SizeOf(typeof(T))]; Buffer.BlockCopy(aSrcBuffer, aOffset, buffer, 0, Marshal.SizeOf(typeof(T))); var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); var temp = (T) Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); handle.Free(); return temp; } public static T ReadStruct(Stream fs) { var buffer = new byte[Marshal.SizeOf(typeof(T))]; fs.Read(buffer, 0, Marshal.SizeOf(typeof(T))); var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); var temp = (T) Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); handle.Free(); return temp; } public static byte[] RawSerialize(object anything) { var rawsize = Marshal.SizeOf(anything); var rawdata = new byte[rawsize]; var handle = GCHandle.Alloc(rawdata, GCHandleType.Pinned); Marshal.StructureToPtr(anything, handle.AddrOfPinnedObject(), false); handle.Free(); return rawdata; } } } ================================================ FILE: StabilityMatrix/Helper/SnackbarService.cs ================================================ using System.Threading.Tasks; using System; using AsyncAwaitBestPractices; using StabilityMatrix.Core.Models; using StabilityMatrix.ViewModels; using Wpf.Ui.Common; using Wpf.Ui.Controls; using Wpf.Ui.Controls.IconElements; namespace StabilityMatrix.Helper; /// /// Generic recoverable error handler using content dialogs. /// public class SnackbarService : ISnackbarService { private readonly Wpf.Ui.Contracts.ISnackbarService snackbarService; private readonly SnackbarViewModel snackbarViewModel; public TimeSpan DefaultTimeout { get; } = TimeSpan.FromSeconds(5); public SnackbarService(Wpf.Ui.Contracts.ISnackbarService snackbarService, SnackbarViewModel snackbarViewModel) { this.snackbarService = snackbarService; this.snackbarViewModel = snackbarViewModel; } /// public async Task ShowSnackbarAsync( string title, string message, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null) { snackbarService.Timeout = (int) (timeout ?? DefaultTimeout).TotalMilliseconds; await snackbarService.ShowAsync(title, message, new SymbolIcon(icon), appearance); } /// public async Task> TryAsync( Task task, string title = "Error", string? message = null, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null) { try { return new TaskResult(await task); } catch (Exception e) { ShowSnackbarAsync(title, message ?? e.Message, appearance, icon, timeout).SafeFireAndForget(); return TaskResult.FromException(e); } } /// public async Task> TryAsync( Task task, string title = "Error", string? message = null, ControlAppearance appearance = ControlAppearance.Danger, SymbolRegular icon = SymbolRegular.ErrorCircle24, TimeSpan? timeout = null) { try { await task; return new TaskResult(true); } catch (Exception e) { ShowSnackbarAsync(title, message ?? e.Message, appearance, icon, timeout).SafeFireAndForget(); return new TaskResult(false, e); } } } ================================================ FILE: StabilityMatrix/InstallerWindow.xaml ================================================ ================================================ FILE: StabilityMatrix/InstallerWindow.xaml.cs ================================================ using System.Windows; using StabilityMatrix.Services; using StabilityMatrix.ViewModels; using Wpf.Ui.Controls.Window; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. namespace StabilityMatrix { /// /// An empty page that can be used on its own or navigated to within a Frame. /// public sealed partial class InstallerWindow : FluentWindow { private readonly InstallerViewModel viewModel; private readonly InstallerWindowDialogService dialogService; public InstallerWindow(InstallerViewModel viewModel, InstallerWindowDialogService dialogService) { this.viewModel = viewModel; this.dialogService = dialogService; InitializeComponent(); DataContext = viewModel; viewModel.PackageInstalled += (_, _) => Close(); dialogService.SetContentPresenter(InstallerContentDialog); } private async void InstallPage_OnLoaded(object sender, RoutedEventArgs e) { await viewModel.OnLoaded(); } } } ================================================ FILE: StabilityMatrix/Interactions/EventTriggerWithoutPropogation.cs ================================================ using System.Windows; namespace StabilityMatrix.Interactions; public class EventTriggerWithoutPropagation : Microsoft.Xaml.Behaviors.EventTrigger { protected override void OnEvent(System.EventArgs eventArgs) { // Prevent event from propagating to parent if (eventArgs is RoutedEventArgs routedEventArgs) { routedEventArgs.Handled = true; } base.OnEvent(eventArgs); } } ================================================ FILE: StabilityMatrix/LaunchOptionsDialog.xaml ================================================  ================================================ FILE: StabilityMatrix/LaunchOptionsDialog.xaml.cs ================================================ using System.Collections.Generic; using System.Windows; using StabilityMatrix.Core.Models; using StabilityMatrix.ViewModels; using Wpf.Ui.Contracts; using Wpf.Ui.Controls.ContentDialogControl; namespace StabilityMatrix; public partial class LaunchOptionsDialog : ContentDialog { private readonly LaunchOptionsDialogViewModel viewModel; public List AsLaunchArgs() => viewModel.AsLaunchArgs(); public LaunchOptionsDialog(IContentDialogService dialogService, LaunchOptionsDialogViewModel viewModel) : base( dialogService.GetContentPresenter()) { this.viewModel = viewModel; InitializeComponent(); DataContext = viewModel; } private void LaunchOptionsDialog_OnLoaded(object sender, RoutedEventArgs e) { viewModel.OnLoad(); } } ================================================ FILE: StabilityMatrix/LaunchPage.xaml ================================================  ================================================ FILE: StabilityMatrix/LaunchPage.xaml.cs ================================================ using System.Windows; using System.Windows.Controls; using StabilityMatrix.ViewModels; namespace StabilityMatrix; public sealed partial class LaunchPage : Page { private readonly LaunchViewModel viewModel; public LaunchPage(LaunchViewModel viewModel) { this.viewModel = viewModel; InitializeComponent(); DataContext = viewModel; } private void LaunchPage_OnLoaded(object sender, RoutedEventArgs e) { viewModel.OnLoaded(); SelectPackageComboBox.ItemsSource = viewModel.InstalledPackages; } } ================================================ FILE: StabilityMatrix/MainWindow.xaml ================================================ ================================================ FILE: StabilityMatrix/MainWindow.xaml.cs ================================================ using System; using System.ComponentModel; using System.Diagnostics; using System.Reactive.Linq; using System.Threading; using System.Windows; using System.Windows.Interop; using StabilityMatrix.Core.Services; using StabilityMatrix.Helper; using StabilityMatrix.Services; using StabilityMatrix.ViewModels; using Wpf.Ui.Contracts; using Wpf.Ui.Controls.Navigation; using Wpf.Ui.Controls.Window; using Application = System.Windows.Application; using EventManager = StabilityMatrix.Core.Helper.EventManager; using ISnackbarService = Wpf.Ui.Contracts.ISnackbarService; namespace StabilityMatrix { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : FluentWindow { private readonly MainWindowViewModel mainWindowViewModel; private readonly ISettingsManager settingsManager; public MainWindow(IPageService pageService, IContentDialogService contentDialogService, MainWindowViewModel mainWindowViewModel, ISettingsManager settingsManager, ISnackbarService snackbarService, INotificationBarService notificationBarService) { InitializeComponent(); this.mainWindowViewModel = mainWindowViewModel; this.settingsManager = settingsManager; DataContext = mainWindowViewModel; RootNavigation.Navigating += (_, _) => Debug.WriteLine("Navigating"); RootNavigation.SetPageService(pageService); snackbarService.SetSnackbarControl(RootSnackbar); notificationBarService.SetSnackbarControl(NotificationSnackbar); contentDialogService.SetContentPresenter(RootContentDialog); EventManager.Instance.PageChangeRequested += InstanceOnPageChangeRequested; } private void InstanceOnPageChangeRequested(object? sender, Type e) { RootNavigation.Navigate(e); } private async void MainWindow_OnLoaded(object sender, RoutedEventArgs e) { RootNavigation.Navigate(typeof(LaunchPage)); RootNavigation.IsPaneOpen = settingsManager.Settings.IsNavExpanded; await mainWindowViewModel.OnLoaded(); ObserveSizeChanged(); } private void RootNavigation_OnPaneOpened(NavigationView sender, RoutedEventArgs args) { if (settingsManager.IsLibraryDirSet) { settingsManager.Transaction(s => s.IsNavExpanded = true); } } private void RootNavigation_OnPaneClosed(NavigationView sender, RoutedEventArgs args) { if (settingsManager.IsLibraryDirSet) { settingsManager.Transaction(s => s.IsNavExpanded = false); } } private void MainWindow_OnClosed(object? sender, EventArgs e) { Application.Current.Shutdown(); } private void ObserveSizeChanged() { var observableSizeChanges = Observable .FromEventPattern( h => SizeChanged += h, h => SizeChanged -= h) .Select(x => x.EventArgs) .Throttle(TimeSpan.FromMilliseconds(150)); observableSizeChanges .ObserveOn(SynchronizationContext.Current) .Subscribe(args => { if (args is {HeightChanged: false, WidthChanged: false}) return; var interopHelper = new WindowInteropHelper(this); var placement = ScreenExtensions.GetPlacement(interopHelper.Handle); settingsManager.Transaction(s => s.Placement = placement.ToString()); }); } private void MainWindow_OnClosing(object? sender, CancelEventArgs e) { var interopHelper = new WindowInteropHelper(this); var placement = ScreenExtensions.GetPlacement(interopHelper.Handle); if (settingsManager.IsLibraryDirSet) { settingsManager.Transaction(s => s.Placement = placement.ToString()); } } } } ================================================ FILE: StabilityMatrix/Models/CheckpointFile.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; using AsyncAwaitBestPractices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Helper; namespace StabilityMatrix.Models; public partial class CheckpointFile : ObservableObject { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IDialogFactory dialogFactory; // Event for when this file is deleted public event EventHandler? Deleted; /// /// Absolute path to the checkpoint file. /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(FileName))] private string filePath = string.Empty; /// /// Custom title for UI. /// [ObservableProperty] private string title = string.Empty; public string? PreviewImagePath { get; set; } public BitmapImage? PreviewImage { get; set; } public bool IsPreviewImageLoaded => PreviewImage != null; [ObservableProperty] private ConnectedModelInfo? connectedModel; public bool IsConnectedModel => ConnectedModel != null; [ObservableProperty] private bool isLoading; public string FileName => Path.GetFileName(FilePath); public ObservableCollection Badges { get; set; } = new(); private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", "bin" }; private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" }; private static readonly string[] SupportedMetadataExtensions = { ".json" }; public CheckpointFile(IDialogFactory dialogFactory) { this.dialogFactory = dialogFactory; } partial void OnConnectedModelChanged(ConnectedModelInfo? value) { // Update title, first check user defined, then connected model name Title = value?.UserTitle ?? value?.ModelName ?? string.Empty; // Update badges Badges.Clear(); var fpType = value.FileMetadata.Fp?.GetStringValue().ToUpperInvariant(); if (fpType != null) { Badges.Add(fpType); } if (!string.IsNullOrWhiteSpace(value.BaseModel)) { Badges.Add(value.BaseModel); } } [RelayCommand] private async Task DeleteAsync() { if (File.Exists(FilePath)) { IsLoading = true; try { await using var delay = new MinimumDelay(200, 500); await Task.Run(() => File.Delete(FilePath)); if (PreviewImagePath != null && File.Exists(PreviewImagePath)) { await Task.Run(() => File.Delete(PreviewImagePath)); } } catch (IOException ex) { Logger.Warn($"Failed to delete checkpoint file {FilePath}: {ex.Message}"); return; // Don't delete from collection } finally { IsLoading = false; } } Deleted?.Invoke(this, this); } [RelayCommand] private async Task RenameAsync() { var responses = await dialogFactory.ShowTextEntryDialog("Rename Model", new [] { ("File Name", FileName) }); var name = responses?.FirstOrDefault(); if (name == null) return; // Rename file in OS try { var newFilePath = Path.Combine(Path.GetDirectoryName(FilePath) ?? "", name); File.Move(FilePath, newFilePath); FilePath = newFilePath; } catch (Exception e) { Console.WriteLine(e); throw; } } [RelayCommand] private void OpenOnCivitAi() { ProcessRunner.OpenUrl($"https://civitai.com/models/{ConnectedModel.ModelId}"); } // Loads image from path private async Task LoadPreviewImage() { if (PreviewImagePath == null) return; var bytes = await File.ReadAllBytesAsync(PreviewImagePath); await Application.Current.Dispatcher.InvokeAsync(() => { var bitmap = new BitmapImage(); using var ms = new MemoryStream(bytes); bitmap.BeginInit(); bitmap.StreamSource = ms; bitmap.CacheOption = BitmapCacheOption.OnLoad; bitmap.EndInit(); PreviewImage = bitmap; }); } /// /// Indexes directory and yields all checkpoint files. /// First we match all files with supported extensions. /// If found, we also look for /// - {filename}.preview.{image-extensions} (preview image) /// - {filename}.cm-info.json (connected model info) /// public static IEnumerable FromDirectoryIndex(IDialogFactory dialogFactory, string directory, SearchOption searchOption = SearchOption.TopDirectoryOnly) { // Get all files with supported extensions var allExtensions = SupportedCheckpointExtensions .Concat(SupportedImageExtensions) .Concat(SupportedMetadataExtensions); var files = allExtensions.AsParallel() .SelectMany(pattern => Directory.EnumerateFiles(directory, $"*{pattern}", searchOption)).ToDictionary(Path.GetFileName); foreach (var file in files.Keys.Where(k => SupportedCheckpointExtensions.Contains(Path.GetExtension(k)))) { var checkpointFile = new CheckpointFile(dialogFactory) { Title = Path.GetFileNameWithoutExtension(file), FilePath = Path.Combine(directory, file), }; // Check for connected model info var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); var cmInfoPath = $"{fileNameWithoutExtension}.cm-info.json"; if (files.TryGetValue(cmInfoPath, out var jsonPath)) { try { var jsonData = File.ReadAllText(jsonPath); checkpointFile.ConnectedModel = ConnectedModelInfo.FromJson(jsonData); } catch (IOException e) { Debug.WriteLine($"Failed to parse {cmInfoPath}: {e}"); } } // Check for preview image var previewImage = SupportedImageExtensions.Select(ext => $"{fileNameWithoutExtension}.preview{ext}").FirstOrDefault(files.ContainsKey); if (previewImage != null) { checkpointFile.PreviewImagePath = files[previewImage]; checkpointFile.LoadPreviewImage().SafeFireAndForget(); } yield return checkpointFile; } } /// /// Index with progress reporting. /// public static IEnumerable FromDirectoryIndex(IDialogFactory dialogFactory, string directory, IProgress progress, SearchOption searchOption = SearchOption.TopDirectoryOnly) { var current = 0ul; foreach (var checkpointFile in FromDirectoryIndex(dialogFactory, directory, searchOption)) { current++; progress.Report(new ProgressReport(current, "Indexing", checkpointFile.FileName)); yield return checkpointFile; } } } ================================================ FILE: StabilityMatrix/Models/CheckpointFolder.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; using StabilityMatrix.Helper; using StabilityMatrix.ViewModels; namespace StabilityMatrix.Models; public partial class CheckpointFolder : ObservableObject { private readonly IDialogFactory dialogFactory; private readonly ISettingsManager settingsManager; private readonly IDownloadService downloadService; private readonly ModelFinder modelFinder; // ReSharper disable once FieldCanBeMadeReadOnly.Local private bool useCategoryVisibility; /// /// Absolute path to the folder. /// public string DirectoryPath { get; init; } = string.Empty; /// /// Custom title for UI. /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(FolderType))] [NotifyPropertyChangedFor(nameof(TitleWithFilesCount))] private string title = string.Empty; [ObservableProperty] private SharedFolderType folderType; /// /// True if the category is enabled for the manager page. /// [ObservableProperty] private bool isCategoryEnabled = true; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] private bool isCurrentDragTarget; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDragBlurEnabled))] private bool isImportInProgress; public bool IsDragBlurEnabled => IsCurrentDragTarget || IsImportInProgress; public string TitleWithFilesCount => CheckpointFiles.Any() ? $"{Title} ({CheckpointFiles.Count})" : Title; public ProgressViewModel Progress { get; } = new(); public ObservableCollection SubFolders { get; init; } = new(); public ObservableCollection CheckpointFiles { get; init; } = new(); public RelayCommand OnPreviewDragEnterCommand => new(() => IsCurrentDragTarget = true); public RelayCommand OnPreviewDragLeaveCommand => new(() => IsCurrentDragTarget = false); public CheckpointFolder( IDialogFactory dialogFactory, ISettingsManager settingsManager, IDownloadService downloadService, ModelFinder modelFinder, bool useCategoryVisibility = true ) { this.dialogFactory = dialogFactory; this.settingsManager = settingsManager; this.downloadService = downloadService; this.modelFinder = modelFinder; this.useCategoryVisibility = useCategoryVisibility; CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged; } /// /// When title is set, set the category enabled state from settings. /// // ReSharper disable once UnusedParameterInPartialMethod partial void OnTitleChanged(string value) { if (!useCategoryVisibility) return; // Update folder type var result = Enum.TryParse(Title, out SharedFolderType type); FolderType = result ? type : new SharedFolderType(); IsCategoryEnabled = settingsManager.IsSharedFolderCategoryVisible(FolderType); } /// /// When toggling the category enabled state, save it to settings. /// partial void OnIsCategoryEnabledChanged(bool value) { if (!useCategoryVisibility) return; if (value != settingsManager.IsSharedFolderCategoryVisible(FolderType)) { settingsManager.SetSharedFolderCategoryVisible(FolderType, value); } } // On collection changes private void OnCheckpointFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(nameof(TitleWithFilesCount)); if (e.NewItems == null) return; // On new added items, add event handler for deletion foreach (CheckpointFile item in e.NewItems) { item.Deleted += OnCheckpointFileDelete; } } /// /// Handler for CheckpointFile requesting to be deleted from the collection. /// /// /// private void OnCheckpointFileDelete(object? sender, CheckpointFile file) { Application.Current.Dispatcher.Invoke(() => CheckpointFiles.Remove(file)); } [RelayCommand] private async Task OnPreviewDropAsync(DragEventArgs e) { IsImportInProgress = true; IsCurrentDragTarget = false; if (e.Data.GetData(DataFormats.FileDrop) is not string[] files || files.Length < 1) { IsImportInProgress = false; return; } await ImportFilesAsync(files, settingsManager.Settings.IsImportAsConnected); } [RelayCommand] private void ShowInExplorer(string path) { Process.Start("explorer.exe", path); } /// /// Imports files to the folder. Reports progress to instance properties. /// public async Task ImportFilesAsync( IEnumerable files, bool convertToConnected = false, bool copyFiles = true ) { Progress.IsIndeterminate = true; Progress.IsProgressVisible = true; var copyPaths = files.ToDictionary( k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)) ); var progress = new Progress(report => { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; // For multiple files, add count Progress.Text = 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) { var modelFilesCount = copyPaths.Count; var modelFiles = copyPaths.Values.Select(path => new FilePath(path)); // Holds tasks for model queries after hash var modelQueryTasks = new List>(); foreach (var (i, modelFile) in modelFiles.Enumerate()) { var hashProgress = new Progress(report => { Progress.IsIndeterminate = false; Progress.Value = report.Percentage; Progress.Text = modelFilesCount > 1 ? $"Computing metadata for {modelFile.Info.Name} ({i}/{modelFilesCount})" : $"Computing metadata for {report.Title}"; }); var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress); if (!string.IsNullOrWhiteSpace(hashBlake3)) { settingsManager.Transaction(s => { s.InstalledModelHashes ??= new HashSet(); s.InstalledModelHashes.Add(hashBlake3); }); } // Start a task to query the model in background var queryTask = Task.Run(async () => { var result = await modelFinder.LocalFindModel(hashBlake3); result ??= await modelFinder.RemoteFindModel(hashBlake3); if (result is null) return false; // Not found var (model, version, file) = result.Value; // Save connected model info json var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name); var modelInfo = new ConnectedModelInfo( model, version, file, DateTimeOffset.UtcNow ); await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName); // If available, save thumbnail var image = version.Images?.FirstOrDefault(); if (image != null) { var imageExt = Path.GetExtension(image.Url).TrimStart('.'); if (imageExt is "jpg" or "jpeg" or "png") { var imageDownloadPath = Path.GetFullPath( Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}") ); await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath); } } return true; }); modelQueryTasks.Add(queryTask); } // Set progress to indeterminate Progress.IsIndeterminate = true; Progress.Text = "Checking connected model information"; // Wait for all model queries to finish var modelQueryResults = await Task.WhenAll(modelQueryTasks); var successCount = modelQueryResults.Count(r => r); var totalCount = modelQueryResults.Length; var failCount = totalCount - successCount; await IndexAsync(); Progress.Value = 100; Progress.Text = successCount switch { 0 when failCount > 0 => "Import complete. No connected data found.", > 0 when failCount > 0 => $"Import complete. Found connected data for {successCount} of {totalCount} models.", _ => $"Import complete. Found connected data for all {totalCount} models." }; DelayedClearProgress(TimeSpan.FromSeconds(1)); } else { await IndexAsync(); Progress.Value = 100; Progress.Text = "Import complete"; DelayedClearProgress(TimeSpan.FromSeconds(1)); } } /// /// Clears progress after a delay. /// private void DelayedClearProgress(TimeSpan delay) { Task.Delay(delay) .ContinueWith(_ => { IsImportInProgress = false; Progress.IsProgressVisible = false; Progress.Value = 0; Progress.Text = string.Empty; }); } /// /// Gets checkpoint files from folder index /// private async Task> GetCheckpointFilesAsync( IProgress? progress = default ) { if (!Directory.Exists(DirectoryPath)) { return new List(); } return await ( progress switch { null => Task.Run( () => CheckpointFile.FromDirectoryIndex(dialogFactory, DirectoryPath).ToList() ), _ => Task.Run( () => CheckpointFile .FromDirectoryIndex(dialogFactory, DirectoryPath, progress) .ToList() ) } ); } /// /// Indexes the folder for checkpoint files and refreshes the CheckPointFiles collection. /// public async Task IndexAsync(IProgress? progress = default) { SubFolders.Clear(); foreach (var folder in Directory.GetDirectories(DirectoryPath)) { // Inherit our folder type var subFolder = new CheckpointFolder( dialogFactory, settingsManager, downloadService, modelFinder, useCategoryVisibility: false ) { Title = Path.GetFileName(folder), DirectoryPath = folder, FolderType = FolderType }; await subFolder.IndexAsync(progress); SubFolders.Add(subFolder); } var checkpointFiles = await GetCheckpointFilesAsync(); CheckpointFiles.Clear(); foreach (var checkpointFile in checkpointFiles) { CheckpointFiles.Add(checkpointFile); } } } ================================================ FILE: StabilityMatrix/OneClickInstallDialog.xaml ================================================  ================================================ FILE: StabilityMatrix/PackageManagerPage.xaml.cs ================================================ using System.Windows; using System.Windows.Controls; using StabilityMatrix.ViewModels; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. namespace StabilityMatrix { /// /// An empty page that can be used on its own or navigated to within a Frame. /// public sealed partial class PackageManagerPage : Page { private readonly PackageManagerViewModel viewModel; public PackageManagerPage(PackageManagerViewModel viewModel) { this.viewModel = viewModel; InitializeComponent(); DataContext = viewModel; } private async void InstallPage_OnLoaded(object sender, RoutedEventArgs e) { await viewModel.OnLoaded(); } } } ================================================ FILE: StabilityMatrix/Properties/launchSettings.json ================================================ { "profiles": { "StabilityMatrix (Package)": { "commandName": "MsixPackage", "environmentVariables": { "DOTNET_ENVIRONMENT": "Production" } }, "StabilityMatrix (Unpackaged)": { "commandName": "Project", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } }, "StabilityMatrix (Unpackaged) with Exception Window": { "commandName": "Project", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development", "DEBUG_EXCEPTION_WINDOW": "true" } } } } ================================================ FILE: StabilityMatrix/SelectInstallLocationsDialog.xaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs ================================================ using System.IO; using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Avalonia.Controls; public partial class AdvancedImageBoxView : UserControl { public AdvancedImageBoxView() { InitializeComponent(); } public static AsyncRelayCommand FlyoutCopyCommand { get; } = new(FlyoutCopy); public static AsyncRelayCommand FlyoutCopyAsBitmapCommand { get; } = new(FlyoutCopyAsBitmap); private static async Task FlyoutCopy(ImageSource? imageSource) { if (imageSource is null) return; if (imageSource.LocalFile is { } imagePath) { await App.Clipboard.SetFileDataObjectAsync(imagePath); } else if (await imageSource.GetBitmapAsync() is { } bitmap) { // Write to temp file var tempFile = new FilePath(Path.GetTempFileName() + ".png"); bitmap.Save(tempFile); await App.Clipboard.SetFileDataObjectAsync(tempFile); } } private static async Task FlyoutCopyAsBitmap(ImageSource? imageSource) { if (imageSource is null || !Compat.IsWindows) return; if (await imageSource.GetBitmapAsync() is { } bitmap) { await WindowsClipboard.SetBitmapAsync(bitmap); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/AppWindowBase.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Windowing; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] public class AppWindowBase : AppWindow { public CancellationTokenSource? ShowAsyncCts { get; set; } protected AppWindowBase() { } public void ShowWithCts(CancellationTokenSource cts) { ShowAsyncCts?.Cancel(); ShowAsyncCts = cts; Show(); } public Task ShowAsync() { ShowAsyncCts?.Cancel(); ShowAsyncCts = new CancellationTokenSource(); var tcs = new TaskCompletionSource(); ShowAsyncCts.Token.Register(s => { ((TaskCompletionSource) s!).SetResult(true); }, tcs); Show(); return tcs.Task; } protected override void OnClosed(EventArgs e) { base.OnClosed(e); if (ShowAsyncCts is not null) { ShowAsyncCts.Cancel(); ShowAsyncCts = null; } } protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); if (DataContext is ViewModelBase viewModel) { // Run synchronous load then async load viewModel.OnLoaded(); // Can't block here so we'll run as async on UI thread Dispatcher.UIThread.InvokeAsync(async () => { await viewModel.OnLoadedAsync(); }).SafeFireAndForget(); } } protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); if (DataContext is not ViewModelBase viewModel) return; // Run synchronous load then async unload viewModel.OnUnloaded(); // Can't block here so we'll run as async on UI thread Dispatcher.UIThread.InvokeAsync(async () => { await viewModel.OnUnloadedAsync(); }).SafeFireAndForget(); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/ApplicationSplashScreen.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Avalonia.Media; using FluentAvalonia.UI.Windowing; namespace StabilityMatrix.Avalonia.Controls; internal class ApplicationSplashScreen : IApplicationSplashScreen { public string? AppName { get; init; } public IImage? AppIcon { get; init; } public object? SplashScreenContent { get; init; } public int MinimumShowTime { get; init; } public Func? InitApp { get; init; } public Task RunTasks(CancellationToken cancellationToken) { return InitApp?.Invoke(cancellationToken) ?? Task.CompletedTask; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/AutoGrid.cs ================================================ // Modified from https://github.com/AvaloniaUI/AvaloniaAutoGrid /*The MIT License (MIT) Copyright (c) 2013 Charles Brown (carbonrobot) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Layout; namespace StabilityMatrix.Avalonia.Controls; /// /// Defines a flexible grid area that consists of columns and rows. /// Depending on the orientation, either the rows or the columns are auto-generated, /// and the children's position is set according to their index. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class AutoGrid : Grid { /// /// Gets or sets the child horizontal alignment. /// /// The child horizontal alignment. [Category("Layout"), Description("Presets the horizontal alignment of all child controls")] public HorizontalAlignment? ChildHorizontalAlignment { get => (HorizontalAlignment?)GetValue(ChildHorizontalAlignmentProperty); set => SetValue(ChildHorizontalAlignmentProperty, value); } /// /// Gets or sets the child margin. /// /// The child margin. [Category("Layout"), Description("Presets the margin of all child controls")] public Thickness? ChildMargin { get => (Thickness?)GetValue(ChildMarginProperty); set => SetValue(ChildMarginProperty, value); } /// /// Gets or sets the child vertical alignment. /// /// The child vertical alignment. [Category("Layout"), Description("Presets the vertical alignment of all child controls")] public VerticalAlignment? ChildVerticalAlignment { get => (VerticalAlignment?)GetValue(ChildVerticalAlignmentProperty); set => SetValue(ChildVerticalAlignmentProperty, value); } /// /// Gets or sets the column count /// [Category("Layout"), Description("Defines a set number of columns")] public int ColumnCount { get => (int)GetValue(ColumnCountProperty)!; set => SetValue(ColumnCountProperty, value); } /// /// Gets or sets the fixed column width /// [Category("Layout"), Description("Presets the width of all columns set using the ColumnCount property")] public GridLength ColumnWidth { get => (GridLength)GetValue(ColumnWidthProperty)!; set => SetValue(ColumnWidthProperty, value); } /// /// Gets or sets a value indicating whether the children are automatically indexed. /// /// The default is true. /// Note that if children are already indexed, setting this property to false will not remove their indices. /// /// [Category("Layout"), Description("Set to false to disable the auto layout functionality")] public bool IsAutoIndexing { get => (bool)GetValue(IsAutoIndexingProperty)!; set => SetValue(IsAutoIndexingProperty, value); } /// /// Gets or sets the orientation. /// The default is Vertical. /// /// The orientation. [Category("Layout"), Description("Defines the directionality of the autolayout. Use vertical for a column first layout, horizontal for a row first layout.")] public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty)!; set => SetValue(OrientationProperty, value); } /// /// Gets or sets the number of rows /// [Category("Layout"), Description("Defines a set number of rows")] public int RowCount { get => (int)GetValue(RowCountProperty)!; set => SetValue(RowCountProperty, value); } /// /// Gets or sets the fixed row height /// [Category("Layout"), Description("Presets the height of all rows set using the RowCount property")] public GridLength RowHeight { get => (GridLength)GetValue(RowHeightProperty)!; set => SetValue(RowHeightProperty, value); } /// /// Handles the column count changed event /// public static void ColumnCountChanged(AvaloniaPropertyChangedEventArgs e) { if ((int)e.NewValue! < 0) return; var grid = (AutoGrid)e.Sender; // look for an existing column definition for the height var width = grid.ColumnWidth; if (!grid.IsSet(ColumnWidthProperty) && grid.ColumnDefinitions.Count > 0) width = grid.ColumnDefinitions[0].Width; // clear and rebuild grid.ColumnDefinitions.Clear(); for (var i = 0; i < (int)e.NewValue; i++) grid.ColumnDefinitions.Add( new ColumnDefinition() { Width = width }); } /// /// Handle the fixed column width changed event /// public static void FixedColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) { var grid = (AutoGrid)e.Sender; // add a default column if missing if (grid.ColumnDefinitions.Count == 0) grid.ColumnDefinitions.Add(new ColumnDefinition()); // set all existing columns to this width foreach (var t in grid.ColumnDefinitions) t.Width = (GridLength)e.NewValue!; } /// /// Handle the fixed row height changed event /// public static void FixedRowHeightChanged(AvaloniaPropertyChangedEventArgs e) { var grid = (AutoGrid)e.Sender; // add a default row if missing if (grid.RowDefinitions.Count == 0) grid.RowDefinitions.Add(new RowDefinition()); // set all existing rows to this height foreach (var t in grid.RowDefinitions) t.Height = (GridLength)e.NewValue!; } /// /// Handles the row count changed event /// public static void RowCountChanged(AvaloniaPropertyChangedEventArgs e) { if ((int)e.NewValue! < 0) return; var grid = (AutoGrid)e.Sender; // look for an existing row to get the height var height = grid.RowHeight; if (!grid.IsSet(RowHeightProperty) && grid.RowDefinitions.Count > 0) height = grid.RowDefinitions[0].Height; // clear and rebuild grid.RowDefinitions.Clear(); for (var i = 0; i < (int)e.NewValue; i++) grid.RowDefinitions.Add( new RowDefinition() { Height = height }); } /// /// Called when [child horizontal alignment changed]. /// private static void OnChildHorizontalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) { var grid = (AutoGrid)e.Sender; foreach (var child in grid.Children) { child.SetValue(HorizontalAlignmentProperty, grid.ChildHorizontalAlignment ?? AvaloniaProperty.UnsetValue); } } /// /// Called when [child layout changed]. /// private static void OnChildMarginChanged(AvaloniaPropertyChangedEventArgs e) { var grid = (AutoGrid)e.Sender; foreach (var child in grid.Children) { child.SetValue(MarginProperty, grid.ChildMargin ?? AvaloniaProperty.UnsetValue); } } /// /// Called when [child vertical alignment changed]. /// private static void OnChildVerticalAlignmentChanged(AvaloniaPropertyChangedEventArgs e) { var grid = (AutoGrid)e.Sender; foreach (var child in grid.Children) { child.SetValue(VerticalAlignmentProperty, grid.ChildVerticalAlignment ?? AvaloniaProperty.UnsetValue); } } /// /// Apply child margins and layout effects such as alignment /// private void ApplyChildLayout(Control child) { if (ChildMargin != null) { child.SetValue(MarginProperty, ChildMargin.Value, BindingPriority.Template); } if (ChildHorizontalAlignment != null) { child.SetValue(HorizontalAlignmentProperty, ChildHorizontalAlignment.Value, BindingPriority.Template); } if (ChildVerticalAlignment != null) { child.SetValue(VerticalAlignmentProperty, ChildVerticalAlignment.Value, BindingPriority.Template); } } /// /// Clamp a value to its maximum. /// private int Clamp(int value, int max) { return (value > max) ? max : value; } /// /// Perform the grid layout of row and column indexes /// private void PerformLayout() { var fillRowFirst = Orientation == Orientation.Horizontal; var rowCount = RowDefinitions.Count; var colCount = ColumnDefinitions.Count; if (rowCount == 0 || colCount == 0) return; var position = 0; var skip = new bool[rowCount, colCount]; foreach (var child in Children.OfType()) { var childIsCollapsed = !child.IsVisible; if (IsAutoIndexing && !childIsCollapsed) { if (fillRowFirst) { var row = Clamp(position / colCount, rowCount - 1); var col = Clamp(position % colCount, colCount - 1); if (skip[row, col]) { position++; row = (position / colCount); col = (position % colCount); } SetRow(child, row); SetColumn(child, col); position += GetColumnSpan(child); var offset = GetRowSpan(child) - 1; while (offset > 0) { skip[row + offset--, col] = true; } } else { var row = Clamp(position % rowCount, rowCount - 1); var col = Clamp(position / rowCount, colCount - 1); if (skip[row, col]) { position++; row = position % rowCount; col = position / rowCount; } SetRow(child, row); SetColumn(child, col); position += GetRowSpan(child); var offset = GetColumnSpan(child) - 1; while (offset > 0) { skip[row, col + offset--] = true; } } } ApplyChildLayout(child); } } public static readonly AvaloniaProperty ChildHorizontalAlignmentProperty = AvaloniaProperty.Register("ChildHorizontalAlignment"); public static readonly AvaloniaProperty ChildMarginProperty = AvaloniaProperty.Register("ChildMargin"); public static readonly AvaloniaProperty ChildVerticalAlignmentProperty = AvaloniaProperty.Register("ChildVerticalAlignment"); public static readonly AvaloniaProperty ColumnCountProperty = AvaloniaProperty.RegisterAttached("ColumnCount", typeof(AutoGrid), 1); public static readonly AvaloniaProperty ColumnWidthProperty = AvaloniaProperty.RegisterAttached("ColumnWidth", typeof(AutoGrid), GridLength.Auto); public static readonly AvaloniaProperty IsAutoIndexingProperty = AvaloniaProperty.Register("IsAutoIndexing", true); public static readonly AvaloniaProperty OrientationProperty = AvaloniaProperty.Register("Orientation", Orientation.Vertical); public static readonly AvaloniaProperty RowCountProperty = AvaloniaProperty.RegisterAttached("RowCount", typeof(AutoGrid), 1); public static readonly AvaloniaProperty RowHeightProperty = AvaloniaProperty.RegisterAttached("RowHeight", typeof(AutoGrid), GridLength.Auto); static AutoGrid() { AffectsMeasure(ChildHorizontalAlignmentProperty, ChildMarginProperty, ChildVerticalAlignmentProperty, ColumnCountProperty, ColumnWidthProperty, IsAutoIndexingProperty, OrientationProperty, RowHeightProperty); ChildHorizontalAlignmentProperty.Changed.Subscribe(OnChildHorizontalAlignmentChanged); ChildMarginProperty.Changed.Subscribe(OnChildMarginChanged); ChildVerticalAlignmentProperty.Changed.Subscribe(OnChildVerticalAlignmentChanged); ColumnCountProperty.Changed.Subscribe(ColumnCountChanged); RowCountProperty.Changed.Subscribe(RowCountChanged); ColumnWidthProperty.Changed.Subscribe(FixedColumnWidthChanged); RowHeightProperty.Changed.Subscribe(FixedRowHeightChanged); } #region Overrides /// /// Measures the children of a in anticipation of arranging them during the pass. /// /// Indicates an upper limit size that should not be exceeded. /// /// that represents the required size to arrange child content. /// protected override Size MeasureOverride(Size constraint) { PerformLayout(); return base.MeasureOverride(constraint); } #endregion Overrides } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; using AsyncImageLoader; using Avalonia; using Avalonia.Layout; using Avalonia.Media; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "UnusedMember.Local")] public class BetterAdvancedImage : AdvancedImage { #region Reflection Shenanigans to access private parent fields [NotNull] private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField( "_isCornerRadiusUsed", BindingFlags.Instance | BindingFlags.NonPublic ); [NotNull] private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField( "_cornerRadiusClip", BindingFlags.Instance | BindingFlags.NonPublic ); private bool IsCornerRadiusUsed { get => IsCornerRadiusUsedField.GetValue(this) as bool? ?? false; set => IsCornerRadiusUsedField.SetValue(this, value); } private RoundedRect CornerRadiusClip { get => (RoundedRect)CornerRadiusClipField.GetValue(this)!; set => CornerRadiusClipField.SetValue(this, value); } static BetterAdvancedImage() { if (IsCornerRadiusUsedField is null) { throw new NullReferenceException("IsCornerRadiusUsedField was not resolved"); } if (CornerRadiusClipField is null) { throw new NullReferenceException("CornerRadiusClipField was not resolved"); } } #endregion protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage); public BetterAdvancedImage(Uri? baseUri) : base(baseUri) { } public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider) { } /// /// public override void Render(DrawingContext context) { var source = CurrentImage; if (source != null && Bounds is { Width: > 0, Height: > 0 }) { var viewPort = new Rect(Bounds.Size); var sourceSize = source.Size; var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); var scaledSize = sourceSize * scale; // Calculate starting points for dest var destX = HorizontalContentAlignment switch { HorizontalAlignment.Left => 0, HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2, HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width), // Stretch is default, use center HorizontalAlignment.Stretch => (int)(viewPort.Width - scaledSize.Width) / 2, _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) }; var destY = VerticalContentAlignment switch { VerticalAlignment.Top => 0, VerticalAlignment.Center => (int)(viewPort.Height - scaledSize.Height) / 2, VerticalAlignment.Bottom => (int)(viewPort.Height - scaledSize.Height), VerticalAlignment.Stretch => 0, // Stretch is default, use top _ => throw new ArgumentException(nameof(VerticalContentAlignment)) }; var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort); var destRectUnscaledSize = destRect.Size / scale; // Calculate starting points for source var sourceX = HorizontalContentAlignment switch { HorizontalAlignment.Left => 0, HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2, HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width, // Stretch is default, use center HorizontalAlignment.Stretch => (int)(sourceSize - destRectUnscaledSize).Width / 2, _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) }; var sourceY = VerticalContentAlignment switch { VerticalAlignment.Top => 0, VerticalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Height / 2, VerticalAlignment.Bottom => (int)(sourceSize - destRectUnscaledSize).Height, VerticalAlignment.Stretch => 0, // Stretch is default, use top _ => throw new ArgumentException(nameof(VerticalContentAlignment)) }; var sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)) .WithX(sourceX) .WithY(sourceY); if (IsCornerRadiusUsed) { using (context.PushClip(CornerRadiusClip)) { context.DrawImage(source, sourceRect, destRect); } } else { context.DrawImage(source, sourceRect, destRect); } } else { base.Render(context); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterComboBox.cs ================================================ using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Media; using Avalonia.Threading; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; public class BetterComboBox : ComboBox { private readonly Subject inputSubject = new(); private readonly IDisposable subscription; private readonly Popup inputPopup; private readonly TextBlock inputTextBlock; private string currentInput = string.Empty; public BetterComboBox() { // Create an observable that buffers input over a short period var inputObservable = inputSubject .Do(text => currentInput += text) .Throttle(TimeSpan.FromMilliseconds(500)) .Where(_ => !string.IsNullOrEmpty(currentInput)) .Select(_ => currentInput); // Subscribe to the observable to filter the ComboBox items subscription = inputObservable .ObserveOn(SynchronizationContext.Current) .Subscribe(OnInputReceived, _ => ResetPopupText()); // Initialize the popup inputPopup = new Popup { IsLightDismissEnabled = true, Placement = PlacementMode.AnchorAndGravity, PlacementAnchor = PopupAnchor.Bottom, PlacementGravity = PopupGravity.Top, }; // Initialize the TextBlock with custom styling inputTextBlock = new TextBlock { Foreground = Brushes.White, // White text color Background = Brush.Parse("#333333"), // Dark gray background Padding = new Thickness(8), // Add padding FontSize = 14 // Optional: adjust font size }; inputPopup.Child = inputTextBlock; } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); // Set the Popup's anchor to the ComboBox itself inputPopup.PlacementTarget = this; if (e.NameScope.Find("ContentPresenter") is { } contentPresenter) { if (SelectionBoxItemTemplate is { } template) { contentPresenter.ContentTemplate = template; } } } protected override void OnTextInput(TextInputEventArgs e) { if (e.Handled) return; if (!string.IsNullOrWhiteSpace(e.Text)) { // Push the input text to the subject inputSubject.OnNext(e.Text); UpdatePopupText(e.Text); e.Handled = true; } base.OnTextInput(e); } private void OnInputReceived(string input) { if (Items.OfType().ToList() is { Count: > 0 } enumItems) { var foundEnum = enumItems.FirstOrDefault( x => x.GetStringValue().StartsWith(input, StringComparison.OrdinalIgnoreCase) ); if (foundEnum is not null) { Dispatcher.UIThread.Post(() => { SelectedItem = foundEnum; }); } } else if (Items.OfType().ToList() is { } modelFiles) { var found = modelFiles.FirstOrDefault( x => x.SearchText.StartsWith(input, StringComparison.OrdinalIgnoreCase) ); if (found is not null) { Dispatcher.UIThread.Post(() => { SelectedItem = found; }); } } Dispatcher.UIThread.Post(ResetPopupText); } private void UpdatePopupText(string text) { inputTextBlock.Text += text; // Accumulate text in the popup if (!inputPopup.IsOpen) { inputPopup.IsOpen = true; } } private void ResetPopupText() { currentInput = string.Empty; inputTextBlock.Text = string.Empty; inputPopup.IsOpen = false; } // Ensure proper disposal of resources protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); subscription.Dispose(); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs ================================================ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Logging; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public class BetterContentDialog : ContentDialog { #region Reflection Shenanigans for setting content dialog result [NotNull] protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField( "_result", BindingFlags.Instance | BindingFlags.NonPublic ); protected ContentDialogResult Result { get => (ContentDialogResult)ResultField.GetValue(this)!; set => ResultField.SetValue(this, value); } [NotNull] protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod( "HideCore", BindingFlags.Instance | BindingFlags.NonPublic ); protected void HideCore() { HideCoreMethod.Invoke(this, null); } // Also get button properties to hide on command execution change [NotNull] protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField( "_primaryButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? PrimaryButton { get => (Button?)PrimaryButtonField.GetValue(this)!; set => PrimaryButtonField.SetValue(this, value); } [NotNull] protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField( "_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? SecondaryButton { get => (Button?)SecondaryButtonField.GetValue(this)!; set => SecondaryButtonField.SetValue(this, value); } [NotNull] protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField( "_closeButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? CloseButton { get => (Button?)CloseButtonField.GetValue(this)!; set => CloseButtonField.SetValue(this, value); } static BetterContentDialog() { if (ResultField is null) { throw new NullReferenceException("ResultField was not resolved"); } if (HideCoreMethod is null) { throw new NullReferenceException("HideCoreMethod was not resolved"); } if (PrimaryButtonField is null || SecondaryButtonField is null || CloseButtonField is null) { throw new NullReferenceException("Button fields were not resolved"); } } #endregion private Border? backgroundPart; protected override Type StyleKeyOverride { get; } = typeof(ContentDialog); public static readonly StyledProperty IsFooterVisibleProperty = AvaloniaProperty.Register< BetterContentDialog, bool >("IsFooterVisible", true); public bool IsFooterVisible { get => GetValue(IsFooterVisibleProperty); set => SetValue(IsFooterVisibleProperty, value); } public static readonly StyledProperty ContentVerticalScrollBarVisibilityProperty = AvaloniaProperty.Register( "ContentScrollBarVisibility", ScrollBarVisibility.Auto ); public ScrollBarVisibility ContentVerticalScrollBarVisibility { get => GetValue(ContentVerticalScrollBarVisibilityProperty); set => SetValue(ContentVerticalScrollBarVisibilityProperty, value); } public static readonly StyledProperty MinDialogWidthProperty = AvaloniaProperty.Register< BetterContentDialog, double >("MinDialogWidth"); public double MinDialogWidth { get => GetValue(MinDialogWidthProperty); set => SetValue(MinDialogWidthProperty, value); } public static readonly StyledProperty MaxDialogWidthProperty = AvaloniaProperty.Register< BetterContentDialog, double >("MaxDialogWidth"); public double MaxDialogWidth { get => GetValue(MaxDialogWidthProperty); set => SetValue(MaxDialogWidthProperty, value); } public static readonly StyledProperty MinDialogHeightProperty = AvaloniaProperty.Register< BetterContentDialog, double >("MinDialogHeight"); public double MinDialogHeight { get => GetValue(MaxDialogHeightProperty); set => SetValue(MaxDialogHeightProperty, value); } public static readonly StyledProperty MaxDialogHeightProperty = AvaloniaProperty.Register< BetterContentDialog, double >("MaxDialogHeight"); public double MaxDialogHeight { get => GetValue(MaxDialogHeightProperty); set => SetValue(MaxDialogHeightProperty, value); } public static readonly StyledProperty ContentMarginProperty = AvaloniaProperty.Register< BetterContentDialog, Thickness >("ContentMargin"); public Thickness ContentMargin { get => GetValue(ContentMarginProperty); set => SetValue(ContentMarginProperty, value); } public static readonly StyledProperty CloseOnClickOutsideProperty = AvaloniaProperty.Register< BetterContentDialog, bool >("CloseOnClickOutside"); /// /// Whether to close the dialog when clicking outside of it (on the blurred background) /// public bool CloseOnClickOutside { get => GetValue(CloseOnClickOutsideProperty); set => SetValue(CloseOnClickOutsideProperty, value); } public BetterContentDialog() { AddHandler(LoadedEvent, OnLoaded); } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (CloseOnClickOutside) { if (e.Source is Popup || backgroundPart is null) return; var point = e.GetPosition(this); if (!backgroundPart.Bounds.Contains(point)) { // Use vm if available if ((Content as Control)?.DataContext is ContentDialogViewModelBase vm) { vm.OnCloseButtonClick(); } else { Hide(ContentDialogResult.None); } } } } private void TrySetButtonCommands() { // If commands provided, bind OnCanExecuteChanged to hide buttons // otherwise link visibility to IsEnabled if (PrimaryButton is not null) { if (PrimaryButtonCommand is not null) { PrimaryButtonCommand.CanExecuteChanged += (_, _) => PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); // Also set initial state PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); } else { PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText); } } if (SecondaryButton is not null) { if (SecondaryButtonCommand is not null) { SecondaryButtonCommand.CanExecuteChanged += (_, _) => SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); // Also set initial state SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); } else { SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText); } } if (CloseButton is not null) { if (CloseButtonCommand is not null) { CloseButtonCommand.CanExecuteChanged += (_, _) => CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); // Also set initial state CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); } } } private void TryBindButtonEvents() { if ((Content as Control)?.DataContext is ContentDialogViewModelBase viewModel) { viewModel.PrimaryButtonClick += OnDialogButtonClick; viewModel.SecondaryButtonClick += OnDialogButtonClick; viewModel.CloseButtonClick += OnDialogButtonClick; } else if (Content is ContentDialogViewModelBase viewModelDirect) { viewModelDirect.PrimaryButtonClick += OnDialogButtonClick; viewModelDirect.SecondaryButtonClick += OnDialogButtonClick; viewModelDirect.CloseButtonClick += OnDialogButtonClick; } else if ((Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel) { progressViewModel.PrimaryButtonClick += OnDialogButtonClick; progressViewModel.SecondaryButtonClick += OnDialogButtonClick; progressViewModel.CloseButtonClick += OnDialogButtonClick; } } protected void OnDialogButtonClick(object? sender, ContentDialogResult e) { Dispatcher.UIThread.Post(() => { Result = e; HideCore(); }); } protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); TryBindButtonEvents(); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); backgroundPart = e.NameScope.Find("BackgroundElement"); if (backgroundPart is not null) { backgroundPart.Margin = ContentMargin; } } private void OnLoaded(object? sender, RoutedEventArgs? e) { TryBindButtonEvents(); try { // Find the named grid // https://github.com/amwx/FluentAvalonia/blob/master/src/FluentAvalonia/Styling/ // ControlThemes/FAControls/ContentDialogStyles.axaml#L96 var containerBorder = VisualChildren[0] as Border; var layoutRootPanel = containerBorder?.Child as Panel; var backgroundElementBorder = (layoutRootPanel?.Children[0] as Border).Unwrap(); // Set dialog bounds if (MaxDialogWidth > 0) { backgroundElementBorder.MaxWidth = MaxDialogWidth; } if (MinDialogWidth > 0) { backgroundElementBorder.MinWidth = MinDialogWidth; } // This kind of bork for some reason /*if (MinDialogHeight > 0) { faBorder!.MinHeight = MinDialogHeight; }*/ if (MaxDialogHeight > 0) { backgroundElementBorder!.MaxHeight = MaxDialogHeight; } var border2 = backgroundElementBorder?.Child as Border; // Named Grid 'DialogSpace' var dialogSpaceGrid = (border2?.Child as Grid).Unwrap(); // Get the parent border, which is what we want to hide var scrollViewer = (dialogSpaceGrid.Children[0] as ScrollViewer).Unwrap(); var actualBorder = (dialogSpaceGrid.Children[1] as Border).Unwrap(); var subBorder = (scrollViewer.Content as Border).Unwrap(); var subGrid = (subBorder.Child as Grid).Unwrap(); var contentControlTitle = (subGrid.Children[0] as ContentControl).Unwrap(); // Hide title if empty if (Title is null or string { Length: 0 }) { contentControlTitle.IsVisible = false; } // Set footer and scrollbar visibility states actualBorder.IsVisible = IsFooterVisible; scrollViewer.VerticalScrollBarVisibility = ContentVerticalScrollBarVisibility; } catch (ArgumentNullException) { Logger .TryGet(LogEventLevel.Error, nameof(BetterContentDialog)) ?.Log(this, "OnLoaded - Unable to find elements"); return; } // Also call the vm's OnLoad // (UserControlBase handles this now, so we don't need to) /*if (Content is Control { DataContext: ViewModelBase viewModel }) { viewModel.OnLoaded(); Dispatcher.UIThread.InvokeAsync(viewModel.OnLoadedAsync).SafeFireAndForget(); }*/ } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterContextDragBehavior.cs ================================================ using System; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Xaml.Interactions.DragAndDrop; using Avalonia.Xaml.Interactivity; namespace StabilityMatrix.Avalonia.Controls; public class BetterContextDragBehavior : Behavior { private Point _dragStartPoint; private PointerEventArgs? _triggerEvent; private bool _lock; private bool _captured; public static readonly StyledProperty ContextProperty = AvaloniaProperty.Register< ContextDragBehavior, object? >(nameof(Context)); public static readonly StyledProperty HandlerProperty = AvaloniaProperty.Register< ContextDragBehavior, IDragHandler? >(nameof(Handler)); public static readonly StyledProperty HorizontalDragThresholdProperty = AvaloniaProperty.Register< ContextDragBehavior, double >(nameof(HorizontalDragThreshold), 3); public static readonly StyledProperty VerticalDragThresholdProperty = AvaloniaProperty.Register< ContextDragBehavior, double >(nameof(VerticalDragThreshold), 3); public static readonly StyledProperty DataFormatProperty = AvaloniaProperty.Register< BetterContextDragBehavior, string >("DataFormat"); public string DataFormat { get => GetValue(DataFormatProperty); set => SetValue(DataFormatProperty, value); } public object? Context { get => GetValue(ContextProperty); set => SetValue(ContextProperty, value); } public IDragHandler? Handler { get => GetValue(HandlerProperty); set => SetValue(HandlerProperty, value); } public double HorizontalDragThreshold { get => GetValue(HorizontalDragThresholdProperty); set => SetValue(HorizontalDragThresholdProperty, value); } public double VerticalDragThreshold { get => GetValue(VerticalDragThresholdProperty); set => SetValue(VerticalDragThresholdProperty, value); } /// protected override void OnAttachedToVisualTree() { AssociatedObject?.AddHandler( InputElement.PointerPressedEvent, AssociatedObject_PointerPressed, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble ); AssociatedObject?.AddHandler( InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble ); AssociatedObject?.AddHandler( InputElement.PointerMovedEvent, AssociatedObject_PointerMoved, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble ); AssociatedObject?.AddHandler( InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble ); } /// protected override void OnDetachedFromVisualTree() { AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed); AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased); AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved); AssociatedObject?.RemoveHandler(InputElement.PointerCaptureLostEvent, AssociatedObject_CaptureLost); } private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value) { var data = new DataObject(); data.Set(DataFormat, value!); var effect = DragDropEffects.None; if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Alt)) { effect |= DragDropEffects.Link; } else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Shift)) { effect |= DragDropEffects.Move; } else if (triggerEvent.KeyModifiers.HasFlag(KeyModifiers.Control)) { effect |= DragDropEffects.Copy; } else { effect |= DragDropEffects.Move; } await DragDrop.DoDragDrop(triggerEvent, data, effect); } private void Released() { _triggerEvent = null; _lock = false; } private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e) { var properties = e.GetCurrentPoint(AssociatedObject).Properties; if (properties.IsLeftButtonPressed) { if (e.Source is Control control && AssociatedObject?.DataContext == control.DataContext) { _dragStartPoint = e.GetPosition(null); _triggerEvent = e; _lock = true; _captured = true; } } } private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e) { if (_captured) { if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { }) { Released(); } _captured = false; } } private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e) { var properties = e.GetCurrentPoint(AssociatedObject).Properties; if (_captured && properties.IsLeftButtonPressed && _triggerEvent is { }) { var point = e.GetPosition(null); var diff = _dragStartPoint - point; var horizontalDragThreshold = HorizontalDragThreshold; var verticalDragThreshold = VerticalDragThreshold; if (Math.Abs(diff.X) > horizontalDragThreshold || Math.Abs(diff.Y) > verticalDragThreshold) { if (_lock) { _lock = false; } else { return; } var context = Context ?? AssociatedObject?.DataContext; Handler?.BeforeDragDrop(sender, _triggerEvent, context); await DoDragDrop(_triggerEvent, context); Handler?.AfterDragDrop(sender, _triggerEvent, context); _triggerEvent = null; } } } private void AssociatedObject_CaptureLost(object? sender, PointerCaptureLostEventArgs e) { Released(); _captured = false; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterDownloadableComboBox.cs ================================================ using System; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; public partial class BetterDownloadableComboBox : BetterComboBox { protected override Type StyleKeyOverride => typeof(BetterComboBox); static BetterDownloadableComboBox() { SelectionChangedEvent.AddClassHandler( (comboBox, args) => comboBox.OnSelectionChanged(args) ); } protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) { // On downloadable added if (e.AddedItems.Count > 0 && e.AddedItems[0] is IDownloadableResource { IsDownloadable: true } item) { // Reset the selection e.Handled = true; if ( e.RemovedItems.Count > 0 && e.RemovedItems[0] is IDownloadableResource { IsDownloadable: false } removedItem ) { SelectedItem = removedItem; } else { SelectedItem = null; } // Show dialog to download the model PromptDownloadCommand.ExecuteAsync(item).SafeFireAndForget(); } } [RelayCommand] private static async Task PromptDownloadAsync(IDownloadableResource downloadable) { if (downloadable.DownloadableResource is not { } resource) return; var vmFactory = App.Services.GetRequiredService>(); var confirmDialog = vmFactory.Get(); confirmDialog.Resource = resource; confirmDialog.FileName = resource.FileName; if (await confirmDialog.GetDialog().ShowAsync() == ContentDialogResult.Primary) { confirmDialog.StartDownload(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterFlyout.cs ================================================ using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.VisualTree; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class BetterFlyout : Flyout { public static readonly StyledProperty VerticalScrollBarVisibilityProperty = AvaloniaProperty.Register( "VerticalScrollBarVisibility"); public ScrollBarVisibility VerticalScrollBarVisibility { get => GetValue(VerticalScrollBarVisibilityProperty); set => SetValue(VerticalScrollBarVisibilityProperty, value); } public static readonly StyledProperty HorizontalScrollBarVisibilityProperty = AvaloniaProperty.Register( "HorizontalScrollBarVisibility"); public ScrollBarVisibility HorizontalScrollBarVisibility { get => GetValue(HorizontalScrollBarVisibilityProperty); set => SetValue(HorizontalScrollBarVisibilityProperty, value); } protected override void OnOpened() { base.OnOpened(); var presenter = Popup.Child; if (presenter.FindDescendantOfType() is { } scrollViewer) { scrollViewer.VerticalScrollBarVisibility = VerticalScrollBarVisibility; scrollViewer.HorizontalScrollBarVisibility = HorizontalScrollBarVisibility; } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterImage.cs ================================================ using System; using Avalonia; using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Automation.Peers; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Metadata; namespace StabilityMatrix.Avalonia.Controls; public class BetterImage : Control { /// /// Defines the property. /// public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. /// public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); /// /// Defines the property. /// public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register( nameof(StretchDirection), StretchDirection.Both); static BetterImage() { AffectsRender(SourceProperty, StretchProperty, StretchDirectionProperty); AffectsMeasure(SourceProperty, StretchProperty, StretchDirectionProperty); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue( AutomationControlType.Image); } /// /// Gets or sets the image that will be displayed. /// [Content] public IImage? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } /// /// Gets or sets a value controlling how the image will be stretched. /// public Stretch Stretch { get { return GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } /// /// Gets or sets a value controlling in what direction the image will be stretched. /// public StretchDirection StretchDirection { get { return GetValue(StretchDirectionProperty); } set { SetValue(StretchDirectionProperty, value); } } /// protected override bool BypassFlowDirectionPolicies => true; /// /// Renders the control. /// /// The drawing context. public sealed override void Render(DrawingContext context) { var source = Source; if (source == null || Bounds is not {Width: > 0, Height: > 0}) return; var viewPort = new Rect(Bounds.Size); var sourceSize = source.Size; var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); var scaledSize = sourceSize * scale; // Calculate starting points for dest var destX = HorizontalAlignment switch { HorizontalAlignment.Left => 0, HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2, HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width), // Stretch is default, use center HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2, _ => throw new ArgumentException(nameof(HorizontalAlignment)) }; var destRect = viewPort .CenterRect(new Rect(scaledSize)) .WithX(destX) .WithY(0) .Intersect(viewPort); var destRectUnscaledSize = destRect.Size / scale; var sourceX = HorizontalAlignment switch { HorizontalAlignment.Left => 0, HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2, HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width, // Stretch is default, use center HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2, _ => throw new ArgumentException(nameof(HorizontalAlignment)) }; var sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)) .WithX(sourceX) .WithY(0); context.DrawImage(source, sourceRect, destRect); } /// /// Measures the control. /// /// The available size. /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { var source = Source; var result = new Size(); if (source != null) { result = Stretch.CalculateSize(availableSize, source.Size, StretchDirection); } return result; } /// protected override Size ArrangeOverride(Size finalSize) { var source = Source; if (source != null) { var sourceSize = source.Size; var result = Stretch.CalculateSize(finalSize, sourceSize); return result; } else { return new Size(); } } protected override AutomationPeer OnCreateAutomationPeer() { return new ImageAutomationPeer(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/BetterMarkdownScrollViewer.cs ================================================ using Markdown.Avalonia; using StabilityMatrix.Avalonia.Styles.Markdown; namespace StabilityMatrix.Avalonia.Controls; /// /// Fix MarkdownScrollViewer IBrush errors and not working with Avalonia 11.2.0 /// public class BetterMarkdownScrollViewer : MarkdownScrollViewer { public BetterMarkdownScrollViewer() { MarkdownStyleName = "Empty"; MarkdownStyle = new MarkdownStyleFluentAvalonia(); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Card.cs ================================================ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; namespace StabilityMatrix.Avalonia.Controls; public class Card : ContentControl { protected override Type StyleKeyOverride => typeof(Card); // ReSharper disable MemberCanBePrivate.Global public static readonly StyledProperty IsCardVisualsEnabledProperty = AvaloniaProperty.Register("IsCardVisualsEnabled", true); /// /// Whether to show card visuals. /// When false, the card will have a padding of 0 and be transparent. /// public bool IsCardVisualsEnabled { get => GetValue(IsCardVisualsEnabledProperty); set => SetValue(IsCardVisualsEnabledProperty, value); } // ReSharper restore MemberCanBePrivate.Global public Card() { MinHeight = 8; MinWidth = 8; } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); // When IsCardVisualsEnabled is false, add the disabled pseudo class if (change.Property == IsCardVisualsEnabledProperty) { PseudoClasses.Set("disabled", !change.GetNewValue()); } } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); PseudoClasses.Set("disabled", !IsCardVisualsEnabled); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CheckerboardBorder.cs ================================================ using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Media; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CheckerboardBorder : Control { public static readonly StyledProperty GridCellSizeProperty = AvaloniaProperty.Register< AdvancedImageBox, byte >(nameof(GridCellSize), 15); public byte GridCellSize { get => GetValue(GridCellSizeProperty); set => SetValue(GridCellSizeProperty, value); } public static readonly StyledProperty GridColorProperty = AvaloniaProperty.Register( nameof(GridColor), SolidColorBrush.Parse("#181818") ); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColor { get => GetValue(GridColorProperty); set => SetValue(GridColorProperty, value); } public static readonly StyledProperty GridColorAlternateProperty = AvaloniaProperty.Register( nameof(GridColorAlternate), SolidColorBrush.Parse("#252525") ); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColorAlternate { get => GetValue(GridColorAlternateProperty); set => SetValue(GridColorAlternateProperty, value); } static CheckerboardBorder() { AffectsRender(GridCellSizeProperty); AffectsRender(GridColorProperty); AffectsRender(GridColorAlternateProperty); } /// public override void Render(DrawingContext context) { var size = GridCellSize; var square1Drawing = new GeometryDrawing { Brush = GridColorAlternate, Geometry = new RectangleGeometry(new Rect(0.0, 0.0, size, size)) }; var square2Drawing = new GeometryDrawing { Brush = GridColorAlternate, Geometry = new RectangleGeometry(new Rect(size, size, size, size)) }; var drawingGroup = new DrawingGroup { Children = { square1Drawing, square2Drawing } }; var tileBrush = new DrawingBrush(drawingGroup) { AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top, DestinationRect = new RelativeRect(new Size(2 * size, 2 * size), RelativeUnit.Absolute), Stretch = Stretch.None, TileMode = TileMode.Tile, }; context.FillRectangle(GridColor, Bounds); // context.DrawRectangle(new Pen(Brushes.Blue), new Rect(0.5, 0.5, Bounds.Width - 1.0, Bounds.Height - 1.0)); context.FillRectangle(tileBrush, Bounds); // base.Render(context); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs ================================================ using System; using Avalonia.Controls.Documents; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// Provides entries in AvaloniaEdit completion window. /// public class CompletionData : ICompletionData { /// public string Text { get; } /// public string? Description { get; init; } /// public ImageSource? ImageSource { get; set; } /// /// Title of the image. /// public string? ImageTitle { get; set; } /// /// Subtitle of the image. /// public string? ImageSubtitle { get; set; } /// public IconData? Icon { get; init; } private InlineCollection? _textInlines; /// /// Get the current inlines /// public InlineCollection TextInlines => _textInlines ??= CreateInlines(); /// public double Priority { get; init; } public CompletionData(string text) { Text = text; } /// /// Create text block inline runs from text. /// private InlineCollection CreateInlines() { // Create a span for each character in the text. var chars = Text.ToCharArray(); var inlines = new InlineCollection(); foreach (var c in chars) { var run = new Run(c.ToString()); inlines.Add(run); } return inlines; } /// public void Complete( TextArea textArea, ISegment completionSegment, InsertionRequestEventArgs eventArgs, Func? prepareText = null ) { var text = Text; if (prepareText is not null) { text = prepareText(this); } // Capture initial offset before replacing text, since it will change var initialOffset = completionSegment.Offset; // Replace text textArea.Document.Replace(completionSegment, text); // Append text if requested if (eventArgs.AppendText is { } appendText && !string.IsNullOrEmpty(appendText)) { var end = initialOffset + text.Length; textArea.Document.Insert(end, appendText); textArea.Caret.Offset = end + appendText.Length; } } /// public void UpdateCharHighlighting(string searchText) { if (TextInlines is null) { throw new NullReferenceException("TextContent is null"); } var defaultColor = ThemeColors.CompletionForegroundBrush; var highlightColor = ThemeColors.CompletionSelectionForegroundBrush; // Match characters in the text with the search text from the start foreach (var (i, currentChar) in Text.Enumerate()) { var inline = TextInlines[i]; // If longer than text, set to default color if (i >= searchText.Length) { inline.Foreground = defaultColor; continue; } // If char matches, highlight it if (currentChar == searchText[i]) { inline.Foreground = highlightColor; } // For mismatch, set to default color else { inline.Foreground = defaultColor; } } } /// public void ResetCharHighlighting() { // TODO: handle light theme foreground variant var defaultColor = ThemeColors.CompletionForegroundBrush; foreach (var inline in TextInlines) { inline.Foreground = defaultColor; } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Styles; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class CompletionIcons { public static readonly IconData General = new() { FAIcon = "fa-solid fa-star-of-life", Foreground = ThemeColors.LightSteelBlue, }; public static readonly IconData Artist = new() { FAIcon = "fa-solid fa-palette", Foreground = ThemeColors.AmericanYellow, }; public static readonly IconData Character = new() { FAIcon = "fa-solid fa-user", Foreground = ThemeColors.LuminousGreen, }; public static readonly IconData Copyright = new() { FAIcon = "fa-solid fa-copyright", Foreground = ThemeColors.DeepMagenta, }; public static readonly IconData Species = new() { FAIcon = "fa-solid fa-dragon", FontSize = 14, Foreground = ThemeColors.HalloweenOrange, }; public static readonly IconData Invalid = new() { FAIcon = "fa-solid fa-question", Foreground = ThemeColors.CompletionForegroundBrush, }; public static readonly IconData Keyword = new() { FAIcon = "fa-solid fa-key", Foreground = ThemeColors.CompletionForegroundBrush, }; public static readonly IconData Model = new() { FAIcon = "fa-solid fa-cube", Foreground = ThemeColors.CompletionForegroundBrush, }; public static readonly IconData ModelType = new() { FAIcon = "fa-solid fa-shapes", Foreground = ThemeColors.BrilliantAzure, }; public static IconData? GetIconForTagType(TagType tagType) { return tagType switch { TagType.General => General, TagType.Artist => Artist, TagType.Character => Character, TagType.Species => Species, TagType.Invalid => Invalid, TagType.Copyright => Copyright, _ => null }; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs ================================================ // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml.Templates; using AvaloniaEdit.Utils; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// The listbox used inside the CompletionWindow, contains CompletionListBox. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CompletionList : TemplatedControlBase { private CompletionListBox? _listBox; public CompletionList() { AddHandler(DoubleTappedEvent, OnDoubleTapped); } /// /// If true, the CompletionList is filtered to show only matching items. Also enables search by substring. /// If false, enables the old behavior: no filtering, search by string.StartsWith. /// public bool IsFiltering { get; set; } = true; /// /// Dependency property for . /// public static readonly StyledProperty EmptyTemplateProperty = AvaloniaProperty.Register< CompletionList, ControlTemplate >(nameof(EmptyTemplate)); /// /// Content of EmptyTemplate will be shown when CompletionList contains no items. /// If EmptyTemplate is null, nothing will be shown. /// public ControlTemplate EmptyTemplate { get => GetValue(EmptyTemplateProperty); set => SetValue(EmptyTemplateProperty, value); } /// /// Dependency property for . /// public static readonly StyledProperty FooterTextProperty = AvaloniaProperty.Register< CompletionList, string? >("FooterText", "Press Enter to insert, Tab to replace"); /// /// Gets/Sets the text displayed in the footer of the completion list. /// public string? FooterText { get => GetValue(FooterTextProperty); set => SetValue(FooterTextProperty, value); } /// /// Is raised when the completion list indicates that the user has chosen /// an entry to be completed. /// public event EventHandler? InsertionRequested; /// /// Raised when the completion list indicates that it should be closed. /// public event EventHandler? CloseRequested; /// /// Raises the InsertionRequested event. /// public void RequestInsertion( ICompletionData item, RoutedEventArgs triggeringEvent, string? appendText = null ) { InsertionRequested?.Invoke( this, new InsertionRequestEventArgs { Item = item, TriggeringEvent = triggeringEvent, AppendText = appendText } ); } /// /// Raises the CloseRequested event. /// public void RequestClose() { CloseRequested?.Invoke(this, EventArgs.Empty); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _listBox = e.NameScope.Find("PART_ListBox") as CompletionListBox; if (_listBox is not null) { // _listBox.ItemsSource = _completionData; _listBox.ItemsSource = FilteredCompletionData; } } /// /// Gets the list box. /// public CompletionListBox? ListBox { get { if (_listBox == null) ApplyTemplate(); return _listBox; } } /// /// Dictionary of keys that request insertion of the completion /// mapped to strings that will be appended to the completion when selected. /// The string may be empty. /// public Dictionary CompletionAcceptKeys { get; init; } = new() { [Key.Enter] = "", [Key.Tab] = "" }; /// /// Gets the scroll viewer used in this list box. /// public ScrollViewer? ScrollViewer => _listBox?.ScrollViewer; private readonly ObservableCollection _completionData = new(); /// /// Gets the list to which completion data can be added. /// public IList CompletionData => _completionData; public ObservableCollection FilteredCompletionData { get; } = new(); /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (!e.Handled) { HandleKey(e); } } /// /// Handles a key press. Used to let the completion list handle key presses while the /// focus is still on the text editor. /// [SuppressMessage("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault")] public void HandleKey(KeyEventArgs e) { if (_listBox == null) return; // We have to do some key handling manually, because the default doesn't work with // our simulated events. // Also, the default PageUp/PageDown implementation changes the focus, so we avoid it. switch (e.Key) { case Key.Down: e.Handled = true; _listBox.SelectNextIndexWithLoop(); break; case Key.Up: e.Handled = true; _listBox.SelectPreviousIndexWithLoop(); break; case Key.PageDown: e.Handled = true; _listBox.SelectIndex(_listBox.SelectedIndex + _listBox.VisibleItemCount); break; case Key.PageUp: e.Handled = true; _listBox.SelectIndex(_listBox.SelectedIndex - _listBox.VisibleItemCount); break; case Key.Home: e.Handled = true; _listBox.SelectIndex(0); break; case Key.End: e.Handled = true; _listBox.SelectIndex(_listBox.ItemCount - 1); break; default: // Check insertion keys if (CompletionAcceptKeys.TryGetValue(e.Key, out var appendText) && CurrentList?.Count > 0) { e.Handled = true; if (SelectedItem is { } item) { RequestInsertion(item, e, appendText); } else { RequestClose(); } } break; } } protected void OnDoubleTapped(object? sender, RoutedEventArgs e) { //TODO TEST if ( ((AvaloniaObject?)e.Source) .VisualAncestorsAndSelf() .TakeWhile(obj => obj != this) .Any(obj => obj is ListBoxItem) ) { e.Handled = true; if (SelectedItem is { } item) { RequestInsertion(item, e); } else { RequestClose(); } } } /// /// Gets/Sets the selected item. /// /// /// The setter of this property does not scroll to the selected item. /// You might want to also call . /// public ICompletionData? SelectedItem { get => _listBox?.SelectedItem as ICompletionData; set { if (_listBox == null && value != null) ApplyTemplate(); if (_listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null _listBox.SelectedItem = value; } } /// /// Scrolls the specified item into view. /// public void ScrollIntoView(ICompletionData item) { if (_listBox == null) ApplyTemplate(); _listBox?.ScrollIntoView(item); } /// /// Occurs when the SelectedItem property changes. /// public event EventHandler SelectionChanged { add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); } // SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once private string? _currentText; private ObservableCollection? _currentList; public List? CurrentList => ListBox?.Items.Cast().ToList(); /// /// Selects the best match, and filter the items if turned on using . /// public void SelectItem(string text, bool fullUpdate = false) { if (text == _currentText) { return; } using var _ = CodeTimer.StartDebug(); if (_listBox == null) { ApplyTemplate(); } if (IsFiltering) { SelectItemFilteringLive(text, fullUpdate); } else { SelectItemWithStart(text); } _currentText = text; } private IReadOnlyList FilterItems(IEnumerable items, string query) { using var _ = CodeTimer.StartDebug(); // Order first by quality, then by priority var matchingItems = items .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) .Where(x => x.Quality > 0) .OrderByDescending(x => x.Quality) .ThenByDescending(x => x.Item.Priority) .Select(x => x.Item) .ToList(); return matchingItems; } /// /// Filters CompletionList items to show only those matching given query, and selects the best match. /// private void SelectItemFilteringLive(string query, bool fullUpdate = false) { var listToFilter = _completionData; // if the user just typed one more character, don't filter all data but just filter what we are already displaying if ( !fullUpdate && FilteredCompletionData.Count > 0 && !string.IsNullOrEmpty(_currentText) && !string.IsNullOrEmpty(query) && query.StartsWith(_currentText, StringComparison.Ordinal) ) { listToFilter = FilteredCompletionData; } var matchingItems = FilterItems(listToFilter, query); // Close if no items match if (matchingItems.Count == 0) { RequestClose(); return; } // Fast path if both only 1 item, and item is the same if ( FilteredCompletionData.Count == 1 && matchingItems.Count == 1 && FilteredCompletionData[0] == matchingItems[0] ) { // Just update the character highlighting matchingItems[0].UpdateCharHighlighting(query); } else { // Clear current items and set new ones FilteredCompletionData.Clear(); foreach (var item in matchingItems) { item.UpdateCharHighlighting(query); FilteredCompletionData.Add(item); } // Set index to 0 if not already if (_listBox != null && _listBox.SelectedIndex != 0) { _listBox.SelectedIndex = 0; } } } /// /// Filters CompletionList items to show only those matching given query, and selects the best match. /// private void SelectItemFiltering(string query, bool fullUpdate = false) { if (_listBox is null) throw new NullReferenceException("ListBox not set"); var listToFilter = _completionData; // if the user just typed one more character, don't filter all data but just filter what we are already displaying if ( !fullUpdate && _currentList != null && !string.IsNullOrEmpty(_currentText) && !string.IsNullOrEmpty(query) && query.StartsWith(_currentText, StringComparison.Ordinal) ) { listToFilter = _currentList; } // Order first by quality, then by priority var matchingItems = listToFilter .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) .Where(x => x.Quality > 0) .OrderByDescending(x => x.Quality) .ThenByDescending(x => x.Item.Priority) .ToImmutableArray(); /*var matchingItems = from item in listToFilter let quality = GetMatchQuality(item.Text, query) where quality > 0 orderby quality select new { Item = item, Quality = quality };*/ var suggestedItem = _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; var listBoxItems = new ObservableCollection(); var bestIndex = -1; var bestQuality = -1; double bestPriority = 0; var i = 0; foreach (var matchingItem in matchingItems) { var priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority; var quality = matchingItem.Quality; if (quality > bestQuality || quality == bestQuality && priority > bestPriority) { bestIndex = i; bestPriority = priority; bestQuality = quality; } // Add to listbox listBoxItems.Add(matchingItem.Item); // Update the character highlighting matchingItem.Item.UpdateCharHighlighting(query); i++; } _currentList = listBoxItems; //_listBox.Items = null; Makes no sense? Tooltip disappeared because of this _listBox.ItemsSource = listBoxItems; SelectIndex(bestIndex); } /// /// Selects the item that starts with the specified query. /// private void SelectItemWithStart(string query) { if (string.IsNullOrEmpty(query)) return; var suggestedIndex = _listBox?.SelectedIndex ?? -1; if (suggestedIndex == -1) { return; } var bestIndex = -1; var bestQuality = -1; double bestPriority = 0; for (var i = 0; i < _completionData.Count; ++i) { var quality = GetMatchQuality(_completionData[i].Text, query); if (quality < 0) continue; var priority = _completionData[i].Priority; bool useThisItem; if (bestQuality < quality) { useThisItem = true; } else { if (bestIndex == suggestedIndex) { useThisItem = false; } else if (i == suggestedIndex) { // prefer recommendedItem, regardless of its priority useThisItem = bestQuality == quality; } else { useThisItem = bestQuality == quality && bestPriority < priority; } } if (useThisItem) { bestIndex = i; bestPriority = priority; bestQuality = quality; } } SelectIndexCentered(bestIndex); } private void SelectIndexCentered(int index) { if (_listBox is null) { throw new NullReferenceException("ListBox not set"); } if (index < 0) { _listBox.ClearSelection(); } else { var firstItem = _listBox.FirstVisibleItem; if (index < firstItem || firstItem + _listBox.VisibleItemCount <= index) { // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null _listBox.CenterViewOn(index); _listBox.SelectIndex(index); } else { _listBox.SelectIndex(index); } } } private void SelectIndex(int index) { if (_listBox is null) { throw new NullReferenceException("ListBox not set"); } if (index == _listBox.SelectedIndex) return; if (index < 0) { _listBox.ClearSelection(); } else { _listBox.SelectedIndex = index; } } private int GetMatchQuality(string itemText, string query) { if (itemText == null) throw new ArgumentNullException(nameof(itemText), "ICompletionData.Text returned null"); // Qualities: // 8 = full match case sensitive // 7 = full match // 6 = match start case sensitive // 5 = match start // 4 = match CamelCase when length of query is 1 or 2 characters // 3 = match substring case sensitive // 2 = match substring // 1 = match CamelCase // -1 = no match if (query == itemText) return 8; if (string.Equals(itemText, query, StringComparison.CurrentCultureIgnoreCase)) return 7; if (itemText.StartsWith(query, StringComparison.CurrentCulture)) return 6; if (itemText.StartsWith(query, StringComparison.CurrentCultureIgnoreCase)) return 5; bool? camelCaseMatch = null; if (query.Length <= 2) { camelCaseMatch = CamelCaseMatch(itemText, query); if (camelCaseMatch == true) return 4; } // search by substring, if filtering (i.e. new behavior) turned on if (IsFiltering) { if (itemText.Contains(query, StringComparison.CurrentCulture)) return 3; if (itemText.Contains(query, StringComparison.CurrentCultureIgnoreCase)) return 2; } if (!camelCaseMatch.HasValue) camelCaseMatch = CamelCaseMatch(itemText, query); if (camelCaseMatch == true) return 1; return -1; } private static bool CamelCaseMatch(string text, string query) { // We take the first letter of the text regardless of whether or not it's upper case so we match // against camelCase text as well as PascalCase text ("cct" matches "camelCaseText") var theFirstLetterOfEachWord = text.AsEnumerable() .Take(1) .Concat(text.AsEnumerable().Skip(1).Where(char.IsUpper)); var i = 0; foreach (var letter in theFirstLetterOfEachWord) { if (i > query.Length - 1) return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis") if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter)) return false; i++; } if (i >= query.Length) return true; return false; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs ================================================ // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using Avalonia.Controls; using Avalonia.Controls.Primitives; using AvaloniaEdit.Utils; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// The list box used inside the CompletionList. /// public class CompletionListBox : ListBox { internal ScrollViewer? ScrollViewer; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); ScrollViewer = e.NameScope.Find("PART_ScrollViewer") as ScrollViewer; } /// /// Gets the number of the first visible item. /// public int FirstVisibleItem { get { if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) { return 0; } return (int)(ItemCount * ScrollViewer.Offset.Y / ScrollViewer.Extent.Height); } set { value = value.CoerceValue(0, ItemCount - VisibleItemCount); if (ScrollViewer != null) { ScrollViewer.Offset = ScrollViewer.Offset.WithY( (double)value / ItemCount * ScrollViewer.Extent.Height ); } } } /// /// Gets the number of visible items. /// public int VisibleItemCount { get { if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) { return 10; } return Math.Max( 3, (int) Math.Ceiling( ItemCount * ScrollViewer.Viewport.Height / ScrollViewer.Extent.Height ) ); } } /// /// Removes the selection. /// public void ClearSelection() { SelectedIndex = -1; } /// /// Selects the next item. If the last item is already selected, selects the first item. /// public void SelectNextIndexWithLoop() { if (ItemCount <= 0) return; SelectIndex((SelectedIndex + 1) % ItemCount); } /// /// Selects the previous item. If the first item is already selected, selects the last item. /// public void SelectPreviousIndexWithLoop() { if (ItemCount <= 0) return; SelectIndex((SelectedIndex - 1 + ItemCount) % ItemCount); } /// /// Selects the item with the specified index and scrolls it into view. /// public void SelectIndex(int index) { if (index >= ItemCount) index = ItemCount - 1; if (index < 0) index = 0; SelectedIndex = index; if (SelectedItem is { } item) { ScrollIntoView(item); } } /// /// Centers the view on the item with the specified index. /// public void CenterViewOn(int index) { FirstVisibleItem = index - VisibleItemCount / 2; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs ================================================ // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Diagnostics; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Media; using Avalonia.Threading; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using AvaloniaEdit.Utils; using StabilityMatrix.Avalonia.Models.TagCompletion; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// The code completion window. /// public class CompletionWindow : CompletionWindowBase { private readonly ICompletionProvider completionProvider; private readonly ITokenizerProvider tokenizerProvider; private PopupWithCustomPosition? _toolTip; private ContentControl? _toolTipContent; public bool ToolTipIsOpen => _toolTip is not null; /// /// Max number of items in the completion list. /// public int MaxListLength { get; set; } = 40; /// /// Gets the completion list used in this completion window. /// public CompletionList CompletionList { get; } = new(); /// /// Whether selection tooltips are shown. /// public bool IsSelectionTooltipEnabled { get; set; } /// /// Creates a new code completion window. /// public CompletionWindow( TextArea textArea, ICompletionProvider completionProvider, ITokenizerProvider tokenizerProvider ) : base(textArea) { this.completionProvider = completionProvider; this.tokenizerProvider = tokenizerProvider; CloseAutomatically = true; MaxHeight = 225; Width = 350; Child = CompletionList; // prevent user from resizing window to 0x0 MinHeight = 15; MinWidth = 30; _toolTipContent = new ContentControl(); _toolTipContent.Classes.Add("ToolTip"); _toolTip = new PopupWithCustomPosition { IsLightDismissEnabled = true, PlacementTarget = this, Placement = PlacementMode.RightEdgeAlignedTop, Child = _toolTipContent, }; LogicalChildren.Add(_toolTip); _toolTip.Closed += (s, e) => { Hide(); // (s as Popup)!.Child = null; }; AttachEvents(); } protected override void OnClosed() { base.OnClosed(); if (_toolTip != null) { _toolTip.IsOpen = false; _toolTip = null; _toolTipContent = null; } } #region ToolTip handling private void CompletionList_SelectionChanged(object? sender, SelectionChangedEventArgs e) { // Skip if tooltip not enabled if (!IsSelectionTooltipEnabled) return; if (_toolTipContent == null || _toolTip == null) return; var item = CompletionList.SelectedItem; if (item?.Description is { } descriptionText) { _toolTipContent.Content = new TextBlock { Text = descriptionText, TextWrapping = TextWrapping.Wrap }; _toolTip.IsOpen = false; // Popup needs to be closed to change position // Calculate offset for tooltip var popupRoot = Host as PopupRoot; if (CompletionList.CurrentList != null) { double yOffset = 0; var itemContainer = CompletionList.ListBox!.ContainerFromItem(item); if (popupRoot != null && itemContainer != null) { var position = itemContainer.TranslatePoint(new Point(0, 0), popupRoot); if (position.HasValue) yOffset = position.Value.Y; } _toolTip.Offset = new Point(2, yOffset); } _toolTip.PlacementTarget = popupRoot; _toolTip.IsOpen = true; } else { _toolTip.IsOpen = false; } } #endregion private void CompletionList_InsertionRequested(object? sender, InsertionRequestEventArgs e) { Hide(); // The window must close before Complete() is called. // If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. var length = EndOffset - StartOffset; e.Item.Complete( TextArea, new AnchorSegment(TextArea.Document, StartOffset, length), e, completionProvider.PrepareInsertionText ); } private void CompletionList_CloseRequested(object? sender, EventArgs e) { Hide(); } private void AttachEvents() { CompletionList.CloseRequested += CompletionList_CloseRequested; CompletionList.InsertionRequested += CompletionList_InsertionRequested; CompletionList.SelectionChanged += CompletionList_SelectionChanged; TextArea.Caret.PositionChanged += CaretPositionChanged; TextArea.PointerWheelChanged += TextArea_MouseWheel; TextArea.TextInput += TextArea_PreviewTextInput; } /// protected override void DetachEvents() { CompletionList.CloseRequested -= CompletionList_CloseRequested; CompletionList.InsertionRequested -= CompletionList_InsertionRequested; CompletionList.SelectionChanged -= CompletionList_SelectionChanged; TextArea.Caret.PositionChanged -= CaretPositionChanged; TextArea.PointerWheelChanged -= TextArea_MouseWheel; TextArea.TextInput -= TextArea_PreviewTextInput; base.DetachEvents(); } /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (!e.Handled) { CompletionList.HandleKey(e); } } private void TextArea_PreviewTextInput(object? sender, TextInputEventArgs e) { e.Handled = RaiseEventPair( this, null, TextInputEvent, new TextInputEventArgs { Text = e.Text } ); } private void TextArea_MouseWheel(object? sender, PointerWheelEventArgs e) { e.Handled = RaiseEventPair(GetScrollEventTarget(), null, PointerWheelChangedEvent, e); } private Control GetScrollEventTarget() { /*if (CompletionList == null) return this;*/ return CompletionList.ScrollViewer ?? CompletionList.ListBox ?? (Control)CompletionList; } /// /// Gets/Sets whether the completion window should close automatically. /// The default value is true. /// public bool CloseAutomatically { get; set; } /// protected override bool CloseOnFocusLost => CloseAutomatically; /// /// When this flag is set, code completion closes if the caret moves to the /// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing", /// but not in dot-completion. /// Has no effect if CloseAutomatically is false. /// public bool CloseWhenCaretAtBeginning { get; set; } private void CaretPositionChanged(object? sender, EventArgs e) { Debug.WriteLine($"Caret Position changed: {e}"); var offset = TextArea.Caret.Offset; if (offset == StartOffset) { if (CloseAutomatically && CloseWhenCaretAtBeginning) { Hide(); } else { CompletionList.SelectItem(string.Empty); IsVisible = CompletionList.ListBox!.ItemCount != 0; } return; } if (offset < StartOffset || offset > EndOffset) { if (CloseAutomatically) { Hide(); } } else { var document = TextArea.Document; if (document != null) { var newText = document.GetText(StartOffset, offset - StartOffset); Debug.WriteLine("CaretPositionChanged newText: " + newText); if (lastSearchRequest is not { } lastRequest) { return; } // CompletionList.SelectItem(newText); Dispatcher.UIThread.Post(() => UpdateQuery(lastRequest with { Text = newText })); // UpdateQuery(newText); IsVisible = CompletionList.ListBox!.ItemCount != 0; } } } private TextCompletionRequest? lastSearchRequest; private int lastCompletionLength; /// /// Update the completion window's current search term. /// public void UpdateQuery(TextCompletionRequest completionRequest) { var searchTerm = completionRequest.Text; // Fast path if the search term starts with the last search term // and the last completion count was less than the max list length // (such we won't get new results by searching again) if ( lastSearchRequest is not null && completionRequest.Type == lastSearchRequest.Type && searchTerm.StartsWith(lastSearchRequest.Text) && lastCompletionLength < MaxListLength ) { CompletionList.SelectItem(searchTerm); lastSearchRequest = completionRequest; return; } var results = completionProvider.GetCompletions(completionRequest, MaxListLength, true); CompletionList.CompletionData.Clear(); CompletionList.CompletionData.AddRange(results); CompletionList.SelectItem(searchTerm, true); lastSearchRequest = completionRequest; lastCompletionLength = CompletionList.CompletionData.Count; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs ================================================ // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaEdit; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using AvaloniaEdit.Rendering; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// Base class for completion windows. Handles positioning the window at the caret. /// [SuppressMessage("ReSharper", "MemberCanBeProtected.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CompletionWindowBase : Popup { protected override Type StyleKeyOverride => typeof(PopupRoot); /// /// Gets the parent TextArea. /// public TextArea TextArea { get; } private readonly Window? _parentWindow; private TextDocument? _document; /// /// Gets/Sets the start of the text range in which the completion window stays open. /// This text portion is used to determine the text used to select an entry in the completion list by typing. /// public int StartOffset { get; set; } /// /// Gets/Sets the end of the text range in which the completion window stays open. /// This text portion is used to determine the text used to select an entry in the completion list by typing. /// public int EndOffset { get; set; } /// /// Gets whether the window was opened above the current line. /// protected bool IsUp { get; private set; } /// /// Creates a new CompletionWindowBase. /// public CompletionWindowBase(TextArea textArea) { TextArea = textArea ?? throw new ArgumentNullException(nameof(textArea)); _parentWindow = textArea.GetVisualRoot() as Window ?? throw new InvalidOperationException("CompletionWindow requires a visual root."); AddHandler(PointerReleasedEvent, OnMouseUp, handledEventsToo: true); StartOffset = EndOffset = TextArea.Caret.Offset; PlacementTarget = TextArea.TextView; Placement = PlacementMode.AnchorAndGravity; PlacementAnchor = global::Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor.TopLeft; PlacementGravity = global::Avalonia.Controls.Primitives.PopupPositioning.PopupGravity.BottomRight; //Deactivated += OnDeactivated; //Not needed? Closed += (_, _) => DetachEvents(); AttachEvents(); Initialize(); } protected virtual void OnClosed() { DetachEvents(); } private void Initialize() { if (_document != null && StartOffset != TextArea.Caret.Offset) { SetPosition(new TextViewPosition(_document.GetLocation(StartOffset))); } else { SetPosition(TextArea.Caret.Position); } } public void Show() { UpdatePosition(); Open(); Height = double.NaN; MinHeight = 0; } public void Hide() { Close(); OnClosed(); } #region Event Handlers private void AttachEvents() { ((ISetLogicalParent)this).SetParent(TextArea.GetVisualRoot() as ILogical); _document = TextArea.Document; if (_document != null) { _document.Changing += TextArea_Document_Changing; } // LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729 TextArea.LostFocus += TextAreaLostFocus; TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged; TextArea.DocumentChanged += TextAreaDocumentChanged; if (_parentWindow != null) { _parentWindow.PositionChanged += ParentWindow_LocationChanged; _parentWindow.Deactivated += ParentWindow_Deactivated; } // close previous completion windows of same type foreach (var x in TextArea.StackedInputHandlers.OfType()) { if (x.Window.GetType() == GetType()) TextArea.PopStackedInputHandler(x); } _myInputHandler = new InputHandler(this); TextArea.PushStackedInputHandler(_myInputHandler); } /// /// Detaches events from the text area. /// protected virtual void DetachEvents() { ((ISetLogicalParent)this).SetParent(null); if (_document != null) { _document.Changing -= TextArea_Document_Changing; } TextArea.LostFocus -= TextAreaLostFocus; TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged; TextArea.DocumentChanged -= TextAreaDocumentChanged; if (_parentWindow != null) { _parentWindow.PositionChanged -= ParentWindow_LocationChanged; _parentWindow.Deactivated -= ParentWindow_Deactivated; } TextArea.PopStackedInputHandler(_myInputHandler); } #region InputHandler private InputHandler? _myInputHandler; /// /// A dummy input handler (that justs invokes the default input handler). /// This is used to ensure the completion window closes when any other input handler /// becomes active. /// private sealed class InputHandler : TextAreaStackedInputHandler { internal readonly CompletionWindowBase Window; public InputHandler(CompletionWindowBase window) : base(window.TextArea) { Debug.Assert(window != null); Window = window; } public override void Detach() { base.Detach(); Window.Hide(); } public override void OnPreviewKeyDown(KeyEventArgs e) { // prevents crash when typing deadchar while CC window is open if (e.Key == Key.DeadCharProcessed) return; e.Handled = RaiseEventPair(Window, null, KeyDownEvent, new KeyEventArgs { Key = e.Key }); } public override void OnPreviewKeyUp(KeyEventArgs e) { if (e.Key == Key.DeadCharProcessed) return; e.Handled = RaiseEventPair(Window, null, KeyUpEvent, new KeyEventArgs { Key = e.Key }); } } #endregion private void TextViewScrollOffsetChanged(object? sender, EventArgs e) { ILogicalScrollable textView = TextArea; var visibleRect = new Rect(textView.Offset.X, textView.Offset.Y, textView.Viewport.Width, textView.Viewport.Height); //close completion window when the user scrolls so far that the anchor position is leaving the visible area if (visibleRect.Contains(_visualLocation) || visibleRect.Contains(_visualLocationTop)) { UpdatePosition(); } else { Hide(); } } private void TextAreaDocumentChanged(object? sender, EventArgs e) { Hide(); } private void TextAreaLostFocus(object? sender, RoutedEventArgs e) { Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background); } private void ParentWindow_Deactivated(object? sender, EventArgs e) { Hide(); } private void ParentWindow_LocationChanged(object? sender, EventArgs e) { UpdatePosition(); } /* private void OnDeactivated(object sender, EventArgs e) { Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background); }*/ #endregion /// /// Raises a tunnel/bubble event pair for a control. /// /// The control for which the event should be raised. /// The tunneling event. /// The bubbling event. /// The event args to use. /// The value of the event args. protected static bool RaiseEventPair(Control target, RoutedEvent? previewEvent, RoutedEvent @event, RoutedEventArgs args) { if (target == null) throw new ArgumentNullException(nameof(target)); if (args == null) throw new ArgumentNullException(nameof(args)); if (previewEvent != null) { args.RoutedEvent = previewEvent; target.RaiseEvent(args); } args.RoutedEvent = @event ?? throw new ArgumentNullException(nameof(@event)); target.RaiseEvent(args); return args.Handled; } // Special handler: handledEventsToo private void OnMouseUp(object? sender, PointerReleasedEventArgs e) { ActivateParentWindow(); } /// /// Activates the parent window. /// protected virtual void ActivateParentWindow() { _parentWindow?.Activate(); } private void CloseIfFocusLost() { if (CloseOnFocusLost) { Debug.WriteLine("CloseIfFocusLost: this.IsFocues=" + IsFocused + " IsTextAreaFocused=" + IsTextAreaFocused); if (!IsFocused && !IsTextAreaFocused) { Hide(); } } } /// /// Gets whether the completion window should automatically close when the text editor looses focus. /// protected virtual bool CloseOnFocusLost => true; private bool IsTextAreaFocused { get { if (_parentWindow != null && !_parentWindow.IsActive) return false; return TextArea.IsFocused; } } /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (!e.Handled && e.Key == Key.Escape) { e.Handled = true; Hide(); } } private Point _visualLocation; private Point _visualLocationTop; /// /// Positions the completion window at the specified position. /// protected void SetPosition(TextViewPosition position) { var textView = TextArea.TextView; _visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom); _visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop); UpdatePosition(); } /// /// Updates the position of the CompletionWindow based on the parent TextView position and the screen working area. /// It ensures that the CompletionWindow is completely visible on the screen. /// protected void UpdatePosition() { var textView = TextArea.TextView; var position = _visualLocation - textView.ScrollOffset; this.HorizontalOffset = position.X; this.VerticalOffset = position.Y; } // TODO: check if needed //protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) //{ // base.OnRenderSizeChanged(sizeInfo); // if (sizeInfo.HeightChanged && IsUp) // { // this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height; // } //} /// /// Gets/sets whether the completion window should expect text insertion at the start offset, /// which not go into the completion region, but before it. /// /// This property allows only a single insertion, it is reset to false /// when that insertion has occurred. public bool ExpectInsertionBeforeStart { get; set; } protected virtual void TextArea_Document_Changing(object? sender, DocumentChangeEventArgs e) { if (e.Offset + e.RemovalLength == StartOffset && e.RemovalLength > 0) { Hide(); // removal immediately in front of completion segment: close the window // this is necessary when pressing backspace after dot-completion } if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) { StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion); ExpectInsertionBeforeStart = false; } else { StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion); } EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs ================================================ // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Documents; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// /// Describes an entry in the . /// /// /// Note that the CompletionList uses data binding against the properties in this interface. /// Thus, your implementation of the interface must use public properties; not explicit interface implementation. /// public interface ICompletionData { /// /// Gets the text. This property is used to filter the list of visible elements. /// string Text { get; } /// /// Gets the description. /// public string? Description { get; } /// /// Gets the image. /// ImageSource? ImageSource { get; } /// /// Title of the image. /// string? ImageTitle { get; } /// /// Subtitle of the image. /// string? ImageSubtitle { get; } /// /// Whether the image is available. /// [MemberNotNullWhen(true, nameof(ImageSource))] bool HasImage => ImageSource != null; /// /// Gets the icon shown on the left. /// IconData? Icon { get; } /// /// Gets inline text fragments. /// InlineCollection TextInlines { get; } /// /// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items /// which the user is accessing most frequently. /// double Priority { get; } /// /// Perform the completion. /// /// The text area on which completion is performed. /// The text segment that was used by the completion window if /// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset). /// The EventArgs used for the insertion request. /// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how /// the insertion was triggered. /// Optional function to transform the text to be inserted void Complete( TextArea textArea, ISegment completionSegment, InsertionRequestEventArgs eventArgs, Func? prepareText = null ); /// /// Update the text character highlighting /// void UpdateCharHighlighting(string searchText); /// /// Reset the text character highlighting /// void ResetCharHighlighting(); } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs ================================================ using System; using Avalonia.Interactivity; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; public class InsertionRequestEventArgs : EventArgs { public required ICompletionData Item { get; init; } public required RoutedEventArgs TriggeringEvent { get; init; } public string? AppendText { get; init; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs ================================================ using Avalonia; using Avalonia.Controls.Primitives; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; internal class PopupWithCustomPosition : Popup { public Point Offset { get => new(HorizontalOffset, VerticalOffset); set { HorizontalOffset = value.X; VerticalOffset = value.Y; // this.Revalidate(VerticalOffsetProperty); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/ComfyUpscalerTemplateSelector.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class ComfyUpscalerTemplateSelector : IDataTemplate { // ReSharper disable once CollectionNeverUpdated.Global [Content] public Dictionary Templates { get; } = new(); // Check if we can accept the provided data public bool Match(object? data) { return data is ComfyUpscaler; } // Build the DataTemplate here public Control Build(object? data) { if (data is not ComfyUpscaler card) throw new ArgumentException(null, nameof(data)); if (Templates.TryGetValue(card.Type, out var type)) { return type.Build(card)!; } // Fallback to None return Templates[ComfyUpscalerType.None].Build(card)!; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs ================================================ using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; using JetBrains.Annotations; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.Controls; /// /// Selector for objects implementing /// [PublicAPI] public class DataTemplateSelector : IDataTemplate where TKey : notnull { /// /// Key that is used when no other key matches /// public TKey? DefaultKey { get; set; } [Content] public Dictionary Templates { get; } = new(); public bool Match(object? data) => data is ITemplateKey; /// public Control Build(object? data) { if (data is not ITemplateKey key) throw new ArgumentException(null, nameof(data)); if (Templates.TryGetValue(key.TemplateKey, out var template)) { return template.Build(data)!; } if (DefaultKey is not null && Templates.TryGetValue(DefaultKey, out var defaultTemplate)) { return defaultTemplate.Build(data)!; } throw new ArgumentException(null, nameof(data)); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs ================================================ using System; using System.Text.Json.Nodes; using System.Threading.Tasks; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Threading; using Dock.Avalonia.Controls; using Dock.Model; using Dock.Model.Core; using Dock.Serializer; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.Controls.Dock; /// /// Base for Dock controls /// Expects a named "Dock" in the XAML /// public abstract class DockUserControlBase : DropTargetUserControlBase { private DockControl? baseDock; private readonly DockSerializer dockSerializer = new(typeof(AvaloniaList<>)); private readonly DockState dockState = new(); private readonly DockState initialDockState = new(); private IDock? initialLayout; /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); baseDock = this.FindControl("Dock") ?? throw new NullReferenceException("DockControl not found"); if (baseDock.Layout is { } layout) { dockState.Save(layout); initialDockState.Save(layout); initialLayout = layout; // Dispatcher.UIThread.Post(() => dockState.Save(layout), DispatcherPriority.Background); } } /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); // Attach handlers for view state saving and loading if (DataContext is InferenceTabViewModelBase vm) { vm.SaveViewStateRequested += DataContext_OnSaveViewStateRequested; vm.LoadViewStateRequested += DataContext_OnLoadViewStateRequested; } } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); // Detach handlers for view state saving and loading if (DataContext is InferenceTabViewModelBase vm) { vm.SaveViewStateRequested -= DataContext_OnSaveViewStateRequested; vm.LoadViewStateRequested -= DataContext_OnLoadViewStateRequested; } } private void DataContext_OnSaveViewStateRequested(object? sender, SaveViewStateEventArgs args) { var saveTcs = new TaskCompletionSource(); Dispatcher.UIThread.Post(() => { var state = new ViewState { DockLayout = SaveDockLayout() }; saveTcs.SetResult(state); }); args.StateTask ??= saveTcs.Task; } private void DataContext_OnLoadViewStateRequested(object? sender, LoadViewStateEventArgs args) { if (args.State?.DockLayout is { } layout) { // Provided LoadDockLayout(layout); } else { // Restore default RestoreDockLayout(); } } private void LoadDockLayout(JsonObject data) { LoadDockLayout(data.ToJsonString()); } private void LoadDockLayout(string data) { if (baseDock is null) return; if (dockSerializer.Deserialize(data) is { } layout) { baseDock.Layout = layout; dockState.Restore(baseDock.Layout); } } private void RestoreDockLayout() { if (baseDock != null && initialLayout != null) { baseDock.Layout = initialLayout; initialDockState.Restore(baseDock.Layout); } } protected string? SaveDockLayout() { return baseDock is null ? null : dockSerializer.Serialize(baseDock.Layout); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs ================================================ using Avalonia.Input; using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Controls; public abstract class DropTargetTemplatedControlBase : TemplatedControlBase { protected DropTargetTemplatedControlBase() { AddHandler(DragDrop.DropEvent, DropHandler); AddHandler(DragDrop.DragOverEvent, DragOverHandler); DragDrop.SetAllowDrop(this, true); } protected virtual void DragOverHandler(object? sender, DragEventArgs e) { if (DataContext is IDropTarget dropTarget) { dropTarget.DragOver(sender, e); } } protected virtual void DropHandler(object? sender, DragEventArgs e) { if (DataContext is IDropTarget dropTarget) { dropTarget.Drop(sender, e); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/DropTargetUserControlBase.cs ================================================ using Avalonia.Input; using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Controls; public abstract class DropTargetUserControlBase : UserControlBase { protected DropTargetUserControlBase() { AddHandler(DragDrop.DropEvent, DropHandler); AddHandler(DragDrop.DragOverEvent, DragOverHandler); DragDrop.SetAllowDrop(this, true); } private void DragOverHandler(object? sender, DragEventArgs e) { if (DataContext is IDropTarget dropTarget) { dropTarget.DragOver(sender, e); } } private void DropHandler(object? sender, DragEventArgs e) { if (DataContext is IDropTarget dropTarget) { dropTarget.Drop(sender, e); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/EditorCommands.cs ================================================ using AvaloniaEdit; using CommunityToolkit.Mvvm.Input; namespace StabilityMatrix.Avalonia.Controls; public static class EditorCommands { public static RelayCommand CopyCommand { get; } = new(editor => editor?.Copy(), editor => editor?.CanCopy ?? false); public static RelayCommand CutCommand { get; } = new(editor => editor?.Cut(), editor => editor?.CanCut ?? false); public static RelayCommand PasteCommand { get; } = new(editor => editor?.Paste(), editor => editor?.CanPaste ?? false); public static RelayCommand UndoCommand { get; } = new(editor => editor?.Undo(), editor => editor?.CanUndo ?? false); public static RelayCommand RedoCommand { get; } = new(editor => editor?.Redo(), editor => editor?.CanRedo ?? false); } ================================================ FILE: StabilityMatrix.Avalonia/Controls/EditorFlyouts.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs ================================================ using System; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; // ReSharper disable once InconsistentNaming public partial class FADownloadableComboBox : FAComboBox { protected override Type StyleKeyOverride => typeof(FAComboBox); private Popup? dropDownPopup; private IDisposable? openSubscription; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); CleanupSubscription(); DropDownOpened -= OnDropDownOpenedHandler; DropDownClosed -= OnDropDownClosedHandler; // Template part name is "Popup" per FAComboBox.properties.cs (s_tpPopup = "Popup") dropDownPopup = e.NameScope.Find("Popup"); DropDownOpened += OnDropDownOpenedHandler; DropDownClosed += OnDropDownClosedHandler; } private void OnDropDownOpenedHandler(object? sender, EventArgs e) { CleanupSubscription(); if (dropDownPopup?.Child is not Control popupChild) return; var scrollViewer = popupChild.GetVisualDescendants().OfType().FirstOrDefault(); if (scrollViewer == null) return; // On Unix-like systems, overlay popups share the same TopLevel visual root as the main window. // FAComboBox.OnPopupOpened adds a TopLevel tunnel handler that marks all wheel eventsas handled while the dropdown is open, // which inadvertently blocks scroll-wheelevents in popup menus in Inference model cards. // Resetting e.Handled on the ScrollViewer's tunnel phase counters this. if (!Compat.IsUnix) return; openSubscription = scrollViewer.AddDisposableHandler( PointerWheelChangedEvent, static (_, ev) => { if (ev.Handled) ev.Handled = false; }, RoutingStrategies.Tunnel, handledEventsToo: true ); } private void OnDropDownClosedHandler(object? sender, EventArgs e) { CleanupSubscription(); } private void CleanupSubscription() { openSubscription?.Dispose(); openSubscription = null; } static FADownloadableComboBox() { SelectionChangedEvent.AddClassHandler( (comboBox, args) => comboBox.OnSelectionChanged(args) ); } protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) { // On downloadable added if (e.AddedItems.Count > 0 && e.AddedItems[0] is IDownloadableResource { IsDownloadable: true } item) { // Reset the selection e.Handled = true; if ( e.RemovedItems.Count > 0 && e.RemovedItems[0] is IDownloadableResource { IsDownloadable: false } removedItem ) { SelectedItem = removedItem; } else { SelectedItem = null; } // Show dialog to download the model PromptDownloadCommand.ExecuteAsync(item).SafeFireAndForget(); } } [RelayCommand] private static async Task PromptDownloadAsync(IDownloadableResource downloadable) { if (downloadable.DownloadableResource is not { } resource) return; var vmFactory = App.Services.GetRequiredService>(); var confirmDialog = vmFactory.Get(); confirmDialog.Resource = resource; confirmDialog.FileName = resource.FileName; if (await confirmDialog.GetDialog().ShowAsync() == ContentDialogResult.Primary) { confirmDialog.StartDownload(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/FASymbolIconSource.cs ================================================ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia; using Avalonia.Controls.Documents; using Avalonia.Media; using FluentAvalonia.UI.Controls; using Projektanker.Icons.Avalonia; namespace StabilityMatrix.Avalonia.Controls; [TypeConverter(typeof(FASymbolIconSourceConverter))] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public class FASymbolIconSource : PathIconSource { public static readonly StyledProperty SymbolProperty = AvaloniaProperty.Register< FASymbolIconSource, string >(nameof(Symbol)); public static readonly StyledProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(); public FASymbolIconSource() { Stretch = Stretch.None; // FontSize = 20; // Override value inherited from visual parents. InvalidateData(); } public string Symbol { get => GetValue(SymbolProperty); set => SetValue(SymbolProperty, value); } public double FontSize { get => GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == SymbolProperty || change.Property == FontSizeProperty) { InvalidateData(); } } private void InvalidateData() { var path = IconProvider.Current.GetIcon(Symbol).Path; var geometry = Geometry.Parse(path); var scale = FontSize / 20; Data = geometry; // TODO: Scaling not working Data.Transform = new ScaleTransform(scale, scale); } } public class FASymbolIconSourceConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { if (sourceType == typeof(string)) { return true; } return base.CanConvertFrom(context, sourceType); } public override object? ConvertFrom( ITypeDescriptorContext? context, CultureInfo? culture, object value ) { return value switch { string val => new FASymbolIconSource { Symbol = val, }, _ => base.ConvertFrom(context, culture, value) }; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/FrameCarousel.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/FrameCarousel.axaml.cs ================================================ using System; using System.Collections; using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using FluentAvalonia.UI.Navigation; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class FrameCarousel : SelectingItemsControl { public static readonly StyledProperty ContentTemplateProperty = AvaloniaProperty.Register( "ContentTemplate"); public IDataTemplate? ContentTemplate { get => GetValue(ContentTemplateProperty); set => SetValue(ContentTemplateProperty, value); } public static readonly StyledProperty SourcePageTypeProperty = AvaloniaProperty.Register( "SourcePageType"); public Type SourcePageType { get => GetValue(SourcePageTypeProperty); set => SetValue(SourcePageTypeProperty, value); } private Frame? frame; private int previousIndex = -1; private static readonly FrameNavigationOptions ForwardNavigationOptions = new() { TransitionInfoOverride = new BetterSlideNavigationTransition { Effect = SlideNavigationTransitionEffect.FromRight, FromHorizontalOffset = 200 } }; private static readonly FrameNavigationOptions BackNavigationOptions = new() { TransitionInfoOverride = new BetterSlideNavigationTransition { Effect = SlideNavigationTransitionEffect.FromLeft, FromHorizontalOffset = 200 } }; private static readonly FrameNavigationOptions DirectionlessNavigationOptions = new() { TransitionInfoOverride = new SuppressNavigationTransitionInfo() }; /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); frame = e.NameScope.Find("PART_Frame") ?? throw new NullReferenceException("Frame not found"); frame.NavigationPageFactory = new FrameNavigationFactory(SourcePageType); if (SelectedItem is not null) { frame.NavigateFromObject(SelectedItem, DirectionlessNavigationOptions); } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (frame is null) return; if (change.Property == SelectedItemProperty) { if (change.GetNewValue() is not { } value) return; if (SelectedIndex > previousIndex) { // Going forward frame.NavigateFromObject(value, ForwardNavigationOptions); } else if (SelectedIndex < previousIndex) { // Going back frame.NavigateFromObject(value, BackNavigationOptions); } else { frame.NavigateFromObject(value, DirectionlessNavigationOptions); } previousIndex = SelectedIndex; } else if (change.Property == ItemCountProperty) { // On item count change to 0, clear the frame cache var value = change.GetNewValue(); if (value == 0) { var pageCache = frame.GetPrivateField("_pageCache"); pageCache?.Clear(); } } } /// /// Moves to the next item in the carousel. /// public void Next() { if (SelectedIndex < ItemCount - 1) { ++SelectedIndex; } } /// /// Moves to the previous item in the carousel. /// public void Previous() { if (SelectedIndex > 0) { --SelectedIndex; } } internal class FrameNavigationFactory : INavigationPageFactory { private readonly Type _sourcePageType; public FrameNavigationFactory(Type sourcePageType) { _sourcePageType = sourcePageType; } /// public Control GetPage(Type srcType) { return (Control) Activator.CreateInstance(srcType)!; } /// public Control GetPageFromObject(object target) { var view = GetPage(_sourcePageType); view.DataContext = target; return view; } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/GitVersionSelector.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/GitVersionSelector.axaml.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Logging; using CommunityToolkit.Mvvm.Input; using Nito.Disposables.Internals; using StabilityMatrix.Avalonia.Controls.Models; using StabilityMatrix.Core.Git; namespace StabilityMatrix.Avalonia.Controls; [Localizable(false)] public partial class GitVersionSelector : TemplatedControlBase { public static readonly StyledProperty GitVersionProviderProperty = AvaloniaProperty.Register(nameof(GitVersionProvider)); public IGitVersionProvider? GitVersionProvider { get => GetValue(GitVersionProviderProperty); set => SetValue(GitVersionProviderProperty, value); } public static readonly StyledProperty BranchSelectionModeProperty = AvaloniaProperty.Register(nameof(BranchSelectionMode)); public SelectionMode BranchSelectionMode { get => GetValue(BranchSelectionModeProperty); set => SetValue(BranchSelectionModeProperty, value); } public static readonly StyledProperty CommitSelectionModeProperty = AvaloniaProperty.Register(nameof(CommitSelectionMode)); public SelectionMode CommitSelectionMode { get => GetValue(CommitSelectionModeProperty); set => SetValue(CommitSelectionModeProperty, value); } public static readonly StyledProperty TagSelectionModeProperty = AvaloniaProperty.Register< GitVersionSelector, SelectionMode >(nameof(TagSelectionMode)); public SelectionMode TagSelectionMode { get => GetValue(TagSelectionModeProperty); set => SetValue(TagSelectionModeProperty, value); } public static readonly StyledProperty DefaultBranchProperty = AvaloniaProperty.Register< GitVersionSelector, string? >(nameof(DefaultBranch), "main"); /// /// The default branch to use when no branch is selected. Shows as a placeholder. /// public string? DefaultBranch { get => GetValue(DefaultBranchProperty); set => SetValue(DefaultBranchProperty, value); } public static readonly StyledProperty DefaultCommitProperty = AvaloniaProperty.Register< GitVersionSelector, string? >(nameof(DefaultCommit), "latest"); /// /// The default commit to use when no commit is selected. Shows as a placeholder. /// public string? DefaultCommit { get => GetValue(DefaultCommitProperty); set => SetValue(DefaultCommitProperty, value); } public static readonly StyledProperty> BranchSourceProperty = AvaloniaProperty.Register>(nameof(BranchSource), []); public IReadOnlyList BranchSource { get => GetValue(BranchSourceProperty); set => SetValue(BranchSourceProperty, value); } public static readonly StyledProperty> CommitSourceProperty = AvaloniaProperty.Register>(nameof(CommitSource), []); public IReadOnlyList CommitSource { get => GetValue(CommitSourceProperty); set => SetValue(CommitSourceProperty, value); } public static readonly StyledProperty> TagSourceProperty = AvaloniaProperty.Register>(nameof(TagSource), []); public IReadOnlyList TagSource { get => GetValue(TagSourceProperty); set => SetValue(TagSourceProperty, value); } public static readonly StyledProperty SelectedBranchProperty = AvaloniaProperty.Register< GitVersionSelector, string? >(nameof(SelectedBranch), defaultBindingMode: BindingMode.TwoWay); public string? SelectedBranch { get => GetValue(SelectedBranchProperty); set => SetValue(SelectedBranchProperty, value); } public static readonly StyledProperty SelectedCommitProperty = AvaloniaProperty.Register< GitVersionSelector, string? >(nameof(SelectedCommit), defaultBindingMode: BindingMode.TwoWay); public string? SelectedCommit { get => GetValue(SelectedCommitProperty); set => SetValue(SelectedCommitProperty, value); } public static readonly StyledProperty SelectedTagProperty = AvaloniaProperty.Register< GitVersionSelector, string? >(nameof(SelectedTag), defaultBindingMode: BindingMode.TwoWay); public string? SelectedTag { get => GetValue(SelectedTagProperty); set => SetValue(SelectedTagProperty, value); } public static readonly StyledProperty SelectedVersionTypeProperty = AvaloniaProperty.Register( nameof(SelectedVersionType), defaultBindingMode: BindingMode.TwoWay ); public GitVersionSelectorVersionType SelectedVersionType { get => GetValue(SelectedVersionTypeProperty); set => SetValue(SelectedVersionTypeProperty, value); } public static readonly DirectProperty< GitVersionSelector, IAsyncRelayCommand > PopulateBranchesCommandProperty = AvaloniaProperty.RegisterDirect< GitVersionSelector, IAsyncRelayCommand >(nameof(PopulateBranchesCommand), o => o.PopulateBranchesCommand); public static readonly DirectProperty< GitVersionSelector, IAsyncRelayCommand > PopulateCommitsForCurrentBranchCommandProperty = AvaloniaProperty.RegisterDirect< GitVersionSelector, IAsyncRelayCommand >(nameof(PopulateCommitsForCurrentBranchCommand), o => o.PopulateCommitsForCurrentBranchCommand); public static readonly DirectProperty< GitVersionSelector, IAsyncRelayCommand > PopulateTagsCommandProperty = AvaloniaProperty.RegisterDirect( nameof(PopulateTagsCommand), o => o.PopulateTagsCommand ); protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); if (GitVersionProvider is not null) { PopulateBranchesCommand.Execute(null); if (SelectedBranch is not null) { PopulateCommitsForCurrentBranchCommand.Execute(null); } PopulateTagsCommand.Execute(null); } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); // On branch change, fetch commits if (change.Property == SelectedBranchProperty) { PopulateCommitsForCurrentBranchCommand.Execute(null); } } [RelayCommand] public async Task PopulateBranches() { if (GitVersionProvider is null) return; var branches = await GitVersionProvider.FetchBranchesAsync(); BranchSource = branches.Select(v => v.Branch).WhereNotNull().ToImmutableList(); } [RelayCommand] public async Task PopulateCommitsForCurrentBranch() { if (string.IsNullOrEmpty(SelectedBranch)) { CommitSource = []; return; } if (GitVersionProvider is null) return; try { var commits = await GitVersionProvider.FetchCommitsAsync(SelectedBranch); CommitSource = commits.Select(v => v.CommitSha).WhereNotNull().ToImmutableList(); } catch (Exception e) { Logger .TryGet(LogEventLevel.Error, nameof(GitVersionSelector)) ?.Log(this, "Failed to fetch commits for branch {Branch}: {Exception}", SelectedBranch, e); } } [RelayCommand] public async Task PopulateTags() { if (GitVersionProvider is null) return; var tags = await GitVersionProvider.FetchTagsAsync(); TagSource = tags.Select(v => v.Tag).WhereNotNull().ToImmutableList(); } public enum SelectionMode { Disabled, Required, Optional } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/HybridModelTemplateSelector.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class HybridModelTemplateSelector : IDataTemplate { // ReSharper disable once CollectionNeverUpdated.Global [Content] public Dictionary Templates { get; } = new(); // Check if we can accept the provided data public bool Match(object? data) { return data is HybridModelFile; } // Build the DataTemplate here public Control Build(object? data) { if (data is not HybridModelFile modelFile) throw new ArgumentException(null, nameof(data)); if (Templates.TryGetValue(modelFile.Type, out var type)) { return type.Build(modelFile)!; } // Fallback to Local return Templates[HybridModelType.None].Build(modelFile)!; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/HyperlinkIconButton.cs ================================================ using System; using System.Diagnostics; using System.IO; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Logging; using StabilityMatrix.Core.Processes; using Symbol = FluentIcons.Common.Symbol; namespace StabilityMatrix.Avalonia.Controls; /// /// Like , but with a link icon left of the text content. /// public class HyperlinkIconButton : Button { private Uri? _navigateUri; /// /// Defines the property /// public static readonly DirectProperty NavigateUriProperty = AvaloniaProperty.RegisterDirect( nameof(NavigateUri), x => x.NavigateUri, (x, v) => x.NavigateUri = v ); /// /// Gets or sets the Uri that the button should navigate to upon clicking. In assembly paths are not supported, (e.g., avares://...) /// public Uri? NavigateUri { get => _navigateUri; set => SetAndRaise(NavigateUriProperty, ref _navigateUri, value); } public static readonly StyledProperty IconProperty = AvaloniaProperty.Register< HyperlinkIconButton, Symbol >("Icon", Symbol.Link); public Symbol Icon { get => GetValue(IconProperty); set => SetValue(IconProperty, value); } protected override Type StyleKeyOverride => typeof(HyperlinkIconButton); /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); // Update icon if (change.Property == NavigateUriProperty) { var uri = change.GetNewValue(); if (uri is not null && uri.IsFile && Icon == Symbol.Link) { Icon = Symbol.Open; } } } protected override void OnClick() { base.OnClick(); if (NavigateUri is null) return; // File or Folder URIs if (NavigateUri.IsFile) { var path = NavigateUri.LocalPath; if (Directory.Exists(path)) { ProcessRunner .OpenFolderBrowser(path) .SafeFireAndForget(ex => { Logger.TryGet(LogEventLevel.Error, $"Unable to open directory Uri {NavigateUri}"); }); } else if (File.Exists(path)) { ProcessRunner .OpenFileBrowser(path) .SafeFireAndForget(ex => { Logger.TryGet(LogEventLevel.Error, $"Unable to open file Uri {NavigateUri}"); }); } } // Web else { try { Process.Start( new ProcessStartInfo(NavigateUri.ToString()) { UseShellExecute = true, Verb = "open" } ); } catch { Logger.TryGet(LogEventLevel.Error, $"Unable to open Uri {NavigateUri}"); } } } protected override bool RegisterContentPresenter(ContentPresenter presenter) { return presenter.Name == "ContentPresenter"; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/ImageLoaders.cs ================================================ using System; using System.ComponentModel; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using Apizr; using Fusillade; using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; namespace StabilityMatrix.Avalonia.Controls; [Localizable(false)] internal static class ImageLoaders { private static string BaseFileCachePath => Path.Combine(Path.GetTempPath(), "StabilityMatrix", "Cache"); private static readonly Lazy OutputsPageImageCacheLazy = new( () => new MemoryImageCache { MaxMemoryCacheCount = 64 }, LazyThreadSafetyMode.ExecutionAndPublication ); public static IImageCache OutputsPageImageCache => OutputsPageImageCacheLazy.Value; private static readonly Lazy OpenModelDbImageCacheLazy = new( () => new ImageCache( new CacheOptions { BaseCachePath = BaseFileCachePath, CacheFolderName = "OpenModelDbImageCache", CacheDuration = TimeSpan.FromDays(1), HttpClient = new HttpClient(NetCache.Background) { DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue("StabilityMatrix", "2.0") }, Referrer = new Uri("https://openmodelsdb.info/"), } } } ), LazyThreadSafetyMode.ExecutionAndPublication ); public static IImageCache OpenModelDbImageCache => OpenModelDbImageCacheLazy.Value; } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class BatchSizeCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/CfzCudnnToggleCard.axaml ================================================ ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/CfzCudnnToggleCard.axaml.cs ================================================ using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class CfzCudnnToggleCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ControlNetCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class ControlNetCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/DiscreteModelSamplingCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/DiscreteModelSamplingCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class DiscreteModelSamplingCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ExtraNetworkCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ExtraNetworkCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class ExtraNetworkCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/FaceDetailerCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/FaceDetailerCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class FaceDetailerCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/FreeUCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/FreeUCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class FreeUCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class ImageFolderCard : DropTargetTemplatedControlBase { private ItemsRepeater? imageRepeater; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { imageRepeater = e.NameScope.Find("ImageRepeater"); base.OnApplyTemplate(e); } /// protected override void DropHandler(object? sender, DragEventArgs e) { base.DropHandler(sender, e); e.Handled = true; } /// protected override void DragOverHandler(object? sender, DragEventArgs e) { base.DragOverHandler(sender, e); e.Handled = true; } protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { if (e.KeyModifiers != KeyModifiers.Control) return; if (DataContext is not ImageFolderCardViewModel vm) return; if (e.Delta.Y > 0) { if (vm.ImageSize.Height >= 500) return; vm.ImageSize += new Size(15, 19); } else { if (vm.ImageSize.Height <= 200) return; vm.ImageSize -= new Size(15, 19); } imageRepeater?.InvalidateArrange(); imageRepeater?.InvalidateMeasure(); e.Handled = true; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ImageGalleryCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class ImageGalleryCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/LayerDiffuseCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class LayerDiffuseCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class ModelCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/NrsCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/NrsCard.axaml.cs ================================================ using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class NrsCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PlasmaNoiseCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PlasmaNoiseCard.axaml.cs ================================================ using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public partial class PlasmaNoiseCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using AvaloniaEdit; using AvaloniaEdit.Editing; using AvaloniaEdit.Utils; using Injectio.Attributes; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Inference; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class PromptCard : TemplatedControlBase { /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); FixGrids(e); InitializeEditors(e); } private static void InitializeEditors(TemplateAppliedEventArgs e) { foreach ( var editor in new[] { e.NameScope.Find("PromptEditor"), e.NameScope.Find("NegativePromptEditor") } ) { if (editor is not null) { TextEditorConfigs.Configure(editor, TextEditorPreset.Prompt); editor.TextArea.Margin = new Thickness(0, 0, 4, 0); if (editor.TextArea.ActiveInputHandler is TextAreaInputHandler inputHandler) { // Add some aliases for editor shortcuts inputHandler.KeyBindings.AddRange( new KeyBinding[] { new() { Command = ApplicationCommands.Cut, Gesture = new KeyGesture(Key.Delete, KeyModifiers.Shift) }, new() { Command = ApplicationCommands.Paste, Gesture = new KeyGesture(Key.Insert, KeyModifiers.Shift) } } ); } } } } private void FixGrids(TemplateAppliedEventArgs e) { if (DataContext is not PromptCardViewModel { IsNegativePromptEnabled: false }) { return; } // When negative prompt disabled, rearrange grid if (e.NameScope.Find("PART_RootGrid") is not { } rootGrid) return; // Change `*,16,*,16,Auto` to `*,16,Auto` (Remove index 2 and 3) rootGrid.RowDefinitions.RemoveRange(2, 2); // Set the last children to row 2 rootGrid.Children[4].SetValue(Grid.RowProperty, 2); // Remove the negative prompt row and the separator row (index 2 and 3) rootGrid.Children.RemoveRange(2, 2); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class PromptExpansionCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/RescaleCfgCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/RescaleCfgCard.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class RescaleCfgCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SamplerCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class SamplerCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SeedCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SeedCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class SeedCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs ================================================ using System; using System.Reactive.Linq; using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Primitives; using DynamicData.Binding; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Inference; using Size = Avalonia.Size; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class SelectImageCard : DropTargetTemplatedControlBase { /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); if (DataContext is not SelectImageCardViewModel vm) return; if (e.NameScope.Find("PART_BetterAdvancedImage") is not { } imageControl) return; imageControl .WhenPropertyChanged(x => x.CurrentImage) .ObserveOn(SynchronizationContext.Current) .Subscribe(propertyValue => { if (propertyValue.Value is { } image) { // Sometimes Avalonia Bitmap.Size getter throws a NullReferenceException depending on skia lifetimes (probably) // so just catch it and ignore it Size? size = null; try { size = image.Size; } catch (NullReferenceException) { } if (size is not null) { vm.CurrentBitmapSize = new System.Drawing.Size( Convert.ToInt32(size.Value.Width), Convert.ToInt32(size.Value.Height) ); return; } } vm.CurrentBitmapSize = System.Drawing.Size.Empty; }); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class SharpenCard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs ================================================ using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [RegisterTransient] public class StackCard : TemplatedControlBase { public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register( "Spacing", 4 ); public int Spacing { get => GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml ================================================  --> ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml.cs ================================================ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.LogicalTree; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Extensions; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.Controls; [PseudoClasses(":editEnabled")] [RegisterTransient] public class StackEditableCard : TemplatedControlBase { private ListBox? listBoxPart; // ReSharper disable once MemberCanBePrivate.Global public static readonly StyledProperty IsListBoxEditEnabledProperty = AvaloniaProperty.Register< StackEditableCard, bool >("IsListBoxEditEnabled"); public bool IsListBoxEditEnabled { get => GetValue(IsListBoxEditEnabledProperty); set => SetValue(IsListBoxEditEnabledProperty, value); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); listBoxPart = e.NameScope.Find("PART_ListBox"); if (listBoxPart != null) { // Register handlers to attach container behavior // Forward container index changes to view model ((IChildIndexProvider)listBoxPart).ChildIndexChanged += (_, args) => { if (args.Child is Control { DataContext: StackExpanderViewModel vm }) { vm.OnContainerIndexChanged(args.Index); } }; } if (e.NameScope.Find ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class StackExpander : TemplatedControlBase { public static readonly StyledProperty IsExpandedProperty = Expander.IsExpandedProperty.AddOwner(); public static readonly StyledProperty ExpandDirectionProperty = Expander.ExpandDirectionProperty.AddOwner(); public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register( "Spacing", 8 ); public ExpandDirection ExpandDirection { get => GetValue(ExpandDirectionProperty); set => SetValue(ExpandDirectionProperty, value); } public bool IsExpanded { get => GetValue(IsExpandedProperty); set => SetValue(IsExpandedProperty, value); } public int Spacing { get => GetValue(SpacingProperty); set => SetValue(SpacingProperty, value); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/TiledVAECard.axaml ================================================ ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/TiledVAECard.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class TiledVAECard : TemplatedControlBase { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/UnetModelCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/UnetModelCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class UnetModelCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml.cs ================================================ using Avalonia.Controls.Primitives; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class UpscalerCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/WanModelCard.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Inference/WanModelCard.axaml.cs ================================================ using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public class WanModelCard : TemplatedControlBase; ================================================ FILE: StabilityMatrix.Avalonia/Controls/LaunchOptionCardTemplateSelector.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Metadata; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class LaunchOptionCardTemplateSelector : IDataTemplate { // public bool SupportsRecycling => false; // ReSharper disable once CollectionNeverUpdated.Global [Content] public Dictionary Templates { get; } = new(); // Check if we can accept the provided data public bool Match(object? data) { return data is LaunchOptionCard; } // Build the DataTemplate here public Control Build(object? data) { if (data is not LaunchOptionCard card) throw new ArgumentException(null, nameof(data)); return Templates[card.Type].Build(card)!; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/LineDashFrame.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Media; using FluentAvalonia.UI.Controls; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class LineDashFrame : Frame { protected override Type StyleKeyOverride { get; } = typeof(Frame); public static readonly StyledProperty StrokeProperty = AvaloniaProperty.Register< LineDashFrame, ISolidColorBrush >("Stroke"); public ISolidColorBrush Stroke { get => GetValue(StrokeProperty); set => SetValue(StrokeProperty, value); } public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register< LineDashFrame, double >("StrokeThickness"); public double StrokeThickness { get => GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public static readonly StyledProperty StrokeDashLineProperty = AvaloniaProperty.Register< LineDashFrame, double >("StrokeDashLine"); public double StrokeDashLine { get => GetValue(StrokeDashLineProperty); set => SetValue(StrokeDashLineProperty, value); } public static readonly StyledProperty StrokeDashSpaceProperty = AvaloniaProperty.Register< LineDashFrame, double >("StrokeDashSpace"); public double StrokeDashSpace { get => GetValue(StrokeDashSpaceProperty); set => SetValue(StrokeDashSpaceProperty, value); } public static readonly StyledProperty FillProperty = AvaloniaProperty.Register< LineDashFrame, ISolidColorBrush >("Fill"); public ISolidColorBrush Fill { get => GetValue(FillProperty); set => SetValue(FillProperty, value); } public LineDashFrame() { UseLayoutRounding = true; } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if ( change.Property == StrokeProperty || change.Property == StrokeThicknessProperty || change.Property == StrokeDashLineProperty || change.Property == StrokeDashSpaceProperty || change.Property == FillProperty ) { InvalidateVisual(); } } /// public override void Render(DrawingContext context) { var width = Bounds.Width; var height = Bounds.Height; context.DrawRectangle(Fill, null, new Rect(0, 0, width, height)); var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) }; context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0)); context.DrawLine(dashPen, new Point(0, height), new Point(width, height)); context.DrawLine(dashPen, new Point(0, 0), new Point(0, height)); context.DrawLine(dashPen, new Point(width, 0), new Point(width, height)); } private IEnumerable GetDashArray(double length) { var availableLength = length - StrokeDashLine; var lines = (int)Math.Round(availableLength / (StrokeDashLine + StrokeDashSpace)); availableLength -= lines * StrokeDashLine; var actualSpacing = availableLength / lines; yield return StrokeDashLine / StrokeThickness; yield return actualSpacing / StrokeThickness; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/MarkdownViewer.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/MarkdownViewer.axaml.cs ================================================ using System.IO; using Avalonia; using Avalonia.Controls.Primitives; using Markdig; using TheArtOfDev.HtmlRenderer.Avalonia; namespace StabilityMatrix.Avalonia.Controls; public class MarkdownViewer : TemplatedControlBase { public static readonly StyledProperty TextProperty = AvaloniaProperty.Register< MarkdownViewer, string >(nameof(Text)); public string Text { get => GetValue(TextProperty); set { SetValue(TextProperty, value); ParseText(value); } } public static readonly StyledProperty HtmlProperty = AvaloniaProperty.Register< MarkdownViewer, string >(nameof(Html)); private string Html { get => GetValue(HtmlProperty); set => SetValue(HtmlProperty, value); } public static readonly StyledProperty CustomCssProperty = AvaloniaProperty.Register< MarkdownViewer, string >(nameof(CustomCss)); public string CustomCss { get => GetValue(CustomCssProperty); set => SetValue(CustomCssProperty, value); } private void ParseText(string value) { if (string.IsNullOrWhiteSpace(value)) return; var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); var html = $"""{Markdig.Markdown.ToHtml(value, pipeline)}"""; Html = html; } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); if (e.NameScope.Find("PART_HtmlPanel") is not HtmlPanel htmlPanel) return; using var cssFile = Assets.MarkdownCss.Open(); using var reader = new StreamReader(cssFile); var css = reader.ReadToEnd(); htmlPanel.BaseStylesheet = $"{css}\n{CustomCss}"; if (string.IsNullOrWhiteSpace(Html) && !string.IsNullOrWhiteSpace(Text)) { ParseText(Text); } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == TextProperty && change.NewValue != null) { ParseText(change.NewValue.ToString()); } base.OnPropertyChanged(change); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Models/GitVersionSelectorVersionType.cs ================================================ namespace StabilityMatrix.Avalonia.Controls.Models; public enum GitVersionSelectorVersionType { BranchCommit, Tag } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Models/PenPath.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; using SkiaSharp; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Avalonia.Controls.Models; public readonly record struct PenPath() { [JsonConverter(typeof(SKColorJsonConverter))] public SKColor FillColor { get; init; } public bool IsErase { get; init; } public List Points { get; init; } = []; public SKPath ToSKPath() { var skPath = new SKPath(); if (Points.Count <= 0) { return skPath; } // First move to the first point skPath.MoveTo(Points[0].X, Points[0].Y); // Add the rest of the points for (var i = 1; i < Points.Count; i++) { skPath.LineTo(Points[i].X, Points[i].Y); } return skPath; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Models/PenPoint.cs ================================================ using System; using SkiaSharp; namespace StabilityMatrix.Avalonia.Controls.Models; public readonly record struct PenPoint(ulong X, ulong Y) { public PenPoint(double x, double y) : this(Convert.ToUInt64(x), Convert.ToUInt64(y)) { } public PenPoint(SKPoint skPoint) : this(Convert.ToUInt64(skPoint.X), Convert.ToUInt64(skPoint.Y)) { } /// /// Radius of the point. /// public double Radius { get; init; } = 1; /// /// Optional pressure of the point. If null, the pressure is unknown. /// public double? Pressure { get; init; } /// /// True if the point was created by a pen, false if it was created by a mouse. /// public bool IsPen { get; init; } public SKPoint ToSKPoint() => new(X, Y); } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Models/SKLayer.cs ================================================ using System.Collections.Immutable; using SkiaSharp; namespace StabilityMatrix.Avalonia.Controls.Models; public class SKLayer { /// /// Surface from Canvas that contains the layer. /// public SKSurface? Surface { get; set; } /// /// Optional bitmaps that will be drawn on the layer, in order. /// (Last index will be drawn on top over previous ones) /// public ImmutableList Bitmaps { get; set; } = []; } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Paginator.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/Paginator.axaml.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Windows.Input; using Avalonia; using Avalonia.Controls.Primitives; using CommunityToolkit.Mvvm.Input; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class Paginator : TemplatedControlBase { private bool isFirstTemplateApplied; private ICommand? firstPageCommandBinding; private ICommand? previousPageCommandBinding; private ICommand? nextPageCommandBinding; private ICommand? lastPageCommandBinding; public static readonly StyledProperty CurrentPageNumberProperty = AvaloniaProperty.Register< Paginator, int >("CurrentPageNumber", 1); public int CurrentPageNumber { get => GetValue(CurrentPageNumberProperty); set => SetValue(CurrentPageNumberProperty, value); } public static readonly StyledProperty TotalPagesProperty = AvaloniaProperty.Register( "TotalPages", 1 ); public int TotalPages { get => GetValue(TotalPagesProperty); set => SetValue(TotalPagesProperty, value); } public static readonly StyledProperty FirstPageCommandProperty = AvaloniaProperty.Register< Paginator, ICommand? >("FirstPageCommand"); public ICommand? FirstPageCommand { get => GetValue(FirstPageCommandProperty); set => SetValue(FirstPageCommandProperty, value); } public static readonly StyledProperty PreviousPageCommandProperty = AvaloniaProperty.Register< Paginator, ICommand? >("PreviousPageCommand"); public ICommand? PreviousPageCommand { get => GetValue(PreviousPageCommandProperty); set => SetValue(PreviousPageCommandProperty, value); } public static readonly StyledProperty NextPageCommandProperty = AvaloniaProperty.Register< Paginator, ICommand? >("NextPageCommand"); public ICommand? NextPageCommand { get => GetValue(NextPageCommandProperty); set => SetValue(NextPageCommandProperty, value); } public static readonly StyledProperty LastPageCommandProperty = AvaloniaProperty.Register< Paginator, ICommand? >("LastPageCommand"); public ICommand? LastPageCommand { get => GetValue(LastPageCommandProperty); set => SetValue(LastPageCommandProperty, value); } public static readonly StyledProperty CanNavForwardProperty = AvaloniaProperty.Register< Paginator, bool >("CanNavForward"); public bool CanNavForward { get => GetValue(CanNavForwardProperty); set => SetValue(CanNavForwardProperty, value); } public static readonly StyledProperty CanNavBackProperty = AvaloniaProperty.Register< Paginator, bool >("CanNavBack"); public bool CanNavBack { get => GetValue(CanNavBackProperty); set => SetValue(CanNavBackProperty, value); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); if (!isFirstTemplateApplied) { firstPageCommandBinding = FirstPageCommand; previousPageCommandBinding = PreviousPageCommand; nextPageCommandBinding = NextPageCommand; lastPageCommandBinding = LastPageCommand; isFirstTemplateApplied = true; } // Wrap the commands FirstPageCommand = new RelayCommand(() => { if (CurrentPageNumber > 1) { CurrentPageNumber = 1; } firstPageCommandBinding?.Execute(null); }); PreviousPageCommand = new RelayCommand(() => { if (CurrentPageNumber > 1) { CurrentPageNumber--; } previousPageCommandBinding?.Execute(null); }); NextPageCommand = new RelayCommand(() => { if (CurrentPageNumber < TotalPages) { CurrentPageNumber++; } nextPageCommandBinding?.Execute(null); }); LastPageCommand = new RelayCommand(() => { if (CurrentPageNumber < TotalPages) { CurrentPageNumber = TotalPages; } lastPageCommandBinding?.Execute(null); }); } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); // Update the CanNavForward and CanNavBack properties if (change.Property == CurrentPageNumberProperty && change.NewValue is int) { CanNavForward = (int)change.NewValue < TotalPages; CanNavBack = (int)change.NewValue > 1; } else if (change.Property == TotalPagesProperty && change.NewValue is int) { CanNavForward = CurrentPageNumber < (int)change.NewValue; CanNavBack = CurrentPageNumber > 1; } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml ================================================  --> ================================================ FILE: StabilityMatrix.Avalonia/Controls/Painting/PaintCanvas.axaml.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using SkiaSharp; using StabilityMatrix.Avalonia.Controls.Models; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Controls; namespace StabilityMatrix.Avalonia.Controls; public class PaintCanvas : TemplatedControlBase { private ConcurrentDictionary TemporaryPaths => ViewModel!.TemporaryPaths; private ImmutableList Paths { get => ViewModel!.Paths; set => ViewModel!.Paths = value; } private IDisposable? viewModelSubscription; private bool isPenDown; private PaintCanvasViewModel? ViewModel { get; set; } private SkiaCustomCanvas? MainCanvas { get; set; } static PaintCanvas() { AffectsRender(BoundsProperty); } public void RefreshCanvas() { Dispatcher.UIThread.Post(() => MainCanvas?.InvalidateVisual(), DispatcherPriority.Render); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); MainCanvas = e.NameScope.Find("PART_MainCanvas"); Debug.Assert(MainCanvas != null); if (MainCanvas is not null) { if (DataContext is PaintCanvasViewModel { CanvasSize: var canvasSize }) { MainCanvas.Width = canvasSize.Width; MainCanvas.Height = canvasSize.Height; } MainCanvas.RenderSkia += OnRenderSkia; MainCanvas.PointerEntered += MainCanvas_OnPointerEntered; MainCanvas.PointerExited += MainCanvas_OnPointerExited; } var zoomBorder = e.NameScope.Find("PART_ZoomBorder"); if (zoomBorder is not null) { zoomBorder.ZoomChanged += (_, zoomEventArgs) => { if (ViewModel is not null) { ViewModel.CurrentZoom = zoomEventArgs.ZoomX; UpdateCanvasCursor(); } }; if (ViewModel is not null) { ViewModel.CurrentZoom = zoomBorder.ZoomX; UpdateCanvasCursor(); } } OnDataContextChanged(EventArgs.Empty); } /// protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is PaintCanvasViewModel viewModel) { // Set the remote actions viewModel.RefreshCanvas = RefreshCanvas; viewModelSubscription?.Dispose(); viewModelSubscription = viewModel .WhenPropertyChanged(vm => vm.CanvasSize) .ObserveOn(SynchronizationContext.Current) .Subscribe(change => { if (MainCanvas is not null && !change.Value.IsEmpty) { MainCanvas.Width = change.Value.Width; MainCanvas.Height = change.Value.Height; MainCanvas.InvalidateVisual(); } }); ViewModel = viewModel; } } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsEnabledProperty) { var newIsEnabled = change.GetNewValue(); if (!newIsEnabled) { isPenDown = false; } // On any enabled change, flush temporary paths if (!TemporaryPaths.IsEmpty) { Paths = Paths.AddRange(TemporaryPaths.Values); TemporaryPaths.Clear(); } } } /// protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); UpdateMainCanvasBounds(); } private void HandlePointerEvent(PointerEventArgs e) { // Ignore if disabled if (!IsEnabled) { return; } if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) { TemporaryPaths.TryRemove(e.Pointer.Id, out _); return; } e.Handled = true; // Must have this or stylus inputs lost after a while // https://github.com/AvaloniaUI/Avalonia/issues/12289#issuecomment-1695620412 e.PreventGestureRecognition(); if (DataContext is not PaintCanvasViewModel viewModel) { return; } var currentPoint = e.GetCurrentPoint(this); if (e.RoutedEvent == PointerPressedEvent) { // Ignore if mouse and not left button if (e.Pointer.Type == PointerType.Mouse && !currentPoint.Properties.IsLeftButtonPressed) { return; } isPenDown = true; HandlePointerMoved(e); } else if (e.RoutedEvent == PointerReleasedEvent) { if (isPenDown) { HandlePointerMoved(e); isPenDown = false; } if (TemporaryPaths.TryGetValue(e.Pointer.Id, out var path)) { Paths = Paths.Add(path); } TemporaryPaths.TryRemove(e.Pointer.Id, out _); } else { // Moved event if (!isPenDown || currentPoint.Properties.Pressure == 0) { return; } HandlePointerMoved(e); } Dispatcher.UIThread.Post(() => MainCanvas?.InvalidateVisual(), DispatcherPriority.Render); } private void HandlePointerMoved(PointerEventArgs e) { if (DataContext is not PaintCanvasViewModel viewModel) { return; } // Use intermediate points to include past events we missed var points = e.GetIntermediatePoints(MainCanvas); Debug.WriteLine($"Points: {string.Join(",", points.Select(p => p.Position.ToString()))}"); if (points.Count == 0) { return; } viewModel.CurrentPenPressure = points.FirstOrDefault().Properties.Pressure; // Get or create a temp path if (!TemporaryPaths.TryGetValue(e.Pointer.Id, out var penPath)) { penPath = new PenPath { FillColor = viewModel.PaintBrushSKColor.WithAlpha((byte)(viewModel.PaintBrushAlpha * 255)), IsErase = viewModel.SelectedTool == PaintCanvasTool.Eraser }; TemporaryPaths[e.Pointer.Id] = penPath; } // Add line for path // var cursorPosition = e.GetPosition(MainCanvas); // penPath.Path.LineTo(cursorPosition.ToSKPoint()); // Get bounds for discarding invalid points var canvasBounds = new Rect(0, 0, MainCanvas?.Bounds.Width ?? 0, MainCanvas?.Bounds.Height ?? 0); // Add points foreach (var point in points) { // Discard invalid points if (!canvasBounds.Contains(point.Position) || point.Position.X < 0 || point.Position.Y < 0) { continue; } var penPoint = new PenPoint(point.Position.X, point.Position.Y) { Pressure = point.Pointer.Type == PointerType.Mouse ? null : point.Properties.Pressure, Radius = viewModel.PaintBrushSize, IsPen = point.Pointer.Type == PointerType.Pen }; penPath.Points.Add(penPoint); } } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { HandlePointerEvent(e); base.OnPointerPressed(e); } /// protected override void OnPointerReleased(PointerReleasedEventArgs e) { HandlePointerEvent(e); base.OnPointerReleased(e); } /// protected override void OnPointerMoved(PointerEventArgs e) { HandlePointerEvent(e); base.OnPointerMoved(e); } /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (e.Key == Key.Escape) { e.Handled = true; } } /// /// Update the bounds of the main canvas to match the background image /// private void UpdateMainCanvasBounds() { if (MainCanvas is null || DataContext is not PaintCanvasViewModel vm) { return; } var canvasSize = vm.CanvasSize; // Set size if mismatch if ( ((int)Math.Round(MainCanvas.Width) != canvasSize.Width) || ((int)Math.Round(MainCanvas.Height) != canvasSize.Height) ) { MainCanvas.Width = vm.CanvasSize.Width; MainCanvas.Height = vm.CanvasSize.Height; MainCanvas.InvalidateVisual(); } } private int lastCanvasCursorRadius; private Cursor? lastCanvasCursor; private void UpdateCanvasCursor() { if (MainCanvas is not { } canvas) { return; } var currentZoom = ViewModel?.CurrentZoom ?? 1; // Get brush size var currentBrushSize = Math.Max((ViewModel?.PaintBrushSize ?? 1) - 2, 1); var brushRadius = (int)Math.Ceiling(currentBrushSize * 2 * currentZoom); // Only update cursor if brush size has changed if (brushRadius == lastCanvasCursorRadius) { canvas.Cursor = lastCanvasCursor; return; } lastCanvasCursorRadius = brushRadius; var brushDiameter = brushRadius * 2; const int padding = 4; var canvasCenter = brushRadius + padding; var canvasSize = brushDiameter + padding * 2; using var cursorBitmap = new SKBitmap(canvasSize, canvasSize); using var cursorCanvas = new SKCanvas(cursorBitmap); cursorCanvas.Clear(SKColors.Transparent); cursorCanvas.DrawCircle( brushRadius + padding, brushRadius + padding, brushRadius, new SKPaint { Color = SKColors.Black, Style = SKPaintStyle.Stroke, StrokeWidth = 1.5f, StrokeCap = SKStrokeCap.Round, StrokeJoin = SKStrokeJoin.Round, IsDither = true, IsAntialias = true } ); cursorCanvas.Flush(); using var data = cursorBitmap.Encode(SKEncodedImageFormat.Png, 100); using var stream = data.AsStream(); var bitmap = WriteableBitmap.Decode(stream); canvas.Cursor = new Cursor(bitmap, new PixelPoint(canvasCenter, canvasCenter)); lastCanvasCursor?.Dispose(); lastCanvasCursor = canvas.Cursor; } private void MainCanvas_OnPointerEntered(object? sender, PointerEventArgs e) { UpdateCanvasCursor(); } private void MainCanvas_OnPointerExited(object? sender, PointerEventArgs e) { if (sender is SkiaCustomCanvas canvas) { canvas.Cursor = new Cursor(StandardCursorType.Arrow); } } private Point GetRelativePosition(Point pt, Visual? relativeTo) { if (VisualRoot is not Visual visualRoot) return default; if (relativeTo == null) return pt; return pt * visualRoot.TransformToVisual(relativeTo) ?? default; } public AsyncRelayCommand ClearCanvasCommand => new(ClearCanvasAsync); public async Task ClearCanvasAsync() { Paths = ImmutableList.Empty; TemporaryPaths.Clear(); await Dispatcher.UIThread.InvokeAsync(() => MainCanvas?.InvalidateVisual()); } private void OnRenderSkia(SKSurface surface) { ViewModel?.RenderToSurface(surface, renderBackgroundFill: true, renderBackgroundImage: true); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/ProgressRing.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; namespace StabilityMatrix.Avalonia.Controls; /// /// A control used to indicate the progress of an operation. /// [PseudoClasses(":preserveaspect", ":indeterminate")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class ProgressRing : RangeBase { private Arc? fillArc; public static readonly StyledProperty IsIndeterminateProperty = ProgressBar .IsIndeterminateProperty .AddOwner(); public bool IsIndeterminate { get => GetValue(IsIndeterminateProperty); set => SetValue(IsIndeterminateProperty, value); } public static readonly StyledProperty PreserveAspectProperty = AvaloniaProperty.Register( nameof(PreserveAspect), true ); public bool PreserveAspect { get => GetValue(PreserveAspectProperty); set => SetValue(PreserveAspectProperty, value); } public static readonly StyledProperty StrokeThicknessProperty = Shape .StrokeThicknessProperty .AddOwner(); public double StrokeThickness { get => GetValue(StrokeThicknessProperty); set => SetValue(StrokeThicknessProperty, value); } public static readonly StyledProperty StartAngleProperty = AvaloniaProperty.Register( nameof(StartAngle) ); public double StartAngle { get => GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } public static readonly StyledProperty SweepAngleProperty = AvaloniaProperty.Register( nameof(SweepAngle) ); public double SweepAngle { get => GetValue(SweepAngleProperty); set => SetValue(SweepAngleProperty, value); } public static readonly StyledProperty EndAngleProperty = AvaloniaProperty.Register( nameof(EndAngle), 360 ); public double EndAngle { get => GetValue(EndAngleProperty); set => SetValue(EndAngleProperty, value); } static ProgressRing() { AffectsRender(SweepAngleProperty, StartAngleProperty, EndAngleProperty); ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged); SweepAngleProperty.Changed.AddClassHandler(OnSweepAnglePropertyChanged); } public ProgressRing() { UpdatePseudoClasses(IsIndeterminate, PreserveAspect); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); fillArc = e.NameScope.Find("PART_Fill"); if (fillArc is not null) { fillArc.StartAngle = StartAngle; fillArc.SweepAngle = SweepAngle; } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); var e = change as AvaloniaPropertyChangedEventArgs; if (e is null) return; if (e.Property == IsIndeterminateProperty) { UpdatePseudoClasses(e.NewValue.GetValueOrDefault(), null); } else if (e.Property == PreserveAspectProperty) { UpdatePseudoClasses(null, e.NewValue.GetValueOrDefault()); } } private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect) { if (isIndeterminate.HasValue) { PseudoClasses.Set(":indeterminate", isIndeterminate.Value); } if (preserveAspect.HasValue) { PseudoClasses.Set(":preserveaspect", preserveAspect.Value); } } private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) { sender.SweepAngle = ((double)e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum); } private static void OnSweepAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) { if (sender.fillArc is { } arc) { arc.SweepAngle = Math.Round(e.GetNewValue()); } } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Avalonia; using Avalonia.PropertyGrid.Services; using JetBrains.Annotations; using PropertyModels.ComponentModel; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.Controls; /// [PublicAPI] public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid { protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid); public static readonly StyledProperty> ExcludedCategoriesProperty = AvaloniaProperty.Register>("ExcludedCategories"); public IEnumerable ExcludedCategories { get => GetValue(ExcludedCategoriesProperty); set => SetValue(ExcludedCategoriesProperty, value); } public static readonly StyledProperty> IncludedCategoriesProperty = AvaloniaProperty.Register>("IncludedCategories"); public IEnumerable IncludedCategories { get => GetValue(IncludedCategoriesProperty); set => SetValue(IncludedCategoriesProperty, value); } static BetterPropertyGrid() { // Register factories CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory()); // Initialize localization and name resolver LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService()); ExcludedCategoriesProperty.Changed.AddClassHandler( (grid, args) => { if (args.NewValue is IEnumerable excludedCategories) { grid.FilterExcludeCategories(excludedCategories); } } ); IncludedCategoriesProperty.Changed.AddClassHandler( (grid, args) => { if (args.NewValue is IEnumerable includedCategories) { grid.FilterIncludeCategories(includedCategories); } } ); } protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is null) return; SetViewModelContext(DataContext); // Apply filters again FilterExcludeCategories(ExcludedCategories); FilterIncludeCategories(IncludedCategories); } public void FilterExcludeCategories(IEnumerable? excludedCategories) { excludedCategories ??= []; if (DataContext is null) return; var categoryFilter = GetCategoryFilter(); categoryFilter.BeginUpdate(); // Uncheck All, then check all except All categoryFilter.UnCheck(categoryFilter.All); foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All)) { categoryFilter.Check(mask); } // Uncheck excluded categories foreach (var mask in excludedCategories) { categoryFilter.UnCheck(mask); } categoryFilter.EndUpdate(); } public void FilterIncludeCategories(IEnumerable? includeCategories) { includeCategories ??= []; if (DataContext is null) return; var categoryFilter = GetCategoryFilter(); categoryFilter.BeginUpdate(); // Uncheck non-included categories foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m))) { categoryFilter.UnCheck(mask); } categoryFilter.UnCheck(categoryFilter.All); // Check included categories foreach (var mask in includeCategories) { categoryFilter.Check(mask); } categoryFilter.EndUpdate(); } private void SetViewModelContext(object? context) { // Get internal property `ViewModel` of internal type `PropertyGridViewModel` var propertyGridViewModelType = typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid).Assembly.GetType( "Avalonia.PropertyGrid.ViewModels.PropertyGridViewModel", true )!; var gridVm = this.GetProtectedProperty("ViewModel").Unwrap(); // Set `Context` public property var contextProperty = propertyGridViewModelType .GetProperty("Context", BindingFlags.Instance | BindingFlags.Public) .Unwrap(); contextProperty.SetValue(gridVm, context); // Trigger update that builds some stuff from `Context` and maybe initializes `Context` and `CategoryFilter` var buildPropertiesViewMethod = typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid) .GetMethod("BuildPropertiesView", BindingFlags.Instance | BindingFlags.NonPublic) .Unwrap(); buildPropertiesViewMethod.Invoke(this, [DataContext, ShowStyle]); // Call this to ensure `CategoryFilter` is initialized var method = propertyGridViewModelType .GetMethod("RefreshProperties", BindingFlags.Instance | BindingFlags.Public) .Unwrap(); method.Invoke(gridVm, null); } private CheckedMaskModel GetCategoryFilter() { // Get internal property `ViewModel` of internal type `PropertyGridViewModel` var propertyGridViewModelType = typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid).Assembly.GetType( "Avalonia.PropertyGrid.ViewModels.PropertyGridViewModel", true )!; var gridVm = this.GetProtectedProperty("ViewModel").Unwrap(); // Call this to ensure `CategoryFilter` is initialized var method = propertyGridViewModelType .GetMethod("RefreshProperties", BindingFlags.Instance | BindingFlags.Public) .Unwrap(); method.Invoke(gridVm, null); // Get public property `CategoryFilter` return gridVm.GetProtectedProperty("CategoryFilter").Unwrap(); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs ================================================ using System; using System.Globalization; using PropertyModels.Localilzation; using StabilityMatrix.Avalonia.Languages; namespace StabilityMatrix.Avalonia.Controls; internal class PropertyGridCultureData : ICultureData { /// public bool Reload() => false; /// public CultureInfo Culture => Cultures.Current ?? Cultures.Default; /// public Uri Path => new(""); /// public string this[string key] { get { if (Resources.ResourceManager.GetString(key) is { } result) { return result; } return key; } } /// public bool IsLoaded => true; } ================================================ FILE: StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs ================================================ using System; using PropertyModels.ComponentModel; using PropertyModels.Localilzation; using StabilityMatrix.Avalonia.Languages; namespace StabilityMatrix.Avalonia.Controls; /// /// Implements using static . /// internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService { /// public ICultureData CultureData { get; } = new PropertyGridCultureData(); /// public string this[string key] => CultureData[key]; /// public event EventHandler? OnCultureChanged; /// public ILocalizationService[] GetExtraServices() => Array.Empty(); /// public void AddExtraService(ILocalizationService service) { } /// public void RemoveExtraService(ILocalizationService service) { } /// public ICultureData[] GetCultures() => new[] { CultureData }; /// public void SelectCulture(string cultureName) { } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs ================================================ using Avalonia.Controls; using Avalonia.PropertyGrid.Controls; using Avalonia.PropertyGrid.Controls.Factories; using Avalonia.PropertyGrid.Localization; namespace StabilityMatrix.Avalonia.Controls; internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory { // make this extend factor only effect on TestExtendPropertyGrid public override bool Accept(object accessToken) { return accessToken is BetterPropertyGrid; } public override Control? HandleNewProperty(PropertyCellContext context) { var propertyDescriptor = context.Property; var target = context.Target; if (propertyDescriptor.PropertyType != typeof(bool)) { return null; } var control = new ToggleSwitch(); control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On"); control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off"); control.IsCheckedChanged += (s, e) => { SetAndRaise(context, control, control.IsChecked); }; return control; } public override bool HandlePropertyChanged(PropertyCellContext context) { var propertyDescriptor = context.Property; var target = context.Target; var control = context.CellEdit; if (propertyDescriptor.PropertyType != typeof(bool)) { return false; } ValidateProperty(control, propertyDescriptor, target); if (control is ToggleSwitch ts) { ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false); return true; } return false; } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/RefreshBadge.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Injectio.Attributes; namespace StabilityMatrix.Avalonia.Controls; [RegisterTransient] public partial class RefreshBadge : UserControlBase { public RefreshBadge() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollContentPresenter.cs ================================================ using Avalonia.Controls.Presenters; using Avalonia.Input; namespace StabilityMatrix.Avalonia.Controls.Scroll; public class BetterScrollContentPresenter : ScrollContentPresenter { protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { if (e.KeyModifiers == KeyModifiers.Control) return; base.OnPointerWheelChanged(e); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.axaml ================================================  Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 7 Item 8 Item 9 ================================================ FILE: StabilityMatrix.Avalonia/Controls/Scroll/BetterScrollViewer.cs ================================================ using Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls.Scroll; public class BetterScrollViewer : ScrollViewer { } ================================================ FILE: StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs ================================================ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard; public class SelectableImageButton : Button { public static readonly StyledProperty IsSelectedProperty = ToggleButton.IsCheckedProperty.AddOwner(); public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register< SelectableImageButton, Uri? >("Source"); public static readonly StyledProperty ImageWidthProperty = AvaloniaProperty.Register< SelectableImageButton, double >("ImageWidth", 300); public static readonly StyledProperty ImageHeightProperty = AvaloniaProperty.Register< SelectableImageButton, double >("ImageHeight", 300); static SelectableImageButton() { AffectsRender(ImageWidthProperty, ImageHeightProperty); AffectsArrange(ImageWidthProperty, ImageHeightProperty); } public double ImageHeight { get => GetValue(ImageHeightProperty); set => SetValue(ImageHeightProperty, value); } public double ImageWidth { get => GetValue(ImageWidthProperty); set => SetValue(ImageWidthProperty, value); } public bool? IsSelected { get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } public Uri? Source { get => GetValue(SourceProperty); set => SetValue(SourceProperty, value); } } ================================================ FILE: StabilityMatrix.Avalonia/Controls/SettingsAccountLinkExpander.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Controls/SettingsAccountLinkExpander.axaml.cs ================================================ using System; using System.Collections.Generic; using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Metadata; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.Controls; public class SettingsAccountLinkExpander : TemplatedControlBase { private readonly List _items = new(); [Content] public List Items => _items; // ReSharper disable MemberCanBePrivate.Global public static readonly StyledProperty HeaderProperty = HeaderedItemsControl.HeaderProperty.AddOwner(); public object? Header { get => GetValue(HeaderProperty); set => SetValue(HeaderProperty, value); } public static readonly StyledProperty HeaderTargetUriProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, Uri? >("HeaderTargetUri"); public Uri? HeaderTargetUri { get => GetValue(HeaderTargetUriProperty); set => SetValue(HeaderTargetUriProperty, value); } public static readonly StyledProperty IconSourceProperty = SettingsExpander.IconSourceProperty.AddOwner(); public IconSource? IconSource { get => GetValue(IconSourceProperty); set => SetValue(IconSourceProperty, value); } public static readonly StyledProperty IsConnectedProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, bool >("IsConnected"); public bool IsConnected { get => GetValue(IsConnectedProperty); set => SetValue(IsConnectedProperty, value); } public static readonly StyledProperty OnDescriptionProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, object? >("OnDescription", Languages.Resources.Label_Connected); public object? OnDescription { get => GetValue(OnDescriptionProperty); set => SetValue(OnDescriptionProperty, value); } public static readonly StyledProperty OnDescriptionExtraProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, object? >("OnDescriptionExtra"); public object? OnDescriptionExtra { get => GetValue(OnDescriptionExtraProperty); set => SetValue(OnDescriptionExtraProperty, value); } public static readonly StyledProperty OffDescriptionProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, object? >("OffDescription"); public object? OffDescription { get => GetValue(OffDescriptionProperty); set => SetValue(OffDescriptionProperty, value); } public static readonly StyledProperty ConnectCommandProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, ICommand? >(nameof(ConnectCommand), enableDataValidation: true); public ICommand? ConnectCommand { get => GetValue(ConnectCommandProperty); set => SetValue(ConnectCommandProperty, value); } public static readonly StyledProperty DisconnectCommandProperty = AvaloniaProperty.Register< SettingsAccountLinkExpander, ICommand? >(nameof(DisconnectCommand), enableDataValidation: true); public ICommand? DisconnectCommand { get => GetValue(DisconnectCommandProperty); set => SetValue(DisconnectCommandProperty, value); } /*public static readonly StyledProperty IsLoading2Property = AvaloniaProperty.Register( nameof(IsLoading2)); public bool IsLoading2 { get => GetValue(IsLoading2Property); set => SetValue(IsLoading2Property, value); }*/ private bool _isLoading; public static readonly DirectProperty IsLoadingProperty = AvaloniaProperty.RegisterDirect( "IsLoading", o => o.IsLoading, (o, v) => o.IsLoading = v ); public bool IsLoading { get => _isLoading; set => SetAndRaise(IsLoadingProperty, ref _isLoading, value); } // ReSharper restore MemberCanBePrivate.Global /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); // Bind tapped event on header if ( HeaderTargetUri is { } headerTargetUri && e.NameScope.Find("PART_HeaderTextBlock") is { } headerTextBlock ) { headerTextBlock.Tapped += (_, _) => { ProcessRunner.OpenUrl(headerTargetUri.ToString()); }; } if (e.NameScope.Find("PART_SettingsExpander") is { } expander) { expander.ItemsSource = Items; } if (ConnectCommand is { } command) { var connectButton = e.NameScope.Get ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/ButtonStyles.Accelerator.axaml ================================================  #ff822d #ff822d ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/LabelStyles.Dark.axaml ================================================  0.3 ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/LabelStyles.axaml ================================================  1 8 2 8 4 20 24 12 3 16 ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/ListBoxStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/ControlThemes/_index.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/DockStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/ListBoxStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml.cs ================================================ using Avalonia.Markup.Xaml; namespace StabilityMatrix.Avalonia.Styles.Markdown; public partial class MarkdownStyleFluentAvalonia : global::Avalonia.Styling.Styles { public MarkdownStyleFluentAvalonia() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Styles/ProgressRing.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/SemiStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/SemiStyles.axaml.cs ================================================ using System; using Avalonia.Markup.Xaml; namespace StabilityMatrix.Avalonia.Styles; public partial class SemiStyles : global::Avalonia.Styling.Styles { public SemiStyles(IServiceProvider? provider = null) { AvaloniaXamlLoader.Load(provider, this); } } ================================================ FILE: StabilityMatrix.Avalonia/Styles/SplitButtonStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/TextBoxStyles.axaml ================================================ ================================================ FILE: StabilityMatrix.Avalonia/Styles/ThemeColors.axaml ================================================  #333333 #952923 #C2362E #F44336 #D65A5A #AA2A2A #E91E63 #9C27B0 #673AB7 #3F51B5 #1A72BD #AA1A72BD #2196F3 #AA2196F3 #03A9F4 #AA03A9F4 #00BCD4 #009688 #2C582C #2C582C #3A783C #4BA04F #AA4BA04F #6CCB5F #8BC34A #CDDC39 #FFEB3B #FFC107 #FF9800 #FF5722 #AAFF5722 #FF4F00 #795548 #9E9E9E #C0C0C0 #607D8B ================================================ FILE: StabilityMatrix.Avalonia/Styles/ThemeColors.cs ================================================ using Avalonia.Media; namespace StabilityMatrix.Avalonia.Styles; public static class ThemeColors { public static readonly SolidColorBrush ThemeGreen = SolidColorBrush.Parse("#4caf50"); public static readonly SolidColorBrush ThemeRed = SolidColorBrush.Parse("#f44336"); public static readonly SolidColorBrush ThemeYellow = SolidColorBrush.Parse("#ffeb3b"); public static readonly SolidColorBrush AmericanYellow = SolidColorBrush.Parse("#f2ac08"); public static readonly SolidColorBrush HalloweenOrange = SolidColorBrush.Parse("#ed5D1f"); public static readonly SolidColorBrush LightSteelBlue = SolidColorBrush.Parse("#b4c7d9"); public static readonly SolidColorBrush DeepMagenta = SolidColorBrush.Parse("#dd00dd"); public static readonly SolidColorBrush LuminousGreen = SolidColorBrush.Parse("#00aa00"); public static readonly SolidColorBrush BrilliantAzure = SolidColorBrush.Parse("#3990f6"); public static readonly SolidColorBrush CompletionSelectionBackgroundBrush = SolidColorBrush.Parse("#2E436E"); public static readonly SolidColorBrush CompletionSelectionForegroundBrush = SolidColorBrush.Parse("#5389F4"); public static readonly SolidColorBrush CompletionForegroundBrush = SolidColorBrush.Parse( "#B4B8BF" ); public static readonly SolidColorBrush EditorSelectionBrush = SolidColorBrush.Parse("#214283"); } ================================================ FILE: StabilityMatrix.Avalonia/Styles/ThemeMaterials.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Styles/ToggleButtonStyles.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/ViewLocator.cs ================================================ using System; using Avalonia.Controls; using Avalonia.Controls.Templates; using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia; public class ViewLocator : IDataTemplate, INavigationPageFactory { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /*/// /// Weak Dictionary of (DataContext, View) pairs to keep the view and layout alive /// private static readonly ConditionalWeakTable PersistentViewCache = new();*/ /// public Control Build(object? data) { if (data is null) throw new ArgumentNullException(nameof(data)); var type = data.GetType(); if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr) { var viewType = viewAttr.ViewType; return GetView(viewType, data, viewAttr.IsPersistent); } return new TextBlock { Text = "View Model Not Found: " + data.GetType().FullName }; } private Control GetView(Type viewType) { if (App.Services.GetService(viewType) is Control view) { return view; } return new TextBlock { Text = "View Not Found: " + viewType.FullName }; } private Control GetView(Type viewType, object context, bool persistent) { // Disregard persistent settings in design mode if (Design.IsDesignMode) { persistent = false; } if (persistent) { // Check assignable from IPersistentViewProvider if (context is not IPersistentViewProvider persistentViewProvider) { throw new InvalidOperationException( $"View {viewType.Name} is marked as persistent but does not implement IPersistentViewProvider" ); } // Try get from context if (persistentViewProvider.AttachedPersistentView is { } view) { Logger.Trace("Got persistent view {ViewType} from context", viewType.Name); return view; } // Otherwise get from service provider if (App.Services.GetService(viewType) is Control newView) { // Set as attached view persistentViewProvider.AttachedPersistentView = newView; Logger.Trace("Attached persistent view {ViewType}", viewType.Name); return newView; } } else { // Get from service provider if (App.Services.GetService(viewType) is Control view) { return view; } } return new TextBlock { Text = "View Not Found: " + viewType.FullName }; } /// public bool Match(object? data) { return data is ViewModelBase; } /// public Control? GetPage(Type srcType) { if ( Attribute.GetCustomAttribute(srcType, typeof(ViewAttribute)) is not ViewAttribute viewAttr ) { throw new InvalidOperationException("View not found for " + srcType.FullName); } // Get new view var view = GetView(viewAttr.ViewType); view.DataContext ??= App.Services.GetService(srcType); return view; } /// public Control GetPageFromObject(object target) { if ( Attribute.GetCustomAttribute(target.GetType(), typeof(ViewAttribute)) is not ViewAttribute viewAttr ) { throw new InvalidOperationException("View not found for " + target.GetType().FullName); } var viewType = viewAttr.ViewType; var view = GetView(viewType, target, viewAttr.IsPersistent); view.DataContext ??= target; return view; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ConsoleProgressViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; namespace StabilityMatrix.Avalonia.ViewModels.Base; public partial class ConsoleProgressViewModel : ProgressViewModel { public ConsoleViewModel Console { get; } = new(); [ObservableProperty] private bool closeWhenFinished; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs ================================================ using System; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; namespace StabilityMatrix.Avalonia.ViewModels.Base; public partial class ContentDialogProgressViewModelBase : ConsoleProgressViewModel { [ObservableProperty] private bool hideCloseButton; [ObservableProperty] private bool autoScrollToBottom = true; public event EventHandler? PrimaryButtonClick; public event EventHandler? SecondaryButtonClick; public event EventHandler? CloseButtonClick; public virtual void OnPrimaryButtonClick() { PrimaryButtonClick?.Invoke(this, ContentDialogResult.Primary); } public virtual void OnSecondaryButtonClick() { SecondaryButtonClick?.Invoke(this, ContentDialogResult.Secondary); } public virtual void OnCloseButtonClick() { CloseButtonClick?.Invoke(this, ContentDialogResult.None); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogViewModelBase.cs ================================================ using System; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.ViewModels.Base; public class ContentDialogViewModelBase : DisposableViewModelBase { public virtual string? Title { get; set; } // Events for button clicks public event EventHandler? PrimaryButtonClick; public event EventHandler? SecondaryButtonClick; public event EventHandler? CloseButtonClick; public virtual void OnPrimaryButtonClick() { PrimaryButtonClick?.Invoke(this, ContentDialogResult.Primary); } public virtual void OnSecondaryButtonClick() { SecondaryButtonClick?.Invoke(this, ContentDialogResult.Secondary); } public virtual void OnCloseButtonClick() { CloseButtonClick?.Invoke(this, ContentDialogResult.None); } /// /// Return a that uses this view model as its content /// public virtual BetterContentDialog GetDialog() { Dispatcher.UIThread.VerifyAccess(); var dialog = new BetterContentDialog { Title = Title, Content = this }; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/DisposableLoadableViewModelBase.cs ================================================ using System; using System.Reactive.Disposables; using JetBrains.Annotations; namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract class DisposableLoadableViewModelBase : LoadableViewModelBase, IDisposable { private readonly CompositeDisposable instanceDisposables = new(); /// /// Adds a disposable to be disposed when this view model is disposed. /// /// The disposable to add. protected void AddDisposable([HandlesResourceDisposal] IDisposable disposable) { instanceDisposables.Add(disposable); } /// /// Adds disposables to be disposed when this view model is disposed. /// /// The disposables to add. protected void AddDisposable([HandlesResourceDisposal] params IDisposable[] disposables) { foreach (var disposable in disposables) { instanceDisposables.Add(disposable); } } /// /// Adds a disposable to be disposed when this view model is disposed. /// /// The disposable to add. /// The type of the disposable. /// The disposable that was added. protected T AddDisposable([HandlesResourceDisposal] T disposable) where T : IDisposable { instanceDisposables.Add(disposable); return disposable; } /// /// Adds disposables to be disposed when this view model is disposed. /// /// The disposables to add. /// The type of the disposables. /// The disposables that were added. protected T[] AddDisposable([HandlesResourceDisposal] params T[] disposables) where T : IDisposable { foreach (var disposable in disposables) { instanceDisposables.Add(disposable); } return disposables; } protected virtual void Dispose(bool disposing) { if (disposing) { instanceDisposables.Dispose(); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/DisposableViewModelBase.cs ================================================ using System; using System.Reactive.Disposables; using JetBrains.Annotations; namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract class DisposableViewModelBase : ViewModelBase, IDisposable { private readonly CompositeDisposable instanceDisposables = new(); /// /// Adds a disposable to be disposed when this view model is disposed. /// /// The disposable to add. protected void AddDisposable([HandlesResourceDisposal] IDisposable disposable) { instanceDisposables.Add(disposable); } /// /// Adds disposables to be disposed when this view model is disposed. /// /// The disposables to add. protected void AddDisposable([HandlesResourceDisposal] params IDisposable[] disposables) { foreach (var disposable in disposables) { instanceDisposables.Add(disposable); } } /// /// Adds a disposable to be disposed when this view model is disposed. /// /// The disposable to add. /// The type of the disposable. /// The disposable that was added. protected T AddDisposable([HandlesResourceDisposal] T disposable) where T : IDisposable { instanceDisposables.Add(disposable); return disposable; } /// /// Adds disposables to be disposed when this view model is disposed. /// /// The disposables to add. /// The type of the disposables. /// The disposables that were added. protected T[] AddDisposable([HandlesResourceDisposal] params T[] disposables) where T : IDisposable { foreach (var disposable in disposables) { instanceDisposables.Add(disposable); } return disposables; } protected virtual void Dispose(bool disposing) { if (disposing) { instanceDisposables.Dispose(); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using ExifLibrary; using FluentAvalonia.UI.Controls; using NLog; using Refit; using Semver; using SkiaSharp; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using Notification = DesktopNotifications.Notification; namespace StabilityMatrix.Avalonia.ViewModels.Base; /// /// Abstract base class for tab view models that generate images using ClientManager. /// This includes a progress reporter, image output view model, and generation virtual methods. /// [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] public abstract partial class InferenceGenerationViewModelBase : InferenceTabViewModelBase, IImageGalleryComponent { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly ISettingsManager settingsManager; private readonly RunningPackageService runningPackageService; private readonly INotificationService notificationService; private readonly IServiceManager vmFactory; [JsonPropertyName("ImageGallery")] public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } [JsonIgnore] public ImageFolderCardViewModel ImageFolderCardViewModel { get; } [JsonIgnore] public ProgressViewModel OutputProgress { get; } = new(); [JsonIgnore] public IInferenceClientManager ClientManager { get; } /// protected InferenceGenerationViewModelBase( IServiceManager vmFactory, IInferenceClientManager inferenceClientManager, INotificationService notificationService, ISettingsManager settingsManager, RunningPackageService runningPackageService ) : base(notificationService) { this.notificationService = notificationService; this.settingsManager = settingsManager; this.runningPackageService = runningPackageService; this.vmFactory = vmFactory; ClientManager = inferenceClientManager; ImageGalleryCardViewModel = vmFactory.Get(); ImageFolderCardViewModel = AddDisposable(vmFactory.Get()); GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); } /// /// Write an image to the default output folder /// protected Task WriteOutputImageAsync( Stream imageStream, ImageGenerationEventArgs args, int batchNum = 0, int batchTotal = 0, bool isGrid = false, string fileExtension = "png" ) { var defaultOutputDir = settingsManager.ImagesInferenceDirectory; defaultOutputDir.Create(); return WriteOutputImageAsync( imageStream, defaultOutputDir, args, batchNum, batchTotal, isGrid, fileExtension ); } /// /// Write an image to an output folder /// protected async Task WriteOutputImageAsync( Stream imageStream, DirectoryPath outputDir, ImageGenerationEventArgs args, int batchNum = 0, int batchTotal = 0, bool isGrid = false, string fileExtension = "png" ) { var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat; var formatProvider = new FileNameFormatProvider { GenerationParameters = args.Parameters, ProjectType = args.Project?.ProjectType, ProjectName = ProjectFile?.NameWithoutExtension, }; // Parse to format if ( string.IsNullOrEmpty(formatTemplateStr) || !FileNameFormat.TryParse(formatTemplateStr, formatProvider, out var format) ) { // Fallback to default Logger.Warn( "Failed to parse format template: {FormatTemplate}, using default", formatTemplateStr ); format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider); } if (isGrid) { format = format.WithGridPrefix(); } if (batchNum >= 1 && batchTotal > 1) { format = format.WithBatchPostFix(batchNum, batchTotal); } var fileName = format.GetFileName(); var file = outputDir.JoinFile($"{fileName}.{fileExtension}"); // Until the file is free, keep adding _{i} to the end for (var i = 0; i < 100; i++) { if (!file.Exists) break; file = outputDir.JoinFile($"{fileName}_{i + 1}.{fileExtension}"); } // If that fails, append an 7-char uuid if (file.Exists) { var uuid = Guid.NewGuid().ToString("N")[..7]; file = outputDir.JoinFile($"{fileName}_{uuid}.{fileExtension}"); } if (file.Info.DirectoryName != null) { Directory.CreateDirectory(file.Info.DirectoryName); } await using var fileStream = file.Info.OpenWrite(); await imageStream.CopyToAsync(fileStream); return file; } /// /// Builds the image generation prompt /// protected virtual void BuildPrompt(BuildPromptEventArgs args) { } /// /// Uploads files required for the prompt /// protected virtual async Task UploadPromptFiles( IEnumerable<(string SourcePath, string DestinationRelativePath)> files, ComfyClient client ) { foreach (var (sourcePath, destinationRelativePath) in files) { Logger.Debug( "Uploading prompt file {SourcePath} to relative path {DestinationPath}", sourcePath, destinationRelativePath ); await client.UploadFileAsync(sourcePath, destinationRelativePath); } } /// /// Gets ImageSources that need to be uploaded as inputs /// protected virtual IEnumerable GetInputImages() { return Enumerable.Empty(); } protected async Task UploadInputImages(ComfyClient client) { foreach (var image in GetInputImages()) { await ClientManager.UploadInputImageAsync(image); } } public async Task RunCustomGeneration( InferenceQueueCustomPromptEventArgs args, CancellationToken cancellationToken = default ) { if (ClientManager.Client is not { } client) { throw new InvalidOperationException("Client is not connected"); } var generationArgs = new ImageGenerationEventArgs { Client = client, Nodes = args.Builder.ToNodeDictionary(), OutputNodeNames = args.Builder.Connections.OutputNodeNames.ToArray(), Project = InferenceProjectDocument.FromLoadable(this), FilesToTransfer = args.FilesToTransfer, Parameters = new GenerationParameters(), ClearOutputImages = true, }; await RunGeneration(generationArgs, cancellationToken); } /// /// Runs a generation task /// /// Thrown if args.Parameters or args.Project are null protected async Task RunGeneration(ImageGenerationEventArgs args, CancellationToken cancellationToken) { var client = args.Client; var nodes = args.Nodes; // Checks if (args.Parameters is null) throw new InvalidOperationException("Parameters is null"); if (args.Project is null) throw new InvalidOperationException("Project is null"); if (args.OutputNodeNames.Count == 0) throw new InvalidOperationException("OutputNodeNames is empty"); if (client.OutputImagesDir is null) throw new InvalidOperationException("OutputImagesDir is null"); // Only check extensions for first batch index if (args.BatchIndex == 0) { if (!await CheckPromptExtensionsInstalled(args.Nodes)) { throw new ValidationException("Prompt extensions not installed"); } } // Upload input images await UploadInputImages(client); // Upload required files await UploadPromptFiles(args.FilesToTransfer, client); // Connect preview image handler client.PreviewImageReceived += OnPreviewImageReceived; // Register to interrupt if user cancels var promptInterrupt = cancellationToken.Register(() => { Logger.Info("Cancelling prompt"); client .InterruptPromptAsync(new CancellationTokenSource(5000).Token) .SafeFireAndForget(ex => { Logger.Warn(ex, "Error while interrupting prompt"); }); }); ComfyTask? promptTask = null; try { var timer = Stopwatch.StartNew(); try { promptTask = await client.QueuePromptAsync(nodes, cancellationToken); } catch (ApiException e) { Logger.Warn(e, "Api exception while queuing prompt"); await DialogHelper.CreateApiExceptionDialog(e, "Api Error").ShowAsync(); return; } // Register progress handler promptTask.ProgressUpdate += OnProgressUpdateReceived; // Delay attaching running node change handler to not show indeterminate progress // if progress updates are received before the prompt starts Task.Run( async () => { try { var delayTime = 250 - (int)timer.ElapsedMilliseconds; if (delayTime > 0) { await Task.Delay(delayTime, cancellationToken); } // ReSharper disable once AccessToDisposedClosure AttachRunningNodeChangedHandler(promptTask); } catch (TaskCanceledException) { } }, cancellationToken ) .SafeFireAndForget(ex => { if (ex is TaskCanceledException) return; Logger.Error(ex, "Error while attaching running node change handler"); }); // Wait for prompt to finish try { await promptTask.Task.WaitAsync(cancellationToken); Logger.Debug($"Prompt task {promptTask.Id} finished"); } catch (ComfyNodeException e) { Logger.Warn(e, "Comfy node exception while queuing prompt"); await DialogHelper .CreateJsonDialog(e.JsonData, "Comfy Error", "Node execution encountered an error") .ShowAsync(); return; } // Get output images var imageOutputs = await client.GetImagesForExecutedPromptAsync(promptTask.Id, cancellationToken); if (imageOutputs.Values.All(images => images is null or { Count: 0 })) { // No images match notificationService.Show( "No output", "Did not receive any output images", NotificationType.Warning ); return; } // Disable cancellation await promptInterrupt.DisposeAsync(); if (args.ClearOutputImages) { ImageGalleryCardViewModel.ImageSources.Clear(); } var outputImages = await ProcessAllOutputImages(imageOutputs, args); var notificationImage = outputImages.FirstOrDefault()?.LocalFile; await notificationService.ShowAsync( NotificationKey.Inference_PromptCompleted, new Notification { Title = "Prompt Completed", Body = $"Prompt [{promptTask.Id[..7].ToLower()}] completed successfully", BodyImagePath = notificationImage?.FullPath, } ); } finally { // Disconnect progress handler client.PreviewImageReceived -= OnPreviewImageReceived; // Clear progress OutputProgress.ClearProgress(); // ImageGalleryCardViewModel.PreviewImage?.Dispose(); ImageGalleryCardViewModel.PreviewImage = null; ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; // Cleanup tasks promptTask?.Dispose(); } } private async Task> ProcessAllOutputImages( IReadOnlyDictionary?> images, ImageGenerationEventArgs args ) { var results = new List(); foreach (var (nodeName, imageList) in images) { if (imageList is null) { Logger.Warn("No images for node {NodeName}", nodeName); continue; } results.AddRange(await ProcessOutputImages(imageList, args, nodeName.Replace('_', ' '))); } return results; } /// /// Handles image output metadata for generation runs /// private async Task> ProcessOutputImages( IReadOnlyCollection images, ImageGenerationEventArgs args, string? imageLabel = null ) { var client = args.Client; // Write metadata to images var outputImagesBytes = new List(); var outputImages = new List(); foreach (var (i, comfyImage) in images.Enumerate()) { Logger.Debug("Downloading image: {FileName}", comfyImage.FileName); var imageStream = await client.GetImageStreamAsync(comfyImage); using var ms = new MemoryStream(); await imageStream.CopyToAsync(ms); var imageArray = ms.ToArray(); outputImagesBytes.Add(imageArray); var parameters = args.Parameters!; var project = args.Project!; // Lock seed project.TryUpdateModel("Seed", model => model with { IsRandomizeEnabled = false }); // Seed and batch override for batches if (images.Count > 1 && project.ProjectType is InferenceProjectType.TextToImage) { project = (InferenceProjectDocument)project.Clone(); // Set batch size indexes project.TryUpdateModel( "BatchSize", node => { node[nameof(BatchSizeCardViewModel.BatchCount)] = 1; node[nameof(BatchSizeCardViewModel.IsBatchIndexEnabled)] = true; node[nameof(BatchSizeCardViewModel.BatchIndex)] = i + 1; return node; } ); } if (comfyImage.FileName.EndsWith(".png")) { var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project); // Write using generated name var filePath = await WriteOutputImageAsync( new MemoryStream(bytesWithMetadata), args, i + 1, images.Count ); outputImages.Add(new ImageSource(filePath) { Label = imageLabel }); EventManager.Instance.OnImageFileAdded(filePath); } else if (comfyImage.FileName.EndsWith(".webp")) { var opts = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter() }, }; var paramsJson = JsonSerializer.Serialize(parameters, opts); var smProject = JsonSerializer.Serialize(project, opts); var metadata = new Dictionary { { ExifTag.ImageDescription, paramsJson }, { ExifTag.Software, smProject }, }; var bytesWithMetadata = ImageMetadata.AddMetadataToWebp(imageArray, metadata); // Write using generated name var filePath = await WriteOutputImageAsync( new MemoryStream(bytesWithMetadata.ToArray()), args, i + 1, images.Count, fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "") ); outputImages.Add(new ImageSource(filePath) { Label = imageLabel }); EventManager.Instance.OnImageFileAdded(filePath); } else { // Write using generated name var filePath = await WriteOutputImageAsync( new MemoryStream(imageArray), args, i + 1, images.Count, fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "") ); outputImages.Add(new ImageSource(filePath) { Label = imageLabel }); EventManager.Instance.OnImageFileAdded(filePath); } } // Download all images to make grid, if multiple if (outputImages.Count > 1) { var loadedImages = outputImagesBytes.Select(SKImage.FromEncodedData).ToImmutableArray(); var project = args.Project!; // Lock seed project.TryUpdateModel("Seed", model => model with { IsRandomizeEnabled = false }); var grid = ImageProcessor.CreateImageGrid(loadedImages); var gridBytes = grid.Encode().ToArray(); var gridBytesWithMetadata = PngDataHelper.AddMetadata(gridBytes, args.Parameters!, args.Project!); // Save to disk var gridPath = await WriteOutputImageAsync( new MemoryStream(gridBytesWithMetadata), args, isGrid: true ); // Insert to start of images var gridImage = new ImageSource(gridPath); outputImages.Insert(0, gridImage); EventManager.Instance.OnImageFileAdded(gridPath); } foreach (var img in outputImages) { // Preload await img.GetBitmapAsync(); // Add images ImageGalleryCardViewModel.ImageSources.Add(img); } return outputImages; } /// /// Implementation for Generate Image /// protected virtual Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken) { return Task.CompletedTask; } /// /// Command for the Generate Image button /// /// Optional overrides (side buttons) /// Cancellation token [RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)] private async Task GenerateImage( GenerateFlags options = default, CancellationToken cancellationToken = default ) { var overrides = GenerateOverrides.FromFlags(options); try { await GenerateImageImpl(overrides, cancellationToken); } catch (OperationCanceledException) { Logger.Debug("Image Generation Canceled"); } catch (ValidationException e) { Logger.Debug("Image Generation Validation Error: {Message}", e.Message); notificationService.Show("Validation Error", e.Message, NotificationType.Error); } } /// /// Shows a prompt and return false if client not connected /// protected async Task CheckClientConnectedWithPrompt() { if (ClientManager.IsConnected) return true; var vm = vmFactory.Get(); await vm.CreateDialog().ShowAsync(); return ClientManager.IsConnected; } /// /// Shows a dialog and return false if prompt required extensions not installed /// private async Task CheckPromptExtensionsInstalled(NodeDictionary nodeDictionary) { // Get prompt required extensions // Just static for now but could do manifest lookup when we support custom workflows var requiredExtensionSpecifiers = nodeDictionary .RequiredExtensions.DistinctBy(ext => ext.Name) .ToList(); // Skip if no extensions required if (requiredExtensionSpecifiers.Count == 0) { return true; } // Get installed extensions var localPackagePair = ClientManager.Client?.LocalServerPackage.Unwrap()!; var manager = localPackagePair.BasePackage.ExtensionManager.Unwrap(); var localExtensions = ( await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync( localPackagePair.InstalledPackage ) ).ToList(); var localExtensionsByGitUrl = localExtensions .Where(ext => ext.GitRepositoryUrl is not null) .ToDictionary(ext => ext.GitRepositoryUrl!, ext => ext); var requiredExtensionReferences = requiredExtensionSpecifiers .Select(specifier => specifier.Name) .ToHashSet(); var missingExtensions = new List(); var outOfDateExtensions = new List<(ExtensionSpecifier Specifier, InstalledPackageExtension Installed)>(); // Check missing extensions and out of date extensions foreach (var specifier in requiredExtensionSpecifiers) { if (!localExtensionsByGitUrl.TryGetValue(specifier.Name, out var localExtension)) { missingExtensions.Add(specifier); continue; } // Check if constraint is specified if (specifier.Constraint is not null && specifier.TryGetSemVersionRange(out var semVersionRange)) { // Get version to compare localExtension = await manager.GetInstalledExtensionInfoAsync(localExtension); // Try to parse local tag to semver if ( localExtension.Version?.Tag is not null && SemVersion.TryParse( localExtension.Version.Tag, SemVersionStyles.AllowV, out var localSemVersion ) ) { // Check if not satisfied if (!semVersionRange.Contains(localSemVersion)) { outOfDateExtensions.Add((specifier, localExtension)); } } } } if (missingExtensions.Count == 0 && outOfDateExtensions.Count == 0) { return true; } var dialog = DialogHelper.CreateMarkdownDialog( $"#### The following extensions are required for this workflow:\n" + $"{string.Join("\n- ", missingExtensions.Select(ext => ext.Name))}" + $"{string.Join("\n- ", outOfDateExtensions.Select(pair => $"{pair.Item1.Name} {pair.Specifier.Constraint} {pair.Specifier.Version} (Current Version: {pair.Installed.Version?.Tag})"))}", "Install Required Extensions?" ); dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; dialog.PrimaryButtonText = $"{Resources.Action_Install} ({localPackagePair.InstalledPackage.DisplayName.ToRepr()} will restart)"; dialog.CloseButtonText = Resources.Action_Cancel; if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var manifestExtensionsMap = await manager.GetManifestExtensionsMapAsync( manager.GetManifests(localPackagePair.InstalledPackage) ); var steps = new List(); // Add install for missing extensions foreach (var missingExtension in missingExtensions) { if (!manifestExtensionsMap.TryGetValue(missingExtension.Name, out var extension)) { Logger.Warn( "Extension {MissingExtensionUrl} not found in manifests", missingExtension.Name ); continue; } steps.Add(new InstallExtensionStep(manager, localPackagePair.InstalledPackage, extension)); } // Add update for out of date extensions foreach (var (specifier, installed) in outOfDateExtensions) { if (!manifestExtensionsMap.TryGetValue(specifier.Name, out var extension)) { Logger.Warn("Extension {MissingExtensionUrl} not found in manifests", specifier.Name); continue; } steps.Add(new UpdateExtensionStep(manager, localPackagePair.InstalledPackage, installed)); } var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = "Extensions Installed", ModificationCompleteMessage = "Finished installing required extensions", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); runner .ExecuteSteps(steps) .ContinueWith(async _ => { if (runner.Failed) return; // Restart Package try { await Dispatcher.UIThread.InvokeAsync(async () => { await runningPackageService.StopPackage(localPackagePair.InstalledPackage.Id); await runningPackageService.StartPackage(localPackagePair.InstalledPackage); }); } catch (Exception e) { Logger.Error(e, "Error while restarting package"); notificationService.ShowPersistent( new AppException( "Could not restart package", "Please manually restart the package for extension changes to take effect" ) ); } }) .SafeFireAndForget(); } return false; } /// /// Handles the preview image received event from the websocket. /// Updates the preview image in the image gallery. /// protected virtual void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args) { ImageGalleryCardViewModel.SetPreviewImage(args.ImageBytes); } /// /// Handles the progress update received event from the websocket. /// Updates the progress view model. /// protected virtual void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) { Dispatcher.UIThread.Post(() => { OutputProgress.Value = args.Value; OutputProgress.Maximum = args.Maximum; OutputProgress.IsIndeterminate = false; OutputProgress.Text = $"({args.Value} / {args.Maximum})" + (args.RunningNode != null ? $" {args.RunningNode}" : ""); }); } private void AttachRunningNodeChangedHandler(ComfyTask comfyTask) { // Do initial update if (comfyTask.RunningNodesHistory.TryPeek(out var lastNode)) { OnRunningNodeChanged(comfyTask, lastNode); } comfyTask.RunningNodeChanged += OnRunningNodeChanged; } /// /// Handles the node executing updates received event from the websocket. /// protected virtual void OnRunningNodeChanged(object? sender, string? nodeName) { var task = sender as ComfyTask; if (task == null) { return; } // Ignore if regular progress updates started, unless the running node is different from the one reporting progress if (task.HasProgressUpdateStarted && task.LastProgressUpdate?.RunningNode == nodeName) { return; } Dispatcher.UIThread.Post(() => { OutputProgress.IsIndeterminate = true; OutputProgress.Value = 100; OutputProgress.Maximum = 100; OutputProgress.Text = nodeName; }); } public class ImageGenerationEventArgs : EventArgs { public required ComfyClient Client { get; init; } public required NodeDictionary Nodes { get; init; } public required IReadOnlyList OutputNodeNames { get; init; } public int BatchIndex { get; init; } public GenerationParameters? Parameters { get; init; } public InferenceProjectDocument? Project { get; init; } public bool ClearOutputImages { get; init; } = true; public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = []; } public class BuildPromptEventArgs : EventArgs { public ComfyNodeBuilder Builder { get; } = new(); public GenerateOverrides Overrides { get; init; } = new(); public long? SeedOverride { get; init; } public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = []; public ModuleApplyStepEventArgs ToModuleApplyStepEventArgs() { var overrides = new Dictionary(); if (Overrides.IsHiresFixEnabled.HasValue) { overrides[typeof(HiresFixModule)] = Overrides.IsHiresFixEnabled.Value; } return new ModuleApplyStepEventArgs { Builder = Builder, IsEnabledOverrides = overrides, FilesToTransfer = FilesToTransfer, }; } public static implicit operator ModuleApplyStepEventArgs(BuildPromptEventArgs args) { return args.ToModuleApplyStepEventArgs(); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using Microsoft.Extensions.DependencyInjection; using NLog; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract partial class InferenceTabViewModelBase : DisposableLoadableViewModelBase, IPersistentViewProvider, IDropTarget { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; /// /// The title of the tab /// public virtual string TabTitle => ProjectFile?.NameWithoutExtension ?? "New Project"; /// /// Whether there are unsaved changes /// [ObservableProperty] [property: JsonIgnore] private bool hasUnsavedChanges; /// /// The tab's project file /// [ObservableProperty] [NotifyPropertyChangedFor(nameof(TabTitle))] [property: JsonIgnore] private FilePath? projectFile; /// Control? IPersistentViewProvider.AttachedPersistentView { get; set; } #region Weak Events private WeakEventManager? loadViewStateRequestedEventManager; public event EventHandler LoadViewStateRequested { add { loadViewStateRequestedEventManager ??= new WeakEventManager(); loadViewStateRequestedEventManager.AddEventHandler(value); } remove => loadViewStateRequestedEventManager?.RemoveEventHandler(value); } protected void LoadViewState(LoadViewStateEventArgs args) => loadViewStateRequestedEventManager?.RaiseEvent(this, args, nameof(LoadViewStateRequested)); protected void ResetViewState() => LoadViewState(new LoadViewStateEventArgs()); private WeakEventManager? saveViewStateRequestedEventManager; public event EventHandler SaveViewStateRequested { add { saveViewStateRequestedEventManager ??= new WeakEventManager(); saveViewStateRequestedEventManager.AddEventHandler(value); } remove => saveViewStateRequestedEventManager?.RemoveEventHandler(value); } protected async Task SaveViewState() { var eventArgs = new SaveViewStateEventArgs(); saveViewStateRequestedEventManager?.RaiseEvent(this, eventArgs, nameof(SaveViewStateRequested)); if (eventArgs.StateTask is not { } stateTask) { throw new InvalidOperationException( "SaveViewStateRequested event handler did not set the StateTask property" ); } return await stateTask; } #endregion protected InferenceTabViewModelBase(INotificationService notificationService) { this.notificationService = notificationService; } [RelayCommand] private void RestoreDefaultViewState() { // ResetViewState(); // TODO: Dock reset not working, using this hack for now to get a new view var navService = App.Services.GetRequiredService>(); navService.NavigateTo(new SuppressNavigationTransitionInfo()); ((IPersistentViewProvider)this).AttachedPersistentView = null; navService.NavigateTo(new BetterEntranceNavigationTransition()); } [RelayCommand] private async Task DebugSaveViewState() { var state = await SaveViewState(); if (state.DockLayout is { } layout) { await DialogHelper.CreateJsonDialog(layout).ShowAsync(); } else { await DialogHelper.CreateTaskDialog("Failed", "No layout data").ShowAsync(); } } [RelayCommand] private async Task DebugLoadViewState() { var textFields = new TextBoxField[] { new() { Label = "Json Data" } }; var dialog = DialogHelper.CreateTextEntryDialog("Load Dock State", "", textFields); var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary && textFields[0].Text is { } json) { LoadViewState(new LoadViewStateEventArgs { State = new ViewState { DockLayout = json } }); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { ((IPersistentViewProvider)this).AttachedPersistentView = null; } } /// /// Loads image and metadata from a file path /// /// This is safe to call from non-UI threads /// File path public void LoadImageMetadata(FilePath? filePath) { Logger.Info("Loading image metadata from '{Path}'", filePath?.FullPath); if (filePath is not { Exists: true }) { throw new FileNotFoundException("File does not exist", filePath?.FullPath); } var metadata = ImageMetadata.GetAllFileMetadata(filePath); LoadImageMetadata(filePath.FullPath, metadata); } /// /// Loads image and metadata from a LocalImageFile /// /// LocalImageFile public void LoadImageMetadata(LocalImageFile localImageFile) { Logger.Info("Loading image metadata from LocalImageFile at '{Path}'", localImageFile.AbsolutePath); var metadata = localImageFile.ReadMetadata(); LoadImageMetadata(localImageFile.AbsolutePath, metadata); } /// /// Loads image and metadata from a file path and metadata tuple /// private void LoadImageMetadata( string imageFilePath, ( string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes, string? CivitParameters ) metadata ) { // Has SMProject metadata if (metadata.SMProject is not null) { var project = JsonSerializer.Deserialize(metadata.SMProject); // Check project type matches var projectType = project?.ProjectType.ToViewModelType(); if (projectType != GetType()) { Logger.Warn( "Attempted to load project of mismatched type {Type} into {ThisType}, skipping", projectType?.Name, GetType().Name ); // Fallback to try loading as parameters } else if (project?.State is null) { Logger.Warn("Project State is null, skipping"); // Fallback to try loading as parameters } else { Logger.Info("Loading Project State (Type: {Type})", projectType.Name); Dispatcher.UIThread.Invoke(() => LoadStateFromJsonObject(project.State)); // Load image if (this is IImageGalleryComponent imageGalleryComponent) { Dispatcher.UIThread.Invoke(() => imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) ); } return; } } // Has generic metadata if (metadata.Parameters is not null) { Logger.Info("Loading Parameters from metadata"); if (!GenerationParameters.TryParse(metadata.Parameters, out var parameters)) { throw new ApplicationException("Failed to parse parameters"); } if (this is IParametersLoadableState paramsLoadableVm) { Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters)); } else { Logger.Warn( "Load parameters target {Type} does not implement IParametersLoadableState, skipping", GetType().Name ); } // Load image if (this is IImageGalleryComponent imageGalleryComponent) { Dispatcher.UIThread.Invoke(() => imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) ); } return; } // Civit generator metadata if (metadata.CivitParameters is not null) { Logger.Info("Loading Parameters from metadata"); if (!GenerationParameters.TryParse(metadata.CivitParameters, out var parameters)) { throw new ApplicationException("Failed to parse parameters"); } if (this is IParametersLoadableState paramsLoadableVm) { Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters)); } else { Logger.Warn( "Load parameters target {Type} does not implement IParametersLoadableState, skipping", GetType().Name ); } // Load image if (this is IImageGalleryComponent imageGalleryComponent) { Dispatcher.UIThread.Invoke(() => imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) ); } return; } throw new ApplicationException("File does not contain SMProject or Parameters Metadata"); } /// public void DragOver(object? sender, DragEventArgs e) { // 1. Context drop for LocalImageFile if (e.Data.GetDataFormats().Contains("Context")) { if (e.Data.GetContext() is { } imageFile) { e.Handled = true; return; } e.DragEffects = DragDropEffects.None; } // 2. OS Files if (e.Data.GetDataFormats().Contains(DataFormats.Files)) { e.Handled = true; return; } // Other kinds - not supported e.DragEffects = DragDropEffects.None; } /// public void Drop(object? sender, DragEventArgs e) { // 1. Context drop for LocalImageFile if (e.Data.GetDataFormats().Contains("Context")) { if (e.Data.GetContext() is { } imageFile) { e.Handled = true; Dispatcher.UIThread.Post(() => { try { LoadImageMetadata(imageFile); } catch (Exception ex) { Logger.Warn(ex, "Failed to load image from context drop"); notificationService.ShowPersistent( $"Could not parse image metadata", $"{imageFile.FileName} - {ex.Message}", NotificationType.Warning ); } }); return; } } // 2. OS Files if (e.Data.GetDataFormats().Contains(DataFormats.Files)) { e.Handled = true; if (e.Data.Get(DataFormats.Files) is IEnumerable files) { if (files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path) { var file = new FilePath(path); Dispatcher.UIThread.Post(() => { try { LoadImageMetadata(file); } catch (Exception ex) { Logger.Warn(ex, "Failed to load image from OS file drop"); notificationService.ShowPersistent( $"Could not parse image metadata", $"{file.Name} - {ex.Message}", NotificationType.Warning ); } }); } } } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs ================================================ using System; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Windows.Input; using CommunityToolkit.Mvvm.Input; using NLog; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; namespace StabilityMatrix.Avalonia.ViewModels.Base; [JsonDerivedType(typeof(StackExpanderViewModel), StackExpanderViewModel.ModuleKey)] [JsonDerivedType(typeof(SamplerCardViewModel), SamplerCardViewModel.ModuleKey)] [JsonDerivedType(typeof(FreeUCardViewModel), FreeUCardViewModel.ModuleKey)] [JsonDerivedType(typeof(UpscalerCardViewModel), UpscalerCardViewModel.ModuleKey)] [JsonDerivedType(typeof(ControlNetCardViewModel), ControlNetCardViewModel.ModuleKey)] [JsonDerivedType(typeof(PromptExpansionCardViewModel), PromptExpansionCardViewModel.ModuleKey)] [JsonDerivedType(typeof(ExtraNetworkCardViewModel), ExtraNetworkCardViewModel.ModuleKey)] [JsonDerivedType(typeof(LayerDiffuseCardViewModel), LayerDiffuseCardViewModel.ModuleKey)] [JsonDerivedType(typeof(FaceDetailerViewModel), FaceDetailerViewModel.ModuleKey)] [JsonDerivedType(typeof(DiscreteModelSamplingCardViewModel), DiscreteModelSamplingCardViewModel.ModuleKey)] [JsonDerivedType(typeof(RescaleCfgCardViewModel), RescaleCfgCardViewModel.ModuleKey)] [JsonDerivedType(typeof(PlasmaNoiseCardViewModel), PlasmaNoiseCardViewModel.ModuleKey)] [JsonDerivedType(typeof(NrsCardViewModel), NrsCardViewModel.ModuleKey)] [JsonDerivedType(typeof(CfzCudnnToggleCardViewModel), CfzCudnnToggleCardViewModel.ModuleKey)] [JsonDerivedType(typeof(TiledVAECardViewModel), TiledVAECardViewModel.ModuleKey)] [JsonDerivedType(typeof(FreeUModule))] [JsonDerivedType(typeof(HiresFixModule))] [JsonDerivedType(typeof(FluxHiresFixModule))] [JsonDerivedType(typeof(UpscalerModule))] [JsonDerivedType(typeof(ControlNetModule))] [JsonDerivedType(typeof(SaveImageModule))] [JsonDerivedType(typeof(PromptExpansionModule))] [JsonDerivedType(typeof(LoraModule))] [JsonDerivedType(typeof(LayerDiffuseModule))] [JsonDerivedType(typeof(FaceDetailerModule))] [JsonDerivedType(typeof(FluxGuidanceModule))] [JsonDerivedType(typeof(DiscreteModelSamplingModule))] [JsonDerivedType(typeof(RescaleCfgModule))] [JsonDerivedType(typeof(PlasmaNoiseModule))] [JsonDerivedType(typeof(NRSModule))] [JsonDerivedType(typeof(CfzCudnnToggleModule))] [JsonDerivedType(typeof(TiledVAEModule))] public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Type[] SerializerIgnoredTypes = { typeof(ICommand), typeof(IRelayCommand) }; private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors) }; private static readonly JsonSerializerOptions SerializerOptions = new() { IgnoreReadOnlyProperties = true, }; private static bool ShouldIgnoreProperty(PropertyInfo property) { // Skip if read-only and not IJsonLoadableState if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { Logger.ConditionalTrace("Skipping {Property} - read-only", property.Name); return true; } // Check not JsonIgnore if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0) { Logger.ConditionalTrace("Skipping {Property} - has [JsonIgnore]", property.Name); return true; } // Check not excluded type if (SerializerIgnoredTypes.Contains(property.PropertyType)) { Logger.ConditionalTrace( "Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType ); return true; } // Check not ignored name if (SerializerIgnoredNames.Contains(property.Name, StringComparer.Ordinal)) { Logger.ConditionalTrace("Skipping {Property} - serializer ignored name", property.Name); return true; } return false; } /// /// True if we should include property without checking exclusions /// private static bool ShouldIncludeProperty(PropertyInfo property) { // Has JsonIncludeAttribute if (property.GetCustomAttributes(typeof(JsonIncludeAttribute), true).Length > 0) { Logger.ConditionalTrace("Including {Property} - has [JsonInclude]", property.Name); return true; } return false; } /// /// Load the state of this view model from a JSON object. /// The default implementation is a mirror of . /// For the following properties on this class, we will try to set from the JSON object: /// /// Public /// Not read-only /// Not marked with [JsonIgnore] /// Not a type within the SerializerIgnoredTypes /// Not a name within the SerializerIgnoredNames /// /// public virtual void LoadStateFromJsonObject(JsonObject state) { // Get all of our properties using reflection var properties = GetType().GetProperties(); Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length); foreach (var property in properties) { var name = property.Name; // If JsonPropertyName provided, use that as the key if ( property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault() is JsonPropertyNameAttribute jsonPropertyName ) { Logger.ConditionalTrace( "Deserializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", property.Name, property.PropertyType, jsonPropertyName.Name ); name = jsonPropertyName.Name; } // Check if property is in the JSON object if (!state.TryGetPropertyValue(name, out var value)) { Logger.ConditionalTrace("Skipping {Property} - not in JSON object", property.Name); continue; } // Check if we should ignore this property if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } // For types that also implement IJsonLoadableState, defer to their load implementation if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { Logger.ConditionalTrace( "Loading {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType ); // Value must be non-null if (value is null) { throw new InvalidOperationException( $"Property {property.Name} is IJsonLoadableState but value to be loaded is null" ); } // Check if the current object at this property is null if (property.GetValue(this) is not IJsonLoadableState propertyValue) { // If null, it must have a default constructor if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo) { throw new InvalidOperationException( $"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor" ); } // Create a new instance and set it propertyValue = (IJsonLoadableState)constructorInfo.Invoke(null); property.SetValue(this, propertyValue); } // Load the state from the JSON object propertyValue.LoadStateFromJsonObject(value.AsObject()); } else { Logger.ConditionalTrace("Loading {Property} ({Type})", property.Name, property.PropertyType); var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions); property.SetValue(this, propertyValue); } } } /// /// Saves the state of this view model to a JSON object. /// The default implementation uses reflection to /// save all properties that are: /// /// Public /// Not read-only /// Not marked with [JsonIgnore] /// Not a type within the SerializerIgnoredTypes /// Not a name within the SerializerIgnoredNames /// /// public virtual JsonObject SaveStateToJsonObject() { // Get all of our properties using reflection. var properties = GetType().GetProperties(); Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length); // Create a JSON object to store the state. var state = new JsonObject(); // Serialize each property marked with JsonIncludeAttribute. foreach (var property in properties) { if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } var name = property.Name; // If JsonPropertyName provided, use that as the key. if ( property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault() is JsonPropertyNameAttribute jsonPropertyName ) { Logger.ConditionalTrace( "Serializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", property.Name, property.PropertyType, jsonPropertyName.Name ); name = jsonPropertyName.Name; } // For types that also implement IJsonLoadableState, defer to their implementation. if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { Logger.ConditionalTrace( "Serializing {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType ); var value = property.GetValue(this); if (value is not null) { var model = (IJsonLoadableState)value; var modelState = model.SaveStateToJsonObject(); state.Add(name, modelState); } } else { Logger.ConditionalTrace( "Serializing {Property} ({Type})", property.Name, property.PropertyType ); var value = property.GetValue(this); if (value is not null) { state.Add(name, JsonSerializer.SerializeToNode(value, SerializerOptions)); } } } return state; } public virtual void LoadStateFromJsonObject(JsonObject state, int version) { LoadStateFromJsonObject(state); } /// /// Serialize a model to a JSON object. /// protected static JsonObject SerializeModel(T model) { var node = JsonSerializer.SerializeToNode(model); return node?.AsObject() ?? throw new NullReferenceException("Failed to serialize state to JSON object."); } /// /// Deserialize a model from a JSON object. /// protected static T DeserializeModel(JsonObject state) { return state.Deserialize() ?? throw new NullReferenceException("Failed to deserialize state from JSON object."); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs ================================================ using FluentAvalonia.UI.Controls; namespace StabilityMatrix.Avalonia.ViewModels.Base; /// /// An abstract class for enabling page navigation. /// public abstract class PageViewModelBase : DisposableViewModelBase { /// /// Gets if the user can navigate to the next page /// public virtual bool CanNavigateNext { get; protected set; } /// /// Gets if the user can navigate to the previous page /// public virtual bool CanNavigatePrevious { get; protected set; } public abstract string Title { get; } public abstract IconSource IconSource { get; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/PausableProgressItemViewModelBase.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels.Base; [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] public abstract partial class PausableProgressItemViewModelBase : ProgressItemViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor( nameof(IsPaused), nameof(IsCompleted), nameof(CanPauseResume), nameof(CanCancel) )] private ProgressState state = ProgressState.Inactive; /// /// Whether the progress is paused /// public bool IsPaused => State is ProgressState.Inactive or ProgressState.Paused; public bool IsPending => State == ProgressState.Pending; /// /// Whether the progress has succeeded, failed or was cancelled /// public override bool IsCompleted => State is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled; public virtual bool SupportsPauseResume => true; public virtual bool SupportsCancel => true; public bool CanPauseResume => SupportsPauseResume && !IsCompleted && !IsPending; public bool CanCancel => SupportsCancel && !IsCompleted; private AsyncRelayCommand? pauseCommand; public IAsyncRelayCommand PauseCommand => pauseCommand ??= new AsyncRelayCommand(Pause); public virtual Task Pause() => Task.CompletedTask; private AsyncRelayCommand? resumeCommand; public IAsyncRelayCommand ResumeCommand => resumeCommand ??= new AsyncRelayCommand(Resume); public virtual Task Resume() => Task.CompletedTask; private AsyncRelayCommand? cancelCommand; public IAsyncRelayCommand CancelCommand => cancelCommand ??= new AsyncRelayCommand(Cancel); public virtual Task Cancel() => Task.CompletedTask; [RelayCommand] private Task TogglePauseResume() { return IsPaused ? Resume() : Pause(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ProgressItemViewModelBase.cs ================================================ using System; using CommunityToolkit.Mvvm.ComponentModel; namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract partial class ProgressItemViewModelBase : ViewModelBase { [ObservableProperty] private Guid id; [ObservableProperty] private string? name; [ObservableProperty] private bool failed; public virtual bool IsCompleted => Progress.Value >= 100 || Failed; public ContentDialogProgressViewModelBase Progress { get; init; } = new(); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; namespace StabilityMatrix.Avalonia.ViewModels.Base; /// /// Generic view model for progress reporting. /// public partial class ProgressViewModel : DisposableViewModelBase { [ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))] private string? text; [ObservableProperty] private string? description; [ObservableProperty, NotifyPropertyChangedFor(nameof(IsProgressVisible))] private double value; [ObservableProperty] private double maximum = 100; [ObservableProperty, NotifyPropertyChangedFor(nameof(IsProgressVisible))] private bool isIndeterminate; [ObservableProperty, NotifyPropertyChangedFor(nameof(FormattedDownloadSpeed))] private double downloadSpeedInMBps; public string FormattedDownloadSpeed => $"{DownloadSpeedInMBps:0.00} MB/s"; public virtual bool IsProgressVisible => Value > 0 || IsIndeterminate; public virtual bool IsTextVisible => !string.IsNullOrWhiteSpace(Text); public void ClearProgress() { Value = 0; Text = null; IsIndeterminate = false; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/SelectableViewModelBase.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace StabilityMatrix.Avalonia.ViewModels.Base; public partial class SelectableViewModelBase : DisposableViewModelBase { [ObservableProperty] private bool isSelected; [RelayCommand] private void ToggleSelection() { IsSelected = !IsSelected; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs ================================================ namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract class TabViewModelBase : DisposableViewModelBase { public abstract string Header { get; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/TaskDialogViewModelBase.cs ================================================ using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Languages; namespace StabilityMatrix.Avalonia.ViewModels.Base; /// /// Base class for view models that are used in /// public abstract class TaskDialogViewModelBase : ViewModelBase { private TaskDialog? dialog; public virtual string? Title { get; set; } protected static TaskDialogCommand GetCommandButton(string text, ICommand command) { return new TaskDialogCommand { Text = text, DialogResult = TaskDialogStandardResult.None, Command = command, IsDefault = true, ClosesOnInvoked = false }; } protected static TaskDialogButton GetCloseButton() { return new TaskDialogButton { Text = Resources.Action_Close, DialogResult = TaskDialogStandardResult.Close }; } protected static TaskDialogButton GetCloseButton(string text) { return new TaskDialogButton { Text = text, DialogResult = TaskDialogStandardResult.Close }; } /// /// Return a that uses this view model as its content /// public virtual TaskDialog GetDialog() { Dispatcher.UIThread.VerifyAccess(); dialog = new TaskDialog { Header = Title, Content = this, XamlRoot = App.VisualRoot, Buttons = { GetCloseButton() } }; dialog.AttachedToVisualTree += (s, _) => { ((TaskDialog)s!).Closing += OnDialogClosing; }; dialog.DetachedFromVisualTree += (s, _) => { ((TaskDialog)s!).Closing -= OnDialogClosing; }; return dialog; } /// /// Show the dialog from and return the result /// public async Task ShowDialogAsync() { return (TaskDialogStandardResult)await GetDialog().ShowAsync(true); } protected void CloseDialog(TaskDialogStandardResult result) { dialog?.Hide(result); } protected virtual async void OnDialogClosing(object? sender, TaskDialogClosingEventArgs e) { } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Base/ViewModelBase.cs ================================================ using System; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using JetBrains.Annotations; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.ViewModels.Base; public partial class ViewModelBase : ObservableValidator, IRemovableListItem { [PublicAPI] protected ViewModelState ViewModelState { get; private set; } private WeakEventManager? parentListRemoveRequestedEventManager; public event EventHandler ParentListRemoveRequested { add { parentListRemoveRequestedEventManager ??= new WeakEventManager(); parentListRemoveRequestedEventManager.AddEventHandler(value); } remove => parentListRemoveRequestedEventManager?.RemoveEventHandler(value); } [RelayCommand] protected void RemoveFromParentList() => parentListRemoveRequestedEventManager?.RaiseEvent( this, EventArgs.Empty, nameof(ParentListRemoveRequested) ); /// /// Called when the view's LoadedEvent is fired. /// public virtual void OnLoaded() { if (!ViewModelState.HasFlag(ViewModelState.InitialLoaded)) { ViewModelState |= ViewModelState.InitialLoaded; OnInitialLoaded(); Dispatcher.UIThread.InvokeAsync(OnInitialLoadedAsync).SafeFireAndForget(); } } /// /// Called the first time the view's LoadedEvent is fired. /// Sets the flag. /// protected virtual void OnInitialLoaded() { } /// /// Called asynchronously when the view's LoadedEvent is fired. /// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync. /// The view loading will not wait for this to complete. /// public virtual Task OnLoadedAsync() => Task.CompletedTask; /// /// Called the first time the view's LoadedEvent is fired. /// Sets the flag. /// protected virtual Task OnInitialLoadedAsync() => Task.CompletedTask; /// /// Called when the view's UnloadedEvent is fired. /// public virtual void OnUnloaded() { } /// /// Called asynchronously when the view's UnloadedEvent is fired. /// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync. /// The view loading will not wait for this to complete. /// public virtual Task OnUnloadedAsync() => Task.CompletedTask; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs ================================================ using System.Text.RegularExpressions; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [ManagedService] [RegisterTransient] public partial class CheckpointBrowserCardViewModel : ProgressViewModel { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IDownloadService downloadService; private readonly ITrackedDownloadService trackedDownloadService; private readonly ISettingsManager settingsManager; private readonly IServiceManager dialogFactory; private readonly INotificationService notificationService; private readonly IModelIndexService modelIndexService; private readonly IModelImportService modelImportService; private readonly ILiteDbContext liteDbContext; private readonly CivitCompatApiManager civitApi; private readonly INavigationService navigationService; public Action? OnDownloadStart { get; set; } public CivitModel CivitModel { get => civitModel; set { civitModel = value; IsFavorite = settingsManager.Settings.FavoriteModels.Contains(value.Id); UpdateImage(); CheckIfInstalled(); } } private CivitModel civitModel; public int Order { get; set; } public override bool IsTextVisible => Value > 0; [ObservableProperty] private Uri? cardImage; [ObservableProperty] private bool isImporting; [ObservableProperty] private bool isLoading; [ObservableProperty] private string updateCardText = string.Empty; [ObservableProperty] private bool showUpdateCard; [ObservableProperty] private bool isFavorite; [ObservableProperty] private bool showSantaHats = true; public CheckpointBrowserCardViewModel( IDownloadService downloadService, ITrackedDownloadService trackedDownloadService, ISettingsManager settingsManager, IServiceManager dialogFactory, INotificationService notificationService, IModelIndexService modelIndexService, IModelImportService modelImportService, ILiteDbContext liteDbContext, CivitCompatApiManager civitApi, INavigationService navigationService ) { this.downloadService = downloadService; this.trackedDownloadService = trackedDownloadService; this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.notificationService = notificationService; this.modelIndexService = modelIndexService; this.modelImportService = modelImportService; this.liteDbContext = liteDbContext; this.civitApi = civitApi; this.navigationService = navigationService; // Update image when nsfw setting changes AddDisposable( settingsManager.RegisterPropertyChangedHandler( s => s.ModelBrowserNsfwEnabled, _ => Dispatcher.UIThread.Post(UpdateImage) ), settingsManager.RegisterPropertyChangedHandler( s => s.HideEarlyAccessModels, _ => Dispatcher.UIThread.Post(UpdateImage) ) ); ShowSantaHats = settingsManager.Settings.IsHolidayModeActive; } private void CheckIfInstalled() { if (Design.IsDesignMode) { UpdateCardText = "Installed"; ShowUpdateCard = true; return; } if (CivitModel.ModelVersions == null) return; var installedModels = modelIndexService.ModelIndexBlake3Hashes; if (installedModels.Count == 0) return; // check if latest version is installed var latestVersion = CivitModel.ModelVersions.FirstOrDefault(); if (latestVersion == null) return; var latestVersionInstalled = latestVersion.Files != null && latestVersion.Files.Any(file => file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && installedModels.Contains(file.Hashes.BLAKE3) ); // check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list var anyVersionInstalled = latestVersionInstalled || CivitModel.ModelVersions.Any(version => version.Files != null && version.Files.Any(file => file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && installedModels.Contains(file.Hashes.BLAKE3) ) ); UpdateCardText = latestVersionInstalled ? "Installed" : anyVersionInstalled ? "Update Available" : string.Empty; ShowUpdateCard = anyVersionInstalled; } private void UpdateImage() { var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; var hideEarlyAccessModels = settingsManager.Settings.HideEarlyAccessModels; var version = CivitModel.ModelVersions?.FirstOrDefault(v => !hideEarlyAccessModels || !v.IsEarlyAccess ); var images = version?.Images; // Try to find a valid image var image = images ?.Where(img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains) && img.Type == "image" ) .FirstOrDefault(image => nsfwEnabled || image.NsfwLevel <= 1); if (image != null) { CardImage = new Uri(image.Url); return; } // If no valid image found, use no image CardImage = Assets.NoImage; } [RelayCommand] private void OpenModel() { ProcessRunner.OpenUrl($"https://civitai.com/models/{CivitModel.Id}"); } [RelayCommand] private void ToggleFavorite() { if (settingsManager.Settings.FavoriteModels.Contains(CivitModel.Id)) { settingsManager.Transaction(s => s.FavoriteModels.Remove(CivitModel.Id)); } else { settingsManager.Transaction(s => s.FavoriteModels.Add(CivitModel.Id)); } IsFavorite = settingsManager.Settings.FavoriteModels.Contains(CivitModel.Id); } [RelayCommand] public void SearchAuthor() { EventManager.Instance.OnNavigateAndFindCivitAuthorRequested(CivitModel.Creator?.Username); } private async Task DoImport( CivitModel model, DirectoryPath downloadFolder, CivitModelVersion? selectedVersion = null, CivitFile? selectedFile = null ) { IsImporting = true; IsLoading = true; Text = "Downloading..."; OnDownloadStart?.Invoke(this); // Get latest version var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault(); if (modelVersion is null) { notificationService.Show( new Notification( "Model has no versions available", "This model has no versions available for download", NotificationType.Warning ) ); Text = "Unable to Download"; return; } // Get latest version file var modelFile = selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model); if (modelFile is null) { notificationService.Show( new Notification( "Model has no files available", "This model has no files available for download", NotificationType.Warning ) ); Text = "Unable to Download"; return; } await modelImportService.DoImport( model, downloadFolder, modelVersion, modelFile, onImportComplete: () => { Text = "Import Complete"; IsIndeterminate = false; Value = 100; CheckIfInstalled(); DelayedClearProgress(TimeSpan.FromMilliseconds(800)); return Task.CompletedTask; }, onImportCanceled: () => { Text = "Cancelled"; DelayedClearProgress(TimeSpan.FromMilliseconds(500)); return Task.CompletedTask; }, onImportFailed: () => { Text = "Download Failed"; DelayedClearProgress(TimeSpan.FromMilliseconds(800)); return Task.CompletedTask; } ); } private void DelayedClearProgress(TimeSpan delay) { Task.Delay(delay) .ContinueWith(_ => { Text = string.Empty; Value = 0; IsImporting = false; IsLoading = false; }); } [GeneratedRegex("<[^>]+>")] private static partial Regex HtmlRegex(); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs ================================================ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Reactive.Linq; using System.Text.Json; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; using NLog; using Refit; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; 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.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using ILogger = Microsoft.Extensions.Logging.ILogger; using Notification = Avalonia.Controls.Notifications.Notification; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [View(typeof(CivitAiBrowserPage))] [RegisterSingleton] public sealed partial class CivitAiBrowserViewModel : TabViewModelBase, IInfinitelyScroll { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly CivitCompatApiManager civitApi; private readonly ISettingsManager settingsManager; private readonly IServiceManager dialogFactory; private readonly ILiteDbContext liteDbContext; private readonly IConnectedServiceManager connectedServiceManager; private readonly INotificationService notificationService; private readonly ICivitBaseModelTypeService baseModelTypeService; private readonly INavigationService navigationService; private bool dontSearch = false; private readonly SourceCache, int> modelCache = new(static ov => ov.Value.Id); private const int TargetPageItemCount = 30; [ObservableProperty] private IObservableCollection modelCards = new ObservableCollectionExtended(); [ObservableProperty] private string searchQuery = string.Empty; [ObservableProperty] private bool showNsfw; [ObservableProperty] private bool showMainLoadingSpinner; [ObservableProperty] private CivitPeriod selectedPeriod = CivitPeriod.AllTime; [ObservableProperty] private CivitSortMode sortMode = CivitSortMode.HighestRated; [ObservableProperty] private CivitModelType selectedModelType = CivitModelType.Checkpoint; [ObservableProperty] private bool hasSearched; [ObservableProperty] private bool isIndeterminate; [ObservableProperty] private bool noResultsFound; [ObservableProperty] private string noResultsText = string.Empty; [ObservableProperty] private ObservableCollection selectedBaseModels = []; [ObservableProperty] private bool showSantaHats = true; [ObservableProperty] private string? nextPageCursor; [ObservableProperty] private bool hideInstalledModels; [ObservableProperty] private bool hideEarlyAccessModels; [ObservableProperty] [NotifyPropertyChangedFor(nameof(StatsResizeFactor))] private double resizeFactor; private readonly SourceCache baseModelCache = new(static s => s); [ObservableProperty] private IObservableCollection allBaseModels = new ObservableCollectionExtended(); [ObservableProperty] private bool civitUseDiscoveryApi; public bool UseLocalCache => true; public double StatsResizeFactor => Math.Clamp(ResizeFactor, 0.75d, 1.25d); public IEnumerable AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast(); public IEnumerable AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast(); public IEnumerable AllModelTypes => Enum.GetValues(typeof(CivitModelType)) .Cast() .Where(t => t == CivitModelType.All || t.ConvertTo() > 0) .OrderBy(t => t.ToString()); public string ClearButtonText => SelectedBaseModels.Count == AllBaseModels.Count ? Resources.Action_ClearSelection : Resources.Action_SelectAll; public bool ShowFilterNumber => SelectedBaseModels.Count > 0 && SelectedBaseModels.Count < AllBaseModels.Count; public CivitAiBrowserViewModel( CivitCompatApiManager civitApi, ISettingsManager settingsManager, IServiceManager dialogFactory, ILiteDbContext liteDbContext, IConnectedServiceManager connectedServiceManager, INotificationService notificationService, ICivitBaseModelTypeService baseModelTypeService, INavigationService navigationService ) { this.civitApi = civitApi; this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.liteDbContext = liteDbContext; this.connectedServiceManager = connectedServiceManager; this.notificationService = notificationService; this.baseModelTypeService = baseModelTypeService; this.navigationService = navigationService; EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; var filterPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(HideInstalledModels) or nameof(ShowNsfw) or nameof(HideEarlyAccessModels) ) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => (Func)FilterModelCardsPredicate) .StartWith(FilterModelCardsPredicate) .ObserveOn(SynchronizationContext.Current) .AsObservable(); var sortPredicate = SortExpressionComparer.Ascending(static x => x.Order ); AddDisposable( modelCache .Connect() .DeferUntilLoaded() .Transform(ov => dialogFactory.Get(vm => { vm.CivitModel = ov.Value; vm.Order = ov.Order; return vm; }) ) .DisposeMany() .Filter(filterPredicate) .SortAndBind(ModelCards, sortPredicate) .ObserveOn(SynchronizationContext.Current) .Subscribe() ); AddDisposable( baseModelCache .Connect() .DeferUntilLoaded() .Transform(baseModel => new BaseModelOptionViewModel { ModelType = baseModel, IsSelected = settingsManager.Settings.SelectedCivitBaseModels.Contains(baseModel), }) .SortAndBind( AllBaseModels, SortExpressionComparer.Ascending(m => m.ModelType) ) .WhenPropertyChanged(p => p.IsSelected) .ObserveOn(SynchronizationContext.Current) .Subscribe(next => { if (next.Sender.IsSelected) SelectedBaseModels.Add(next.Sender.ModelType); else SelectedBaseModels.Remove(next.Sender.ModelType); OnPropertyChanged(nameof(ClearButtonText)); OnPropertyChanged(nameof(SelectedBaseModels)); OnPropertyChanged(nameof(ShowFilterNumber)); }) ); if (Design.IsDesignMode) return; var settingsTransactionObservable = this.WhenPropertyChanged(x => x.SelectedBaseModels) .Throttle(TimeSpan.FromMilliseconds(50)) .Skip(1) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { if (!settingsManager.IsLibraryDirSet) return; settingsManager.Transaction(settings => settings.SelectedCivitBaseModels = SelectedBaseModels.ToList() ); if (!dontSearch) { TrySearchAgain().SafeFireAndForget(); } }); AddDisposable(settingsTransactionObservable); AddDisposable( settingsManager.RelayPropertyFor( this, model => model.ShowNsfw, settings => settings.ModelBrowserNsfwEnabled, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, model => model.HideInstalledModels, settings => settings.HideInstalledModelsInModelBrowser, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, model => model.ResizeFactor, settings => settings.CivitBrowserResizeFactor, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, model => model.HideEarlyAccessModels, settings => settings.HideEarlyAccessModels, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, model => model.CivitUseDiscoveryApi, settings => settings.CivitUseDiscoveryApi, true ) ); EventManager.Instance.NavigateAndFindCivitAuthorRequested += OnNavigateAndFindCivitAuthorRequested; } private void OnNavigateAndFindCivitAuthorRequested(object? sender, string? e) { if (string.IsNullOrWhiteSpace(e)) return; SearchQuery = $"@{e}"; SearchModelsCommand.ExecuteAsync(false).SafeFireAndForget(); } private void OnNavigateAndFindCivitModelRequested(object? sender, int e) { if (e <= 0) return; SearchQuery = $"$#{e}"; SearchModelsCommand.ExecuteAsync(false).SafeFireAndForget(); } 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, string.Empty ) ); searchOptions = settingsManager.Settings.ModelSearchOptions; } SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.AllTime; SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated; SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint; base.OnLoaded(); } protected override async Task OnInitialLoadedAsync() { if (Design.IsDesignMode) return; await base.OnInitialLoadedAsync(); if (settingsManager.Settings.AutoLoadCivitModels) { await SearchModelsCommand.ExecuteAsync(false); } } public override async Task OnLoadedAsync() { if (Design.IsDesignMode) return; var baseModels = await baseModelTypeService.GetBaseModelTypes(includeAllOption: false); baseModels = baseModels.Except(settingsManager.Settings.DisabledBaseModelTypes).ToList(); if (baseModels.Count == 0) { return; } dontSearch = true; baseModelCache.EditDiff(baseModels, static (a, b) => a.Equals(b, StringComparison.OrdinalIgnoreCase)); dontSearch = false; } /// /// Filter predicate for model cards /// private bool FilterModelCardsPredicate(CheckpointBrowserCardViewModel card) { if (HideInstalledModels && card.UpdateCardText == "Installed") return false; if ( HideEarlyAccessModels && card.CivitModel.ModelVersions != null && card.CivitModel.ModelVersions.All(x => x.Availability == "EarlyAccess") ) return false; return !card.CivitModel.Nsfw || ShowNsfw; } [RelayCommand] private async Task OnUseDiscoveryToggle() { if (CivitUseDiscoveryApi) { CivitUseDiscoveryApi = false; } else { if (!await connectedServiceManager.PromptEnableCivitUseDiscoveryApi()) return; CivitUseDiscoveryApi = true; } // Reset cache in case model differences Logger.Info("Toggled Discovery API, clearing cache"); await liteDbContext.CivitModels.DeleteAllAsync(); await liteDbContext.CivitModelVersions.DeleteAllAsync(); var items = await liteDbContext.CivitModelQueryCache.DeleteAllAsync(); Logger.Info("Deleted {Count} Civit model query cache entries", items); } /// /// Background update task /// private async Task CivitModelQuery(CivitModelsRequest request, bool isInfiniteScroll = false) { var timer = Stopwatch.StartNew(); var queryText = request.Query; var models = new List(); // Store original request for caching var originalRequestStr = JsonSerializer.Serialize(request); CivitModelsResponse? modelsResponse = null; try { if (!string.IsNullOrWhiteSpace(request.CommaSeparatedModelIds)) { // count IDs var ids = request.CommaSeparatedModelIds.Split(','); if (ids.Length > 100) { var idChunks = ids.Chunk(100); foreach (var chunk in idChunks) { request.CommaSeparatedModelIds = string.Join(",", chunk); request.Limit = 100; var chunkModelsResponse = await civitApi.GetModels(request); if (chunkModelsResponse.Items != null) { models.AddRange(chunkModelsResponse.Items); } } } else { modelsResponse = await civitApi.GetModels(request); models = modelsResponse.Items; } } else { // Auto-paginate via cursor until we fill the target page size or run out var collectedById = new HashSet(); var targetCount = request.Limit ?? TargetPageItemCount; var safetyGuard = 0; while (true) { var resp = await civitApi.GetModels(request); modelsResponse = resp; if (resp.Items != null) { foreach (var item in resp.Items) { if (collectedById.Add(item.Id)) { models.Add(item); } } } // Check how many items survive local filtering var filteredCount = models .Where(m => m.Type.ConvertTo() > 0) .Count(m => m.Mode == null); var next = resp.Metadata?.NextCursor; if (filteredCount >= targetCount || string.IsNullOrEmpty(next)) { break; } request.Cursor = next; if (++safetyGuard >= 10) { // Avoid unbounded looping on unexpected cursors break; } } } 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() > 0) .Where(m => m.Mode == null) .ToList(); var cacheNew = true; if (UseLocalCache) { // Database update calls will invoke `OnModelsUpdated` // Add to database await liteDbContext.UpsertCivitModelAsync(models); // Add as cache entry var originalRequest = JsonSerializer.Deserialize(originalRequestStr); cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync( new CivitModelQueryCacheEntry { Id = ObjectHash.GetMd5Guid(originalRequest), InsertedAt = DateTimeOffset.UtcNow, Request = request, Items = models, Metadata = modelsResponse?.Metadata, } ); } if (cacheNew) { var doesBaseModelTypeMatch = SelectedBaseModels.Count == 0 ? request.BaseModels == null || request.BaseModels.Length == 0 : SelectedBaseModels.SequenceEqual(request.BaseModels ?? []); var doesModelTypeMatch = SelectedModelType == CivitModelType.All ? request.Types == null || request.Types.Length == 0 : SelectedModelType == request.Types?.FirstOrDefault(); if (doesBaseModelTypeMatch && doesModelTypeMatch) { UpdateModelCards(models, isInfiniteScroll); } } NextPageCursor = modelsResponse?.Metadata?.NextCursor; } 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) { // Additional details var responseContent = e.Content ?? "[No Content]"; var responseCode = e.StatusCode; var responseCodeName = e.StatusCode.ToString(); Logger.Warn( e, "CivitAI query ApiException ({Request}), ({Code}: {Response})", request, responseCode, responseContent ); notificationService.Show( new Notification( "CivitAI can't be reached right now", $"Please try again in a few minutes. ({responseCode}: {responseCodeName})", NotificationType.Warning, expiration: TimeSpan.Zero, onClick: () => Dispatcher.UIThread.InvokeAsync(async () => await DialogHelper.CreateApiExceptionDialog(e).ShowAsync() ) ) ); } 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(); } } /// /// Updates model cards using api response object. /// private void UpdateModelCards(List? models, bool addCards = false) { if (models is null) { modelCache.Clear(); return; } var startIndex = modelCache.Count; var modelsToAdd = models.Select((m, i) => new OrderedValue(startIndex + i, m)); if (addCards) { var newModels = modelsToAdd.Where(x => !modelCache.Keys.Contains(x.Value.Id)); modelCache.AddOrUpdate(newModels); } else { modelCache.EditDiff(modelsToAdd, static (a, b) => a.Order == b.Order && a.Value.Id == b.Value.Id); } // Status update ShowMainLoadingSpinner = false; IsIndeterminate = false; HasSearched = true; } private string previousSearchQuery = string.Empty; [RelayCommand] private async Task SearchModels(bool isInfiniteScroll = false) { var timer = Stopwatch.StartNew(); if (SearchQuery != previousSearchQuery || !isInfiniteScroll) { // Reset page number previousSearchQuery = SearchQuery; NextPageCursor = null; } // Build request var modelRequest = new CivitModelsRequest { Limit = TargetPageItemCount + 20, // Fetch a few extra to account for local filtering Nsfw = "true", // Handled by local view filter Sort = SortMode, Period = SelectedPeriod, }; if (NextPageCursor != null) { modelRequest.Cursor = NextPageCursor; } if (SelectedModelType != CivitModelType.All) { modelRequest.Types = [SelectedModelType]; } if (SelectedBaseModels.Count > 0 && SelectedBaseModels.Count < AllBaseModels.Count) { modelRequest.BaseModels = SelectedBaseModels.ToArray(); } if (SearchQuery.StartsWith("#")) { modelRequest.Tag = SearchQuery[1..]; } else if (SearchQuery.StartsWith("@")) { modelRequest.Username = SearchQuery[1..]; } else if (SearchQuery.StartsWith("$#")) { modelRequest.Period = CivitPeriod.AllTime; modelRequest.BaseModels = null; modelRequest.Types = null; modelRequest.CommaSeparatedModelIds = SearchQuery[2..]; if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed) { SortMode = CivitSortMode.HighestRated; modelRequest.Sort = CivitSortMode.HighestRated; } } else if (SearchQuery.StartsWith("https://civitai.com/models/")) { /* extract model ID from URL, could be one of: https://civitai.com/models/443821?modelVersionId=1957537 https://civitai.com/models/443821/cyberrealistic-pony https://civitai.com/models/443821 */ var modelId = SearchQuery .Replace("https://civitai.com/models/", string.Empty) .Split(['?', '/'], StringSplitOptions.RemoveEmptyEntries) .FirstOrDefault(); modelRequest.Period = CivitPeriod.AllTime; modelRequest.BaseModels = null; modelRequest.Types = null; modelRequest.CommaSeparatedModelIds = modelId; if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed) { SortMode = CivitSortMode.HighestRated; modelRequest.Sort = CivitSortMode.HighestRated; } } else { modelRequest.Query = SearchQuery; } if (SortMode == CivitSortMode.Installed) { var connectedModels = await liteDbContext.LocalModelFiles.FindAsync(m => m.ConnectedModelInfo != null ); connectedModels = connectedModels.Where(x => x.HasCivitMetadata); modelRequest.CommaSeparatedModelIds = string.Join( ",", connectedModels .Select(c => c.ConnectedModelInfo!.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; if (UseLocalCache) { cachedQuery = await liteDbContext.TryQueryWithClearOnExceptionAsync( liteDbContext.CivitModelQueryCache, liteDbContext .CivitModelQueryCache.IncludeAll() .FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)) ); } // 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 ); NextPageCursor = cachedQuery.Metadata?.NextCursor; UpdateModelCards(cachedQuery.Items, isInfiniteScroll); // 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, isInfiniteScroll).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, isInfiniteScroll); } UpdateResultsText(); } [RelayCommand] private void ClearOrSelectAllBaseModels() { if (SelectedBaseModels.Count == AllBaseModels.Count) AllBaseModels.ForEach(x => x.IsSelected = false); else AllBaseModels.ForEach(x => x.IsSelected = true); } [RelayCommand] private void ShowVersionDialog(CivitModel model) { var versions = model.ModelVersions; if (versions is null || versions.Count == 0) { notificationService.Show( new Notification( "Model has no versions available", "This model has no versions available for download", NotificationType.Warning ) ); return; } var newVm = dialogFactory.Get(vm => { var allModelIds = ModelCards.Select(x => x.CivitModel.Id).Distinct().ToList(); var index = ModelCards .Select((x, i) => (x.CivitModel.Id, Index: i)) .FirstOrDefault(x => x.Id == model.Id) .Index; vm.ModelIdList = allModelIds; vm.CurrentIndex = index; vm.CivitModel = model; return vm; }); navigationService.NavigateTo(newVm, BetterSlideNavigationTransition.PageSlideFromRight); } public void ClearSearchQuery() { SearchQuery = string.Empty; } public async Task LoadNextPageAsync() { if (NextPageCursor != null) { await SearchModelsCommand.ExecuteAsync(true); } } partial void OnSelectedPeriodChanged(CivitPeriod value) { TrySearchAgain().SafeFireAndForget(); settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(value, SortMode, SelectedModelType, string.Empty) ); NextPageCursor = null; } partial void OnSortModeChanged(CivitSortMode value) { TrySearchAgain().SafeFireAndForget(); settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( SelectedPeriod, value, SelectedModelType, string.Empty ) ); NextPageCursor = null; } partial void OnSelectedModelTypeChanged(CivitModelType value) { TrySearchAgain().SafeFireAndForget(); settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, value, string.Empty) ); NextPageCursor = null; } private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) { if (!HasSearched) return; modelCache.Clear(); if (shouldUpdatePageNumber) { NextPageCursor = null; } // execute command instead of calling method directly so that the IsRunning property gets updated await SearchModelsCommand.ExecuteAsync(false); } private void UpdateResultsText() { NoResultsFound = ModelCards?.Count <= 0; NoResultsText = "No results found"; } public override string Header => Resources.Label_CivitAi; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs ================================================ using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Reactive.Linq; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Refit; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.CivitTRPC; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [View(typeof(CivitDetailsPage))] [ManagedService] [RegisterTransient] public partial class CivitDetailsPageViewModel( ISettingsManager settingsManager, CivitCompatApiManager civitApi, ICivitTRPCApi civitTrpcApi, ILogger logger, INotificationService notificationService, INavigationService navigationService, IModelIndexService modelIndexService, IServiceManager vmFactory, IModelImportService modelImportService ) : DisposableViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowInferenceDefaultsSection))] public required partial CivitModel CivitModel { get; set; } [ObservableProperty] public required partial List ModelIdList { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanGoNext), nameof(CanGoPrevious))] public required partial int CurrentIndex { get; set; } private List ignoredFileNameFormatVars = [ "seed", "prompt", "negative_prompt", "model_hash", "sampler", "cfgscale", "steps", "width", "height", "project_type", "project_name", ]; public IEnumerable ModelFileNameFormatVars => FileNameFormatProvider .GetSampleForModelBrowser() .Substitutions.Where(kv => !ignoredFileNameFormatVars.Contains(kv.Key)) .Select(kv => new FileNameFormatVar { Variable = $"{{{kv.Key}}}", Example = kv.Value.Invoke() }); private SourceCache imageCache = new(x => x.Url); public IObservableCollection ImageSources { get; set; } = new ObservableCollectionExtended(); private SourceCache modelVersionCache = new(x => x.Id); public IObservableCollection ModelVersions { get; set; } = new ObservableCollectionExtended(); private SourceCache civitFileCache = new(x => x.Id); public IObservableCollection CivitFiles { get; set; } = new ObservableCollectionExtended(); [ObservableProperty] public partial ObservableCollection SelectedFiles { get; set; } = []; [ObservableProperty] [NotifyPropertyChangedFor( nameof(LastUpdated), nameof(ShortSha256), nameof(BaseModelType), nameof(ModelFileNameFormat), nameof(IsEarlyAccess) )] public partial ModelVersionViewModel? SelectedVersion { get; set; } [ObservableProperty] public partial string Description { get; set; } = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DescriptionRowSpan))] public partial string ModelVersionDescription { get; set; } = string.Empty; [ObservableProperty] public partial bool ShowNsfw { get; set; } [ObservableProperty] public partial bool HideEarlyAccess { get; set; } [ObservableProperty] public partial bool ShowTrainingData { get; set; } [ObservableProperty] public partial bool HideInstalledModels { get; set; } [ObservableProperty] public partial string SelectedInstallLocation { get; set; } = string.Empty; [ObservableProperty] public partial ObservableCollection AvailableInstallLocations { get; set; } = []; [ObservableProperty] [CustomValidation(typeof(CivitDetailsPageViewModel), nameof(ValidateModelFileNameFormat))] public partial string? ModelFileNameFormat { get; set; } [ObservableProperty] public partial string? ModelNameFormatSample { get; set; } [ObservableProperty] public partial SamplerCardViewModel SamplerCardViewModel { get; set; } = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.DenoiseStrength = 1.0d; samplerCard.EnableAddons = false; samplerCard.IsDenoiseStrengthEnabled = false; }); [ObservableProperty] public partial bool IsInferenceDefaultsEnabled { get; set; } = false; public string LastUpdated => SelectedVersion?.ModelVersion.PublishedAt?.ToString("g", CultureInfo.CurrentCulture) ?? string.Empty; public string ShortSha256 => SelectedVersion?.ModelVersion.Files?.FirstOrDefault()?.Hashes.ShortSha256 ?? string.Empty; public string BaseModelType => SelectedVersion?.ModelVersion.BaseModel?.Trim() ?? string.Empty; public bool IsEarlyAccess => SelectedVersion?.ModelVersion.IsEarlyAccess ?? false; public string CivitUrl => $@"https://civitai.com/models/{CivitModel.Id}"; public int DescriptionRowSpan => string.IsNullOrWhiteSpace(ModelVersionDescription) ? 3 : 1; public bool ShowInferenceDefaultsSection => CivitModel.Type == CivitModelType.Checkpoint; public bool CanGoNext => CurrentIndex < ModelIdList.Count - 1; public bool CanGoPrevious => CurrentIndex > 0; protected override async Task OnInitialLoadedAsync() { if ( !Design.IsDesignMode && (CivitModel.ModelVersions?.Select(x => x.Files).Any(x => x == null || x.Count == 0) ?? true) ) { try { CivitModel = await civitApi.GetModelById(CivitModel.Id); } catch (Exception e) { logger.LogError(e, "Failed to load CivitModel {Id}", CivitModel.Id); notificationService.Show( Resources.Label_UnexpectedErrorOccurred, e.Message, NotificationType.Error ); return; } } AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ShowNsfw, settings => settings.ModelBrowserNsfwEnabled, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.HideEarlyAccess, settings => settings.HideEarlyAccessModels, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ShowTrainingData, settings => settings.ShowTrainingDataInModelBrowser, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.HideInstalledModels, settings => settings.HideInstalledModelsInModelBrowser, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ModelFileNameFormat, settings => settings.CivitModelBrowserFileNamePattern, true ) ); AddDisposable( this.WhenPropertyChanged(vm => vm.ModelFileNameFormat) .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .Subscribe(formatProperty => { var provider = new FileNameFormatProvider { CivitModel = CivitModel, CivitModelVersion = SelectedVersion?.ModelVersion, CivitFile = SelectedVersion?.ModelVersion.Files?.FirstOrDefault(), }; var template = formatProperty.Value ?? string.Empty; if ( !string.IsNullOrEmpty(template) && provider.Validate(template) == ValidationResult.Success ) { var format = FileNameFormat.Parse(template, provider); ModelNameFormatSample = "Example: " + format.GetFileName() + ".safetensors"; } else { // Use default format if empty var defaultFormat = FileNameFormat.Parse( FileNameFormat.DefaultModelBrowserTemplate, provider ); ModelNameFormatSample = "Example: " + defaultFormat.GetFileName() + ".safetensors"; } }) ); var earlyAccessPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(HideEarlyAccess) or nameof(HideInstalledModels)) .Select(_ => (Func)ShouldIncludeVersion) .StartWith(ShouldIncludeVersion) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AddDisposable( modelVersionCache .Connect() .DeferUntilLoaded() .Transform(modelVersion => new ModelVersionViewModel(modelIndexService, modelVersion)) .DisposeMany() .Filter(earlyAccessPredicate) .SortAndBind( ModelVersions, SortExpressionComparer.Descending(v => v.ModelVersion.PublishedAt) ) .ObserveOn(SynchronizationContext.Current!) .DisposeMany() .Subscribe() ); var showNsfwPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(ShowNsfw)) .Select(_ => (Func)ShouldShowNsfw) .StartWith(ShouldShowNsfw) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AddDisposable( imageCache .Connect() .Filter(showNsfwPredicate) .Filter(img => img.Type == "image") .Transform(x => new ImageSource(new Uri(x.Url))) .Bind(ImageSources) .ObserveOn(SynchronizationContext.Current!) .DisposeMany() .Subscribe() ); var includeTrainingDataPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(ShowTrainingData)) .Select(_ => (Func)(ShouldIncludeCivitFile)) .StartWith(ShouldIncludeCivitFile) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AddDisposable( civitFileCache .Connect() .Filter(includeTrainingDataPredicate) .Transform(file => new CivitFileViewModel( modelIndexService, settingsManager, file, vmFactory, DownloadModelAsync ) { InstallLocations = new ObservableCollection(LoadInstallLocations(file)), }) .Bind(CivitFiles) .ObserveOn(SynchronizationContext.Current!) .DisposeMany() .Subscribe() ); modelVersionCache.EditDiff(CivitModel.ModelVersions ?? [], (a, b) => a.Id == b.Id); SelectedVersion = ModelVersions.FirstOrDefault(); Description = $"""{CivitModel.Description}"""; } [RelayCommand] private void GoBack() => navigationService.GoBack(); public async Task DownloadModelAsync(CivitFileViewModel viewModel, string? locationKey = null) { DirectoryPath? finalDestinationDir = null; var effectiveLocationKeyForPreference = string.Empty; switch (locationKey) { case null: { var preferenceUsed = false; if ( settingsManager.Settings.ModelTypeDownloadPreferences.TryGetValue( CivitModel.Type.ToString(), out var preference ) ) { if ( preference.SelectedInstallLocation == "Custom..." && !string.IsNullOrWhiteSpace(preference.CustomInstallLocation) ) { finalDestinationDir = new DirectoryPath(preference.CustomInstallLocation); effectiveLocationKeyForPreference = "Custom..."; preferenceUsed = true; } else if ( !string.IsNullOrWhiteSpace(preference.SelectedInstallLocation) && viewModel.InstallLocations.Contains(preference.SelectedInstallLocation) ) { var basePath = new DirectoryPath(settingsManager.ModelsDirectory); finalDestinationDir = new DirectoryPath( Path.Combine( basePath.ToString(), preference .SelectedInstallLocation.Replace("Models\\", "") .Replace("Models/", "") ) ); effectiveLocationKeyForPreference = preference.SelectedInstallLocation; preferenceUsed = true; } } if (!preferenceUsed) { finalDestinationDir = GetSharedFolderPath( settingsManager.ModelsDirectory, viewModel.CivitFile.Type, CivitModel.Type, CivitModel.BaseModelType ); effectiveLocationKeyForPreference = viewModel.InstallLocations.FirstOrDefault(loc => loc != "Custom..." && finalDestinationDir .ToString() .Contains(loc.Replace("Models\\", "").Replace("Models/", "")) ) ?? viewModel.InstallLocations.First(); } break; } case "Custom...": { effectiveLocationKeyForPreference = "Custom..."; var files = await App.StorageProvider.OpenFolderPickerAsync( new FolderPickerOpenOptions { Title = "Select Download Folder", AllowMultiple = false, SuggestedStartLocation = await App.StorageProvider.TryGetFolderFromPathAsync( Path.Combine( settingsManager.ModelsDirectory, CivitModel.Type.ConvertTo().GetStringValue() ) ), } ); if (files.FirstOrDefault()?.TryGetLocalPath() is { } customPath) { finalDestinationDir = new DirectoryPath(customPath); } else { return; } break; } default: { effectiveLocationKeyForPreference = locationKey; var basePath = new DirectoryPath(settingsManager.ModelsDirectory); finalDestinationDir = new DirectoryPath( Path.Combine( basePath.ToString(), locationKey.Replace("Models\\", "").Replace("Models/", "") ) ); break; } } if (finalDestinationDir is null) { notificationService.Show( Resources.Label_UnexpectedErrorOccurred, "Could not determine final destination directory.", NotificationType.Error ); return; } var fileNameOverride = ParseFileNameFormat( CivitModel, SelectedVersion?.ModelVersion, viewModel.CivitFile ); await modelImportService.DoImport( CivitModel, finalDestinationDir, SelectedVersion?.ModelVersion, viewModel.CivitFile, fileNameOverride, inferenceDefaults: IsInferenceDefaultsEnabled ? SamplerCardViewModel : null ); notificationService.Show( Resources.Label_DownloadStarted, string.Format( Resources.Label_DownloadWillBeSavedToLocation, viewModel.CivitFile.Name, finalDestinationDir.JoinFile(fileNameOverride).Directory ) ); if (CivitModel.Type != CivitModelType.Unknown) { var modelTypeKey = CivitModel.Type.ToString(); var newPreference = new LastDownloadLocationInfo { SelectedInstallLocation = effectiveLocationKeyForPreference, CustomInstallLocation = (effectiveLocationKeyForPreference == "Custom...") ? finalDestinationDir.ToString() : null, }; settingsManager.Transaction(s => { s.ModelTypeDownloadPreferences[modelTypeKey] = newPreference; }); } } [RelayCommand] private async Task ShowBulkDownloadDialogAsync() { var dialogVm = vmFactory.Get(vm => vm.Model = CivitModel); var dialog = dialogVm.GetDialog(); var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; foreach (var file in dialogVm.FilesToDownload) { var sharedFolderPath = GetSharedFolderPath( new DirectoryPath(settingsManager.ModelsDirectory), file.FileViewModel.CivitFile.Type, CivitModel.Type, CivitModel.BaseModelType ); var folderName = Path.GetInvalidFileNameChars() .Aggregate(CivitModel.Name, (current, c) => current.Replace(c, '_')); var destinationDir = new DirectoryPath(sharedFolderPath, folderName); destinationDir.Create(); var fileNameOverride = ParseFileNameFormat( CivitModel, file.ModelVersion, file.FileViewModel.CivitFile ); await modelImportService.DoImport( CivitModel, destinationDir, file.ModelVersion, file.FileViewModel.CivitFile, fileNameOverride ); } notificationService.Show( Resources.Label_BulkDownloadStarted, string.Format(Resources.Label_BulkDownloadStartedMessage, dialogVm.FilesToDownload.Count), NotificationType.Success ); } [RelayCommand] private async Task ShowImageDialog(ImageSource? image) { if (image is null) return; var currentIndex = ImageSources.IndexOf(image); // Preload await image.GetBitmapAsync(); var vm = vmFactory.Get(); vm.ImageSource = image; var url = image.RemoteUrl; if (url is null) return; try { var imageId = Path.GetFileNameWithoutExtension(url.Segments.Last()); var imageData = await civitTrpcApi.GetImageGenerationData($$$"""{"json":{"id":{{{imageId}}}}}"""); vm.CivitImageMetadata = imageData.Result.Data.Json; vm.CivitImageMetadata.OtherMetadata = GetOtherMetadata(vm.CivitImageMetadata); } catch (Exception e) { logger.LogError(e, "Failed to load CivitImageMetadata for {Url}", url); } using var onNext = Observable .FromEventPattern( vm, nameof(ImageViewerViewModel.NavigationRequested) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(ctx => { Dispatcher .UIThread.InvokeAsync(async () => { var sender = (ImageViewerViewModel)ctx.Sender!; var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1); if (newIndex >= 0 && newIndex < ImageSources.Count) { var newImageSource = ImageSources[newIndex]; // Preload await newImageSource.GetBitmapAsync(); await newImageSource.GetOrRefreshTemplateKeyAsync(); sender.ImageSource = newImageSource; try { sender.CivitImageMetadata = null; if (newImageSource.RemoteUrl is not { } newUrl) return; var imageId = Path.GetFileNameWithoutExtension(newUrl.Segments.Last()); var imageData = await civitTrpcApi.GetImageGenerationData( $$$"""{"json":{"id":{{{imageId}}}}}""" ); imageData.Result.Data.Json.OtherMetadata = GetOtherMetadata( imageData.Result.Data.Json ); sender.CivitImageMetadata = imageData.Result.Data.Json; } catch (Exception e) { logger.LogError(e, "Failed to load CivitImageMetadata for {Url}", url); } currentIndex = newIndex; } }) .SafeFireAndForget(); }); vm.NavigateToModelRequested += VmOnNavigateToModelRequested; await vm.GetDialog().ShowAsync(); } [RelayCommand] private void SearchByAuthor() { navigationService.GoBack(); EventManager.Instance.OnNavigateAndFindCivitAuthorRequested(CivitModel.Creator?.Username); } [RelayCommand] private async Task DeleteModelVersion(CivitModelVersion modelVersion) { if (modelVersion.Files == null) return; var pathsToDelete = new List(); foreach (var file in modelVersion.Files) { if (file is not { Type: CivitFileType.Model, Hashes.BLAKE3: not null }) continue; var matchingModels = (await modelIndexService.FindByHashAsync(file.Hashes.BLAKE3)).ToList(); if (matchingModels.Count == 0) { await modelIndexService.RefreshIndex(); matchingModels = (await modelIndexService.FindByHashAsync(file.Hashes.BLAKE3)).ToList(); } if (matchingModels.Count == 0) { logger.LogWarning( "No matching models found for file {FileName} with hash {Hash}", file.Name, file.Hashes.BLAKE3 ); continue; } foreach (var localModel in matchingModels) { var checkpointPath = new FilePath(localModel.GetFullPath(settingsManager.ModelsDirectory)); if (File.Exists(checkpointPath)) { pathsToDelete.Add(checkpointPath); } var previewPath = localModel.GetPreviewImageFullPath(settingsManager.ModelsDirectory); if (File.Exists(previewPath)) { pathsToDelete.Add(previewPath); } var cmInfoPath = checkpointPath.ToString().Replace(checkpointPath.Extension, ".cm-info.json"); if (File.Exists(cmInfoPath)) { pathsToDelete.Add(cmInfoPath); } } } var confirmDeleteVm = vmFactory.Get(); confirmDeleteVm.PathsToDelete = pathsToDelete; var dialog = confirmDeleteVm.GetDialog(); var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { logger.LogError(e, "Failed to delete model files for {ModelName}", modelVersion.Name); } finally { await modelIndexService.RefreshIndex(); } } [RelayCommand] public Task NextModel() => CanGoNext ? NavigateToModelByIndexOffset(1) : Task.CompletedTask; [RelayCommand] public Task PreviousModel() => CanGoPrevious ? NavigateToModelByIndexOffset(-1) : Task.CompletedTask; private async Task NavigateToModelByIndexOffset(int offset) { var newIndex = CurrentIndex + offset; var modelId = ModelIdList[newIndex]; try { var newModel = await civitApi.GetModelById(modelId); CivitModel = newModel; CurrentIndex = newIndex; // reload caches for new model modelVersionCache.EditDiff(CivitModel.ModelVersions ?? [], (a, b) => a.Id == b.Id); SelectedVersion = ModelVersions.FirstOrDefault(); imageCache.EditDiff(SelectedVersion?.ModelVersion.Images ?? [], (a, b) => a.Url == b.Url); civitFileCache.EditDiff(SelectedVersion?.ModelVersion.Files ?? [], (a, b) => a.Id == b.Id); Description = $"""{CivitModel.Description}"""; ModelVersionDescription = string.IsNullOrWhiteSpace(SelectedVersion?.ModelVersion.Description) ? string.Empty : $"""{SelectedVersion.ModelVersion.Description}"""; } catch (Exception e) { logger.LogError(e, "Failed to load CivitModel {Id}", modelId); notificationService.Show( Resources.Label_UnexpectedErrorOccurred, e.Message, NotificationType.Error ); } } private void VmOnNavigateToModelRequested(object? sender, int modelId) { if (sender is not ImageViewerViewModel vm) return; var detailsPageVm = vmFactory.Get(x => x.CivitModel = new CivitModel { Id = modelId } ); navigationService.NavigateTo(detailsPageVm, BetterSlideNavigationTransition.PageSlideFromRight); vm.NavigateToModelRequested -= VmOnNavigateToModelRequested; vm.OnCloseButtonClick(); } private bool ShouldIncludeCivitFile(CivitFile file) { if (ShowTrainingData) return true; return file.Type is CivitFileType.Model or CivitFileType.PrunedModel or CivitFileType.VAE; } partial void OnSelectedVersionChanged(ModelVersionViewModel? value) { imageCache.EditDiff(value?.ModelVersion.Images ?? [], (a, b) => a.Url == b.Url); civitFileCache.EditDiff(value?.ModelVersion.Files ?? [], (a, b) => a.Id == b.Id); SelectedFiles = new ObservableCollection([CivitFiles.FirstOrDefault()]); ModelVersionDescription = string.IsNullOrWhiteSpace(value?.ModelVersion.Description) ? string.Empty : $"""{value.ModelVersion.Description}"""; } public override void OnUnloaded() { ModelVersions.ForEach(x => x.Dispose()); CivitFiles.ForEach(x => x.Dispose()); Dispose(true); base.OnUnloaded(); } public static ValidationResult ValidateModelFileNameFormat(string? format, ValidationContext context) { return FileNameFormatProvider.GetSampleForModelBrowser().Validate(format ?? string.Empty); } private bool ShouldShowNsfw(CivitImage? image) { if (Design.IsDesignMode) return true; if (image == null) return false; return image.NsfwLevel switch { null or <= 1 => true, _ => ShowNsfw, }; } private bool ShouldIncludeVersion(ModelVersionViewModel? versionVm) { if (Design.IsDesignMode) return true; if (versionVm == null) return false; var version = versionVm.ModelVersion; if (HideInstalledModels && versionVm.IsInstalled) return false; return !version.IsEarlyAccess || !HideEarlyAccess; } private ObservableCollection LoadInstallLocations(CivitFile selectedFile) { if (Design.IsDesignMode) return ["Models/StableDiffusion", "Custom..."]; var installLocations = new List(); var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var downloadDirectory = GetSharedFolderPath( rootModelsDirectory, selectedFile.Type, CivitModel.Type, CivitModel.BaseModelType ); if (!downloadDirectory.ToString().EndsWith("Unknown")) { installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, downloadDirectory)) ); foreach ( var directory in downloadDirectory.EnumerateDirectories( "*", EnumerationOptionConstants.AllDirectories ) ) { installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, directory)) ); } } if (downloadDirectory.ToString().EndsWith(SharedFolderType.DiffusionModels.GetStringValue())) { // also add StableDiffusion in case we have an AIO version var stableDiffusionDirectory = rootModelsDirectory.JoinDir( SharedFolderType.StableDiffusion.GetStringValue() ); installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, stableDiffusionDirectory)) ); } installLocations.Add("Custom..."); return new ObservableCollection( installLocations.OrderBy(s => s.Replace(Path.DirectorySeparatorChar.ToString(), string.Empty)) ); } private static DirectoryPath GetSharedFolderPath( DirectoryPath rootModelsDirectory, CivitFileType? fileType, CivitModelType modelType, string? baseModelType ) { if (fileType is CivitFileType.VAE) { return rootModelsDirectory.JoinDir(SharedFolderType.VAE.GetStringValue()); } if ( modelType is CivitModelType.Checkpoint && ( baseModelType == CivitBaseModelType.Flux1D.GetStringValue() || baseModelType == CivitBaseModelType.Flux1S.GetStringValue() || baseModelType == CivitBaseModelType.WanVideo.GetStringValue() || baseModelType?.StartsWith("Wan", StringComparison.OrdinalIgnoreCase) is true || baseModelType?.StartsWith("Flux", StringComparison.OrdinalIgnoreCase) is true || baseModelType?.StartsWith("Hunyuan", StringComparison.OrdinalIgnoreCase) is true ) ) { return rootModelsDirectory.JoinDir(SharedFolderType.DiffusionModels.GetStringValue()); } return rootModelsDirectory.JoinDir(modelType.ConvertTo().GetStringValue()); } private IReadOnlyDictionary GetOtherMetadata(CivitImageGenerationDataResponse value) { var metaDict = new Dictionary(); if (value.Metadata?.CfgScale is not null) metaDict["CFG"] = value.Metadata.CfgScale.ToString(); if (value.Metadata?.Steps is not null) metaDict["Steps"] = value.Metadata.Steps.ToString(); if (value.Metadata?.Sampler is not null) metaDict["Sampler"] = value.Metadata.Sampler; if (value.Metadata?.Seed is not null) metaDict["Seed"] = value.Metadata.Seed.ToString(); if (value.Metadata?.ScheduleType is not null) metaDict["Scheduler"] = value.Metadata.ScheduleType; if (value.Metadata?.Scheduler is not null) metaDict["Scheduler"] = value.Metadata.Scheduler; if (value.Metadata?.Rng is not null) metaDict["RNG"] = value.Metadata.Rng; return metaDict; } private string ParseFileNameFormat( CivitModel? civitModel, CivitModelVersion? modelVersion, CivitFile? civitFile ) { var formatProvider = new FileNameFormatProvider { CivitModel = civitModel, CivitModelVersion = modelVersion, CivitFile = civitFile, }; // Parse to format if ( string.IsNullOrEmpty(ModelFileNameFormat) || !FileNameFormat.TryParse(ModelFileNameFormat, formatProvider, out var format) ) { // Fallback to default logger.LogWarning( "Failed to parse format template: {ModelFileNameFormat}, using default", ModelFileNameFormat ); format = FileNameFormat.Parse(FileNameFormat.DefaultModelBrowserTemplate, formatProvider); } return format.GetFileName(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs ================================================ 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; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; 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))] [RegisterSingleton] public partial class HuggingFacePageViewModel : TabViewModelBase { private readonly ITrackedDownloadService trackedDownloadService; private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; public SourceCache ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName); public IObservableCollection Categories { get; set; } = new ObservableCollectionExtended(); public string DownloadPercentText => Math.Abs(TotalProgress.Percentage - 100f) < 0.001f ? "Download Complete" : $"Downloading {TotalProgress.Percentage:0}%"; [ObservableProperty] private int numSelected; private ConcurrentDictionary 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, Design.IsDesignMode ? string.Empty : settingsManager.ModelsDirectory ) { Title = g.Key.GetDescription() ?? g.Key.ToString() } ) .SortBy(vm => vm.Title ?? "") .Bind(Categories) .WhenAnyPropertyChanged() .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .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>( 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; } } public void Refresh() { ItemsCache.Clear(); OnLoaded(); } [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(); var fileName = Path.GetFileName(file); if ( fileName.Equals("ae.safetensors", StringComparison.OrdinalIgnoreCase) && viewModel.Item.ModelName == "HiDream I1 VAE" ) { fileName = "hidream_vae.safetensors"; } var downloadPath = new FilePath( Path.Combine( Design.IsDesignMode ? string.Empty : settingsManager.ModelsDirectory, sharedFolderType.ToString(), viewModel.Item.Subfolder ?? string.Empty, fileName ) ); downloadPath.Directory?.Create(); var download = trackedDownloadService.NewDownload(url, downloadPath); download.ProgressUpdate += DownloadOnProgressUpdate; download.ProgressStateChanged += (_, e) => { if (e == ProgressState.Success) { viewModel.NotifyExistsChanged(); } }; await trackedDownloadService.TryStartDownload(download); await Task.Delay(Random.Shared.Next(50, 100)); } viewModel.IsSelected = false; } 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(); ClearSelection(); 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; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/OpenModelDbBrowserCardViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Nito.Disposables.Internals; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.OpenModelsDb; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [Localizable(false)] public sealed class OpenModelDbBrowserCardViewModel(OpenModelDbManager openModelDbManager) { public OpenModelDbKeyedModel? Model { get; set; } public Uri? ModelUri => Model is { } model ? openModelDbManager.ModelsBaseUri.Append(model.Id) : null; public Uri? ThumbnailUri => Model?.Thumbnail?.GetImageAbsoluteUris().FirstOrDefault() ?? Model ?.Images ?.Select(image => image.GetImageAbsoluteUris().FirstOrDefault()) .WhereNotNull() .FirstOrDefault() ?? Assets.NoImage; public IEnumerable Tags => Model?.Tags?.Select(tagId => openModelDbManager.Tags?.GetValueOrDefault(tagId)).WhereNotNull() ?? []; public OpenModelDbArchitecture? Architecture => Model?.Architecture is { } architectureId ? openModelDbManager.Architectures?.GetValueOrDefault(architectureId) : null; public string? DisplayScale => Model?.Scale is { } scale ? $"{scale}x" : null; public string? DefaultAuthor { get { if (Model?.Author?.Value is string author) { return author; } if (Model?.Author?.Value is string[] { Length: > 0 } authorArray) { return authorArray.First(); } return null; } } public Uri? DefaultAuthorProfileUri => DefaultAuthor is { } author ? openModelDbManager.UsersBaseUri.Append(author) : null; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/OpenModelDbBrowserViewModel.Filters.cs ================================================ using System; using System.ComponentModel; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData.Binding; using StabilityMatrix.Core.Models.Api.OpenModelsDb; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; public partial class OpenModelDbBrowserViewModel { [ObservableProperty] private string? searchQuery; private Subject SearchQueryReload { get; } = new(); private IObservable> SearchQueryPredicate => SearchQueryReload .Select(pv => CreateSearchQueryPredicate(SearchQuery)) .StartWith(CreateSearchQueryPredicate(null)) .ObserveOn(SynchronizationContext.Current!) .AsObservable(); private IObservable> SortComparer => Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(SelectedSortOption)) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => GetSortComparer(SelectedSortOption)) .StartWith(GetSortComparer(SelectedSortOption)) .ObserveOn(SynchronizationContext.Current!) .AsObservable(); private static Func CreateSearchQueryPredicate(string? text) { if (string.IsNullOrWhiteSpace(text)) { return static _ => true; } return x => x.Name?.Contains(text, StringComparison.OrdinalIgnoreCase) == true || x.Tags?.Any(tag => tag.StartsWith(text, StringComparison.OrdinalIgnoreCase)) == true; } private static SortExpressionComparer GetSortComparer( string sortOption ) => sortOption switch { "Latest" => SortExpressionComparer.Descending(x => x.Model?.Date), "Largest Scale" => SortExpressionComparer.Descending(x => x.Model?.Scale), "Smallest Scale" => SortExpressionComparer.Ascending(x => x.Model?.Scale), "Largest Size" => SortExpressionComparer.Descending( x => x.Model?.Size?.FirstOrDefault() ), "Smallest Size" => SortExpressionComparer.Ascending( x => x.Model?.Size?.FirstOrDefault() ), _ => SortExpressionComparer.Descending(x => x.Model?.Date) }; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/OpenModelDbBrowserViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Apizr; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Fusillade; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.OpenModelsDb; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; [View(typeof(OpenModelDbBrowserPage))] [RegisterSingleton] public sealed partial class OpenModelDbBrowserViewModel( ILogger logger, IServiceManager vmManager, OpenModelDbManager openModelDbManager, INotificationService notificationService ) : TabViewModelBase { // ReSharper disable once LocalizableElement public override string Header => "OpenModelDB"; [ObservableProperty] private bool isLoading; [ObservableProperty] private string selectedSortOption = "Latest"; public SourceCache ModelCache { get; } = new(static x => x.Id); public IObservableCollection FilteredModelCards { get; } = new ObservableCollectionExtended(); public List SortOptions => ["Latest", "Largest Scale", "Smallest Scale", "Largest Size", "Smallest Size"]; protected override void OnInitialLoaded() { base.OnInitialLoaded(); ModelCache .Connect() .DeferUntilLoaded() .Filter(SearchQueryPredicate) .Transform(model => new OpenModelDbBrowserCardViewModel(openModelDbManager) { Model = model }) .SortAndBind(FilteredModelCards, SortComparer) .ObserveOn(SynchronizationContext.Current!) .Subscribe(); } [RelayCommand] private async Task SearchAsync() { try { await LoadDataAsync(); } catch (ApizrException e) { logger.LogWarning(e, "Failed to load models from OpenModelDB"); notificationService.ShowPersistent("Failed to load models from OpenModelDB", e.Message); } SearchQueryReload.OnNext(Unit.Default); } [RelayCommand] private async Task OpenModelCardAsync(OpenModelDbBrowserCardViewModel? card) { if (card?.Model is not { } model) { return; } var vm = vmManager.Get(); vm.Model = model; var dialog = vm.GetDialog(); dialog.MaxDialogHeight = 920; await dialog.ShowAsync(); } /// /// Populate the model cache from api. /// private async Task LoadDataAsync(Priority priority = default) { await openModelDbManager.EnsureMetadataLoadedAsync(); var response = await openModelDbManager.ExecuteAsync( api => api.GetModels(), options => options.WithPriority(priority) ); if (ModelCache.Count == 0) { ModelCache.Edit(innerCache => { innerCache.Load(response.GetKeyedModels()); }); } else { ModelCache.EditDiff(response.GetKeyedModels()); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs ================================================ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(CheckpointBrowserPage))] [RegisterSingleton] public partial class CheckpointBrowserViewModel : PageViewModelBase { public override string Title => Resources.Label_ModelBrowser; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IconVariant = IconVariant.Filled }; public IReadOnlyList Pages { get; } [ObservableProperty] private TabItem? selectedPage; /// public CheckpointBrowserViewModel( CivitAiBrowserViewModel civitAiBrowserViewModel, HuggingFacePageViewModel huggingFaceViewModel, OpenModelDbBrowserViewModel openModelDbBrowserViewModel ) { Pages = new List( new List( [civitAiBrowserViewModel, huggingFaceViewModel, openModelDbBrowserViewModel] ).Select(vm => new TabItem { Header = vm.Header, Content = vm }) ); SelectedPage = Pages.FirstOrDefault(); EventManager.Instance.NavigateAndFindCivitModelRequested += OnNavigateAndFindCivitModelRequested; } private void OnNavigateAndFindCivitModelRequested(object? sender, int e) { SelectedPage = Pages.FirstOrDefault(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointManager/BaseModelOptionViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager; public partial class BaseModelOptionViewModel : ObservableObject { [ObservableProperty] private bool isSelected; [ObservableProperty] private string modelType = string.Empty; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs ================================================ using System.Collections.Immutable; using System.ComponentModel; using Avalonia.Controls.Notifications; using Avalonia.Data; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; 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; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager; public partial class CheckpointFileViewModel : SelectableViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasEarlyAccessUpdateOnly))] [NotifyPropertyChangedFor(nameof(HasStandardUpdate))] private LocalModelFile checkpointFile; [ObservableProperty] private string thumbnailUri; [ObservableProperty] private ProgressReport progress; [ObservableProperty] private bool isLoading; [ObservableProperty] private long fileSize; [ObservableProperty] private string? noImageMessage; [ObservableProperty] private bool hideImage; [ObservableProperty] private DateTimeOffset lastModified; [ObservableProperty] private DateTimeOffset created; [ObservableProperty] public partial FilePath FullPath { get; set; } = string.Empty; private readonly ISettingsManager settingsManager; private readonly IModelIndexService modelIndexService; private readonly INotificationService notificationService; private readonly IDownloadService downloadService; private readonly IServiceManager vmFactory; private readonly ILogger logger; public bool CanShowTriggerWords => CheckpointFile.ConnectedModelInfo?.TrainedWords?.Length > 0; public string BaseModelName => CheckpointFile.ConnectedModelInfo?.BaseModel ?? string.Empty; public CivitModelType ModelType => CheckpointFile.ConnectedModelInfo?.ModelType ?? CivitModelType.Unknown; public bool HasEarlyAccessUpdateOnly => CheckpointFile.HasEarlyAccessUpdateOnly; public bool HasStandardUpdate => CheckpointFile.HasUpdate && !CheckpointFile.HasEarlyAccessUpdateOnly; /// public CheckpointFileViewModel( ISettingsManager settingsManager, IModelIndexService modelIndexService, INotificationService notificationService, IDownloadService downloadService, IServiceManager vmFactory, ILogger logger, LocalModelFile checkpointFile ) { this.settingsManager = settingsManager; this.modelIndexService = modelIndexService; this.notificationService = notificationService; this.downloadService = downloadService; this.vmFactory = vmFactory; this.logger = logger; CheckpointFile = checkpointFile; UpdateImage(); if (!settingsManager.IsLibraryDirSet) return; AddDisposable( settingsManager.RegisterPropertyChangedHandler( s => s.ShowNsfwInCheckpointsPage, _ => Dispatcher.UIThread.Post(UpdateImage) ) ); FileSize = GetFileSize(CheckpointFile.GetFullPath(settingsManager.ModelsDirectory)); LastModified = GetLastModified(CheckpointFile.GetFullPath(settingsManager.ModelsDirectory)); Created = GetCreated(CheckpointFile.GetFullPath(settingsManager.ModelsDirectory)); FullPath = new FilePath(CheckpointFile.GetFullPath(settingsManager.ModelsDirectory)); } [RelayCommand] private Task CopyTriggerWords() { if (!CheckpointFile.HasConnectedModel) return Task.CompletedTask; var words = CheckpointFile.ConnectedModelInfo.TrainedWordsString; return string.IsNullOrWhiteSpace(words) ? Task.CompletedTask : App.Clipboard.SetTextAsync(words); } [RelayCommand] private void FindOnModelBrowser() { if (CheckpointFile.ConnectedModelInfo?.ModelId == null) return; EventManager.Instance.OnNavigateAndFindCivitModelRequested( CheckpointFile.ConnectedModelInfo.ModelId.Value ); } [RelayCommand] [Localizable(false)] private void OpenOnCivitAi() { if (CheckpointFile.ConnectedModelInfo?.ModelId == null) return; ProcessRunner.OpenUrl($"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}"); } [RelayCommand] [Localizable(false)] private Task CopyModelUrl() { if (!CheckpointFile.HasConnectedModel) return Task.CompletedTask; return CheckpointFile.ConnectedModelInfo.Source switch { ConnectedModelSource.Civitai when CheckpointFile.ConnectedModelInfo.ModelId == null => Task.CompletedTask, ConnectedModelSource.Civitai when CheckpointFile.ConnectedModelInfo.ModelId != null => App.Clipboard.SetTextAsync( $"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}" ), ConnectedModelSource.OpenModelDb => App.Clipboard.SetTextAsync( $"https://openmodeldb.info/models/{CheckpointFile.ConnectedModelInfo.ModelName}" ), _ => Task.CompletedTask, }; } [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(report => { Progress = report; }); var cmInfo = await importService.GetMetadataForFile( CheckpointFile.GetFullPath(settingsManager.ModelsDirectory), progressReport, forceReimport ); if (cmInfo != null) { CheckpointFile.ConnectedModelInfo = cmInfo; var uri = CheckpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory) ?? cmInfo.ThumbnailImageUrl; if (string.IsNullOrWhiteSpace(uri)) { HideImage = true; NoImageMessage = Resources.Label_NoImageFound; } else { ThumbnailUri = uri; HideImage = false; } await modelIndexService.RefreshIndex(); } } finally { IsLoading = false; } } [RelayCommand] private async Task DeleteAsync(bool showConfirmation = true) { var pathsToDelete = CheckpointFile .GetDeleteFullPaths(settingsManager.ModelsDirectory) .ToImmutableArray(); if (pathsToDelete.IsEmpty) return; var confirmDeleteVm = vmFactory.Get(); confirmDeleteVm.PathsToDelete = pathsToDelete; if (showConfirmation) { if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) { return; } } await using var delay = new MinimumDelay(200, 500); IsLoading = true; Progress = new ProgressReport(0f, "Deleting..."); try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting files", e.Message, NotificationType.Error); await modelIndexService.RefreshIndex(); return; } finally { IsLoading = false; } await modelIndexService.RemoveModelAsync(CheckpointFile); } [RelayCommand] private async Task RenameAsync() { // Parent folder path var parentPath = Path.GetDirectoryName((string?)CheckpointFile.GetFullPath(settingsManager.ModelsDirectory)) ?? ""; var textFields = new TextBoxField[] { new() { Label = "File name", Validator = text => { if (string.IsNullOrWhiteSpace(text)) throw new DataValidationException("File name is required"); if (File.Exists(Path.Combine(parentPath, text))) throw new DataValidationException("File name already exists"); }, Text = CheckpointFile.FileName, }, }; var dialog = DialogHelper.CreateTextEntryDialog("Rename Model", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var name = textFields[0].Text; var nameNoExt = Path.GetFileNameWithoutExtension(name); var originalNameNoExt = Path.GetFileNameWithoutExtension(CheckpointFile.FileName); // Rename file in OS try { var newFilePath = Path.Combine(parentPath, name); File.Move(CheckpointFile.GetFullPath(settingsManager.ModelsDirectory), newFilePath); // If preview image exists, rename it too var previewPath = CheckpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory); if (previewPath != null && File.Exists(previewPath)) { var newPreviewImagePath = Path.Combine( parentPath, $"{nameNoExt}.preview{Path.GetExtension(previewPath)}" ); File.Move(previewPath, newPreviewImagePath); } // If connected model info exists, rename it too (.cm-info.json) if (CheckpointFile.HasConnectedModel) { var cmInfoPath = Path.Combine(parentPath, $"{originalNameNoExt}.cm-info.json"); if (File.Exists(cmInfoPath)) { File.Move(cmInfoPath, Path.Combine(parentPath, $"{nameNoExt}.cm-info.json")); } } await modelIndexService.RefreshIndex(); } catch (Exception e) { logger.LogError(e, "Failed to rename checkpoint file"); } } } [RelayCommand] private async Task OpenSafetensorMetadataViewer() { if ( !settingsManager.IsLibraryDirSet || new DirectoryPath(settingsManager.ModelsDirectory) is not { Exists: true } modelsDir ) { return; } SafetensorMetadata? metadata; try { var safetensorPath = CheckpointFile.GetFullPath(modelsDir); metadata = await SafetensorMetadata.ParseAsync(safetensorPath); } catch (Exception ex) { logger.LogWarning(ex, "Failed to parse safetensor metadata"); notificationService.Show( "No Metadata Found", "This safetensor file does not contain any embedded metadata.", NotificationType.Warning ); return; } var vm = vmFactory.Get(vm => { vm.ModelName = CheckpointFile.DisplayModelName; vm.Metadata = metadata; }); var dialog = vm.GetDialog(); dialog.MinDialogHeight = 800; dialog.MinDialogWidth = 700; dialog.CloseButtonText = "Close"; dialog.DefaultButton = ContentDialogButton.Close; await dialog.ShowAsync(); } [RelayCommand] private async Task OpenMetadataEditor() { var vm = vmFactory.Get(vm => { vm.CheckpointFiles = [this]; }); var dialog = vm.GetDialog(); dialog.MinDialogHeight = 800; dialog.MinDialogWidth = 700; dialog.IsPrimaryButtonEnabled = true; dialog.IsFooterVisible = true; dialog.PrimaryButtonText = "Save"; dialog.DefaultButton = ContentDialogButton.Primary; dialog.CloseButtonText = "Cancel"; var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; // not supported yet if (vm.IsEditingMultipleCheckpoints) return; try { var hasCmInfoAlready = CheckpointFile.HasConnectedModel; var cmInfo = CheckpointFile.ConnectedModelInfo ?? new ConnectedModelInfo(); var hasThumbnailChanged = vm.ThumbnailFilePath != CheckpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory); cmInfo.ModelName = vm.ModelName; cmInfo.ModelDescription = vm.ModelDescription; cmInfo.Nsfw = vm.IsNsfw; cmInfo.Tags = vm.Tags.Split(',').Select(x => x.Trim()).ToArray(); cmInfo.BaseModel = vm.BaseModelType; cmInfo.TrainedWords = string.IsNullOrWhiteSpace(vm.TrainedWords) ? null : vm.TrainedWords.Split(',').Select(x => x.Trim()).ToArray(); cmInfo.ThumbnailImageUrl = vm.ThumbnailFilePath; cmInfo.ModelType = vm.ModelType; cmInfo.VersionName = vm.VersionName; cmInfo.InferenceDefaults = vm.IsInferenceDefaultsEnabled ? new InferenceDefaults { Sampler = vm.SamplerCardViewModel.SelectedSampler, Scheduler = vm.SamplerCardViewModel.SelectedScheduler, Width = vm.SamplerCardViewModel.Width, Height = vm.SamplerCardViewModel.Height, CfgScale = vm.SamplerCardViewModel.CfgScale, Steps = vm.SamplerCardViewModel.Steps, } : null; var modelFilePath = new FilePath( Path.Combine(settingsManager.ModelsDirectory, CheckpointFile.RelativePath) ); IsLoading = true; Progress = new ProgressReport(0f, "Saving metadata...", isIndeterminate: true); if (!hasCmInfoAlready) { cmInfo.Hashes = new CivitFileHashes { BLAKE3 = await FileHash.GetBlake3Async( modelFilePath, new Progress(report => { Progress = report with { Title = "Calculating hash..." }; }) ), }; cmInfo.ImportedAt = DateTimeOffset.Now; } var modelFileName = modelFilePath.NameWithoutExtension; var modelFolder = modelFilePath.Directory ?? Path.Combine(settingsManager.ModelsDirectory, CheckpointFile.SharedFolderType.ToString()); await cmInfo.SaveJsonToDirectory(modelFolder, modelFileName); if (string.IsNullOrWhiteSpace(cmInfo.ThumbnailImageUrl)) return; if (File.Exists(cmInfo.ThumbnailImageUrl) && hasThumbnailChanged) { // delete existing preview image var existingPreviewPath = CheckpointFile.GetPreviewImageFullPath( settingsManager.ModelsDirectory ); if (existingPreviewPath != null && File.Exists(existingPreviewPath)) { File.Delete(existingPreviewPath); } var filePath = new FilePath(cmInfo.ThumbnailImageUrl); var previewPath = new FilePath( modelFolder, @$"{modelFileName}.preview{Path.GetExtension(cmInfo.ThumbnailImageUrl)}" ); await filePath.CopyToAsync(previewPath); } else if (cmInfo.ThumbnailImageUrl.StartsWith("http")) { var imageExtension = Path.GetExtension(cmInfo.ThumbnailImageUrl).TrimStart('.'); if (imageExtension is "jpg" or "jpeg" or "png" or "webp") { var imageDownloadPath = modelFilePath.Directory!.JoinFile( $"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}" ); var imageTask = downloadService.DownloadToFileAsync( cmInfo.ThumbnailImageUrl, imageDownloadPath, new Progress(report => { Progress = report with { Title = "Downloading image" }; }) ); await notificationService.TryAsync(imageTask, "Could not download preview image"); } } await modelIndexService.RefreshIndex(); notificationService.Show( "Metadata saved", "Metadata has been saved successfully", NotificationType.Success ); } catch (Exception e) { logger.LogError(e, "Failed to save metadata"); notificationService.Show("Failed to save metadata", e.Message, NotificationType.Error); } finally { IsLoading = false; } } [RelayCommand] private async Task OpenInExplorer() { if (string.IsNullOrWhiteSpace(FullPath)) return; await ProcessRunner.OpenFileBrowser(FullPath); } private long GetFileSize(string filePath) { if (!File.Exists(filePath)) return 0; var fileInfo = new FileInfo(filePath); return fileInfo.Length; } private DateTimeOffset GetLastModified(string filePath) { if (!File.Exists(filePath)) return DateTimeOffset.MinValue; var fileInfo = new FileInfo(filePath); return fileInfo.LastWriteTime; } private DateTimeOffset GetCreated(string filePath) { if (!File.Exists(filePath)) return DateTimeOffset.MinValue; var fileInfo = new FileInfo(filePath); return fileInfo.CreationTime; } private void UpdateImage() { if ( !settingsManager.Settings.ShowNsfwInCheckpointsPage && CheckpointFile.ConnectedModelInfo?.Nsfw == true ) { HideImage = true; NoImageMessage = Resources.Label_ImageHidden; } else { var previewPath = settingsManager.IsLibraryDirSet ? CheckpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory) ?? CheckpointFile.ConnectedModelInfo?.ThumbnailImageUrl : null; if (string.IsNullOrWhiteSpace(previewPath)) { HideImage = true; NoImageMessage = Resources.Label_NoImageFound; } else { ThumbnailUri = previewPath; HideImage = false; } } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs ================================================ using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; using System.Reactive.Linq; using Apizr; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Fusillade; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; 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.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using CheckpointSortMode = StabilityMatrix.Core.Models.CheckpointSortMode; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(CheckpointsPage))] [RegisterSingleton] public partial class CheckpointsPageViewModel( ILogger logger, ISettingsManager settingsManager, IModelIndexService modelIndexService, ModelFinder modelFinder, IDownloadService downloadService, INotificationService notificationService, IMetadataImportService metadataImportService, IModelImportService modelImportService, OpenModelDbManager openModelDbManager, IServiceManager dialogFactory, ICivitBaseModelTypeService baseModelTypeService, INavigationService navigationService ) : PageViewModelBase { public override string Title => Resources.Label_CheckpointManager; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Notebook, IconVariant = IconVariant.Filled }; private SourceCache categoriesCache = new(category => category.GetId()); public IObservableCollection Categories { get; set; } = new ObservableCollectionExtended(); public SourceCache ModelsCache { get; } = new(file => file.RelativePath); public IObservableCollection Models { get; set; } = new ObservableCollectionExtended(); [ObservableProperty] private bool showFolders = true; [ObservableProperty] private bool showModelsInSubfolders = true; [ObservableProperty] private CheckpointCategory? selectedCategory; [ObservableProperty] private string searchQuery = string.Empty; [ObservableProperty] private bool isImportAsConnectedEnabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(NumImagesSelected))] private int numItemsSelected; [ObservableProperty] private bool sortConnectedModelsFirst = true; [ObservableProperty] private CheckpointSortMode selectedSortOption = CheckpointSortMode.Title; public List SortOptions => Enum.GetValues().ToList(); [ObservableProperty] private ListSortDirection selectedSortDirection = ListSortDirection.Ascending; [ObservableProperty] private ProgressViewModel progress = new(); [ObservableProperty] private bool isLoading; [ObservableProperty] private bool isDragOver; [ObservableProperty] private ObservableCollection selectedBaseModels = []; [ObservableProperty] private bool dragMovesAllSelected = true; [ObservableProperty] private bool hideEmptyRootCategories; [ObservableProperty] private bool showNsfwImages; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ModelCardBottomResizeFactor))] private double resizeFactor; public double ModelCardBottomResizeFactor => Math.Clamp(ResizeFactor, 0.85d, 1.25d); public string ClearButtonText => SelectedBaseModels.Count == BaseModelOptions.Count ? Resources.Action_ClearSelection : Resources.Action_SelectAll; public List SortDirections => Enum.GetValues().ToList(); public string ModelsFolder => settingsManager.ModelsDirectory; public string NumImagesSelected => NumItemsSelected == 1 ? Resources.Label_OneImageSelected.Replace("images ", "") : string.Format(Resources.Label_NumImagesSelected, NumItemsSelected).Replace("images ", ""); private SourceCache BaseModelCache { get; } = new(s => s); public IObservableCollection BaseModelOptions { get; set; } = new ObservableCollectionExtended(); protected override async Task OnInitialLoadedAsync() { if (Design.IsDesignMode) return; await base.OnInitialLoadedAsync(); AddDisposable( BaseModelCache .Connect() .DeferUntilLoaded() .Transform(baseModel => new BaseModelOptionViewModel { ModelType = baseModel, IsSelected = settingsManager.Settings.SelectedBaseModels.Contains(baseModel), }) .SortAndBind( BaseModelOptions, SortExpressionComparer.Ascending(vm => vm.ModelType) ) .WhenPropertyChanged(p => p.IsSelected) .ObserveOn(SynchronizationContext.Current) .Subscribe(next => { switch (next.Sender.IsSelected) { case true when !SelectedBaseModels.Contains(next.Sender.ModelType): SelectedBaseModels.Add(next.Sender.ModelType); break; case false when SelectedBaseModels.Contains(next.Sender.ModelType): SelectedBaseModels.Remove(next.Sender.ModelType); break; } OnPropertyChanged(nameof(ClearButtonText)); OnPropertyChanged(nameof(SelectedBaseModels)); }) ); var settingsTransactionObservable = this.WhenPropertyChanged(x => x.SelectedBaseModels) .Throttle(TimeSpan.FromMilliseconds(50)) .Skip(1) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { settingsManager.Transaction(settings => settings.SelectedBaseModels = SelectedBaseModels.ToList() ); }); AddDisposable(settingsTransactionObservable); // Observable predicate from SearchQuery changes var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(100)) .Select(_ => (Func)( file => string.IsNullOrWhiteSpace(SearchQuery) || ( SearchQuery.StartsWith("#") && ( file.ConnectedModelInfo?.Tags.Contains( SearchQuery.Substring(1), StringComparer.OrdinalIgnoreCase ) ?? false ) ) || file.DisplayModelFileName.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || file.DisplayModelName.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || file.DisplayModelVersion.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || ( file.ConnectedModelInfo?.TrainedWordsString.Contains( SearchQuery, StringComparison.OrdinalIgnoreCase ) ?? false ) ) ) .ObserveOn(SynchronizationContext.Current) .AsObservable(); var filterPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(SelectedCategory) or nameof(ShowModelsInSubfolders) or nameof(SelectedBaseModels) ) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => (Func)FilterModels) .ObserveOn(SynchronizationContext.Current) .AsObservable(); var comparerObservable = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(SelectedSortOption) or nameof(SelectedSortDirection) or nameof(SortConnectedModelsFirst) ) .Select(_ => { var comparer = new SortExpressionComparer(); if (SortConnectedModelsFirst) comparer = comparer.ThenByDescending(vm => vm.CheckpointFile.HasConnectedModel); switch (SelectedSortOption) { case CheckpointSortMode.FileName: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.CheckpointFile.FileName) : comparer.ThenByDescending(vm => vm.CheckpointFile.FileName); break; case CheckpointSortMode.Title: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.CheckpointFile.DisplayModelName) : comparer.ThenByDescending(vm => vm.CheckpointFile.DisplayModelName); break; case CheckpointSortMode.BaseModel: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.CheckpointFile.ConnectedModelInfo?.BaseModel ) : comparer.ThenByDescending(vm => vm.CheckpointFile.ConnectedModelInfo?.BaseModel ); comparer = comparer.ThenByAscending(vm => vm.CheckpointFile.DisplayModelName); break; case CheckpointSortMode.SharedFolderType: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.CheckpointFile.SharedFolderType) : comparer.ThenByDescending(vm => vm.CheckpointFile.SharedFolderType); comparer = comparer.ThenByAscending(vm => vm.CheckpointFile.DisplayModelName); break; case CheckpointSortMode.UpdateAvailable: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.CheckpointFile.HasUpdate) : comparer.ThenByDescending(vm => vm.CheckpointFile.HasUpdate); comparer = comparer.ThenByAscending(vm => vm.CheckpointFile.DisplayModelName); comparer = comparer.ThenByDescending(vm => vm.CheckpointFile.DisplayModelVersion); break; case CheckpointSortMode.FileSize: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.FileSize) : comparer.ThenByDescending(vm => vm.FileSize); break; case CheckpointSortMode.Created: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.Created) : comparer.ThenByDescending(vm => vm.Created); break; case CheckpointSortMode.LastModified: comparer = SelectedSortDirection == ListSortDirection.Ascending ? comparer.ThenByAscending(vm => vm.LastModified) : comparer.ThenByDescending(vm => vm.LastModified); break; default: throw new ArgumentOutOfRangeException(); } return comparer; }) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AddDisposable( ModelsCache .Connect() .DeferUntilLoaded() .Filter(filterPredicate) .Filter(searchPredicate) .Transform(x => new CheckpointFileViewModel( settingsManager, modelIndexService, notificationService, downloadService, dialogFactory, logger, x )) .DisposeMany() .SortAndBind(Models, comparerObservable) .WhenPropertyChanged(p => p.IsSelected) .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { NumItemsSelected = Models.Count(o => o.IsSelected); }) ); var categoryFilterPredicate = Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName is nameof(HideEmptyRootCategories)) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => (Func)FilterCategories) .StartWith(FilterCategories) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AddDisposable( categoriesCache .Connect() .DeferUntilLoaded() .Filter(categoryFilterPredicate) .SortAndBind( Categories, SortExpressionComparer .Descending(x => x.Name == "All Models") .ThenByAscending(x => x.Name) ) .ObserveOn(SynchronizationContext.Current) .Subscribe() ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.IsImportAsConnectedEnabled, s => s.IsImportAsConnected, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ResizeFactor, s => s.CheckpointsPageResizeFactor, true ) ); Refresh().SafeFireAndForget(); EventManager.Instance.ModelIndexChanged += (_, _) => { // Dispatch to UI thread to prevent race conditions with Avalonia's selection model. // The ModelIndexChanged event may be raised from a background thread. Dispatcher.UIThread.Post(() => { RefreshCategories(); ModelsCache.EditDiff( modelIndexService.ModelIndex.Values.SelectMany(x => x), LocalModelFile.RelativePathConnectedModelInfoComparer ); }); }; AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.SortConnectedModelsFirst, settings => settings.SortConnectedModelsFirst, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.SelectedSortOption, settings => settings.CheckpointSortMode, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.SelectedSortDirection, settings => settings.CheckpointSortDirection, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ShowModelsInSubfolders, settings => settings.ShowModelsInSubfolders, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.DragMovesAllSelected, settings => settings.DragMovesAllSelected, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.HideEmptyRootCategories, settings => settings.HideEmptyRootCategories, true ) ); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ShowNsfwImages, settings => settings.ShowNsfwInCheckpointsPage, true ) ); // make sure a sort happens OnPropertyChanged(nameof(SortConnectedModelsFirst)); } public override async Task OnLoadedAsync() { if (Design.IsDesignMode) return; var baseModelTypes = await baseModelTypeService.GetBaseModelTypes(includeAllOption: false); baseModelTypes = baseModelTypes.Except(settingsManager.Settings.DisabledBaseModelTypes).ToList(); BaseModelCache.Edit(updater => updater.Load(baseModelTypes)); await ShowFolderMapTipIfNecessaryAsync(); } public void ClearSearchQuery() { SearchQuery = string.Empty; } [RelayCommand] private async Task Refresh() { await modelIndexService.RefreshIndex(); Task.Run(async () => await modelIndexService.CheckModelsForUpdateAsync()).SafeFireAndForget(); } [RelayCommand] private void ClearSelection() { var selected = Models.Where(x => x.IsSelected).ToList(); foreach (var model in selected) model.IsSelected = false; NumItemsSelected = 0; } [RelayCommand] private async Task DeleteAsync() { if ( NumItemsSelected <= 0 || Models.Where(o => o.IsSelected).Select(vm => vm.CheckpointFile).ToList() is not { Count: > 0 } selectedModelFiles ) return; var pathsToDelete = selectedModelFiles .SelectMany(x => x.GetDeleteFullPaths(settingsManager.ModelsDirectory)) .ToList(); var confirmDeleteVm = dialogFactory.Get(); confirmDeleteVm.PathsToDelete = pathsToDelete; if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) return; try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting files", e.Message, NotificationType.Error); await modelIndexService.RefreshIndex(); return; } await modelIndexService.RemoveModelsAsync(selectedModelFiles); NumItemsSelected = 0; } [RelayCommand] private async Task ScanMetadata(bool updateExistingMetadata) { if (SelectedCategory == null) { notificationService.Show( "No Category Selected", "Please select a category to scan for metadata.", NotificationType.Error ); return; } var scanMetadataStep = new ScanMetadataStep( SelectedCategory.Path, metadataImportService, updateExistingMetadata ); var runner = new PackageModificationRunner { ModificationCompleteMessage = "Metadata scan complete", HideCloseButton = false, ShowDialogOnStart = true, }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await Dispatcher.UIThread.InvokeAsync(async () => await runner.ExecuteSteps([scanMetadataStep])); await modelIndexService.RefreshIndex(); var message = updateExistingMetadata ? "Finished updating metadata." : "Finished scanning for missing metadata."; notificationService.Show("Scan Complete", message, NotificationType.Success); } [RelayCommand] private Task OnItemClick(CheckpointFileViewModel item) { // Select item if we're in "select mode" if (NumItemsSelected > 0) item.IsSelected = !item.IsSelected; else if (item.CheckpointFile.HasConnectedModel) return ShowVersionDialog(item); else item.IsSelected = !item.IsSelected; return Task.CompletedTask; } [RelayCommand] private async Task ShowVersionDialog(CheckpointFileViewModel item) { if (item.CheckpointFile is { HasCivitMetadata: false, HasOpenModelDbMetadata: false }) { notificationService.Show( "Cannot show version dialog", "This model has custom metadata.", NotificationType.Error ); return; } if (item.CheckpointFile.HasCivitMetadata) { ShowCivitVersionDialog(item); } else if (item.CheckpointFile.HasOpenModelDbMetadata) { await ShowOpenModelDbDialog(item); } } private void ShowCivitVersionDialog(CheckpointFileViewModel item) { var model = item.CheckpointFile.LatestModelInfo; if (item.CheckpointFile.ConnectedModelInfo?.ModelId == null) { notificationService.Show( "Model not found", "Model not found in index, please try again later.", NotificationType.Error ); return; } var allModelIds = Models .Where(x => x.CheckpointFile.ConnectedModelInfo?.ModelId != null) .Select(x => x.CheckpointFile.ConnectedModelInfo!.ModelId!.Value) .Distinct() .ToList(); var index = Models.IndexOf(item); var newVm = dialogFactory.Get(vm => { vm.CivitModel = model ?? new CivitModel { Id = item.CheckpointFile.ConnectedModelInfo.ModelId.Value }; vm.ModelIdList = allModelIds; vm.CurrentIndex = index; return vm; }); navigationService.NavigateTo(newVm, BetterSlideNavigationTransition.PageSlideFromRight); } private async Task ShowOpenModelDbDialog(CheckpointFileViewModel item) { if (!item.CheckpointFile.HasOpenModelDbMetadata) return; await openModelDbManager.EnsureMetadataLoadedAsync(); var response = await openModelDbManager.ExecuteAsync( api => api.GetModels(), options => options.WithPriority(Priority.UserInitiated) ); var model = response .GetKeyedModels() .FirstOrDefault(x => x.Id == item.CheckpointFile.ConnectedModelInfo.ModelName); if (model == null) { notificationService.Show("Model not found", "Could not find model info", NotificationType.Error); return; } var vm = dialogFactory.Get(); vm.Model = model; var dialog = vm.GetDialog(); dialog.MaxDialogHeight = 800; await dialog.ShowAsync(); } [RelayCommand] private void ClearOrSelectAllBaseModels() { if (SelectedBaseModels.Count == BaseModelOptions.Count) BaseModelOptions.ForEach(x => x.IsSelected = false); else BaseModelOptions.ForEach(x => x.IsSelected = true); } [RelayCommand] private async Task CreateFolder(object? treeViewItem) { if (treeViewItem is not CheckpointCategory category) return; var parentFolder = category.Path; if (string.IsNullOrWhiteSpace(parentFolder)) return; var fields = new TextBoxField[] { new() { Label = "Folder Name", InnerLeftText = $@"{parentFolder.Replace(settingsManager.ModelsDirectory, string.Empty).TrimStart(Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}", MinWidth = 400, }, }; var dialog = DialogHelper.CreateTextEntryDialog("Create Folder", string.Empty, fields); var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; var folderName = fields[0].Text; var folderPath = Path.Combine(parentFolder, folderName); await notificationService.TryAsync( Task.Run(() => Directory.CreateDirectory(folderPath)), message: "Could not create folder" ); RefreshCategories(); SelectedCategory = Categories.SelectMany(c => c.Flatten()).FirstOrDefault(x => x.Path == folderPath); } [RelayCommand] private Task OpenFolderFromTreeview(object? treeViewItem) { return treeViewItem is CheckpointCategory category && !string.IsNullOrWhiteSpace(category.Path) ? ProcessRunner.OpenFolderBrowser(category.Path) : Task.CompletedTask; } [RelayCommand] private async Task DeleteFolderAsync(object? treeViewItem) { if (treeViewItem is not CheckpointCategory category) return; var folderPath = category.Path; if (string.IsNullOrWhiteSpace(folderPath)) return; var confirmDeleteVm = dialogFactory.Get(); confirmDeleteVm.PathsToDelete = category.Flatten().Select(x => x.Path).ToList(); if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) return; confirmDeleteVm.PathsToDelete = [folderPath]; try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting folder", e.Message, NotificationType.Error); return; } RefreshCategories(); } [RelayCommand] private void SelectAll() { Models.ForEach(x => x.IsSelected = true); } [RelayCommand] private async Task ShowFolderReference() { var dialog = DialogHelper.CreateMarkdownDialog(MarkdownSnippets.SMFolderMap); await dialog.ShowAsync(); } public async Task ImportFilesAsync(IEnumerable files, DirectoryPath destinationFolder) { if (destinationFolder.FullPath == settingsManager.ModelsDirectory) { notificationService.Show( "Invalid Folder", "Please select a different folder to import the files into.", NotificationType.Error ); return; } var fileList = files.ToList(); if ( fileList.Any(file => !LocalModelFile.SupportedCheckpointExtensions.Contains(Path.GetExtension(file)) ) ) { notificationService.Show( "Invalid File", "Please select only checkpoint files to import.", NotificationType.Error ); return; } var importModelsStep = new ImportModelsStep( modelFinder, downloadService, modelIndexService, fileList, destinationFolder, IsImportAsConnectedEnabled, settingsManager.Settings.MoveFilesOnImport ); var runner = new PackageModificationRunner { ModificationCompleteMessage = "Import Complete", HideCloseButton = false, ShowDialogOnStart = true, }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps([importModelsStep]); SelectedCategory = Categories .SelectMany(c => c.Flatten()) .FirstOrDefault(x => x.Path == destinationFolder.FullPath); } public async Task MoveBetweenFolders( List? sourceFiles, DirectoryPath destinationFolder ) { if (sourceFiles != null && sourceFiles.Count() > 0) { var sourceDirectory = Path.GetDirectoryName( sourceFiles[0].CheckpointFile.GetFullPath(settingsManager.ModelsDirectory) ); foreach (CheckpointFileViewModel sourceFile in sourceFiles) { if ( destinationFolder.FullPath == settingsManager.ModelsDirectory || (sourceDirectory != null && sourceDirectory == destinationFolder.FullPath) ) { notificationService.Show( "Invalid Folder", "Please select a different folder to import the files into.", NotificationType.Error ); return; } try { var sourcePath = new FilePath( sourceFile.CheckpointFile.GetFullPath(settingsManager.ModelsDirectory) ); 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 destinationFilePath = Path.Combine(destinationFolder, sourcePath.Name); var destinationCmInfoPath = Path.Combine( destinationFolder, $"{fileNameWithoutExt}.cm-info.json" ); var destinationPreviewPath = Path.Combine( destinationFolder, $"{fileNameWithoutExt}.preview.jpeg" ); // Move files if (File.Exists(sourcePath)) await FileTransfers.MoveFileAsync(sourcePath, destinationFilePath); if (File.Exists(sourceCmInfoPath)) await FileTransfers.MoveFileAsync(sourceCmInfoPath, destinationCmInfoPath); if (File.Exists(sourcePreviewPath)) await FileTransfers.MoveFileAsync(sourcePreviewPath, destinationPreviewPath); notificationService.Show( "Model moved successfully", $"Moved '{sourcePath.Name}' to '{Path.GetFileName(destinationFolder)}'" ); } catch (FileTransferExistsException) { notificationService.Show( "Failed to move file", "Destination file exists", NotificationType.Error ); } } SelectedCategory = Categories .SelectMany(c => c.Flatten()) .FirstOrDefault(x => x.Path == sourceDirectory); await modelIndexService.RefreshIndex(); DelayedClearProgress(TimeSpan.FromSeconds(1.5)); } } public async Task MoveBetweenFolders(LocalModelFile sourceFile, DirectoryPath destinationFolder) { var sourceDirectory = Path.GetDirectoryName(sourceFile.GetFullPath(settingsManager.ModelsDirectory)); if ( destinationFolder.FullPath == settingsManager.ModelsDirectory || (sourceDirectory != null && sourceDirectory == destinationFolder.FullPath) ) { notificationService.Show( "Invalid Folder", "Please select a different folder to import the files into.", NotificationType.Error ); return; } try { var sourcePath = new FilePath(sourceFile.GetFullPath(settingsManager.ModelsDirectory)); 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 destinationFilePath = Path.Combine(destinationFolder, sourcePath.Name); var destinationCmInfoPath = Path.Combine(destinationFolder, $"{fileNameWithoutExt}.cm-info.json"); var destinationPreviewPath = Path.Combine( destinationFolder, $"{fileNameWithoutExt}.preview.jpeg" ); // Move files if (File.Exists(sourcePath)) await FileTransfers.MoveFileAsync(sourcePath, destinationFilePath); if (File.Exists(sourceCmInfoPath)) await FileTransfers.MoveFileAsync(sourceCmInfoPath, destinationCmInfoPath); if (File.Exists(sourcePreviewPath)) await FileTransfers.MoveFileAsync(sourcePreviewPath, destinationPreviewPath); notificationService.Show( "Model moved successfully", $"Moved '{sourcePath.Name}' to '{Path.GetFileName(destinationFolder)}'" ); } catch (FileTransferExistsException) { notificationService.Show( "Failed to move file", "Destination file exists", NotificationType.Error ); } finally { SelectedCategory = Categories .SelectMany(c => c.Flatten()) .FirstOrDefault(x => x.Path == destinationFolder.FullPath); await modelIndexService.RefreshIndex(); DelayedClearProgress(TimeSpan.FromSeconds(1.5)); } } private void RefreshCategories() { if (Design.IsDesignMode) return; if (!settingsManager.IsLibraryDirSet) return; var previouslySelectedCategory = SelectedCategory; var modelCategories = Directory .EnumerateDirectories( settingsManager.ModelsDirectory, "*", EnumerationOptionConstants.TopLevelOnly ) // Ignore hacky "diffusion_models" folder for Swarm .Where(d => !Path.GetFileName(d).EndsWith("diffusion_models", StringComparison.OrdinalIgnoreCase)) .Select(d => { var folderName = Path.GetFileName(d); if ( Enum.TryParse(folderName, out SharedFolderType folderType) && folderType.GetDescription() != folderName ) { return new CheckpointCategory { Path = d, Name = folderName, Tooltip = folderType.GetDescription() ?? folderType.GetStringValue(), SubDirectories = GetSubfolders(d), }; } return new CheckpointCategory { Path = d, Name = folderName, SubDirectories = GetSubfolders(d), }; }) .ToList(); foreach (var checkpointCategory in modelCategories.SelectMany(c => c.Flatten())) checkpointCategory.Count = Directory .EnumerateFileSystemEntries( checkpointCategory.Path, "*", EnumerationOptionConstants.AllDirectories ) .Count(x => LocalModelFile.SupportedCheckpointExtensions.Contains(Path.GetExtension(x))); var rootCategory = new CheckpointCategory { Path = settingsManager.ModelsDirectory, Name = "All Models", Tooltip = "All Models", Count = modelIndexService.ModelIndex.Values.SelectMany(x => x).Count(), }; categoriesCache.Edit(updater => { updater.Load([rootCategory, .. modelCategories]); }); SelectedCategory = previouslySelectedCategory ?? Categories.FirstOrDefault(x => x.Path == previouslySelectedCategory?.Path) ?? Categories.FirstOrDefault() ?? (categoriesCache.Items.Any() ? categoriesCache.Items[0] : null); var dirPath = new DirectoryPath(SelectedCategory.Path); while (dirPath.FullPath != settingsManager.ModelsDirectory && dirPath.Parent != null) { var category = Categories .SelectMany(x => x.Flatten()) .FirstOrDefault(x => x.Path == dirPath.FullPath); if (category != null) category.IsExpanded = true; dirPath = dirPath.Parent; } Dispatcher.UIThread.Post(() => { SelectedCategory = previouslySelectedCategory ?? Categories.FirstOrDefault(x => x.Path == previouslySelectedCategory?.Path) ?? Categories.FirstOrDefault() ?? (categoriesCache.Items.Any() ? categoriesCache.Items[0] : null); }); } private ObservableCollection GetSubfolders(string strPath) { var subfolders = new ObservableCollection(); if (!Directory.Exists(strPath)) return subfolders; var directories = Directory.EnumerateDirectories( strPath, "*", EnumerationOptionConstants.TopLevelOnly ); foreach (var dir in directories) { var dirInfo = new DirectoryPath(dir); if (dirInfo.IsSymbolicLink) { if (!Directory.Exists(dirInfo.Info.LinkTarget)) continue; } var folderName = Path.GetFileName(dir); var category = new CheckpointCategory { Name = folderName, Tooltip = folderName, Path = dir, Count = dirInfo .Info.EnumerateFileSystemInfos("*", EnumerationOptionConstants.AllDirectories) .Count(x => LocalModelFile.SupportedCheckpointExtensions.Contains(x.Extension)), }; if (Directory.GetDirectories(dir, "*", EnumerationOptionConstants.TopLevelOnly).Length > 0) category.SubDirectories = GetSubfolders(dir); subfolders.Add(category); } return subfolders; } private string GetConnectedModelInfoFilePath(string filePath) { if (string.IsNullOrEmpty(filePath)) 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) ?? ""; return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json"); } private void DelayedClearProgress(TimeSpan delay) { Task.Delay(delay) .ContinueWith(_ => { IsLoading = false; Progress.Value = 0; }); } private void DelayedClearViewModelProgress(CheckpointFileViewModel viewModel, TimeSpan delay) { Task.Delay(delay) .ContinueWith(_ => { viewModel.IsLoading = false; viewModel.Progress = new ProgressReport(0f, ""); }); } private bool FilterModels(LocalModelFile file) { var folderPath = Path.GetDirectoryName(file.RelativePath); // Ignore hacky "diffusion_models" folder for Swarm if (folderPath?.Contains("diffusion_models", StringComparison.OrdinalIgnoreCase) ?? false) return false; if (SelectedCategory?.Path is null || SelectedCategory?.Path == settingsManager.ModelsDirectory) return file.HasConnectedModel ? SelectedBaseModels.Count == 0 || SelectedBaseModels.Contains(file.ConnectedModelInfo.BaseModel ?? "Other") : SelectedBaseModels.Count == 0 || SelectedBaseModels.Contains("Other"); var categoryRelativePath = SelectedCategory ?.Path.Replace(settingsManager.ModelsDirectory, string.Empty) .TrimStart(Path.DirectorySeparatorChar); if (categoryRelativePath == null || folderPath == null) return false; if ( ( file.HasConnectedModel ? SelectedBaseModels.Count == 0 || SelectedBaseModels.Contains(file.ConnectedModelInfo?.BaseModel ?? "Other") : SelectedBaseModels.Count == 0 || SelectedBaseModels.Contains("Other") ) is false ) return false; // If not showing nested models, just check if the file is directly in this folder if (!ShowModelsInSubfolders) return categoryRelativePath.Equals(folderPath, StringComparison.OrdinalIgnoreCase); // Split paths into segments var categorySegments = categoryRelativePath.Split(Path.DirectorySeparatorChar); var folderSegments = folderPath.Split(Path.DirectorySeparatorChar); // Check if folder is a subfolder of category by comparing path segments if (folderSegments.Length < categorySegments.Length) return false; // Compare each segment of the category path with the folder path return !categorySegments .Where((t, i) => !t.Equals(folderSegments[i], StringComparison.OrdinalIgnoreCase)) .Any(); } private bool FilterCategories(CheckpointCategory category) { return !HideEmptyRootCategories || category is { Count: > 0 }; } private async Task ShowFolderMapTipIfNecessaryAsync() { if ( settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.FolderMapTip) || settingsManager.Settings.InstalledPackages.Count == 0 ) return; var folderReference = DialogHelper.CreateMarkdownDialog(MarkdownSnippets.SMFolderMap); folderReference.CloseButtonText = Resources.Action_OK; await folderReference.ShowAsync(); settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.FolderMapTip)); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs ================================================ using System; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using System.Web; using Avalonia.Threading; using AvaloniaEdit; using AvaloniaEdit.Document; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Nito.AsyncEx; using Nito.AsyncEx.Synchronous; using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels; public partial class ConsoleViewModel : ObservableObject, IDisposable, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private bool isDisposed; // Queue for console updates private BufferBlock buffer = new(); // Task that updates the console (runs on UI thread) private Task? updateTask; // Cancellation token source for updateTask private CancellationTokenSource? updateCts; public int MaxLines { get; set; } = -1; public bool IsUpdatesRunning => updateTask?.IsCompleted == false; [ObservableProperty] private TextDocument document = new(); /// /// Current offset for write operations. /// private int writeCursor; /// /// Lock for accessing /// private readonly AsyncLock writeCursorLock = new(); /// /// Timeout for acquiring locks on /// // ReSharper disable once MemberCanBePrivate.Global public TimeSpan WriteCursorLockTimeout { get; init; } = TimeSpan.FromMilliseconds(100); /// /// Gets a cancellation token using the cursor lock timeout /// private CancellationToken WriteCursorLockTimeoutToken => new CancellationTokenSource(WriteCursorLockTimeout).Token; /// /// Event invoked when an ApcMessage of type Input is received. /// public event EventHandler? ApcInput; /// /// Starts update task for processing Post messages. /// /// If update task is already running public void StartUpdates() { if (updateTask is not null) { throw new InvalidOperationException("Update task is already running"); } updateCts = new CancellationTokenSource(); updateTask = Dispatcher.UIThread.InvokeAsync(ConsoleUpdateLoop, DispatcherPriority.Send); } /// /// Cancels the update task and waits for it to complete. /// public async Task StopUpdatesAsync() { Logger.Trace($"Stopping console updates, current buffer items: {buffer.Count}"); // First complete the buffer buffer.Complete(); // Wait for buffer to complete, max 3 seconds var completionCts = new CancellationTokenSource(3000); try { await buffer.Completion.WaitAsync(completionCts.Token); } catch (TaskCanceledException e) { // We can still continue since this just means we lose // some remaining output Logger.Warn("Buffer completion timed out: " + e.Message); } // Cancel update task updateCts?.Cancel(); updateCts = null; // Wait for update task if (updateTask is not null) { await updateTask; updateTask = null; } Logger.Trace($"Stopped console updates with {buffer.Count} buffer items remaining"); } /// /// Clears the console and sets a new buffer. /// This also resets the write cursor to 0. /// public async Task Clear() { // Clear document Document.Text = string.Empty; // Reset write cursor await ResetWriteCursor(); // Clear buffer and create new one buffer.Complete(); buffer = new BufferBlock(); } /// /// Resets the write cursor to be equal to the document length. /// public async Task ResetWriteCursor() { using (await writeCursorLock.LockAsync(WriteCursorLockTimeoutToken)) { Logger.ConditionalTrace($"Reset cursor to end: ({writeCursor} -> {Document.TextLength})"); writeCursor = Document.TextLength; } DebugPrintDocument(); } [RelayCommand] private async Task CopySelection(TextEditor textEditor) { await App.Clipboard.SetTextAsync(textEditor.SelectedText); } [RelayCommand] private void SelectAll(TextEditor textEditor) { textEditor.SelectAll(); } [Localizable(false)] [RelayCommand] private void SearchWithGoogle(TextEditor textEditor) { var url = $"https://google.com/search?q={HttpUtility.UrlEncode(textEditor.SelectedText)}"; ProcessRunner.OpenUrl(url); } [Localizable(false)] [RelayCommand] private void SearchWithChatGpt(TextEditor textEditor) { var url = $"https://chatgpt.com/?q={HttpUtility.UrlEncode(textEditor.SelectedText)}"; ProcessRunner.OpenUrl(url); } private async Task ConsoleUpdateLoop() { // This must be run in the UI thread Dispatcher.UIThread.VerifyAccess(); // Get cancellation token var ct = updateCts?.Token ?? throw new InvalidOperationException("Update cancellation token must be set"); try { while (!ct.IsCancellationRequested) { ProcessOutput output; try { output = await buffer.ReceiveAsync(ct); } catch (InvalidOperationException e) { // Thrown when buffer is completed, convert to OperationCanceledException throw new OperationCanceledException("Update buffer completed", e); } var outputType = output.IsStdErr ? "stderr" : "stdout"; Logger.ConditionalTrace( $"Processing: [{outputType}] (Text = {output.Text.ToRepr()}, " + $"Raw = {output.RawText?.ToRepr()}, " + $"CarriageReturn = {output.CarriageReturn}, " + $"CursorUp = {output.CursorUp}, " + $"AnsiCommand = {output.AnsiCommand})" ); // Link the cancellation token to the write cursor lock timeout var linkedCt = CancellationTokenSource .CreateLinkedTokenSource(ct, WriteCursorLockTimeoutToken) .Token; using (await writeCursorLock.LockAsync(linkedCt)) { ConsoleUpdateOne(output); } } } catch (OperationCanceledException e) { Logger.Debug($"Console update loop canceled: {e.Message}"); } catch (Exception e) { // Log other errors and continue here to not crash the UI thread Logger.Error(e, $"Unexpected error in console update loop: {e.GetType().Name} {e.Message}"); } } /// /// Handle one instance of ProcessOutput. /// Calls to this function must be synchronized with /// /// Not checked, but must be run in the UI thread. private void ConsoleUpdateOne(ProcessOutput output) { Debug.Assert(Dispatcher.UIThread.CheckAccess()); // Check for Apc messages if (output.ApcMessage is not null) { // Handle Apc message, for now just input audit events var message = output.ApcMessage.Value; if (message.Type == ApcType.Input) { ApcInput?.Invoke(this, message); } // Ignore further processing return; } // If we have a carriage return, // start current write at the beginning of the current line if (output.CarriageReturn > 0) { var currentLine = Document.GetLineByOffset(writeCursor); // Get the start of current line as new write cursor var lineStartOffset = currentLine.Offset; // See if we need to move the cursor if (lineStartOffset == writeCursor) { Logger.ConditionalTrace( $"Cursor already at start for carriage return " + $"(offset = {lineStartOffset}, line = {currentLine.LineNumber})" ); } else { // Also remove everything on current line // We'll temporarily do this for now to fix progress var lineEndOffset = currentLine.EndOffset; var lineLength = lineEndOffset - lineStartOffset; Document.Remove(lineStartOffset, lineLength); Logger.ConditionalTrace( $"Moving cursor to start for carriage return " + $"({writeCursor} -> {lineStartOffset})" ); writeCursor = lineStartOffset; } } // Write new text if (!string.IsNullOrEmpty(output.Text)) { DirectWriteLinesToConsole(output.Text); } // Handle cursor movements if (output.CursorUp > 0) { // Get the line and column of the current cursor var currentLocation = Document.GetLocation(writeCursor); if (currentLocation.Line == 1) { // We are already on the first line, ignore Logger.ConditionalTrace($"Cursor up: Already on first line"); } else { // We want to move up one line var targetLocation = new TextLocation(currentLocation.Line - 1, currentLocation.Column); var targetOffset = Document.GetOffset(targetLocation); // Update cursor to target offset Logger.ConditionalTrace( $"Cursor up: Moving (line {currentLocation.Line}, {writeCursor})" + $" -> (line {targetLocation.Line}, {targetOffset})" ); writeCursor = targetOffset; } } // Handle erase commands, different to cursor move as they don't move the cursor // We'll insert blank spaces instead if (output.AnsiCommand.HasFlag(AnsiCommand.EraseLine)) { // Get the current line, we'll insert spaces from start to end var currentLine = Document.GetLineByOffset(writeCursor); // Must be smaller than total lines currentLine = currentLine.LineNumber < Document.LineCount ? currentLine : Document.GetLineByNumber(Document.LineCount - 1); // Make some spaces to insert var spaces = new string(' ', currentLine.Length); // Insert the text Logger.ConditionalTrace( $"Erasing line {currentLine.LineNumber}: (length = {currentLine.Length})" ); using (Document.RunUpdate()) { Document.Replace(currentLine.Offset, currentLine.Length, spaces); } } DebugPrintDocument(); } /// /// Write text potentially containing line breaks to the console. /// This call will hold a upgradeable read lock /// private void DirectWriteLinesToConsole(string text) { // When our cursor is not at end, newlines should be interpreted as commands to // move cursor forward to the next linebreak instead of inserting a newline. // If text contains no newlines, we can just call DirectWriteToConsole // Also if cursor is equal to document length if (!text.Contains(Environment.NewLine) || writeCursor == Document.TextLength) { DirectWriteToConsole(text); return; } // Otherwise we need to handle how linebreaks are treated // Split text into lines var lines = text.Split(Environment.NewLine).ToList(); foreach (var lineText in lines.SkipLast(1)) { // Insert text DirectWriteToConsole(lineText); // Set cursor to start of next line, if we're not already there var currentLine = Document.GetLineByOffset(writeCursor); // If next line is available, move cursor to start of next line if (currentLine.LineNumber < Document.LineCount) { var nextLine = Document.GetLineByNumber(currentLine.LineNumber + 1); Logger.ConditionalTrace( $"Moving cursor to start of next line " + $"({writeCursor} -> {nextLine.Offset})" ); writeCursor = nextLine.Offset; } else { // Otherwise move to end of current line, and direct insert a newline var lineEndOffset = currentLine.EndOffset; Logger.ConditionalTrace( $"Moving cursor to end of current line " + $"({writeCursor} -> {lineEndOffset})" ); writeCursor = lineEndOffset; DirectWriteToConsole(Environment.NewLine); } } } /// /// Write text to the console, does not handle newlines. /// This should probably only be used by /// This call will hold a upgradeable read lock /// private void DirectWriteToConsole(string text) { CheckMaxLines(); using (Document.RunUpdate()) { // Need to replace text first if cursor lower than document length var replaceLength = Math.Min(Document.TextLength - writeCursor, text.Length); if (replaceLength > 0) { var newText = text[..replaceLength]; Logger.ConditionalTrace( $"Replacing: (cursor = {writeCursor}, length = {replaceLength}, " + $"text = {Document.GetText(writeCursor, replaceLength).ToRepr()} " + $"-> {newText.ToRepr()})" ); Document.Replace(writeCursor, replaceLength, newText); writeCursor += replaceLength; } // If we replaced less than content.Length, we need to insert the rest var remainingLength = text.Length - replaceLength; if (remainingLength > 0) { var textToInsert = text[replaceLength..]; Logger.ConditionalTrace( $"Inserting: (cursor = {writeCursor}, " + $"text = {textToInsert.ToRepr()})" ); Document.Insert(writeCursor, textToInsert); writeCursor += textToInsert.Length; } } } private void CheckMaxLines() { // Ignore limit if MaxLines is negative if (MaxLines < 0) return; if (Document.LineCount <= MaxLines) return; // Minimum lines to remove const int removeLinesBatchSize = 1; using (Document.RunUpdate()) { var currentLines = Document.LineCount; var linesExceeded = currentLines - MaxLines; var linesToRemove = Math.Min(currentLines, Math.Max(linesExceeded, removeLinesBatchSize)); Logger.ConditionalTrace( "Exceeded max lines ({Current} > {Max}), removing {Remove} lines", currentLines, MaxLines, linesToRemove ); // Remove lines from the start var firstLine = Document.GetLineByNumber(1); var lastLine = Document.GetLineByNumber(linesToRemove); var removeStart = firstLine.Offset; // If a next line exists, use the start offset of that instead in case of weird newlines var removeEnd = lastLine.EndOffset; if (lastLine.NextLine is not null) { removeEnd = lastLine.NextLine.Offset; } var removeLength = removeEnd - removeStart; Logger.ConditionalTrace( "Removing {LinesExceeded} lines from start: ({RemoveStart} -> {RemoveEnd})", linesExceeded, removeStart, removeEnd ); Document.Remove(removeStart, removeLength); // Update cursor position writeCursor -= removeLength; } } /// /// Debug function to print the current document to the console. /// Includes formatted cursor position. /// [Conditional("DEBUG")] private void DebugPrintDocument() { if (!Logger.IsTraceEnabled) return; var text = Document.Text; // Add a number for each line // Add an arrow line for where the cursor is, for example (cursor on offset 3): // // 1 | This is the first line // ~~~~~~~^ (3) // 2 | This is the second line // var lines = text.Split(Environment.NewLine).ToList(); var numberPadding = lines.Count.ToString().Length; for (var i = 0; i < lines.Count; i++) { lines[i] = $"{(i + 1).ToString().PadLeft(numberPadding)} | {lines[i]}"; } var cursorLine = Document.GetLineByOffset(writeCursor); var cursorLineOffset = writeCursor - cursorLine.Offset; // Need to account for padding + line number + space + cursor line offset var linePadding = numberPadding + 3 + cursorLineOffset; var cursorLineArrow = new string('~', linePadding) + $"^ ({writeCursor})"; // If more than line count, append to end if (cursorLine.LineNumber >= lines.Count) { lines.Add(cursorLineArrow); } else { lines.Insert(cursorLine.LineNumber, cursorLineArrow); } var textWithCursor = string.Join(Environment.NewLine, lines); Logger.ConditionalTrace("[Current Document]"); Logger.ConditionalTrace(textWithCursor); } /// /// Posts an update to the console /// Safe to call on non-UI threads /// public void Post(ProcessOutput output) { // If update task is running, send to buffer if (updateTask != null) { buffer.Post(output); return; } // Otherwise, use manual update one Logger.Debug("Synchronous post update to console: {@Output}", output); Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output)); } /// /// Posts an update to the console. /// Helper for calling Post(ProcessOutput) with strings /// public void Post(string text) { Post(new ProcessOutput { Text = text }); } /// /// Posts an update to the console. /// Equivalent to Post(text + Environment.NewLine) /// public void PostLine(string text) { Post(new ProcessOutput { Text = text + Environment.NewLine }); } public void Dispose() { if (isDisposed) return; updateCts?.Cancel(); updateCts?.Dispose(); updateCts = null; buffer.Complete(); if (updateTask is not null) { Logger.Debug("Shutting down console update task"); try { updateTask.WaitWithoutException(new CancellationTokenSource(1000).Token); updateTask.Dispose(); updateTask = null; } catch (OperationCanceledException) { Logger.Warn("During shutdown - Console update task cancellation timed out"); } catch (InvalidOperationException e) { Logger.Warn(e, "During shutdown - Console update task dispose failed"); } } isDisposed = true; GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { if (isDisposed) return; updateCts?.Cancel(); updateCts?.Dispose(); updateCts = null; if (updateTask is not null) { Logger.Debug("Waiting for console update task shutdown..."); await updateTask; updateTask.Dispose(); updateTask = null; Logger.Debug("Console update task shutdown complete"); } isDisposed = true; GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Controls/GitVersionSelectorViewModel.cs ================================================ using System; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.Models; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Core.Git; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Controls; public partial class GitVersionSelectorViewModel : ObservableObject { [ObservableProperty] private IGitVersionProvider? gitVersionProvider; [ObservableProperty] private string? selectedBranch; [ObservableProperty] private string? selectedCommit; [ObservableProperty] private string? selectedTag; [ObservableProperty] private GitVersionSelectorVersionType selectedVersionType; /// /// Gets or sets the selected . /// /// public GitVersion SelectedGitVersion { get { return SelectedVersionType switch { GitVersionSelectorVersionType.BranchCommit => new GitVersion { Branch = SelectedBranch, CommitSha = SelectedCommit }, GitVersionSelectorVersionType.Tag => new GitVersion { Tag = SelectedTag }, _ => throw new InvalidOperationException() }; } set { SelectedVersionType = value switch { { Tag: not null } => GitVersionSelectorVersionType.Tag, { Branch: not null, CommitSha: not null } => GitVersionSelectorVersionType.BranchCommit, // Default to branch commit _ => GitVersionSelectorVersionType.BranchCommit }; SelectedBranch = value.Branch; SelectedCommit = value.CommitSha; SelectedTag = value.Tag; } } public BetterContentDialog GetDialog() { Dispatcher.UIThread.VerifyAccess(); var selector = new GitVersionSelector { DataContext = this, Height = 400, Width = 600, [!GitVersionSelector.GitVersionProviderProperty] = new Binding(nameof(GitVersionProvider)), [!GitVersionSelector.SelectedVersionTypeProperty] = new Binding(nameof(SelectedVersionType)), [!GitVersionSelector.SelectedBranchProperty] = new Binding(nameof(SelectedBranch)), [!GitVersionSelector.SelectedCommitProperty] = new Binding(nameof(SelectedCommit)), [!GitVersionSelector.SelectedTagProperty] = new Binding(nameof(SelectedTag)) }; var dialog = new BetterContentDialog { Content = selector, VerticalAlignment = VerticalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Stretch, PrimaryButtonText = Resources.Action_Save, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, MinDialogWidth = 400 }; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.Serializer.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Drawing; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using StabilityMatrix.Avalonia.Controls.Models; using StabilityMatrix.Avalonia.Models; using Color = Avalonia.Media.Color; namespace StabilityMatrix.Avalonia.ViewModels.Controls; public partial class PaintCanvasViewModel { public override JsonObject SaveStateToJsonObject() { var model = SaveState(); return JsonSerializer .SerializeToNode(model, PaintCanvasModelSerializerContext.Default.Options) ?.AsObject() ?? throw new InvalidOperationException(); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = state.Deserialize(PaintCanvasModelSerializerContext.Default.Options); if (model is null) return; LoadState(model); RefreshCanvas?.Invoke(); } protected PaintCanvasModel SaveState() { var model = new PaintCanvasModel { TemporaryPaths = TemporaryPaths.ToDictionary(x => x.Key, x => x.Value), Paths = Paths, PaintBrushColor = PaintBrushColor, PaintBrushSize = PaintBrushSize, PaintBrushAlpha = PaintBrushAlpha, CurrentPenPressure = CurrentPenPressure, CurrentZoom = CurrentZoom, IsPenDown = IsPenDown, SelectedTool = SelectedTool, CanvasSize = CanvasSize }; return model; } protected void LoadState(PaintCanvasModel model) { TemporaryPaths.Clear(); foreach (var (key, value) in model.TemporaryPaths) { TemporaryPaths.TryAdd(key, value); } Paths = model.Paths; PaintBrushColor = model.PaintBrushColor; PaintBrushSize = model.PaintBrushSize; PaintBrushAlpha = model.PaintBrushAlpha; CurrentPenPressure = model.CurrentPenPressure; CurrentZoom = model.CurrentZoom; IsPenDown = model.IsPenDown; SelectedTool = model.SelectedTool; CanvasSize = model.CanvasSize; RefreshCanvas?.Invoke(); } public class PaintCanvasModel { public Dictionary TemporaryPaths { get; init; } = new(); public ImmutableList Paths { get; init; } = ImmutableList.Empty; public Color? PaintBrushColor { get; init; } public double PaintBrushSize { get; init; } public double PaintBrushAlpha { get; init; } public double CurrentPenPressure { get; init; } public double CurrentZoom { get; init; } public bool IsPenDown { get; init; } public PaintCanvasTool SelectedTool { get; init; } public Size CanvasSize { get; init; } } [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)] [JsonSerializable(typeof(PaintCanvasModel))] internal partial class PaintCanvasModelSerializerContext : JsonSerializerContext; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Controls/PaintCanvasViewModel.cs ================================================ using System.Collections.Concurrent; using System.Collections.Immutable; using System.ComponentModel; using System.Text.Json.Serialization; using Avalonia.Media; using Avalonia.Skia; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using Microsoft.Extensions.Logging; using SkiaSharp; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.Models; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using Color = Avalonia.Media.Color; using Size = System.Drawing.Size; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Controls; [RegisterTransient] [ManagedService] public partial class PaintCanvasViewModel(ILogger logger) : LoadableViewModelBase { public ConcurrentDictionary TemporaryPaths { get; set; } = new(); [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(UndoCommand))] private ImmutableList paths = []; [ObservableProperty] private Color? paintBrushColor = Colors.White; public SKColor PaintBrushSKColor => (PaintBrushColor ?? Colors.Transparent).ToSKColor(); [ObservableProperty] private double paintBrushSize = 12; [ObservableProperty] private double paintBrushAlpha = 1; [ObservableProperty] private double currentPenPressure; [ObservableProperty] private double currentZoom; [ObservableProperty] private bool isPenDown; [ObservableProperty] private PaintCanvasTool selectedTool = PaintCanvasTool.PaintBrush; [ObservableProperty] private Size canvasSize = Size.Empty; [JsonIgnore] private SKCanvas? SourceCanvas { set; get; } [Localizable(false)] [JsonIgnore] private OrderedDictionary Layers { get; } = new() { ["Background"] = new SKLayer(), ["Images"] = new SKLayer(), ["Brush"] = new SKLayer(), }; [JsonIgnore] private SKLayer BrushLayer => Layers["Brush"]; [JsonIgnore] private SKLayer ImagesLayer => Layers["Images"]; [JsonIgnore] private SKLayer BackgroundLayer => Layers["Background"]; [JsonIgnore] public SKBitmap? BackgroundImage { get => BackgroundLayer.Bitmaps.FirstOrDefault(); set { if (value is not null) { CanvasSize = new Size(value.Width, value.Height); BackgroundLayer.Bitmaps = [value]; } else { CanvasSize = Size.Empty; BackgroundLayer.Bitmaps = []; } } } /// /// Set by to allow the view model to /// refresh the canvas view after updating points or bitmap layers. /// [JsonIgnore] public Action? RefreshCanvas { get; set; } public void SetSourceCanvas(SKCanvas canvas) { ArgumentNullException.ThrowIfNull(canvas, nameof(canvas)); SourceCanvas = canvas; } public void LoadCanvasFromBitmap(SKBitmap bitmap) { ImagesLayer.Bitmaps = [bitmap]; RefreshCanvas?.Invoke(); } [RelayCommand(CanExecute = nameof(CanExecuteUndo))] public void Undo() { // Remove last path var currentPaths = Paths; if (currentPaths.IsEmpty) { return; } Paths = currentPaths.RemoveAt(currentPaths.Count - 1); RefreshCanvas?.Invoke(); } private bool CanExecuteUndo() { return Paths.Count > 0; } public SKImage? RenderToWhiteChannelImage() { using var _ = CodeTimer.StartDebug(); if (CanvasSize == Size.Empty) { logger.LogWarning($"RenderToImage: {nameof(CanvasSize)} is not set, returning null."); return null; } using var surface = SKSurface.Create(new SKImageInfo(CanvasSize.Width, CanvasSize.Height)); RenderToSurface(surface); using var originalImage = surface.Snapshot(); // Replace all colors to white (255, 255, 255), keep original alpha // csharpier-ignore using var colorFilter = SKColorFilter.CreateColorMatrix( [ // R, G, B, A, Bias -1, 0, 0, 0, 255, 0, -1, 0, 0, 255, 0, 0, -1, 0, 255, 0, 0, 0, 1, 0 ] ); using var paint = new SKPaint(); paint.ColorFilter = colorFilter; surface.Canvas.Clear(SKColors.Transparent); surface.Canvas.DrawImage(originalImage, originalImage.Info.Rect, paint); return surface.Snapshot(); } public SKImage? RenderToImage() { using var _ = CodeTimer.StartDebug(); if (CanvasSize == Size.Empty) { logger.LogWarning($"RenderToImage: {nameof(CanvasSize)} is not set, returning null."); return null; } using var surface = SKSurface.Create(new SKImageInfo(CanvasSize.Width, CanvasSize.Height)); RenderToSurface(surface); return surface.Snapshot(); } public void RenderToSurface( SKSurface surface, bool renderBackgroundFill = false, bool renderBackgroundImage = false ) { // Initialize canvas layers foreach (var layer in Layers.Values) { lock (layer) { if (layer.Surface is null) { layer.Surface = SKSurface.Create(new SKImageInfo(CanvasSize.Width, CanvasSize.Height)); /*layer.Surface = SKSurface.Create( surface.Context, true, new SKImageInfo(CanvasSize.Width, CanvasSize.Height) );*/ } else { // If we need to resize: var currentInfo = layer.Surface.Canvas.DeviceClipBounds; if (currentInfo.Width != CanvasSize.Width || currentInfo.Height != CanvasSize.Height) { // Dispose the old surface layer.Surface.Dispose(); // Create a brand-new SKSurface with the new size layer.Surface = SKSurface.Create( new SKImageInfo(CanvasSize.Width, CanvasSize.Height) ); } else { // No resize needed, just clear layer.Surface.Canvas.Clear(SKColors.Transparent); } } } } // Render all layer images in order foreach (var (layerName, layer) in Layers) { // Skip background image if not requested if (!renderBackgroundImage && layerName == "Background") { continue; } lock (layer) { var layerCanvas = layer.Surface!.Canvas; foreach (var bitmap in layer.Bitmaps) { layerCanvas.DrawBitmap(bitmap, new SKPoint(0, 0)); } } } // Render paint layer var paintLayerCanvas = BrushLayer.Surface!.Canvas; using var paint = new SKPaint(); // Draw the paths foreach (var penPath in Paths) { RenderPenPath(paintLayerCanvas, penPath, paint); } foreach (var penPath in TemporaryPaths.Values) { RenderPenPath(paintLayerCanvas, penPath, paint); } // Draw background color surface.Canvas.Clear(SKColors.Transparent); // Draw the layers to the main surface foreach (var layer in Layers.Values) { lock (layer) { layer.Surface!.Canvas.Flush(); surface.Canvas.DrawSurface(layer.Surface!, new SKPoint(0, 0)); } } surface.Canvas!.Flush(); } private static void RenderPenPath(SKCanvas canvas, PenPath penPath, SKPaint paint) { if (penPath.Points.Count == 0) { return; } // Apply Color if (penPath.IsErase) { // paint.BlendMode = SKBlendMode.SrcIn; paint.BlendMode = SKBlendMode.Clear; paint.Color = SKColors.Transparent; } else { paint.BlendMode = SKBlendMode.SrcOver; paint.Color = penPath.FillColor; } // Defaults paint.IsDither = true; paint.IsAntialias = true; // Track if we have any pen points var hasPenPoints = false; // Can't use foreach since this list may be modified during iteration // ReSharper disable once ForCanBeConvertedToForeach for (var i = 0; i < penPath.Points.Count; i++) { var penPoint = penPath.Points[i]; // Skip non-pen points if (!penPoint.IsPen) { continue; } hasPenPoints = true; var radius = penPoint.Radius; var pressure = penPoint.Pressure ?? 1; var thickness = pressure * radius * 2.5; // Draw path if (i < penPath.Points.Count - 1) { paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = (float)thickness; paint.StrokeCap = SKStrokeCap.Round; paint.StrokeJoin = SKStrokeJoin.Round; var nextPoint = penPath.Points[i + 1]; canvas.DrawLine(penPoint.X, penPoint.Y, nextPoint.X, nextPoint.Y, paint); } // Draw circles for pens paint.Style = SKPaintStyle.Fill; canvas.DrawCircle(penPoint.X, penPoint.Y, (float)thickness / 2, paint); } // Draw paths directly if we didn't have any pen points if (!hasPenPoints) { var point = penPath.Points[0]; var thickness = point.Radius * 2; paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = (float)thickness; paint.StrokeCap = SKStrokeCap.Round; paint.StrokeJoin = SKStrokeJoin.Round; var skPath = penPath.ToSKPath(); canvas.DrawPath(skPath, paint); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/AnalyticsOptInViewModel.cs ================================================ using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(AnalyticsOptInDialog))] [ManagedService] [RegisterTransient] public class AnalyticsOptInViewModel : ContentDialogViewModelBase { public string ChangeThisBehaviorInSettings => string.Format(Resources.TextTemplate_YouCanChangeThisBehavior, "Settings > System > Analytics") .Trim(); public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.IsPrimaryButtonEnabled = true; dialog.PrimaryButtonText = "Don't Share Analytics"; dialog.SecondaryButtonText = "Share Analytics"; dialog.CloseOnClickOutside = false; dialog.DefaultButton = ContentDialogButton.Secondary; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/CivitFileDisplayViewModel.cs ================================================ using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public class CivitFileDisplayViewModel { public required CivitModelVersion ModelVersion { get; init; } public required CivitFileViewModel FileViewModel { get; init; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/CivitFileViewModel.cs ================================================ using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public partial class CivitFileViewModel : DisposableViewModelBase { private readonly IModelIndexService modelIndexService; private readonly ISettingsManager settingsManager; private readonly IServiceManager vmFactory; private readonly Func? downloadAction; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [ObservableProperty] private CivitFile civitFile; [ObservableProperty] private bool isInstalled; [ObservableProperty] public required partial ObservableCollection InstallLocations { get; set; } [ObservableProperty] public partial string DownloadTooltip { get; set; } = string.Empty; [ObservableProperty] public partial bool CanImport { get; set; } = true; public CivitFileViewModel( IModelIndexService modelIndexService, ISettingsManager settingsManager, CivitFile civitFile, IServiceManager vmFactory, Func? downloadAction ) { this.modelIndexService = modelIndexService; this.settingsManager = settingsManager; this.vmFactory = vmFactory; this.downloadAction = downloadAction; CivitFile = civitFile; IsInstalled = CivitFile is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && modelIndexService.ModelIndexBlake3Hashes.Contains(CivitFile.Hashes.BLAKE3); EventManager.Instance.ModelIndexChanged += ModelIndexChanged; try { if (settingsManager.IsLibraryDirSet) { var fileSizeBytes = CivitFile.SizeKb * 1024; var freeSizeBytes = SystemInfo.GetDiskFreeSpaceBytes(settingsManager.ModelsDirectory) ?? long.MaxValue; CanImport = fileSizeBytes < freeSizeBytes; DownloadTooltip = CanImport ? "Free space after download: " + ( freeSizeBytes < long.MaxValue ? Size.FormatBytes(Convert.ToUInt64(freeSizeBytes - fileSizeBytes)) : "Unknown" ) : $"Not enough space on disk. Need {Size.FormatBytes(Convert.ToUInt64(fileSizeBytes))} but only have {Size.FormatBytes(Convert.ToUInt64(freeSizeBytes))}"; } else { DownloadTooltip = "Please set the library directory in settings"; } } catch (Exception e) { LogManager .GetCurrentClassLogger() .Error(e, "Failed to check disk space for {FileName}", civitFile.Name); DownloadTooltip = "Failed to check disk space"; } } private void ModelIndexChanged(object? sender, EventArgs e) { // Dispatch to UI thread since the event may be raised from a background thread Dispatcher.UIThread.Post(() => { IsInstalled = CivitFile is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && modelIndexService.ModelIndexBlake3Hashes.Contains(CivitFile.Hashes.BLAKE3); }); } [RelayCommand(CanExecute = nameof(CanExecuteDownload))] private async Task DownloadToDefaultAsync() { if (downloadAction != null) { await downloadAction(this, null); } } [RelayCommand(CanExecute = nameof(CanExecuteDownload))] private async Task DownloadToSelectedLocationAsync(string locationKey) { if (downloadAction != null) { await downloadAction(this, locationKey); } } [RelayCommand] private async Task Delete() { var hash = CivitFile.Hashes.BLAKE3; if (string.IsNullOrWhiteSpace(hash)) { return; } var matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList(); if (matchingModels.Count == 0) { await modelIndexService.RefreshIndex(); matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList(); if (matchingModels.Count == 0) { return; } } var confirmDeleteVm = vmFactory.Get(); var paths = new List(); foreach (var localModel in matchingModels) { var checkpointPath = new FilePath(localModel.GetFullPath(settingsManager.ModelsDirectory)); if (File.Exists(checkpointPath)) { paths.Add(checkpointPath); } var previewPath = localModel.GetPreviewImageFullPath(settingsManager.ModelsDirectory); if (File.Exists(previewPath)) { paths.Add(previewPath); } var cmInfoPath = checkpointPath.ToString().Replace(checkpointPath.Extension, ".cm-info.json"); if (File.Exists(cmInfoPath)) { paths.Add(cmInfoPath); } } confirmDeleteVm.PathsToDelete = paths; if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) { return; } try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { Logger.Error(e, "Failed to delete model files for {ModelName}", CivitFile.Name); await modelIndexService.RefreshIndex(); return; } finally { IsInstalled = false; } await modelIndexService.RemoveModelsAsync(matchingModels); } private bool CanExecuteDownload() { return downloadAction != null; } protected override void Dispose(bool disposing) { if (disposing) { EventManager.Instance.ModelIndexChanged -= ModelIndexChanged; } base.Dispose(disposing); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/CivitImageViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public partial class CivitImageViewModel : ObservableObject { [ObservableProperty] public partial int ImageId { get; set; } [ObservableProperty] public partial ImageSource ImageSource { get; set; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmBulkDownloadDialogViewModel.cs ================================================ using System.Collections.ObjectModel; using System.Reactive.Linq; using Avalonia.Controls.Primitives; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ConfirmBulkDownloadDialog))] [ManagedService] [RegisterTransient] public partial class ConfirmBulkDownloadDialogViewModel( IModelIndexService modelIndexService, ISettingsManager settingsManager, IServiceManager vmFactory ) : ContentDialogViewModelBase { public required CivitModel Model { get; set; } [ObservableProperty] public partial double TotalSizeKb { get; set; } [ObservableProperty] public partial CivitModelFpType FpTypePreference { get; set; } = CivitModelFpType.fp16; [ObservableProperty] public partial bool IncludeVae { get; set; } [ObservableProperty] public partial string DownloadFollowingFilesText { get; set; } = string.Empty; [ObservableProperty] public partial bool PreferPruned { get; set; } = true; private readonly SourceCache allFilesCache = new(displayVm => displayVm.FileViewModel.CivitFile.Id ); public IObservableCollection FilesToDownload { get; } = new ObservableCollectionExtended(); public ObservableCollection AvailableFpTypes => new(Enum.GetValues()); public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); if (Model.ModelVersions == null || Model.ModelVersions.Count == 0) { FilesToDownload.Clear(); DownloadFollowingFilesText = "No files available for download."; TotalSizeKb = 0; allFilesCache.Clear(); // Clear cache if model is empty return; } var allFilesFromModel = Model .ModelVersions.SelectMany(v => v.Files?.Select(f => new CivitFileDisplayViewModel { ModelVersion = v, FileViewModel = new CivitFileViewModel( modelIndexService, settingsManager, f, vmFactory, null ) { InstallLocations = [], }, }) ?? [] ) .ToList(); allFilesCache.Edit(updater => { updater.Clear(); updater.AddOrUpdate(allFilesFromModel); }); var fpPreferenceObservable = this.WhenPropertyChanged(x => x.FpTypePreference) .Select(_ => (Func)( displayVm => IsPreferredPrecision(displayVm.FileViewModel) ) ); var includeVaeObservable = this.WhenPropertyChanged(x => x.IncludeVae) .Select(include => (Func)( displayVm => include.Value || displayVm.FileViewModel.CivitFile.Type != CivitFileType.VAE ) ); var preferPrunedFilter = this.WhenPropertyChanged(x => x.PreferPruned) .Select(_ => (Func)( displayVm => { var file = displayVm.FileViewModel.CivitFile; if (file.Metadata.Size is null) return true; if ( PreferPruned && file.Metadata.Size.Equals("pruned", StringComparison.OrdinalIgnoreCase) ) return true; if ( !PreferPruned && file.Metadata.Size.Equals("full", StringComparison.OrdinalIgnoreCase) ) return true; return false; } ) ); var defaultFilter = (Func)( displayVm => { var fileVm = displayVm.FileViewModel; if (fileVm.IsInstalled) return false; return fileVm.CivitFile.Type is CivitFileType.Model or CivitFileType.VAE or CivitFileType.PrunedModel; } ); var filteredFilesObservable = allFilesCache .Connect() .Filter(defaultFilter) .Filter(fpPreferenceObservable) .Filter(includeVaeObservable) .Filter(preferPrunedFilter); AddDisposable( filteredFilesObservable .SortAndBind( FilesToDownload, SortExpressionComparer.Ascending(s => s.FileViewModel.CivitFile.DisplayName ) ) .ObserveOn(SynchronizationContext.Current!) .Subscribe() ); AddDisposable( filteredFilesObservable .ToCollection() .ObserveOn(SynchronizationContext.Current!) // Or AvaloniaScheduler.Instance .Subscribe(filteredFiles => { TotalSizeKb = filteredFiles.Sum(f => f.FileViewModel.CivitFile.SizeKb); DownloadFollowingFilesText = $"You are about to download {filteredFiles.Count} files totaling {new FileSizeType(TotalSizeKb)}."; }) ); if ( FilesToDownload.All(x => x.FileViewModel.CivitFile.Metadata.Size?.Equals("full", StringComparison.OrdinalIgnoreCase) ?? false ) && PreferPruned ) { PreferPruned = false; } } public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.MinDialogWidth = 550; dialog.MaxDialogHeight = 600; dialog.IsFooterVisible = false; dialog.CloseOnClickOutside = true; dialog.ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled; return dialog; } private bool IsPreferredPrecision(CivitFileViewModel file) { if (file.CivitFile.Metadata.Fp is null || string.IsNullOrWhiteSpace(file.CivitFile.Metadata.Fp)) return true; var preference = FpTypePreference.GetStringValue(); var fpType = file.CivitFile.Metadata.Fp; return preference == fpType; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmDeleteDialogViewModel.cs ================================================ using Avalonia.Controls.Primitives; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Native; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ConfirmDeleteDialog))] [RegisterTransient] [ManagedService] public partial class ConfirmDeleteDialogViewModel(ILogger logger) : ContentDialogViewModelBase { [ObservableProperty] private string title = "Confirm Delete"; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ConfirmDeleteButtonText))] [NotifyPropertyChangedFor(nameof(IsPermanentDelete))] [NotifyPropertyChangedFor(nameof(DeleteFollowingFilesText))] private bool isRecycleBinAvailable = NativeFileOperations.IsRecycleBinAvailable; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ConfirmDeleteButtonText))] [NotifyPropertyChangedFor(nameof(IsPermanentDelete))] [NotifyPropertyChangedFor(nameof(DeleteFollowingFilesText))] private bool isRecycleBinOptOutChecked; public bool IsPermanentDelete => !IsRecycleBinAvailable || IsRecycleBinOptOutChecked; public string ConfirmDeleteButtonText => IsPermanentDelete ? Resources.Action_Delete : Resources.Action_MoveToTrash; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DeleteFollowingFilesText))] private IReadOnlyList pathsToDelete = []; public string DeleteFollowingFilesText => PathsToDelete.Count is var count and > 1 ? string.Format(Resources.TextTemplate_DeleteFollowingCountItems, count) : Resources.Text_DeleteFollowingItems; public bool ShowActionCannotBeUndoneNotice { get; set; } = true; /// public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.MinDialogWidth = 550; dialog.MaxDialogHeight = 600; dialog.IsFooterVisible = false; dialog.CloseOnClickOutside = true; dialog.ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled; return dialog; } [RelayCommand(CanExecute = nameof(CanExecuteConfirmDelete))] private void OnConfirmDeleteClick() { OnPrimaryButtonClick(); } private bool CanExecuteConfirmDelete() { return !HasErrors && IsValid(); } private bool IsValid() { return true; } public async Task ExecuteCurrentDeleteOperationAsync(bool ignoreErrors = false, bool failFast = false) { var paths = PathsToDelete; var exceptions = new List(); if (!IsPermanentDelete) { // Recycle bin if (!NativeFileOperations.IsRecycleBinAvailable) { throw new NotSupportedException("Recycle bin is not available on this platform"); } try { await NativeFileOperations.RecycleBin.MoveFilesToRecycleBinAsync(paths); } catch (Exception e) { logger.LogWarning(e, "Failed to move path to recycle bin"); if (!ignoreErrors) { exceptions.Add(e); if (failFast) { throw new AggregateException(exceptions); } } } } else { await Task.Run(() => { foreach (var path in paths) { try { if (Directory.Exists(path)) { Directory.Delete(path, true); } else { File.Delete(path); } } catch (Exception e) { logger.LogWarning(e, "Failed to delete path"); if (!ignoreErrors) { exceptions.Add(e); if (failFast) { throw new AggregateException(exceptions); } } } } }); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ConfirmPackageDeleteDialog))] [ManagedService] [RegisterTransient] public partial class ConfirmPackageDeleteDialogViewModel : ContentDialogViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid), nameof(ExpectedPackageName))] public required partial InstalledPackage Package { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsValid))] public partial string PackageName { get; set; } = string.Empty; public string? ExpectedPackageName => Package.DisplayName; public bool IsValid => ExpectedPackageName?.Equals(PackageName, StringComparison.Ordinal) ?? false; public string DeleteWarningText { get { var items = new List { $"• The {ExpectedPackageName} application", $"• {(Package.PackageName == "ComfyUI" ? "Custom nodes" : "Extensions")}", }; if (!Package.UseSharedOutputFolder) items.Add("• Images/outputs"); if (Package.PreferredSharedFolderMethod is SharedFolderMethod.None) items.Add("• Models/checkpoints placed in the package's model folders"); items.Add("• Any custom files in the package folder"); return string.Join(Environment.NewLine, items); } } [RelayCommand] private async Task CopyExpectedPackageName() { await App.Clipboard?.SetTextAsync(ExpectedPackageName); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs ================================================ using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(DownloadResourceDialog))] [ManagedService] [RegisterTransient] public partial class DownloadResourceViewModel( IDownloadService downloadService, ISettingsManager settingsManager, ITrackedDownloadService trackedDownloadService ) : ContentDialogViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(FileNameWithHash))] private string? fileName; public string FileNameWithHash => $"{FileName} [{Resource.HashSha256?.ToLowerInvariant()[..7]}]"; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FileSizeText))] private long fileSize; [ObservableProperty] private RemoteResource resource; public string? FileSizeText => FileSize == 0 ? null : Size.FormatBase10Bytes(FileSize); public string? ShortHash => Resource.HashSha256?.ToLowerInvariant()[..7]; /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); // Get download size if (!Design.IsDesignMode && Resource.Url is { } url) { FileSize = await downloadService.GetFileSizeAsync(url.ToString()); } } [RelayCommand] private void OpenInfoUrl() { if (Resource.InfoUrl is { } url) { ProcessRunner.OpenUrl(url); } } public TrackedDownload StartDownload() { var sharedFolderType = Resource.ContextType as SharedFolderType? ?? throw new InvalidOperationException("ContextType is not SharedFolderType"); var modelsDir = new DirectoryPath(settingsManager.ModelsDirectory).JoinDir( sharedFolderType.GetStringValue() ); if (Resource.RelativeDirectory is not null) { modelsDir = modelsDir.JoinDir(Resource.RelativeDirectory); } var download = trackedDownloadService.NewDownload( Resource.Url, modelsDir.JoinFile(Resource.FileName) ); // Set extraction properties download.AutoExtractArchive = Resource.AutoExtractArchive; download.ExtractRelativePath = Resource.ExtractRelativePath; download.ContextAction = new ModelPostDownloadContextAction(); trackedDownloadService.TryStartDownload(download); EventManager.Instance.OnToggleProgressFlyout(); return download; } public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.MinDialogWidth = 400; dialog.Title = "Download Model"; dialog.Content = new DownloadResourceDialog { DataContext = this }; dialog.PrimaryButtonText = Resources.Action_Continue; dialog.CloseButtonText = Resources.Action_Cancel; dialog.DefaultButton = ContentDialogButton.Primary; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/EnvVarsViewModel.cs ================================================ using System.Collections.ObjectModel; using System.Diagnostics; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(EnvVarsDialog))] [ManagedService] [RegisterTransient] public partial class EnvVarsViewModel : ContentDialogViewModelBase { [ObservableProperty] private string title = Resources.Label_EnvironmentVariables; [ObservableProperty, NotifyPropertyChangedFor(nameof(EnvVarsView))] private ObservableCollection envVars = new(); public DataGridCollectionView EnvVarsView => new(EnvVars); [RelayCommand] private void AddRow() { EnvVars.Add(new EnvVarKeyPair()); } [RelayCommand] private void RemoveSelectedRow(int selectedIndex) { try { EnvVars.RemoveAt(selectedIndex); } catch (ArgumentOutOfRangeException) { Debug.WriteLine($"RemoveSelectedRow: Index {selectedIndex} out of range"); } } public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.PrimaryButtonText = Resources.Action_Save; dialog.IsPrimaryButtonEnabled = true; dialog.CloseButtonText = Resources.Action_Cancel; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs ================================================ using System; using System.ComponentModel; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using NLog; using Sentry; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ExceptionDialog))] [ManagedService] [RegisterTransient] public partial class ExceptionViewModel : ViewModelBase { public Exception? Exception { get; set; } public SentryId? SentryId { get; set; } public bool IsRecoverable { get; set; } public string Description => IsRecoverable ? Resources.Text_UnexpectedErrorRecoverable_Description : Resources.Text_UnexpectedError_Description; public string? Message => Exception?.Message; public string? ExceptionType => Exception?.GetType().Name ?? ""; public bool IsContinueResult { get; set; } public string? LogZipPath { get; set; } public static async Task CreateLogFolderZip() { var tcs = new TaskCompletionSource(); LogManager.Flush( ex => { if (ex is null) { tcs.SetResult(); } else { tcs.SetException(ex); } }, TimeSpan.FromSeconds(15) ); await tcs.Task; using var suspend = LogManager.SuspendLogging(); var logDir = Compat.AppDataHome.JoinDir("Logs"); // Copy logs to temp directory using var tempDir = new TempDirectoryPath(); var tempLogDir = tempDir.JoinDir("Logs"); tempLogDir.Create(); foreach (var logFile in logDir.EnumerateFiles("*.log")) { // Need FileShare.ReadWrite since NLog keeps the file open await logFile.CopyToAsync( tempLogDir.JoinFile(logFile.Name), FileShare.ReadWrite, overwrite: true ); } // Find a unique name for the output archive var archiveNameBase = $"stabilitymatrix-log-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}"; var archiveName = archiveNameBase; var archivePath = Compat.AppDataHome.JoinFile(archiveName + ".zip"); var i = 1; while (File.Exists(archivePath)) { archiveName = $"{archiveNameBase}-{i++}"; archivePath = Compat.AppDataHome.JoinFile(archiveName + ".zip"); } // Create the archive ZipFile.CreateFromDirectory(tempLogDir, archivePath, CompressionLevel.Optimal, false); return archivePath; } [RelayCommand] private async Task OpenLogZipInFileBrowser() { if (string.IsNullOrWhiteSpace(LogZipPath) || !File.Exists(LogZipPath)) { LogZipPath = await CreateLogFolderZip(); } await ProcessRunner.OpenFileBrowser(LogZipPath); } [Localizable(false)] public string? FormatAsMarkdown() { var msgBuilder = new StringBuilder(); msgBuilder.AppendLine(); if (Exception is not null) { msgBuilder.AppendLine("## Exception"); msgBuilder.AppendLine($"```{ExceptionType}: {Message}```"); if (Exception.InnerException is not null) { msgBuilder.AppendLine( $"```{Exception.InnerException.GetType().Name}: {Exception.InnerException.Message}```" ); } } else { msgBuilder.AppendLine("## Exception"); msgBuilder.AppendLine("```(None)```"); } if (SentryId is { } id) { msgBuilder.AppendLine("### Sentry ID"); msgBuilder.AppendLine($"[`{id.ToString()[..8]}`]({GetIssueUrl(id)})"); } if (Exception?.StackTrace is not null) { msgBuilder.AppendLine("### Stack Trace"); msgBuilder.AppendLine($"```{Exception.StackTrace}```"); } if (Exception?.InnerException is { StackTrace: not null } innerException) { msgBuilder.AppendLine($"```{innerException.StackTrace}```"); } return msgBuilder.ToString(); } [Localizable(false)] private static string GetIssueUrl(SentryId sentryId) { return $"https://stability-matrix.sentry.io/issues/?query=id%3A{sentryId.ToString()}&referrer=sm-app-ex&statsPeriod=90d"; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs ================================================ using System; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.Primitives; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.Core; using Injectio.Attributes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api.CivitTRPC; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; using Path = System.IO.Path; using Size = StabilityMatrix.Core.Helper.Size; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ImageViewerDialog))] [ManagedService] [RegisterTransient] public partial class ImageViewerViewModel( ILogger logger, ISettingsManager settingsManager ) : ContentDialogViewModelBase { [ObservableProperty] private ImageSource? imageSource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasLocalGenerationParameters))] private LocalImageFile? localImageFile; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasLocalGenerationParameters))] public partial CivitImageGenerationDataResponse? CivitImageMetadata { get; set; } [ObservableProperty] private bool isFooterEnabled; [ObservableProperty] private string? fileNameText; [ObservableProperty] private string? fileSizeText; [ObservableProperty] private string? imageSizeText; /// /// Whether local generation parameters are available. /// public bool HasLocalGenerationParameters => LocalImageFile?.GenerationParameters is not null; /// /// Whether Civitai image metadata is available. /// public bool HasCivitImageMetadata => CivitImageMetadata is not null; public event EventHandler? NavigationRequested; public event EventHandler? NavigateToModelRequested; partial void OnLocalImageFileChanged(LocalImageFile? value) { if (value?.ImageSize is { IsEmpty: false } size) { ImageSizeText = $"{size.Width} x {size.Height}"; } } partial void OnImageSourceChanged(ImageSource? value) { if (value?.LocalFile is { Exists: true } localFile) { FileNameText = localFile.Name; FileSizeText = Size.FormatBase10Bytes(localFile.GetSize(true)); } } partial void OnCivitImageMetadataChanged(CivitImageGenerationDataResponse? value) { if (value is null) return; ImageSizeText = value.Metadata?.Dimensions ?? string.Empty; } [RelayCommand] private void OnNavigateNext() { NavigationRequested?.Invoke(this, DirectionalNavigationEventArgs.Down); } [RelayCommand] private void OnNavigatePrevious() { NavigationRequested?.Invoke(this, DirectionalNavigationEventArgs.Up); } [RelayCommand] private void OnNavigateToModel(int modelId) { NavigateToModelRequested?.Invoke(this, modelId); } [RelayCommand] private async Task CopyImage(ImageSource? image) { if (image is null) return; if (image.LocalFile is { } imagePath) { await App.Clipboard.SetFileDataObjectAsync(imagePath); } else if (await image.GetBitmapAsync() is { } bitmap) { // Write to temp file var tempFile = new FilePath(Path.GetTempFileName() + ".png"); bitmap.Save(tempFile); await App.Clipboard.SetFileDataObjectAsync(tempFile); } else { logger.LogWarning("Failed to copy image, no file path or bitmap: {Image}", image); } } [RelayCommand] private async Task CopyImageAsBitmap(ImageSource? image) { if (image is null || !Compat.IsWindows) return; if (await image.GetBitmapAsync() is { } bitmap) { await WindowsClipboard.SetBitmapAsync(bitmap); } else { logger.LogWarning("Failed to copy image, no bitmap: {Image}", image); } } [RelayCommand] private async Task CopyThingToClipboard(object? thing) { if (thing is null) return; await App.Clipboard.SetTextAsync(thing.ToString()); } public override BetterContentDialog GetDialog() { var margins = new Thickness(64, 32); var mainWindowSize = App.Services.GetService()?.ClientSize; var dialogSize = new global::Avalonia.Size( Math.Floor((mainWindowSize?.Width * 0.6 ?? 1000) - margins.Horizontal()), Math.Floor((mainWindowSize?.Height ?? 1000) - margins.Vertical()) ); var dialog = new BetterContentDialog { MaxDialogWidth = dialogSize.Width, MaxDialogHeight = dialogSize.Height, ContentMargin = margins, FullSizeDesired = true, IsFooterVisible = false, CloseOnClickOutside = true, ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, Content = new ImageViewerDialog { Width = dialogSize.Width, Height = dialogSize.Height, DataContext = this, }, }; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/InferenceConnectionHelpViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(InferenceConnectionHelpDialog))] [ManagedService] [RegisterTransient] public partial class InferenceConnectionHelpViewModel : ContentDialogViewModelBase { private readonly ISettingsManager settingsManager; private readonly INavigationService navigationService; private readonly IPackageFactory packageFactory; private readonly RunningPackageService runningPackageService; [ObservableProperty] private string title = "Hello"; [ObservableProperty] private IReadOnlyList installedPackages = Array.Empty(); [ObservableProperty] private InstalledPackage? selectedPackage; [ObservableProperty] private bool isFirstTimeWelcome; /// /// When the user has no Comfy packages, and we need to prompt to install /// [ObservableProperty] private bool isInstallMode; /// /// When the user has Comfy packages, and we need to prompt to launch /// [ObservableProperty] private bool isLaunchMode; public InferenceConnectionHelpViewModel( ISettingsManager settingsManager, INavigationService navigationService, IPackageFactory packageFactory, RunningPackageService runningPackageService ) { this.settingsManager = settingsManager; this.navigationService = navigationService; this.packageFactory = packageFactory; this.runningPackageService = runningPackageService; // Get comfy type installed packages var comfyPackages = this.settingsManager.Settings.InstalledPackages.Where( p => p.PackageName is "ComfyUI" or "ComfyUI-Zluda" ) .ToImmutableArray(); InstalledPackages = comfyPackages; // If no comfy packages, install mode, otherwise launch mode if (comfyPackages.Length == 0) { IsInstallMode = true; } else { IsLaunchMode = true; // Use active package if its comfy, otherwise use the first comfy type if ( this.settingsManager.Settings.ActiveInstalledPackage is { PackageName: "ComfyUI" or "ComfyUI-Zluda" } activePackage ) { SelectedPackage = activePackage; } else { SelectedPackage ??= comfyPackages[0]; } } } /// /// Navigate to the package install page /// [RelayCommand] private void NavigateToInstall() { Dispatcher.UIThread.Post(() => { navigationService.NavigateTo( param: new PackageManagerNavigationOptions { OpenInstallerDialog = true, InstallerSelectedPackage = packageFactory .GetAllAvailablePackages() .OfType() .First() } ); }); } /// /// Request launch of the selected package /// [RelayCommand] private async Task LaunchSelectedPackage() { if (SelectedPackage is not null) { await runningPackageService.StartPackage(SelectedPackage); } } /// /// Create a better content dialog for this view model /// public BetterContentDialog CreateDialog() { var dialog = new BetterContentDialog { Content = new InferenceConnectionHelpDialog { DataContext = this }, PrimaryButtonCommand = IsInstallMode ? NavigateToInstallCommand : LaunchSelectedPackageCommand, PrimaryButtonText = IsInstallMode ? Resources.Action_Install : Resources.Action_Launch, CloseButtonText = Resources.Action_Close, DefaultButton = ContentDialogButton.Primary }; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/LaunchOptionsViewModel.cs ================================================ using System.Collections.Immutable; using System.ComponentModel; using System.Reactive.Linq; using CommunityToolkit.Mvvm.ComponentModel; using FuzzySharp; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(LaunchOptionsDialog))] [ManagedService] [RegisterTransient] public partial class LaunchOptionsViewModel : ContentDialogViewModelBase { private readonly ILogger logger; private readonly LRUCache> cache = new(100); [ObservableProperty] private string title = "Launch Options"; [ObservableProperty] private bool isSearchBoxEnabled = true; [ObservableProperty] private string searchText = string.Empty; [ObservableProperty] private IReadOnlyList? filteredCards; public IReadOnlyList? Cards { get; set; } /// /// Return cards that match the search text /// private IReadOnlyList? GetFilteredCards(string? text) { if (string.IsNullOrWhiteSpace(text) || text.Length < 2) { return Cards; } // Try cache if (cache.Get(text, out var cachedCards)) { return cachedCards!; } var searchCard = new LaunchOptionCard { Title = text.ToLowerInvariant(), Type = LaunchOptionType.Bool, Options = Array.Empty(), }; var extracted = Process.ExtractTop(searchCard, Cards, c => c.Title.ToLowerInvariant()); var results = extracted.Where(r => r.Score > 40).Select(r => r.Value).ToImmutableList(); cache.Add(text, results); return results; } public void UpdateFilterCards() => FilteredCards = GetFilteredCards(SearchText); public LaunchOptionsViewModel(ILogger logger) { this.logger = logger; Observable .FromEventPattern(this, nameof(PropertyChanged)) .Where(x => x.EventArgs.PropertyName == nameof(SearchText)) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => SearchText) .ObserveOn(SynchronizationContext.Current!) .Subscribe( text => FilteredCards = GetFilteredCards(text), err => logger.LogError(err, "Error while filtering launch options") ); } public override void OnLoaded() { base.OnLoaded(); UpdateFilterCards(); } /// /// Export the current cards options to a list of strings /// public List AsLaunchArgs() { var launchArgs = new List(); if (Cards is null) return launchArgs; foreach (var card in Cards) { launchArgs.AddRange(card.Options); } return launchArgs; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/LykosLoginViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Net; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Refit; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Validators; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(LykosLoginDialog))] [RegisterTransient, ManagedService] public partial class LykosLoginViewModel( IAccountsService accountsService, IServiceManager vmFactory ) : TaskDialogViewModelBase { [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ContinueButtonClickCommand))] private bool isSignupMode; [ObservableProperty] [NotifyDataErrorInfo, NotifyCanExecuteChangedFor(nameof(ContinueButtonClickCommand))] [EmailAddress(ErrorMessage = "Email is not valid")] private string? email; [ObservableProperty] [NotifyDataErrorInfo, NotifyCanExecuteChangedFor(nameof(ContinueButtonClickCommand))] [Required] private string? username; [ObservableProperty] [NotifyDataErrorInfo, NotifyCanExecuteChangedFor(nameof(ContinueButtonClickCommand))] [Required] private string? password; [ObservableProperty] [NotifyDataErrorInfo, NotifyCanExecuteChangedFor(nameof(ContinueButtonClickCommand))] [Required, RequiresMatch(nameof(Password))] private string? confirmPassword; [ObservableProperty] private AppException? loginError; [ObservableProperty] private AppException? signupError; public string SignupFooterMarkdown { get; } = """ By signing up, you are creating a [lykos.ai](https://lykos.ai) Account and agree to our [Terms](https://lykos.ai/terms-and-conditions) and [Privacy Policy](https://lykos.ai/privacy) """; private bool CanExecuteContinueButtonClick() { return !HasErrors && IsValid(); } [RelayCommand(CanExecute = nameof(CanExecuteContinueButtonClick))] private Task OnContinueButtonClick() { return IsSignupMode ? SignupAsync() : LoginAsync(); } private async Task LoginAsync() { try { await accountsService.LykosLoginAsync(Email!, Password!); CloseDialog(TaskDialogStandardResult.OK); } catch (OperationCanceledException) { LoginError = new AppException("Request timed out", "Please try again later"); } catch (ApiException e) { LoginError = e.StatusCode switch { HttpStatusCode.Unauthorized => new AppException( "Incorrect email or password", "Please try again or reset your password" ), _ => new AppException("Failed to login", $"{e.StatusCode} - {e.Message}"), }; } } private async Task SignupAsync() { try { await accountsService.LykosSignupAsync(Email!, Password!, Username!); CloseDialog(TaskDialogStandardResult.OK); } catch (OperationCanceledException) { SignupError = new AppException("Request timed out", "Please try again later"); } catch (ApiException e) { SignupError = new AppException("Failed to signup", $"{e.StatusCode} - {e.Message}"); } } [RelayCommand] private async Task OnGoogleOAuthButtonClick() { var vm = vmFactory.Get(); if (await vm.GetDialog().ShowAsync() is ContentDialogResult.Primary) { CloseDialog(TaskDialogStandardResult.OK); } } /// public override TaskDialog GetDialog() { var dialog = base.GetDialog(); dialog.Buttons = new List { GetCommandButton(Resources.Action_Continue, ContinueButtonClickCommand), GetCloseButton(), }; return dialog; } private bool IsValid() { if (IsSignupMode) { return !( string.IsNullOrEmpty(Email) || string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password) || string.IsNullOrEmpty(ConfirmPassword) ); } return !(string.IsNullOrEmpty(Email) || string.IsNullOrEmpty(Password)); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/MaskEditorViewModel.cs ================================================ using System; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.Primitives; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using SkiaSharp; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Controls; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using ContentDialogButton = FluentAvalonia.UI.Controls.ContentDialogButton; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [RegisterTransient] [ManagedService] [View(typeof(MaskEditorDialog))] public partial class MaskEditorViewModel(IServiceManager vmFactory) : LoadableViewModelBase, IDisposable { private static FilePickerFileType MaskImageFilePickerType { get; } = new("Mask image or json") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.webp", "*.json" }, AppleUniformTypeIdentifiers = new[] { "public.image", "public.json" }, MimeTypes = new[] { "image/*", "application/json" } }; [JsonIgnore] private ImageSource? _cachedMaskRenderInverseAlphaImage; [JsonIgnore] private ImageSource? _cachedMaskRenderImage; /// /// When true, the mask will be applied to the image. /// [ObservableProperty] private bool isMaskEnabled; /// /// When true, the alpha channel of the image will be used as the mask. /// [ObservableProperty] private bool useImageAlphaAsMask; [JsonInclude] public PaintCanvasViewModel PaintCanvasViewModel { get; } = vmFactory.Get(); [MethodImpl(MethodImplOptions.Synchronized)] public ImageSource GetCachedOrNewMaskRenderInverseAlphaImage() { if (_cachedMaskRenderInverseAlphaImage is null) { using var skImage = PaintCanvasViewModel.RenderToWhiteChannelImage(); if (skImage is null) { throw new InvalidOperationException( "RenderToWhiteChannelImage returned null, BackgroundImageSize likely not set" ); } _cachedMaskRenderInverseAlphaImage = new ImageSource(skImage.ToAvaloniaBitmap()); } return _cachedMaskRenderInverseAlphaImage; } public ImageSource? CachedOrNewMaskRenderImage { get { if (_cachedMaskRenderImage is null) { using var skImage = PaintCanvasViewModel.RenderToImage(); if (skImage is not null) { _cachedMaskRenderImage = new ImageSource(skImage.ToAvaloniaBitmap()); } } return _cachedMaskRenderImage; } } public void InvalidateCachedMaskRenderImage() { _cachedMaskRenderImage?.Dispose(); _cachedMaskRenderImage = null; _cachedMaskRenderInverseAlphaImage?.Dispose(); _cachedMaskRenderInverseAlphaImage = null; OnPropertyChanged(nameof(CachedOrNewMaskRenderImage)); } public BetterContentDialog GetDialog() { Dispatcher.UIThread.VerifyAccess(); var dialog = new BetterContentDialog { Content = this, ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, MaxDialogHeight = 2000, MaxDialogWidth = 2000, ContentMargin = new Thickness(16), FullSizeDesired = true, PrimaryButtonText = Resources.Action_Save, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary }; return dialog; } [RelayCommand] private async Task DebugSelectFileLoadMask() { var files = await App.StorageProvider.OpenFilePickerAsync( new FilePickerOpenOptions { Title = "Select a mask", FileTypeFilter = [MaskImageFilePickerType] } ); if (files.Count == 0) { return; } var file = files[0]; await using var stream = await file.OpenReadAsync(); if (file.Name.EndsWith(".json")) { var json = await JsonSerializer.DeserializeAsync(stream); PaintCanvasViewModel.LoadStateFromJsonObject(json!); } else { var bitmap = SKBitmap.Decode(stream); PaintCanvasViewModel.LoadCanvasFromBitmap(bitmap); } } [RelayCommand] private async Task DebugSelectFileSaveMask() { var file = await App.StorageProvider.SaveFilePickerAsync( new FilePickerSaveOptions { Title = "Save mask image", DefaultExtension = ".json", FileTypeChoices = [MaskImageFilePickerType], SuggestedFileName = "mask.json", } ); if (file is null) { return; } await using var stream = await file.OpenWriteAsync(); if (file.Name.EndsWith(".json")) { var json = PaintCanvasViewModel.SaveStateToJsonObject(); await JsonSerializer.SerializeAsync(stream, json); } else { var image = PaintCanvasViewModel.RenderToImage(); await image! .Encode( Path.GetExtension(file.Name.ToLowerInvariant()) switch { ".png" => SKEncodedImageFormat.Png, ".jpg" or ".jpeg" => SKEncodedImageFormat.Jpeg, ".webp" => SKEncodedImageFormat.Webp, _ => throw new NotSupportedException("Unsupported image format") }, 100 ) .AsStream() .CopyToAsync(stream); } } public override void LoadStateFromJsonObject(JsonObject state) { base.LoadStateFromJsonObject(state); InvalidateCachedMaskRenderImage(); } /* public void LoadStateFromJsonObject(JsonObject state) { var model = state.Deserialize()!; IsMaskEnabled = model.IsMaskEnabled; UseImageAlphaAsMask = model.UseImageAlphaAsMask; if (model.PaintCanvasViewModel is not null) { PaintCanvasViewModel.LoadStateFromJsonObject(model.PaintCanvasViewModel); } } public JsonObject SaveStateToJsonObject() { var model = new MaskEditorModel { IsMaskEnabled = IsMaskEnabled, UseImageAlphaAsMask = UseImageAlphaAsMask, PaintCanvasViewModel = PaintCanvasViewModel.SaveStateToJsonObject() }; return JsonSerializer.SerializeToNode(model)!.AsObject(); } public record MaskEditorModel { public bool IsMaskEnabled { get; init; } public bool UseImageAlphaAsMask { get; init; } public JsonObject? PaintCanvasViewModel { get; init; } }*/ public void Dispose() { _cachedMaskRenderInverseAlphaImage?.Dispose(); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelMetadataEditorDialogViewModel.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(ModelMetadataEditorDialog))] [ManagedService] [RegisterTransient] public partial class ModelMetadataEditorDialogViewModel( ISettingsManager settingsManager, ICivitBaseModelTypeService baseModelTypeService, IServiceManager vmFactory ) : ContentDialogViewModelBase, IDropTarget { [ObservableProperty] private List checkpointFiles = []; [ObservableProperty] private string modelName = string.Empty; [ObservableProperty] private string modelDescription = string.Empty; [ObservableProperty] private bool isNsfw; [ObservableProperty] private string tags = string.Empty; [ObservableProperty] private CivitModelType modelType = CivitModelType.Other; [ObservableProperty] private string versionName = string.Empty; [ObservableProperty] private string baseModelType = "Other"; [ObservableProperty] private List baseModelTypes = []; [ObservableProperty] private string trainedWords = string.Empty; [ObservableProperty] private string thumbnailFilePath = string.Empty; [ObservableProperty] public partial SamplerCardViewModel SamplerCardViewModel { get; set; } = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.DenoiseStrength = 1.0d; samplerCard.EnableAddons = false; samplerCard.IsDenoiseStrengthEnabled = false; }); [ObservableProperty] public partial bool IsInferenceDefaultsEnabled { get; set; } [ObservableProperty] public partial bool ShowInferenceDefaults { get; set; } public bool IsEditingMultipleCheckpoints => CheckpointFiles.Count > 1; [RelayCommand] private async Task OpenFilePickerDialog() { var files = await App.StorageProvider.OpenFilePickerAsync( new FilePickerOpenOptions { Title = "Select an image", FileTypeFilter = [FilePickerFileTypes.ImageAll], } ); if (files.Count == 0) return; var sourceFile = new FilePath(files[0].TryGetLocalPath()!); ThumbnailFilePath = sourceFile.FullPath; } public override async Task OnLoadedAsync() { if (!Design.IsDesignMode) BaseModelTypes = await baseModelTypeService.GetBaseModelTypes(includeAllOption: false); if (IsEditingMultipleCheckpoints) return; var firstCheckpoint = CheckpointFiles.FirstOrDefault(); if (firstCheckpoint == null) return; if (!firstCheckpoint.CheckpointFile.HasConnectedModel) { ModelName = firstCheckpoint.CheckpointFile.DisplayModelName; ThumbnailFilePath = GetImagePath(firstCheckpoint.CheckpointFile); BaseModelType = "Other"; ModelType = CivitModelType.Other; return; } ShowInferenceDefaults = firstCheckpoint.ModelType == CivitModelType.Checkpoint; BaseModelType = firstCheckpoint.CheckpointFile.ConnectedModelInfo.BaseModel ?? "Other"; ModelName = firstCheckpoint.CheckpointFile.ConnectedModelInfo.ModelName; ModelDescription = firstCheckpoint.CheckpointFile.ConnectedModelInfo.ModelDescription; IsNsfw = firstCheckpoint.CheckpointFile.ConnectedModelInfo.Nsfw; Tags = string.Join(", ", firstCheckpoint.CheckpointFile.ConnectedModelInfo.Tags); ModelType = firstCheckpoint.CheckpointFile.ConnectedModelInfo.ModelType; VersionName = firstCheckpoint.CheckpointFile.ConnectedModelInfo.VersionName; TrainedWords = firstCheckpoint.CheckpointFile.ConnectedModelInfo.TrainedWords == null ? string.Empty : string.Join(", ", firstCheckpoint.CheckpointFile.ConnectedModelInfo.TrainedWords); ThumbnailFilePath = GetImagePath(firstCheckpoint.CheckpointFile); IsInferenceDefaultsEnabled = false; if (firstCheckpoint.CheckpointFile.ConnectedModelInfo.InferenceDefaults is { } defaults) { IsInferenceDefaultsEnabled = true; SamplerCardViewModel.Height = defaults.Height; SamplerCardViewModel.Width = defaults.Width; SamplerCardViewModel.CfgScale = defaults.CfgScale; SamplerCardViewModel.Steps = defaults.Steps; SamplerCardViewModel.SelectedSampler = defaults.Sampler; SamplerCardViewModel.SelectedScheduler = defaults.Scheduler; } } private string GetImagePath(LocalModelFile checkpointFile) { return checkpointFile.HasConnectedModel ? checkpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory) ?? checkpointFile.ConnectedModelInfo?.ThumbnailImageUrl ?? Assets.NoImage.ToString() : checkpointFile.GetPreviewImageFullPath(settingsManager.ModelsDirectory) ?? Assets.NoImage.ToString(); } public void DragOver(object? sender, DragEventArgs e) { if ( e.Data.GetDataFormats().Contains(DataFormats.Files) || e.Data.GetContext() is not null ) { e.Handled = true; return; } e.DragEffects = DragDropEffects.None; } public void Drop(object? sender, DragEventArgs e) { if ( e.Data.GetFiles() is not { } files || files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is not { } path ) { return; } e.Handled = true; ThumbnailFilePath = path; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs ================================================ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public partial class ModelVersionViewModel : DisposableViewModelBase { private readonly IModelIndexService modelIndexService; [ObservableProperty] public partial CivitModelVersion ModelVersion { get; set; } [ObservableProperty] public partial bool IsInstalled { get; set; } public ModelVersionViewModel(IModelIndexService modelIndexService, CivitModelVersion modelVersion) { this.modelIndexService = modelIndexService; ModelVersion = modelVersion; IsInstalled = ModelVersion.Files?.Any(file => file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && modelIndexService.ModelIndexBlake3Hashes.Contains(file.Hashes.BLAKE3) ) ?? false; EventManager.Instance.ModelIndexChanged += ModelIndexChanged; } public void RefreshInstallStatus() { IsInstalled = ModelVersion.Files?.Any(file => file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } && modelIndexService.ModelIndexBlake3Hashes.Contains(file.Hashes.BLAKE3) ) ?? false; } private void ModelIndexChanged(object? sender, EventArgs e) { // Dispatch to UI thread since the event may be raised from a background thread Dispatcher.UIThread.Post(RefreshInstallStatus); } protected override void Dispose(bool disposing) { if (disposing) { EventManager.Instance.ModelIndexChanged -= ModelIndexChanged; } base.Dispose(disposing); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models.PackageSteps; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [RegisterTransient] [ManagedService] public partial class NewOneClickInstallViewModel : ContentDialogViewModelBase { private readonly IPackageFactory packageFactory; private readonly ISettingsManager settingsManager; private readonly IPrerequisiteHelper prerequisiteHelper; private readonly ILogger logger; private readonly IPyRunner pyRunner; private readonly INavigationService navigationService; private readonly INotificationService notificationService; public SourceCache AllPackagesCache { get; } = new(p => p.Author + p.Name); public IObservableCollection ShownPackages { get; set; } = new ObservableCollectionExtended(); [ObservableProperty] private bool showIncompatiblePackages; private bool isInferenceInstall; public NewOneClickInstallViewModel( IPackageFactory packageFactory, ISettingsManager settingsManager, IPrerequisiteHelper prerequisiteHelper, ILogger logger, IPyRunner pyRunner, INavigationService navigationService, INotificationService notificationService ) { this.packageFactory = packageFactory; this.settingsManager = settingsManager; this.prerequisiteHelper = prerequisiteHelper; this.logger = logger; this.pyRunner = pyRunner; this.navigationService = navigationService; this.notificationService = notificationService; var incompatiblePredicate = this.WhenPropertyChanged(vm => vm.ShowIncompatiblePackages) .Select(_ => new Func(p => p.IsCompatible || ShowIncompatiblePackages)) .ObserveOn(SynchronizationContext.Current) .AsObservable(); AllPackagesCache .Connect() .DeferUntilLoaded() .Filter(incompatiblePredicate) .Filter(p => p is { OfferInOneClickInstaller: true, PackageType: PackageType.SdInference }) .SortAndBind( ShownPackages, SortExpressionComparer .Ascending(p => p.InstallerSortOrder) .ThenByAscending(p => p.DisplayName) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { if (ShownPackages.Count > 0) return; ShowIncompatiblePackages = true; }); AllPackagesCache.AddOrUpdate(packageFactory.GetAllAvailablePackages()); } public override void OnLoaded() { base.OnLoaded(); if (ShownPackages.Count > 0) return; ShowIncompatiblePackages = true; } [RelayCommand] private async Task InstallComfyForInference() { var comfyPackage = ShownPackages.FirstOrDefault(x => x is ComfyUI); if (comfyPackage == null) return; isInferenceInstall = true; await InstallPackage(comfyPackage); } [RelayCommand] private async Task InstallPackage(BasePackage selectedPackage) { OnPrimaryButtonClick(); var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", selectedPackage.Name); var recommendedPython = selectedPackage.RecommendedPythonVersion; var steps = new List { new SetPackageInstallingStep(settingsManager, selectedPackage.Name), new SetupPrerequisitesStep(prerequisiteHelper, selectedPackage, recommendedPython), }; // get latest version & download & install if (Directory.Exists(installLocation)) { var installPath = new DirectoryPath(installLocation); await installPath.DeleteVerboseAsync(logger); } var downloadVersion = await selectedPackage.GetLatestVersion(); var installedVersion = new InstalledPackageVersion { IsPrerelease = false }; if (selectedPackage.ShouldIgnoreReleases) { installedVersion.InstalledBranch = downloadVersion.BranchName; installedVersion.InstalledCommitSha = downloadVersion.CommitHash; } else { installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag; } var torchVersion = selectedPackage.GetRecommendedTorchVersion(); var recommendedSharedFolderMethod = selectedPackage.RecommendedSharedFolderMethod; var installedPackage = new InstalledPackage { DisplayName = selectedPackage.DisplayName, LibraryPath = Path.Combine("Packages", selectedPackage.Name), Id = Guid.NewGuid(), PackageName = selectedPackage.Name, Version = installedVersion, LaunchCommand = selectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, PreferredTorchIndex = torchVersion, PreferredSharedFolderMethod = recommendedSharedFolderMethod, UseSharedOutputFolder = selectedPackage.SharedOutputFolders is { Count: > 0 }, PythonVersion = recommendedPython.StringValue, }; var downloadStep = new DownloadPackageVersionStep( selectedPackage, installLocation, new DownloadPackageOptions { VersionOptions = downloadVersion } ); steps.Add(downloadStep); var unpackSiteCustomizeStep = new UnpackSiteCustomizeStep(Path.Combine(installLocation, "venv")); steps.Add(unpackSiteCustomizeStep); var installStep = new InstallPackageStep( selectedPackage, installLocation, installedPackage, new InstallPackageOptions { SharedFolderMethod = recommendedSharedFolderMethod, VersionOptions = downloadVersion, PythonOptions = { TorchIndex = torchVersion, PythonVersion = recommendedPython }, } ); steps.Add(installStep); var setupModelFoldersStep = new SetupModelFoldersStep( selectedPackage, recommendedSharedFolderMethod, installLocation ); steps.Add(setupModelFoldersStep); var setupOutputSharingStep = new SetupOutputSharingStep(selectedPackage, installLocation); steps.Add(setupOutputSharingStep); var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage); steps.Add(addInstalledPackageStep); var runner = new PackageModificationRunner { ShowDialogOnStart = false, HideCloseButton = false, ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully", }; runner .ExecuteSteps(steps) .ContinueWith(_ => { notificationService.OnPackageInstallCompleted(runner); EventManager.Instance.OnOneClickInstallFinished(false); if (!isInferenceInstall) return; Dispatcher.UIThread.Post(() => { navigationService.NavigateTo(); }); }) .SafeFireAndForget(); EventManager.Instance.OnPackageInstallProgressAdded(runner); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OAuthConnectViewModel.cs ================================================ using System; using System.Threading.Tasks; using System.Web; using AsyncAwaitBestPractices; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using MessagePipe; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(OAuthConnectDialog))] [RegisterTransient, ManagedService] public partial class OAuthConnectViewModel : ContentDialogViewModelBase { private readonly ILogger logger; private readonly IDistributedSubscriber uriHandlerSubscriber; private IAsyncDisposable? uriHandlerSubscription; [ObservableProperty] private string? title = "Connect OAuth"; [ObservableProperty] private string? url; [ObservableProperty] private string? description = "Please login and click 'Allow' in the opened browser window to connect with StabilityMatrix.\n\n" + "Once you have done so, close this prompt to complete the connection."; [ObservableProperty] private string? footer = "Once you have done so, close this prompt to complete the connection."; public OAuthConnectViewModel( ILogger logger, IDistributedSubscriber uriHandlerSubscriber ) { this.logger = logger; this.uriHandlerSubscriber = uriHandlerSubscriber; } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync( UriHandler.IpcKeySend, receivedUri => { logger.LogDebug("UriHandler Received URI: {Uri}", receivedUri.ToString()); // Ignore if path not matching if ( !receivedUri.PathAndQuery.StartsWith( "/oauth/patreon/callback", StringComparison.OrdinalIgnoreCase ) ) { return; } var queryCollection = HttpUtility.ParseQueryString(receivedUri.Query); var status = queryCollection.Get("status"); var error = queryCollection.Get("error"); if (status == "success") { logger.LogInformation("OAuth connection successful"); OnPrimaryButtonClick(); } else if (status == "failure") { logger.LogError("OAuth connection failed ({Status}): {Error}", status, error); Dispatcher .UIThread.InvokeAsync(async () => { var dialog = DialogHelper.CreateMarkdownDialog( $"- {error}", Resources.Label_ConnectAccountFailed ); await dialog.ShowAsync(); OnCloseButtonClick(); }) .SafeFireAndForget(); } else { logger.LogError("OAuth connection unknown status ({Status}): {Error}", status, error); Dispatcher .UIThread.InvokeAsync(async () => { var dialog = DialogHelper.CreateMarkdownDialog( $"- {error}", Resources.Label_ConnectAccountFailed ); await dialog.ShowAsync(); OnCloseButtonClick(); }) .SafeFireAndForget(); } } ); } /// public override async Task OnUnloadedAsync() { if (uriHandlerSubscription is not null) { await uriHandlerSubscription.DisposeAsync(); uriHandlerSubscription = null; } } public override BetterContentDialog GetDialog() { return new BetterContentDialog { Title = Title, Content = this, CloseButtonText = Resources.Action_Close }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OAuthDeviceAuthViewModel.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Microsoft.Extensions.Logging; using OpenIddict.Client; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; /// /// ViewModel for OAuth Device Authentication /// [View(typeof(OAuthDeviceAuthDialog))] [ManagedService] [RegisterTransient] public partial class OAuthDeviceAuthViewModel( ILogger logger, OpenIddictClientService openIdClient ) : TaskDialogViewModelBase { private CancellationTokenSource authenticationCts = new(); public OpenIddictClientModels.DeviceChallengeRequest? ChallengeRequest { get; set; } public OpenIddictClientModels.DeviceChallengeResult? ChallengeResult { get; private set; } public OpenIddictClientModels.DeviceAuthenticationRequest? AuthenticationRequest { get; private set; } public OpenIddictClientModels.DeviceAuthenticationResult? AuthenticationResult { get; private set; } public virtual string? ServiceName => ChallengeRequest?.ProviderName; [ObservableProperty] private string? description = Resources.Text_OAuthDeviceAuthDescription; [ObservableProperty] private Uri? verificationUri; [ObservableProperty] private string? userCode; [ObservableProperty] private bool isLoading; public override TaskDialog GetDialog() { var dialog = base.GetDialog(); dialog.Title = string.Format(Resources.TextTemplate_OAuthLoginTitle, ServiceName); dialog.Header = dialog.Title; dialog.Buttons = [ GetCommandButton(Resources.Action_CopyAndOpen, CopyCodeAndOpenUrlCommand), GetCloseButton(Resources.Action_Cancel) ]; return dialog; } [RelayCommand] private async Task CopyCodeAndOpenUrl() { if (VerificationUri is null) return; try { await App.Clipboard.SetTextAsync(UserCode); } catch (Exception e) { logger.LogError(e, "Failed to copy user code to clipboard"); } ProcessRunner.OpenUrl(VerificationUri); IsLoading = true; } /*/// /// Prompt to authenticate with the service using a dialog /// public async Task AuthenticateWithDialogAsync() { using var cts = new CancellationTokenSource(); var dialogTask = ShowDialogAsync(); await TryAuthenticateAsync(ChallengeRequest!, cts.Token); var result = await dialogTask; await cts.CancelAsync(); return result switch { TaskDialogStandardResult.OK => AuthenticationResult, _ => null }; }*/ public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); if (!Design.IsDesignMode && ChallengeRequest is not null && ChallengeResult is null) { await TryAuthenticateAsync(); } } protected override void OnDialogClosing(object? sender, TaskDialogClosingEventArgs e) { base.OnDialogClosing(sender, e); authenticationCts.Cancel(); } public async Task ChallengeAsync() { if (ChallengeRequest is null) { throw new InvalidOperationException( "ChallengeRequest must be set before calling StartChallengeAsync" ); } ChallengeResult = await openIdClient.ChallengeUsingDeviceAsync(ChallengeRequest); UserCode = ChallengeResult.DeviceCode; VerificationUri = ChallengeResult.VerificationUri; } public async Task TryAuthenticateAsync() { if (ChallengeRequest is null) { throw new InvalidOperationException("ChallengeRequest must be set"); } try { // Get challenge result ChallengeResult = await openIdClient.ChallengeUsingDeviceAsync(ChallengeRequest); UserCode = ChallengeResult.UserCode; VerificationUri = ChallengeResult.VerificationUri; // Wait for user to complete auth var result = await openIdClient.AuthenticateWithDeviceAsync( new OpenIddictClientModels.DeviceAuthenticationRequest { DeviceCode = ChallengeResult.DeviceCode, Interval = ChallengeResult.Interval, Timeout = ChallengeResult.ExpiresIn, CancellationToken = authenticationCts.Token } ); logger.LogInformation("Device authentication completed"); AuthenticationResult = result; CloseDialog(TaskDialogStandardResult.OK); } catch (OperationCanceledException e) { logger.LogInformation(e, "Device authentication was cancelled"); AuthenticationResult = null; CloseDialog(TaskDialogStandardResult.Close); } catch (Exception e) { logger.LogWarning(e, "Device authentication error"); AuthenticationResult = null; await CloseDialogWithErrorResultAsync(e.Message); } finally { IsLoading = false; } } private async Task CloseDialogWithErrorResultAsync(string message) { var dialog = DialogHelper.CreateMarkdownDialog(message, Resources.Label_ConnectAccountFailed); await dialog.ShowAsync(); CloseDialog(TaskDialogStandardResult.Close); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OAuthGoogleLoginViewModel.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Web; using Avalonia.Controls; using DeviceId.Encoders; using Injectio.Attributes; using MessagePipe; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSec.Cryptography; using Refit; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [RegisterTransient] [ManagedService] [View(typeof(OAuthLoginDialog))] public class OAuthGoogleLoginViewModel( ILogger baseLogger, IDistributedSubscriber uriHandlerSubscriber, ILogger logger, ILykosAuthApiV1 lykosAuthApi, IAccountsService accountsService, IOptions apiOptions ) : OAuthLoginViewModel(baseLogger, uriHandlerSubscriber) { private string? challenge; private string? verifier; private string? state; public override Uri? IconUri { get; set; } = new("avares://StabilityMatrix.Avalonia/Assets/brands-google-oauth-icon.svg"); // ReSharper disable once LocalizableElement public override string ServiceName { get; set; } = "Google"; // ReSharper disable once LocalizableElement public override string CallbackUriPath { get; set; } = "/oauth/google/callback"; protected override async Task OnCallbackUriMatchedAsync(Uri uri) { IsLoading = true; try { // Bring the app to the front (App.TopLevel as Window)?.Activate(); if (string.IsNullOrEmpty(verifier)) { // ReSharper disable once LocalizableElement throw new InvalidOperationException("Verifier is not set"); } var response = GoogleOAuthResponse.ParseFromQueryString(uri.Query); if (!string.IsNullOrEmpty(response.Error)) { logger.LogWarning("Response has error: {Error}", response.Error); OnLoginFailed([("Error", response.Error)]); return; } if (string.IsNullOrEmpty(response.Code) || string.IsNullOrEmpty(response.State)) { logger.LogWarning("Response missing code or state: {Uri}", uri.RedactQueryValues()); OnLoginFailed([("Invalid Response", "code and state are required")]); return; } if (response.State != state) { logger.LogWarning("Response state mismatch: {Uri}", uri.RedactQueryValues()); OnLoginFailed([("Invalid Response", "state mismatch")]); return; } await accountsService.LykosLoginViaGoogleOAuthAsync(response.Code, response.State, verifier); // Success OnPrimaryButtonClick(); } catch (ApiException e) { logger.LogError(e, "Api error while handling callback uri"); OnLoginFailed([(e.StatusCode.ToString(), e.Content)]); } catch (Exception e) { logger.LogError(e, "Failed to handle callback uri"); OnLoginFailed([(e.GetType().Name, e.Message)]); } finally { IsLoading = false; } } public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); IsLoading = true; try { await GenerateUrlAsync(); } catch (Exception e) { logger.LogError(e, "Failed to generate url"); OnLoginFailed([(e.GetType().Name, e.Message)]); return; } finally { IsLoading = false; } // Open in browser ProcessRunner.OpenUrl(Url!); } private async Task GenerateUrlAsync() { (challenge, verifier) = GeneratePkceSha256ChallengePair(); var redirectUri = apiOptions.Value.LykosAuthApiBaseUrl.Append("/api/open/sm/oauth/google/callback"); logger.LogDebug("Requesting Google OAuth URL..."); var link = await lykosAuthApi.GetOAuthGoogleLoginOrSignupLink( redirectUri.ToString(), codeChallenge: challenge, codeChallengeMethod: "S256" ); var queryCollection = HttpUtility.ParseQueryString(link.Query); // ReSharper disable once LocalizableElement state = queryCollection.Get("state"); Url = link.ToString(); logger.LogInformation("Generated Google OAuth URL: {Url}", Url); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OAuthLoginViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using MessagePipe; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; /// /// Like , but for handling full code responses from OAuth providers, /// instead of being able to just close and refresh state. /// [RegisterTransient] [ManagedService] [View(typeof(OAuthLoginDialog))] [Localizable(false)] public partial class OAuthLoginViewModel( ILogger logger, IDistributedSubscriber uriHandlerSubscriber ) : ContentDialogViewModelBase, IAsyncDisposable { private IAsyncDisposable? uriHandlerSubscription; /// /// Name of the service to connect to /// public virtual string ServiceName { get; set; } = ""; /// /// Url to open in the browser /// [ObservableProperty] private string? url; public override string Title => string.Format(TitleTemplate, ServiceName).Trim(); public virtual string TitleTemplate => Resources.TextTemplate_OAuthLoginTitle; public virtual string? Description { get; set; } = Resources.Text_OAuthLoginDescription; public virtual string? AppLinkInstructions { get; set; } = Resources.Text_AllowBrowserOpenAppLink; // ReSharper disable once LocalizableElement public virtual string CallbackUriPath { get; set; } = "/oauth/default/callback"; public virtual Uri? IconUri { get; set; } [ObservableProperty] private bool isLoading = true; /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); uriHandlerSubscription = await uriHandlerSubscriber.SubscribeAsync( UriHandler.IpcKeySend, OnCallbackUriReceived ); } /// public override async Task OnUnloadedAsync() { if (uriHandlerSubscription is not null) { await uriHandlerSubscription.DisposeAsync(); uriHandlerSubscription = null; } } protected virtual void OnLoginFailed(IEnumerable<(string Message, string? Detail)> errors) { // ReSharper disable twice LocalizableElement var content = string.Join( "\n", errors.Select(e => e.Detail is null ? $"- **{e.Message}**" : $"- **{e.Message}**: {e.Detail}") ); Dispatcher.UIThread.Post(() => { var dialog = DialogHelper.CreateMarkdownDialog(content, Resources.Label_ConnectAccountFailed); dialog.ShowAsync().ContinueWith(_ => OnCloseButtonClick()).SafeFireAndForget(); }); } protected virtual Task OnCallbackUriMatchedAsync(Uri uri) => Task.CompletedTask; private void OnCallbackUriReceived(Uri uri) { // Ignore if path not matching if (uri.AbsolutePath != CallbackUriPath) { logger.LogDebug("Received Callback URI: {Uri}", uri.RedactQueryValues()); return; } logger.LogInformation("Matched Callback URI: {Uri}", uri.RedactQueryValues()); OnCallbackUriMatchedAsync(uri).SafeFireAndForget(); } /// public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.CloseButtonText = Resources.Action_Cancel; return dialog; } public async ValueTask DisposeAsync() { if (uriHandlerSubscription is not null) { await uriHandlerSubscription.DisposeAsync(); uriHandlerSubscription = null; } GC.SuppressFinalize(this); } protected static (string Challenge, string Verifier) GeneratePkceSha256ChallengePair() { var verifier = RandomNumberGenerator.GetHexString(128, true); var hash = SHA256.HashData(Encoding.ASCII.GetBytes(verifier)); // Convert to base64url var base64UrlHash = Convert.ToBase64String(hash).TrimEnd('=').Replace('+', '-').Replace('/', '_'); return (base64UrlHash, verifier); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [ManagedService] [RegisterTransient] public partial class OneClickInstallViewModel : ContentDialogViewModelBase { private readonly ISettingsManager settingsManager; private readonly IPackageFactory packageFactory; private readonly IPrerequisiteHelper prerequisiteHelper; private readonly ILogger logger; private readonly IPyRunner pyRunner; private readonly INavigationService navigationService; private const string DefaultPackageName = "stable-diffusion-webui"; [ObservableProperty] private string headerText; [ObservableProperty] private string subHeaderText; [ObservableProperty] private string subSubHeaderText = string.Empty; [ObservableProperty] private bool showInstallButton; [ObservableProperty] private bool isIndeterminate; [ObservableProperty] private bool showIncompatiblePackages; [ObservableProperty] private ObservableCollection allPackages; [ObservableProperty] private BasePackage selectedPackage; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsProgressBarVisible))] private int oneClickInstallProgress; private bool isInferenceInstall; public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate; public OneClickInstallViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory, IPrerequisiteHelper prerequisiteHelper, ILogger logger, IPyRunner pyRunner, INavigationService navigationService ) { this.settingsManager = settingsManager; this.packageFactory = packageFactory; this.prerequisiteHelper = prerequisiteHelper; this.logger = logger; this.pyRunner = pyRunner; this.navigationService = navigationService; HeaderText = Resources.Text_WelcomeToStabilityMatrix; SubHeaderText = Resources.Text_OneClickInstaller_SubHeader; ShowInstallButton = true; var filteredPackages = this .packageFactory.GetAllAvailablePackages() .Where(p => p is { OfferInOneClickInstaller: true, IsCompatible: true }) .ToList(); AllPackages = new ObservableCollection( filteredPackages.Any() ? filteredPackages : this.packageFactory.GetAllAvailablePackages() ); SelectedPackage = AllPackages[0]; } [RelayCommand] private async Task Install() { ShowInstallButton = false; await DoInstall(); ShowInstallButton = true; } [RelayCommand] private Task ToggleAdvancedMode() { EventManager.Instance.OnOneClickInstallFinished(true); return Task.CompletedTask; } [RelayCommand] private async Task InstallComfyForInference() { var comfyPackage = AllPackages.FirstOrDefault(x => x is ComfyUI); if (comfyPackage != null) { SelectedPackage = comfyPackage; isInferenceInstall = true; await InstallCommand.ExecuteAsync(null); } } private async Task DoInstall() { var steps = new List { new SetPackageInstallingStep(settingsManager, SelectedPackage.Name), new SetupPrerequisitesStep( prerequisiteHelper, SelectedPackage, PyInstallationManager.Python_3_10_17 ), }; // get latest version & download & install var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", SelectedPackage.Name); if (Directory.Exists(installLocation)) { var installPath = new DirectoryPath(installLocation); await installPath.DeleteVerboseAsync(logger); } var downloadVersion = await SelectedPackage.GetLatestVersion(); var installedVersion = new InstalledPackageVersion { IsPrerelease = false }; if (SelectedPackage.ShouldIgnoreReleases) { installedVersion.InstalledBranch = downloadVersion.BranchName; installedVersion.InstalledCommitSha = downloadVersion.CommitHash; } else { installedVersion.InstalledReleaseVersion = downloadVersion.VersionTag; } var torchVersion = SelectedPackage.GetRecommendedTorchVersion(); var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; var recommendedPython = SelectedPackage.RecommendedPythonVersion; var downloadStep = new DownloadPackageVersionStep( SelectedPackage, installLocation, new DownloadPackageOptions() { VersionOptions = downloadVersion } ); steps.Add(downloadStep); var installedPackage = new InstalledPackage { DisplayName = SelectedPackage.DisplayName, LibraryPath = Path.Combine("Packages", SelectedPackage.Name), Id = Guid.NewGuid(), PackageName = SelectedPackage.Name, Version = installedVersion, LaunchCommand = SelectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, PreferredTorchIndex = torchVersion, PreferredSharedFolderMethod = recommendedSharedFolderMethod, PythonVersion = recommendedPython.StringValue, }; var installStep = new InstallPackageStep( SelectedPackage, installLocation, installedPackage, new InstallPackageOptions { SharedFolderMethod = recommendedSharedFolderMethod, VersionOptions = downloadVersion, PythonOptions = { TorchIndex = torchVersion, PythonVersion = recommendedPython }, } ); steps.Add(installStep); var setupModelFoldersStep = new SetupModelFoldersStep( SelectedPackage, recommendedSharedFolderMethod, installLocation ); steps.Add(setupModelFoldersStep); var addInstalledPackageStep = new AddInstalledPackageStep(settingsManager, installedPackage); steps.Add(addInstalledPackageStep); var runner = new PackageModificationRunner { ShowDialogOnStart = true, HideCloseButton = true, ModificationCompleteMessage = $"{SelectedPackage.DisplayName} installed successfully", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); EventManager.Instance.OnInstalledPackagesChanged(); HeaderText = $"{SelectedPackage.DisplayName} installed successfully"; for (var i = 3; i > 0; i--) { SubHeaderText = $"{Resources.Text_ProceedingToLaunchPage} ({i}s)"; await Task.Delay(1000); } // should close dialog EventManager.Instance.OnOneClickInstallFinished(false); if (isInferenceInstall) { navigationService.NavigateTo(); } } partial void OnShowIncompatiblePackagesChanged(bool value) { var filteredPackages = packageFactory .GetAllAvailablePackages() .Where(p => p.OfferInOneClickInstaller && (ShowIncompatiblePackages || p.IsCompatible)) .ToList(); AllPackages = new ObservableCollection( filteredPackages.Any() ? filteredPackages : packageFactory.GetAllAvailablePackages() ); SelectedPackage = AllPackages[0]; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs ================================================ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(OpenArtWorkflowDialog))] [ManagedService] [RegisterTransient] public partial class OpenArtWorkflowViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory ) : ContentDialogViewModelBase { public required OpenArtSearchResult Workflow { get; init; } [ObservableProperty] private ObservableCollection customNodes = []; [ObservableProperty] private string prunedDescription = string.Empty; [ObservableProperty] private bool installRequiredNodes = true; [ObservableProperty] private InstalledPackage? selectedPackage; public PackagePair? SelectedPackagePair => SelectedPackage is { } package ? packageFactory.GetPackagePair(package) : null; public List AvailablePackages => settingsManager .Settings.InstalledPackages.Where(package => package.PackageName is "ComfyUI" or "ComfyUI-Zluda") .ToList(); public List MissingNodes { get; } = []; public override async Task OnLoadedAsync() { if (Design.IsDesignMode) return; if (settingsManager.Settings.PreferredWorkflowPackage is { } preferredPackage) { SelectedPackage = preferredPackage; } else { SelectedPackage = AvailablePackages.FirstOrDefault(); } if (SelectedPackage == null) { InstallRequiredNodes = false; } CustomNodes = new ObservableCollection( await ParseNodes(Workflow.NodesIndex.ToList()) ); PrunedDescription = Utilities.RemoveHtml(Workflow.Description); } partial void OnSelectedPackageChanged(InstalledPackage? oldValue, InstalledPackage? newValue) { if (oldValue is null) return; settingsManager.Transaction(settings => { settings.PreferredWorkflowPackage = newValue; }); OnLoadedAsync().SafeFireAndForget(); } [Localizable(false)] private async Task> ParseNodes(List nodes) { var indexOfFirstDot = nodes.IndexOf("."); if (indexOfFirstDot != -1) { nodes = nodes[(indexOfFirstDot + 1)..]; } var installedNodesNames = new HashSet(); var nameToManifestNodes = new Dictionary(); var addedMissingNodes = new HashSet(); var packagePair = SelectedPackagePair; if (packagePair?.BasePackage.ExtensionManager is { } extensionManager) { var installedNodes = ( await extensionManager.GetInstalledExtensionsLiteAsync(packagePair.InstalledPackage) ).ToList(); var manifestExtensionsMap = await extensionManager.GetManifestExtensionsMapAsync( extensionManager.GetManifests(packagePair.InstalledPackage) ); // Add manifestExtensions definition to installedNodes if matching git repository url installedNodes = installedNodes .Select(installedNode => { if ( installedNode.GitRepositoryUrl is not null && manifestExtensionsMap.TryGetValue( installedNode.GitRepositoryUrl, out var manifestExtension ) ) { installedNode = installedNode with { Definition = manifestExtension }; } return installedNode; }) .ToList(); // There may be duplicate titles, deduplicate by using the first one nameToManifestNodes = manifestExtensionsMap .GroupBy(x => x.Value.Title) .ToDictionary(x => x.Key, x => x.First().Value); installedNodesNames = installedNodes.Select(x => x.Title).ToHashSet(); } var sections = new List(); OpenArtCustomNode? currentSection = null; foreach (var node in nodes) { if (node is "." or ",") { currentSection = null; // End of the current section continue; } if (currentSection == null) { currentSection = new OpenArtCustomNode { Title = node, IsInstalled = installedNodesNames.Contains(node), }; // Add missing nodes to the list (deduplicate by title) if ( !currentSection.IsInstalled && addedMissingNodes.Add(node) && nameToManifestNodes.TryGetValue(node, out var manifestNode) ) { MissingNodes.Add(manifestNode); } sections.Add(currentSection); } else { currentSection.Children.Add(node); } } if (sections.FirstOrDefault(x => x.Title == "ComfyUI") != null) { sections = sections.Where(x => x.Title != "ComfyUI").ToList(); } return sections; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenModelDbModelDetailsViewModel.cs ================================================ using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.OpenModelsDb; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(OpenModelDbModelDetailsDialog))] [ManagedService] [RegisterTransient] public partial class OpenModelDbModelDetailsViewModel( OpenModelDbManager openModelDbManager, IModelIndexService modelIndexService, IModelImportService modelImportService, INotificationService notificationService, ISettingsManager settingsManager, IServiceManager dialogFactory ) : ContentDialogViewModelBase { public class ModelResourceViewModel(IModelIndexService modelIndexService) { public required OpenModelDbResource Resource { get; init; } public string DisplayName => $"{Resource.Platform} (.{Resource.Type} file)"; public bool IsInstalled => modelIndexService.FindBySha256Async(Resource.Sha256).GetAwaiter().GetResult().Any(); } [Required] public OpenModelDbKeyedModel? Model { get; set; } public IEnumerable ImageUris => Model?.Images?.SelectImageAbsoluteUris().Any() ?? false ? Model?.Images?.SelectImageAbsoluteUris() ?? [Assets.NoImage] : [Assets.NoImage]; public IEnumerable Resources => Model ?.Resources ?.Select(resource => new ModelResourceViewModel(modelIndexService) { Resource = resource }) ?? []; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanImport))] private ModelResourceViewModel? selectedResource; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsCustomSelected))] [NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))] private string selectedInstallLocation = string.Empty; [ObservableProperty] private ObservableCollection availableInstallLocations = []; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))] private string customInstallLocation = string.Empty; public bool IsCustomSelected => SelectedInstallLocation == "Custom..."; public bool ShowEmptyPathWarning => IsCustomSelected && string.IsNullOrWhiteSpace(CustomInstallLocation); public bool CanImport => SelectedResource is { IsInstalled: false }; public override void OnLoaded() { if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet) return; LoadInstallLocations(); } [RelayCommand(CanExecute = nameof(CanImport))] private async Task ImportAsync(ModelResourceViewModel? resourceVm) { if (resourceVm?.Resource is null || Model is null) return; if ( Model.GetSharedFolderType() is not { } sharedFolderType || sharedFolderType is SharedFolderType.Unknown ) { notificationService.ShowPersistent( "Failed to import model", $"Model Architecture '{Model.Architecture}' not supported", NotificationType.Error ); return; } var downloadFolder = new DirectoryPath( settingsManager.ModelsDirectory, sharedFolderType.GetStringValue() ).ToString(); var useCustomLocation = !string.IsNullOrWhiteSpace(CustomInstallLocation) && Directory.Exists(CustomInstallLocation); if (useCustomLocation) { var customFolder = new DirectoryPath(CustomInstallLocation); customFolder.Create(); downloadFolder = customFolder.ToString(); } if (!string.IsNullOrWhiteSpace(SelectedInstallLocation) && SelectedInstallLocation != "Custom...") { var selectedFolder = SelectedInstallLocation .Replace("Models", string.Empty) .Replace(Path.DirectorySeparatorChar.ToString(), string.Empty); downloadFolder = new DirectoryPath(settingsManager.ModelsDirectory, selectedFolder); } await modelImportService.DoOpenModelDbImport( Model, resourceVm.Resource, downloadFolder, download => download.ContextAction = new ModelPostDownloadContextAction() ); OnPrimaryButtonClick(); } [RelayCommand] private async Task DeleteModel(ModelResourceViewModel? resourceVm) { if (SelectedResource == null) return; var fileToDelete = SelectedResource; var hash = fileToDelete.Resource.Sha256; if (string.IsNullOrWhiteSpace(hash)) { notificationService.Show( "Error deleting file", "Could not delete model, hash is missing.", NotificationType.Error ); return; } var matchingModels = (await modelIndexService.FindBySha256Async(hash)).ToList(); if (matchingModels.Count == 0) { await modelIndexService.RefreshIndex(); matchingModels = (await modelIndexService.FindBySha256Async(hash)).ToList(); if (matchingModels.Count == 0) { notificationService.Show( "Error deleting file", "Could not delete model, model not found in index.", NotificationType.Error ); return; } } var confirmDeleteVm = dialogFactory.Get(); var pathsToDelete = new List(); foreach (var localModel in matchingModels) { var checkpointPath = new FilePath(localModel.GetFullPath(settingsManager.ModelsDirectory)); var previewPath = localModel.GetPreviewImageFullPath(settingsManager.ModelsDirectory); var cmInfoPath = checkpointPath.ToString().Replace(checkpointPath.Extension, ".cm-info.json"); pathsToDelete.Add(checkpointPath); pathsToDelete.Add(previewPath); pathsToDelete.Add(cmInfoPath); } confirmDeleteVm.PathsToDelete = pathsToDelete; if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) return; try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting folder", e.Message, NotificationType.Error); return; } } partial void OnSelectedInstallLocationChanged(string? value) { if (value?.Equals("Custom...", StringComparison.OrdinalIgnoreCase) is true) { Dispatcher.UIThread.InvokeAsync(SelectCustomFolder); } else { CustomInstallLocation = string.Empty; } } public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.IsFooterVisible = false; dialog.CloseOnClickOutside = true; dialog.FullSizeDesired = true; dialog.ContentMargin = new Thickness(8); return dialog; } public async Task SelectCustomFolder() { var files = await App.StorageProvider.OpenFolderPickerAsync( new FolderPickerOpenOptions { Title = "Select Download Folder", AllowMultiple = false, SuggestedStartLocation = await App.StorageProvider.TryGetFolderFromPathAsync( Path.Combine( settingsManager.ModelsDirectory, Model.GetSharedFolderType().GetStringValue() ) ) } ); if (files.FirstOrDefault()?.TryGetLocalPath() is { } path) { CustomInstallLocation = path; } } private void LoadInstallLocations() { var installLocations = new ObservableCollection(); var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var downloadDirectory = new DirectoryPath( rootModelsDirectory, Model.GetSharedFolderType().GetStringValue() ); if (!downloadDirectory.ToString().EndsWith("Unknown")) { installLocations.Add(downloadDirectory.ToString().Replace(rootModelsDirectory, "Models")); foreach ( var directory in downloadDirectory.EnumerateDirectories( "*", EnumerationOptionConstants.AllDirectories ) ) { installLocations.Add(directory.ToString().Replace(rootModelsDirectory, "Models")); } } installLocations.Add("Custom..."); AvailableInstallLocations = installLocations; SelectedInstallLocation = installLocations.FirstOrDefault(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/PackageImportViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(PackageImportDialog))] [ManagedService] [RegisterTransient] public partial class PackageImportViewModel( IPackageFactory packageFactory, ISettingsManager settingsManager, IPyInstallationManager pyInstallationManager ) : ContentDialogViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private bool venvDetected = false; [ObservableProperty] private DirectoryPath? packagePath; [ObservableProperty] private BasePackage? selectedBasePackage; public IReadOnlyList AvailablePackages => [.. packageFactory.GetAllAvailablePackages()]; [ObservableProperty] private PackageVersion? selectedVersion; [ObservableProperty] private ObservableCollection? availableCommits; [ObservableProperty] private ObservableCollection? availableVersions; [ObservableProperty] private GitCommit? selectedCommit; [ObservableProperty] private bool canSelectBasePackage = true; [ObservableProperty] private string? customCommitSha; [ObservableProperty] private bool showCustomCommitSha; [ObservableProperty] public partial bool ShowPythonVersionSelection { get; set; } = true; // Version types (release or commit) [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReleaseLabelText), nameof(IsReleaseMode), nameof(SelectedVersion))] private PackageVersionType selectedVersionType = PackageVersionType.Commit; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsReleaseModeAvailable))] private PackageVersionType availableVersionTypes = PackageVersionType.GithubRelease | PackageVersionType.Commit; [ObservableProperty] public partial ObservableCollection AvailablePythonVersions { get; set; } = []; [ObservableProperty] public partial UvPythonInfo? SelectedPythonVersion { get; set; } public string ReleaseLabelText => IsReleaseMode ? "Version" : "Branch"; public bool IsReleaseMode { get => SelectedVersionType == PackageVersionType.GithubRelease; set => SelectedVersionType = value ? PackageVersionType.GithubRelease : PackageVersionType.Commit; } public bool IsReleaseModeAvailable => AvailableVersionTypes.HasFlag(PackageVersionType.GithubRelease); public override async Task OnLoadedAsync() { SelectedBasePackage ??= AvailablePackages[0]; if (Design.IsDesignMode) return; // Populate available versions try { var versionOptions = await SelectedBasePackage.GetAllVersionOptions(); if (IsReleaseMode) { AvailableVersions = new ObservableCollection( versionOptions.AvailableVersions ); if (!AvailableVersions.Any()) return; SelectedVersion = AvailableVersions[0]; } else { AvailableVersions = new ObservableCollection( versionOptions.AvailableBranches ); UpdateSelectedVersionToLatestMain(); } var pythonVersions = await pyInstallationManager.GetAllAvailablePythonsAsync(); AvailablePythonVersions = new ObservableCollection(pythonVersions); if ( PackagePath is not null && Utilities.TryGetPyVenvVersion(PackagePath.FullPath, out var venvPyVersion) ) { var matchingVenvPy = AvailablePythonVersions.FirstOrDefault(x => x.Version == venvPyVersion); if (matchingVenvPy != default) { SelectedPythonVersion = matchingVenvPy; venvDetected = true; } } SelectedPythonVersion ??= GetRecommendedPyVersion() ?? AvailablePythonVersions.LastOrDefault(); } catch (Exception e) { Logger.Warn("Error getting versions: {Exception}", e.ToString()); } } private static string GetDisplayVersion(string version, string? branch) { return branch == null ? version : $"{branch}@{version[..7]}"; } // When available version types change, reset selected version type if not compatible partial void OnAvailableVersionTypesChanged(PackageVersionType value) { if (!value.HasFlag(SelectedVersionType)) { SelectedVersionType = value; } } // When changing branch / release modes, refresh // ReSharper disable once UnusedParameterInPartialMethod partial void OnSelectedVersionTypeChanged(PackageVersionType value) => OnSelectedBasePackageChanged(SelectedBasePackage); partial void OnSelectedVersionChanged(PackageVersion? value) { if (value == null || IsReleaseMode) return; if (SelectedBasePackage is BaseGitPackage gitPackage) { Dispatcher .UIThread.InvokeAsync(async () => { var commits = (await gitPackage.GetAllCommits(value.TagName))?.ToList(); if (commits is null || commits.Count == 0) return; commits = [.. commits, new GitCommit { Sha = "Custom..." }]; AvailableCommits = new ObservableCollection(commits); SelectedCommit = AvailableCommits[0]; }) .SafeFireAndForget(); } } partial void OnSelectedCommitChanged(GitCommit? value) { ShowCustomCommitSha = value is { Sha: "Custom..." }; } partial void OnSelectedBasePackageChanged(BasePackage? value) { if (value is null || SelectedBasePackage is null) { AvailableVersions?.Clear(); AvailableCommits?.Clear(); return; } AvailableVersions?.Clear(); AvailableCommits?.Clear(); AvailableVersionTypes = SelectedBasePackage.AvailableVersionTypes; if (Design.IsDesignMode) return; Dispatcher .UIThread.InvokeAsync(async () => { Logger.Debug($"Release mode: {IsReleaseMode}"); var versionOptions = await value.GetAllVersionOptions(); AvailableVersions = IsReleaseMode ? new ObservableCollection(versionOptions.AvailableVersions) : new ObservableCollection(versionOptions.AvailableBranches); Logger.Debug($"Available versions: {string.Join(", ", AvailableVersions)}"); SelectedVersion = AvailableVersions[0]; if (!IsReleaseMode) { var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList(); if (commits is null || commits.Count == 0) return; commits = [.. commits, new GitCommit { Sha = "Custom..." }]; AvailableCommits = new ObservableCollection(commits); SelectedCommit = AvailableCommits[0]; UpdateSelectedVersionToLatestMain(); } if (!venvDetected) { SelectedPythonVersion = GetRecommendedPyVersion() ?? AvailablePythonVersions.FirstOrDefault(); } }) .SafeFireAndForget(); } private void UpdateSelectedVersionToLatestMain() { if (AvailableVersions is null) { SelectedVersion = null; } else { // First try to find master var version = AvailableVersions.FirstOrDefault(x => x.TagName == "master"); // If not found, try main version ??= AvailableVersions.FirstOrDefault(x => x.TagName == "main"); // If still not found, just use the first one version ??= AvailableVersions[0]; SelectedVersion = version; } } public async Task AddPackageWithCurrentInputs() { if (SelectedBasePackage is null || PackagePath is null) return; var version = new InstalledPackageVersion(); if (IsReleaseMode) { version.InstalledReleaseVersion = SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); } else { version.InstalledBranch = SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); version.InstalledCommitSha = SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null"); } var torchVersion = SelectedBasePackage.GetRecommendedTorchVersion(); var sharedFolderRecommendation = SelectedBasePackage.RecommendedSharedFolderMethod; var package = new InstalledPackage { Id = Guid.NewGuid(), DisplayName = PackagePath.Name, PackageName = SelectedBasePackage.Name, LibraryPath = Path.Combine("Packages", PackagePath.Name), Version = version, LaunchCommand = SelectedBasePackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, PreferredTorchIndex = torchVersion, PreferredSharedFolderMethod = sharedFolderRecommendation, PythonVersion = SelectedPythonVersion?.Version.StringValue ?? PyInstallationManager.Python_3_10_17.StringValue, }; // Recreate venv if it's a BaseGitPackage if (SelectedBasePackage is BaseGitPackage { UsesVenv: true } gitPackage) { Logger.Info( "Recreating venv for imported package {Name} ({PackageName})", package.DisplayName, package.PackageName ); await gitPackage.SetupVenv( PackagePath, forceRecreate: true, pythonVersion: PyVersion.Parse(package.PythonVersion), onConsoleOutput: output => { Logger.Debug("SetupVenv output: {Output}", output.Text); } ); } // Reconfigure shared links Logger.Info( "Configuring shared links for imported package {Name} ({PackageName})", package.DisplayName, package.PackageName ); var recommendedSharedFolderMethod = SelectedBasePackage.RecommendedSharedFolderMethod; await SelectedBasePackage.UpdateModelFolders(PackagePath, recommendedSharedFolderMethod); settingsManager.Transaction(s => s.InstalledPackages.Add(package)); } private UvPythonInfo? GetRecommendedPyVersion() => AvailablePythonVersions.LastOrDefault(x => x.Version.Major.Equals(SelectedBasePackage?.RecommendedPythonVersion.Major) && x.Version.Minor.Equals(SelectedBasePackage?.RecommendedPythonVersion.Minor) ); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/PropertyGridViewModel.cs ================================================ using System.Collections.Generic; using System.ComponentModel; using Avalonia; using Avalonia.PropertyGrid.ViewModels; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using OneOf; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(PropertyGridDialog))] [ManagedService] [RegisterTransient] public partial class PropertyGridViewModel : ContentDialogViewModelBase { [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedObjectItemsSource))] private OneOf>? selectedObject; public IEnumerable? SelectedObjectItemsSource => SelectedObject?.Match(single => [single], multiple => multiple); [ObservableProperty] private PropertyGridShowStyle showStyle = PropertyGridShowStyle.Alphabetic; [ObservableProperty] private IReadOnlyList? excludeCategories; [ObservableProperty] private IReadOnlyList? includeCategories; /// public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.Padding = new Thickness(0); dialog.CloseOnClickOutside = true; dialog.CloseButtonText = Resources.Action_Close; return dialog; } /// /// Like , but with a primary save button. /// public BetterContentDialog GetSaveDialog() { var dialog = base.GetDialog(); dialog.Padding = new Thickness(0); dialog.CloseOnClickOutside = true; dialog.CloseButtonText = Resources.Action_Close; dialog.PrimaryButtonText = Resources.Action_Save; dialog.DefaultButton = ContentDialogButton.Primary; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackageSpecifiersViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [RegisterTransient] [ManagedService] [View(typeof(PythonPackageSpecifiersDialog))] public partial class PythonPackageSpecifiersViewModel : ContentDialogViewModelBase { [ObservableProperty] private string? title = Resources.Label_PythonDependenciesOverride_Title; [ObservableProperty] private string? description = Resources.Label_PythonDependenciesOverride_Description; [ObservableProperty] private string? helpLinkLabel = Resources.Label_DependencySpecifiers; [ObservableProperty] private Uri? helpLinkUri = new("https://packaging.python.org/en/latest/specifications/dependency-specifiers"); protected ObservableCollection Specifiers { get; } = []; public DataGridCollectionView SpecifiersView { get; } public PythonPackageSpecifiersViewModel() { SpecifiersView = new DataGridCollectionView(Specifiers); } public void LoadSpecifiers(IEnumerable specifiers) { Specifiers.Clear(); Specifiers.AddRange(specifiers.Select(PythonPackageSpecifiersItem.FromSpecifier)); } public IEnumerable GetSpecifiers() { return Specifiers.Select(row => row.ToSpecifier()); } public override BetterContentDialog GetDialog() { var dialog = base.GetDialog(); dialog.PrimaryButtonText = Resources.Action_Save; dialog.DefaultButton = ContentDialogButton.Primary; dialog.CloseButtonText = Resources.Action_Cancel; dialog.MaxDialogWidth = 800; dialog.FullSizeDesired = true; return dialog; } [RelayCommand] private void AddRow() { Specifiers.Add( PythonPackageSpecifiersItem.FromSpecifier( new PipPackageSpecifierOverride { Action = PipPackageSpecifierOverrideAction.Update, Constraint = "==" } ) ); } [RelayCommand] private void RemoveSelectedRow(int selectedIndex) { try { Specifiers.RemoveAt(selectedIndex); } catch (ArgumentOutOfRangeException) { Debug.WriteLine($"RemoveSelectedRow: Index {selectedIndex} out of range"); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesItemViewModel.cs ================================================ using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using Semver; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public partial class PythonPackagesItemViewModel(ISettingsManager settingsManager) : ViewModelBase { [ObservableProperty] private PipPackageInfo package; [ObservableProperty] private string? selectedVersion; [ObservableProperty] private IReadOnlyList? availableVersions; [ObservableProperty] private PipShowResult? pipShowResult; [ObservableProperty] private bool isLoading; /// /// True if selected version is newer than the installed version /// [ObservableProperty] private bool canUpgrade; /// /// True if selected version is older than the installed version /// [ObservableProperty] private bool canDowngrade; partial void OnSelectedVersionChanged(string? value) { if ( value is null || Package.Version == value || !SemVersion.TryParse(Package.Version, out var currentSemver) || !SemVersion.TryParse(value, out var selectedSemver) ) { var compare = string.CompareOrdinal(value, Package.Version); CanUpgrade = compare > 0; CanDowngrade = compare < 0; return; } var precedence = selectedSemver.ComparePrecedenceTo(currentSemver); CanUpgrade = precedence > 0; CanDowngrade = precedence < 0; } /// /// Return the known index URL for a package, currently this is torch, torchvision and torchaudio /// public static string? GetKnownIndexUrl(string packageName, string version) { var torchPackages = new[] { "torch", "torchvision", "torchaudio" }; if (torchPackages.Contains(packageName) && version.Contains('+')) { // Get the metadata for the current version (everything after the +) var indexName = version.Split('+', 2).Last(); var indexUrl = $"https://download.pytorch.org/whl/{indexName}"; return indexUrl; } return null; } /// /// Loads the pip show result if not already loaded /// public async Task LoadExtraInfo(DirectoryPath venvPath, PyBaseInstall pyBaseInstall) { if (PipShowResult is not null) { return; } IsLoading = true; try { if (Design.IsDesignMode) { await LoadExtraInfoDesignMode(); } else { await using var venvRunner = await pyBaseInstall.CreateVenvRunnerAsync( venvPath, workingDirectory: venvPath.Parent, environmentVariables: settingsManager.Settings.EnvironmentVariables ); PipShowResult = await venvRunner.PipShow(Package.Name); // Attempt to get known index url var indexUrl = GetKnownIndexUrl(Package.Name, Package.Version); if (await venvRunner.PipIndex(Package.Name, indexUrl) is { } pipIndexResult) { AvailableVersions = pipIndexResult.AvailableVersions; SelectedVersion = Package.Version; } } } catch (ProcessException) { // Ignore } finally { IsLoading = false; } } private async Task LoadExtraInfoDesignMode() { await using var _ = new MinimumDelay(200, 300); PipShowResult = new PipShowResult { Name = Package.Name, Version = Package.Version }; AvailableVersions = new[] { Package.Version, "1.2.0", "1.1.0", "1.0.0" }; SelectedVersion = Package.Version; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AutoCtor; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(PythonPackagesDialog))] [ManagedService] [RegisterTransient] [AutoConstruct] public partial class PythonPackagesViewModel : ContentDialogViewModelBase { private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly IPyInstallationManager pyInstallationManager; private readonly IPrerequisiteHelper prerequisiteHelper; private PyBaseInstall? pyBaseInstall; public DirectoryPath? VenvPath { get; set; } public PyVersion? PythonVersion { get; set; } [ObservableProperty] private bool isLoading; private readonly SourceCache packageSource = new(p => p.Name); public IObservableCollection Packages { get; } = new ObservableCollectionExtended(); [ObservableProperty] private PythonPackagesItemViewModel? selectedPackage; [ObservableProperty] private string searchQuery = string.Empty; [AutoPostConstruct] private void PostConstruct() { var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(100)) .DistinctUntilChanged() .ObserveOn(SynchronizationContext.Current!) .Select(value => { if (string.IsNullOrWhiteSpace(value.Value)) { return (static _ => true); } return (Func)( p => p.Name.Contains(value.Value, StringComparison.OrdinalIgnoreCase) ); }); packageSource .Connect() .DeferUntilLoaded() .Filter(searchPredicate) .Transform(p => new PythonPackagesItemViewModel(settingsManager) { Package = p }) .SortBy(vm => vm.Package.Name) .ObserveOn(SynchronizationContext.Current!) .Bind(Packages) .Subscribe(); } private async Task Refresh() { if (VenvPath is null) return; IsLoading = true; try { if (Design.IsDesignMode) { await Task.Delay(250); } else { pyBaseInstall ??= new PyBaseInstall( await pyInstallationManager.GetInstallationAsync( PythonVersion ?? PyInstallationManager.Python_3_10_11 ) ); await using var venvRunner = await pyBaseInstall.CreateVenvRunnerAsync( VenvPath, workingDirectory: VenvPath.Parent, environmentVariables: settingsManager.Settings.EnvironmentVariables ); var packages = await venvRunner.PipList(); Dispatcher.UIThread.Post(() => { var currentName = SelectedPackage?.Package.Name; SelectedPackage = null; packageSource.EditDiff(packages); SelectedPackage = Packages.FirstOrDefault(p => p.Package.Name == currentName); }); } } finally { IsLoading = false; } } [RelayCommand] private async Task RefreshBackground() { if (VenvPath is null) return; pyBaseInstall ??= new PyBaseInstall( await pyInstallationManager.GetInstallationAsync( PythonVersion ?? PyInstallationManager.Python_3_10_11 ) ); await using var venvRunner = await pyBaseInstall.CreateVenvRunnerAsync( VenvPath, workingDirectory: VenvPath.Parent, environmentVariables: settingsManager.Settings.EnvironmentVariables ); var packages = await venvRunner.PipList(); Dispatcher.UIThread.Post(() => { // Backup selected package var currentPackageName = SelectedPackage?.Package.Name; SelectedPackage = null; packageSource.EditDiff(packages); // Restore selected package SelectedPackage = Packages.FirstOrDefault(p => p.Package.Name == currentPackageName); }); } /// /// Load the selected package's show info if not already loaded /// partial void OnSelectedPackageChanged(PythonPackagesItemViewModel? value) { if (value is null) { return; } if (value.PipShowResult is null) { value.LoadExtraInfo(VenvPath!, pyBaseInstall!).SafeFireAndForget(); } } /// public override async Task OnLoadedAsync() { if (Design.IsDesignMode) return; await prerequisiteHelper.InstallUvIfNecessary(); await Refresh(); } public void AddPackages(params PipPackageInfo[] packages) { packageSource.AddOrUpdate(packages); } [RelayCommand] private Task ModifySelectedPackage(PythonPackagesItemViewModel? item) { return item?.SelectedVersion != null ? UpgradePackageVersion( item.Package.Name, item.SelectedVersion, PythonPackagesItemViewModel.GetKnownIndexUrl(item.Package.Name, item.SelectedVersion), isDowngrade: item.CanDowngrade ) : Task.CompletedTask; } private async Task UpgradePackageVersion( string packageName, string version, string? extraIndexUrl = null, bool isDowngrade = false ) { if (VenvPath is null || SelectedPackage?.Package is not { } package) return; // Confirmation dialog var dialog = DialogHelper.CreateMarkdownDialog( isDowngrade ? $"Downgrade **{package.Name}** to **{version}**?" : $"Upgrade **{package.Name}** to **{version}**?", Resources.Label_ConfirmQuestion ); dialog.PrimaryButtonText = isDowngrade ? Resources.Action_Downgrade : Resources.Action_Upgrade; dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; dialog.CloseButtonText = Resources.Action_Cancel; if (await dialog.ShowAsync() is not ContentDialogResult.Primary) { return; } var args = new ProcessArgsBuilder("install", $"{packageName}=={version}"); if (extraIndexUrl != null) { args = args.AddArgs("--extra-index-url", extraIndexUrl); } var steps = new List { new PipStep { VenvDirectory = VenvPath, WorkingDirectory = VenvPath.Parent, Args = args, BaseInstall = pyBaseInstall, }, }; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = isDowngrade ? $"Downgraded Python Package '{packageName}' to {version}" : $"Upgraded Python Package '{packageName}' to {version}", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); // Refresh RefreshBackground().SafeFireAndForget(); } [RelayCommand] private async Task InstallPackage() { if (VenvPath is null) return; // Dialog var fields = new TextBoxField[] { new() { Label = "Package Name", InnerLeftText = "pip install" }, }; var dialog = DialogHelper.CreateTextEntryDialog("Install Package", "", fields); var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary || fields[0].Text is not { } packageArgs) { return; } var steps = new List { new PipStep { VenvDirectory = VenvPath, WorkingDirectory = VenvPath.Parent, Args = new ProcessArgs(packageArgs).Prepend("install"), BaseInstall = pyBaseInstall, EnvironmentVariables = settingsManager.Settings.EnvironmentVariables, }, }; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = $"Installed Python Package '{packageArgs}'", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); // Refresh RefreshBackground().SafeFireAndForget(); } [RelayCommand] private async Task UninstallSelectedPackage() { if (VenvPath is null || SelectedPackage?.Package is not { } package) return; // Confirmation dialog var dialog = DialogHelper.CreateMarkdownDialog( $"This will uninstall the package '{package.Name}'", Resources.Label_ConfirmQuestion ); dialog.PrimaryButtonText = Resources.Action_Uninstall; dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; dialog.CloseButtonText = Resources.Action_Cancel; if (await dialog.ShowAsync() is not ContentDialogResult.Primary) { return; } var steps = new List { new PipStep { VenvDirectory = VenvPath, WorkingDirectory = VenvPath.Parent, Args = new[] { "uninstall", "--yes", package.Name }, BaseInstall = pyBaseInstall, }, }; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = $"Uninstalled Python Package '{package.Name}'", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); // Refresh RefreshBackground().SafeFireAndForget(); } public override BetterContentDialog GetDialog() { return new BetterContentDialog { CloseOnClickOutside = true, MinDialogWidth = 800, MaxDialogWidth = 1500, FullSizeDesired = true, ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, Title = Resources.Label_PythonPackages, Content = new PythonPackagesDialog { DataContext = this }, CloseButtonText = Resources.Action_Close, DefaultButton = ContentDialogButton.Close, }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelItemViewModel.cs ================================================ using System; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; public partial class RecommendedModelItemViewModel : ViewModelBase { [ObservableProperty] private bool isSelected; [ObservableProperty] private string author; [ObservableProperty] private CivitModelVersion modelVersion; [ObservableProperty] private CivitModel civitModel; public Uri ThumbnailUrl => ModelVersion.Images?.FirstOrDefault(x => x.Type == "image")?.Url == null ? Assets.NoImage : new Uri(ModelVersion.Images.First(x => x.Type == "image").Url); [RelayCommand] public void ToggleSelection() => IsSelected = !IsSelected; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs ================================================ using System; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Refit; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [RegisterTransient] [ManagedService] public partial class RecommendedModelsViewModel : ContentDialogViewModelBase { private readonly ILogger logger; private readonly IRecommendedModelsApi lykosApi; private readonly ICivitApi civitApi; private readonly ILiteDbContext liteDbContext; private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly ITrackedDownloadService trackedDownloadService; private readonly IDownloadService downloadService; private readonly IModelImportService modelImportService; // Single source cache for all models public SourceCache AllRecommendedModelsCache { get; } = new(p => p.ModelVersion.Id); // Single observable collection bound to the cache public IObservableCollection RecommendedModels { get; } = new ObservableCollectionExtended(); [ObservableProperty] private bool isLoading; public RecommendedModelsViewModel( ILogger logger, IRecommendedModelsApi lykosApi, ICivitApi civitApi, ILiteDbContext liteDbContext, ISettingsManager settingsManager, INotificationService notificationService, ITrackedDownloadService trackedDownloadService, IDownloadService downloadService, IModelImportService modelImportService ) { this.logger = logger; this.lykosApi = lykosApi; this.civitApi = civitApi; this.liteDbContext = liteDbContext; this.settingsManager = settingsManager; this.notificationService = notificationService; this.trackedDownloadService = trackedDownloadService; this.downloadService = downloadService; this.modelImportService = modelImportService; // Bind the single collection to the cache AllRecommendedModelsCache .Connect() .DeferUntilLoaded() .Bind(RecommendedModels) .ObserveOn(SynchronizationContext.Current!) // Use Current! if nullability context allows .Subscribe(); } public override async Task OnLoadedAsync() { if (Design.IsDesignMode) return; IsLoading = true; AllRecommendedModelsCache.Clear(); // Clear cache before loading try { // Call the V2 endpoint var recommendedModelsResponse = await lykosApi.GetRecommendedModels(); var allModels = recommendedModelsResponse .RecommendedModelsByCategory.SelectMany(kvp => kvp.Value) // Flatten the dictionary values (lists of models) .DistinctBy(m => m.Id) // Ensure models appearing in multiple categories are only added once .Select(model => { // Find the first non-Turbo/Lightning version, or default to the first version if none match var suitableVersion = model.ModelVersions?.FirstOrDefault( x => !x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase) && !x.BaseModel.Contains("Lightning", StringComparison.OrdinalIgnoreCase) && x.Files != null && x.Files.Any(f => f.Type == CivitFileType.Model) // Ensure there's a model file ) ?? model.ModelVersions?.FirstOrDefault( x => x.Files != null && x.Files.Any(f => f.Type == CivitFileType.Model) ); if (suitableVersion == null) { logger.LogWarning( "Model {ModelName} (ID: {ModelId}) has no suitable model version file.", model.Name, model.Id ); return null; // Skip this model if no suitable version found } return new RecommendedModelItemViewModel { ModelVersion = suitableVersion, Author = $"by {model.Creator?.Username}", CivitModel = model }; }) .Where(vm => vm != null); // Filter out nulls (models skipped due to no suitable version) AllRecommendedModelsCache.AddOrUpdate(allModels); } catch (ApiException apiEx) { logger.LogError( apiEx, "API Error fetching recommended models V2. Status: {StatusCode}", apiEx.StatusCode ); notificationService.Show( "Failed to get recommended models", $"Could not reach the server. Please try again later. Error: {apiEx.StatusCode}" ); OnCloseButtonClick(); } catch (Exception e) { logger.LogError(e, "Failed to get recommended models V2"); notificationService.Show( "Failed to get recommended models", "An unexpected error occurred. Please try again later or check the Model Browser tab." ); OnCloseButtonClick(); } finally { IsLoading = false; } } [RelayCommand] private async Task DoImport() { var selectedModels = RecommendedModels.Where(x => x.IsSelected).ToList(); // Use the single list if (!selectedModels.Any()) { notificationService.Show("No Models Selected", "Please select at least one model to import."); return; } IsLoading = true; // Optionally show loading indicator during import int successCount = 0; int failCount = 0; foreach (var model in selectedModels) { // Get latest version file that is a Model type and marked primary, or fallback to first model file var modelFile = model.ModelVersion.Files?.FirstOrDefault( f => f is { Type: CivitFileType.Model, IsPrimary: true } ) ?? model.ModelVersion.Files?.FirstOrDefault(f => f.Type == CivitFileType.Model); if (modelFile is null) { logger.LogWarning( "Skipping import for {ModelName}: No suitable model file found in version {VersionId}.", model.CivitModel.Name, model.ModelVersion.Id ); failCount++; continue; // Skip if no suitable file } try { var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var downloadDirectory = rootModelsDirectory.JoinDir( model.CivitModel.Type.ConvertTo().GetStringValue() ); await modelImportService.DoImport( model.CivitModel, downloadDirectory, model.ModelVersion, modelFile ); successCount++; model.IsSelected = false; // De-select after successful import start } catch (Exception ex) { logger.LogError(ex, "Failed to initiate import for model {ModelName}", model.CivitModel.Name); failCount++; // Consider notifying the user about the specific failure notificationService.Show( "Import Failed", $"Could not start import for {model.CivitModel.Name}." ); } } IsLoading = false; // Hide loading indicator if (failCount == 0 && successCount > 0) { notificationService.Show( "Import Started", $"{successCount} model(s) added to the download queue." ); // Optionally close the dialog after successful import initiation // OnCloseButtonClick(); } else if (successCount > 0) { notificationService.Show( "Import Partially Started", $"{successCount} model(s) added to queue. {failCount} failed to start." ); } else if (failCount > 0) { notificationService.Show( "Import Failed", $"Could not start import for {failCount} selected model(s)." ); } // else: No models were actually selected or processed, already handled. } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/SafetensorMetadataViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(SafetensorMetadataDialog))] [ManagedService] [RegisterSingleton] public partial class SafetensorMetadataViewModel : ContentDialogViewModelBase { [ObservableProperty] private string? modelName; [ObservableProperty] private SafetensorMetadata? metadata; [RelayCommand] public void CopyTagToClipboard(string tag) { App.Clipboard?.SetTextAsync(tag); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs ================================================ using System; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(SelectDataDirectoryDialog))] [ManagedService] [RegisterTransient] public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public static string DefaultInstallLocation => Compat.IsLinux ? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "StabilityMatrix" ) : Compat.AppDataHome; private readonly ISettingsManager settingsManager; private const string ValidExistingDirectoryText = "Valid existing data directory found"; private const string InvalidDirectoryText = "Directory must be empty or have a valid settings.json file"; private const string NotEnoughFreeSpaceText = "Not enough free space on the selected drive"; private const string FatWarningText = "FAT32 / exFAT drives are not supported at this time"; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsInTempFolder))] private string dataDirectory = DefaultInstallLocation; [ObservableProperty] private bool isPortableMode; [ObservableProperty] private string directoryStatusText = string.Empty; [ObservableProperty] private bool isStatusBadgeVisible; [ObservableProperty] private bool isDirectoryValid; [ObservableProperty] private bool showFatWarning; public bool IsInTempFolder => Compat .AppCurrentDir.ToString() .StartsWith( Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase ); public RefreshBadgeViewModel ValidatorRefreshBadge { get; } = new() { State = ProgressState.Inactive, SuccessToolTipText = ValidExistingDirectoryText, FailToolTipText = InvalidDirectoryText, }; public SelectDataDirectoryViewModel(ISettingsManager settingsManager) { this.settingsManager = settingsManager; ValidatorRefreshBadge.RefreshFunc = ValidateDataDirectory; } public override void OnLoaded() { ValidatorRefreshBadge.RefreshCommand.ExecuteAsync(null).SafeFireAndForget(); IsPortableMode = true; } // Revalidate on data directory change partial void OnDataDirectoryChanged(string value) { ValidatorRefreshBadge.RefreshCommand.ExecuteAsync(null).SafeFireAndForget(); } private async Task ValidateDataDirectory() { await using var delay = new MinimumDelay(100, 200); ShowFatWarning = IsDriveFat(DataDirectory); if (IsInTempFolder) return false; // Doesn't exist, this is fine as a new install, hide badge if (!Directory.Exists(DataDirectory)) { IsStatusBadgeVisible = false; IsDirectoryValid = true; return true; } // Otherwise check that a settings.json exists var settingsPath = Path.Combine(DataDirectory, "settings.json"); // settings.json exists: Try deserializing it if (File.Exists(settingsPath)) { try { var jsonText = await File.ReadAllTextAsync(settingsPath); var _ = JsonSerializer.Deserialize( jsonText, new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } } ); // If successful, show existing badge IsStatusBadgeVisible = true; IsDirectoryValid = true; DirectoryStatusText = ValidExistingDirectoryText; return true; } catch (Exception e) { Logger.Info("Failed to deserialize settings.json: {Msg}", e.Message); // If not, show error badge, and set directory to invalid to prevent continuing IsStatusBadgeVisible = true; IsDirectoryValid = false; DirectoryStatusText = InvalidDirectoryText; return false; } } // No settings.json // Check if the directory is %APPDATA%\StabilityMatrix: hide badge and set directory valid if (DataDirectory == DefaultInstallLocation) { IsStatusBadgeVisible = false; IsDirectoryValid = true; return true; } // Check if the directory is empty: hide badge and set directory to valid var isEmpty = !Directory.EnumerateFileSystemEntries(DataDirectory).Any(); if (isEmpty) { IsStatusBadgeVisible = false; IsDirectoryValid = true; return true; } // Not empty and not appdata: show error badge, and set directory to invalid IsStatusBadgeVisible = true; IsDirectoryValid = false; DirectoryStatusText = InvalidDirectoryText; return false; } private bool CanPickFolder => App.StorageProvider.CanPickFolder; [RelayCommand(CanExecute = nameof(CanPickFolder))] private async Task ShowFolderBrowserDialog() { var provider = App.StorageProvider; var result = await provider.OpenFolderPickerAsync( new FolderPickerOpenOptions { Title = "Select Data Folder", AllowMultiple = false } ); if (result.Count != 1) return; DataDirectory = result[0].Path.LocalPath; } partial void OnIsPortableModeChanged(bool value) { DataDirectory = value ? Compat.AppCurrentDir + "Data" : DefaultInstallLocation; } private bool IsDriveFat(string path) { try { var drive = new DriveInfo(Path.GetPathRoot(path)); return drive.DriveFormat.Contains("FAT", StringComparison.OrdinalIgnoreCase); } catch (Exception e) { Logger.Warn(e, "Error checking drive FATness"); return false; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs ================================================ using System.Collections.ObjectModel; using Avalonia.Controls.Notifications; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using Size = StabilityMatrix.Core.Helper.Size; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [ManagedService] [RegisterTransient] public partial class SelectModelVersionViewModel( ISettingsManager settingsManager, IDownloadService downloadService, IModelIndexService modelIndexService, INotificationService notificationService ) : ContentDialogViewModelBase { private readonly IDownloadService downloadService = downloadService; public required ContentDialog Dialog { get; set; } public required string Description { get; set; } public new required string Title { get; set; } public required CivitModel CivitModel { get; set; } [ObservableProperty] public IReadOnlyList versions = []; [ObservableProperty] private Bitmap? previewImage; [ObservableProperty] private ModelVersionViewModel? selectedVersionViewModel; [ObservableProperty] private CivitFileViewModel? selectedFile; [ObservableProperty] private bool isImportEnabled; [ObservableProperty] private ObservableCollection imageUrls = new(); [ObservableProperty] private bool canGoToNextImage; [ObservableProperty] private bool canGoToPreviousImage; [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayedPageNumber))] private int selectedImageIndex; [ObservableProperty] private string importTooltip = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsCustomSelected))] [NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))] private string selectedInstallLocation = string.Empty; [ObservableProperty] private ObservableCollection availableInstallLocations = []; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))] private string customInstallLocation = string.Empty; public bool IsCustomSelected => SelectedInstallLocation == "Custom..."; public bool ShowEmptyPathWarning => IsCustomSelected && string.IsNullOrWhiteSpace(CustomInstallLocation); public int DisplayedPageNumber => SelectedImageIndex + 1; public override void OnLoaded() { SelectedVersionViewModel = Versions[0]; CanGoToNextImage = true; // LoadInstallLocations() is called within OnSelectedFileChanged, which is triggered by OnSelectedVersionViewModelChanged. // However, to apply preferences correctly, we need AvailableInstallLocations populated first. // It might be better to ensure LoadInstallLocations is called before trying to apply preferences. // For now, we rely on the chain: OnLoaded -> sets SelectedVersionViewModel -> OnSelectedVersionViewModelChanged -> sets SelectedFile -> OnSelectedFileChanged -> LoadInstallLocations // Then, we apply preferences if available. } partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value) { var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; var allImages = value ?.ModelVersion?.Images?.Where(img => img.Type == "image" && (nsfwEnabled || img.NsfwLevel <= 1)) ?.Select(x => new ImageSource(x.Url)) .ToList(); if (allImages == null || !allImages.Any()) { // allImages = new List { new(Assets.NoImage) }; allImages = []; CanGoToNextImage = false; } else { CanGoToNextImage = allImages.Count > 1; } Dispatcher.UIThread.Post(() => { CanGoToPreviousImage = false; // SelectedFile = SelectedVersionViewModel?.CivitFileViewModels.FirstOrDefault(); ImageUrls = new ObservableCollection(allImages); SelectedImageIndex = 0; // Apply saved preferences after SelectedFile change has potentially called LoadInstallLocations // It's crucial that LoadInstallLocations runs before this to populate AvailableInstallLocations // and set an initial SelectedInstallLocation. ApplySavedDownloadPreference(); }); } partial void OnSelectedFileChanged(CivitFileViewModel? value) { if (value is { IsInstalled: true }) { } var canImport = true; if (settingsManager.IsLibraryDirSet) { LoadInstallLocations(); var fileSizeBytes = value?.CivitFile.SizeKb * 1024; var freeSizeBytes = SystemInfo.GetDiskFreeSpaceBytes(settingsManager.ModelsDirectory) ?? long.MaxValue; canImport = fileSizeBytes < freeSizeBytes; ImportTooltip = canImport ? "Free space after download: " + ( freeSizeBytes < long.MaxValue ? Size.FormatBytes(Convert.ToUInt64(freeSizeBytes - fileSizeBytes)) : "Unknown" ) : $"Not enough space on disk. Need {Size.FormatBytes(Convert.ToUInt64(fileSizeBytes))} but only have {Size.FormatBytes(Convert.ToUInt64(freeSizeBytes))}"; } else { ImportTooltip = "Please set the library directory in settings"; } IsImportEnabled = value?.CivitFile != null && canImport && !ShowEmptyPathWarning; } partial void OnSelectedInstallLocationChanged(string? value) { if (value?.Equals("Custom...", StringComparison.OrdinalIgnoreCase) is true) { // Only invoke the folder picker if a custom location isn't already set (e.g., by loading preferences). // If the user manually selects "Custom..." and CustomInstallLocation was previously cleared (due to a non-custom selection), // then string.IsNullOrWhiteSpace(this.CustomInstallLocation) will be true, and the dialog will show. if (string.IsNullOrWhiteSpace(this.CustomInstallLocation)) { Dispatcher.UIThread.InvokeAsync(SelectCustomFolder); } } else { // If a non-custom location is selected, clear any existing custom path. CustomInstallLocation = string.Empty; } IsImportEnabled = !ShowEmptyPathWarning; } partial void OnCustomInstallLocationChanged(string value) { IsImportEnabled = !ShowEmptyPathWarning; } public void Cancel() { Dialog.Hide(ContentDialogResult.Secondary); } public void Import() { SaveCurrentDownloadPreference(); Dialog.Hide(ContentDialogResult.Primary); } public async Task Delete() { if (SelectedFile == null) return; var fileToDelete = SelectedFile; var originalSelectedVersionVm = SelectedVersionViewModel; var hash = fileToDelete.CivitFile.Hashes.BLAKE3; if (string.IsNullOrWhiteSpace(hash)) { notificationService.Show( "Error deleting file", "Could not delete model, hash is missing.", NotificationType.Error ); return; } var matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList(); if (matchingModels.Count == 0) { await modelIndexService.RefreshIndex(); matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList(); if (matchingModels.Count == 0) { notificationService.Show( "Error deleting file", "Could not delete model, model not found in index.", NotificationType.Error ); return; } } var dialog = new BetterContentDialog { Title = Resources.Label_AreYouSure, MaxDialogWidth = 750, MaxDialogHeight = 850, PrimaryButtonText = Resources.Action_Yes, IsPrimaryButtonEnabled = true, IsSecondaryButtonEnabled = false, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Close, Content = $"The following files:\n{string.Join('\n', matchingModels.Select(x => $"- {x.FileName}"))}\n" + "and all associated metadata files will be deleted. Are you sure?", }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { foreach (var localModel in matchingModels) { var checkpointPath = new FilePath(localModel.GetFullPath(settingsManager.ModelsDirectory)); if (File.Exists(checkpointPath)) { File.Delete(checkpointPath); } var previewPath = localModel.GetPreviewImageFullPath(settingsManager.ModelsDirectory); if (File.Exists(previewPath)) { File.Delete(previewPath); } var cmInfoPath = checkpointPath.ToString().Replace(checkpointPath.Extension, ".cm-info.json"); if (File.Exists(cmInfoPath)) { File.Delete(cmInfoPath); } await modelIndexService.RemoveModelAsync(localModel); } fileToDelete.IsInstalled = false; originalSelectedVersionVm?.RefreshInstallStatus(); } } public void PreviousImage() { if (SelectedImageIndex > 0) SelectedImageIndex--; CanGoToPreviousImage = SelectedImageIndex > 0; CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; } public void NextImage() { if (SelectedImageIndex < ImageUrls.Count - 1) SelectedImageIndex++; CanGoToPreviousImage = SelectedImageIndex > 0; CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1; } public async Task SelectCustomFolder() { var files = await App.StorageProvider.OpenFolderPickerAsync( new FolderPickerOpenOptions { Title = "Select Download Folder", AllowMultiple = false, SuggestedStartLocation = await App.StorageProvider.TryGetFolderFromPathAsync( Path.Combine( settingsManager.ModelsDirectory, CivitModel.Type.ConvertTo().GetStringValue() ) ), } ); if (files.FirstOrDefault()?.TryGetLocalPath() is { } path) { CustomInstallLocation = path; // Potentially save preference here if selection is considered final upon folder picking for custom. // However, saving on Import() is more robust as it's the explicit confirmation. } } private void LoadInstallLocations() { var installLocations = new ObservableCollection(); var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var downloadDirectory = GetSharedFolderPath( rootModelsDirectory, SelectedFile?.CivitFile, CivitModel.Type, CivitModel.BaseModelType ); if (!downloadDirectory.ToString().EndsWith("Unknown")) { installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, downloadDirectory)) ); foreach ( var directory in downloadDirectory.EnumerateDirectories( "*", EnumerationOptionConstants.AllDirectories ) ) { installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, directory)) ); } } if (downloadDirectory.ToString().EndsWith(SharedFolderType.DiffusionModels.GetStringValue())) { // also add StableDiffusion in case we have an AIO version var stableDiffusionDirectory = rootModelsDirectory.JoinDir( SharedFolderType.StableDiffusion.GetStringValue() ); installLocations.Add( Path.Combine("Models", Path.GetRelativePath(rootModelsDirectory, stableDiffusionDirectory)) ); } installLocations.Add("Custom..."); AvailableInstallLocations = installLocations; SelectedInstallLocation = installLocations.FirstOrDefault(); } private static DirectoryPath GetSharedFolderPath( DirectoryPath rootModelsDirectory, CivitFile? civitFile, CivitModelType modelType, string? baseModelType ) { if (civitFile?.Type is CivitFileType.VAE) { return rootModelsDirectory.JoinDir(SharedFolderType.VAE.GetStringValue()); } if ( modelType is CivitModelType.Checkpoint && ( baseModelType == CivitBaseModelType.Flux1D.GetStringValue() || baseModelType == CivitBaseModelType.Flux1S.GetStringValue() || baseModelType == CivitBaseModelType.WanVideo.GetStringValue() || baseModelType == CivitBaseModelType.HunyuanVideo.GetStringValue() || civitFile?.Metadata.Format == CivitModelFormat.GGUF ) ) { return rootModelsDirectory.JoinDir(SharedFolderType.DiffusionModels.GetStringValue()); } return rootModelsDirectory.JoinDir(modelType.ConvertTo().GetStringValue()); } private void ApplySavedDownloadPreference() { if (CivitModel.Type == null || !settingsManager.IsLibraryDirSet) return; var modelTypeKey = CivitModel.Type.ToString(); if ( settingsManager.Settings.ModelTypeDownloadPreferences.TryGetValue( modelTypeKey, out var preference ) ) { if ( preference.SelectedInstallLocation == "Custom..." && !string.IsNullOrWhiteSpace(preference.CustomInstallLocation) ) { // Ensure "Custom..." is an option or add it if necessary, though LoadInstallLocations should handle it. if (AvailableInstallLocations.Contains("Custom...")) { CustomInstallLocation = preference.CustomInstallLocation ?? string.Empty; SelectedInstallLocation = "Custom..."; } } // If the saved SelectedInstallLocation is a custom path directly (legacy or direct set) // and it's not in AvailableInstallLocations, but CustomInstallLocation is set from preference. else if ( !string.IsNullOrWhiteSpace(preference.CustomInstallLocation) && preference.SelectedInstallLocation == preference.CustomInstallLocation ) { if (AvailableInstallLocations.Contains("Custom...")) { CustomInstallLocation = preference.CustomInstallLocation ?? string.Empty; SelectedInstallLocation = "Custom..."; } } else if ( preference.SelectedInstallLocation != null && AvailableInstallLocations.Contains(preference.SelectedInstallLocation) ) { SelectedInstallLocation = preference.SelectedInstallLocation; } } } private void SaveCurrentDownloadPreference() { if ( CivitModel?.Type == null || !settingsManager.IsLibraryDirSet || string.IsNullOrEmpty(SelectedInstallLocation) ) return; var modelTypeKey = CivitModel.Type.ToString(); var preference = new LastDownloadLocationInfo { SelectedInstallLocation = SelectedInstallLocation, CustomInstallLocation = IsCustomSelected ? CustomInstallLocation : null, }; settingsManager.Transaction(s => { s.ModelTypeDownloadPreferences[modelTypeKey] = preference; }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/SponsorshipPromptViewModel.cs ================================================ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(SponsorshipPromptDialog))] [ManagedService] [RegisterTransient] public partial class SponsorshipPromptViewModel( INavigationService navigationService, INavigationService settingsNavService ) : TaskDialogViewModelBase { [Localizable(false)] [ObservableProperty] private string titleEmoji = "\u2764\ufe0f"; [ObservableProperty] private string title = Resources.Sponsorship_Title; [ObservableProperty] private string existingSupporterPreamble = Resources.Sponsorship_ExistingSupporterPreamble; [ObservableProperty] private string? featureText; [ObservableProperty] private bool isPatreonConnected; [ObservableProperty] private bool isExistingSupporter; public void Initialize( LykosAccountStatusUpdateEventArgs status, string featureName, string? requiredTier = null ) { IsPatreonConnected = status.IsPatreonConnected; IsExistingSupporter = status.IsActiveSupporter; if (string.IsNullOrEmpty(requiredTier)) { FeatureText = string.Format(Resources.Sponsorship_ReqAnyTier, featureName); } else { FeatureText = string.Format(Resources.Sponsorship_ReqSpecificTier, featureName, requiredTier); } } [RelayCommand] private async Task NavigateToAccountSettings() { CloseDialog(TaskDialogStandardResult.Close); navigationService.NavigateTo(new SuppressNavigationTransitionInfo()); await Task.Delay(100); settingsNavService.NavigateTo(new SuppressNavigationTransitionInfo()); } [Localizable(false)] [RelayCommand] private static void OpenSupportUrl() { ProcessRunner.OpenUrl("https://www.patreon.com/join/StabilityMatrix"); } public override TaskDialog GetDialog() { var dialog = base.GetDialog(); dialog.Buttons = [ new TaskDialogCommand { Text = Resources.Action_ViewSupportOptions, IsDefault = true, Command = OpenSupportUrlCommand }, new TaskDialogButton { Text = Resources.Action_MaybeLater, DialogResult = TaskDialogStandardResult.Close } ]; return dialog; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs ================================================ using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Semver; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; [View(typeof(UpdateDialog))] [ManagedService] [RegisterSingleton] public partial class UpdateViewModel : ContentDialogViewModelBase { private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly IHttpClientFactory httpClientFactory; private readonly IUpdateHelper updateHelper; private bool isLoaded; [ObservableProperty] private bool isUpdateAvailable; [ObservableProperty] private UpdateInfo? updateInfo; [ObservableProperty] private string? releaseNotes; [ObservableProperty] private string? updateText; [ObservableProperty] private int progressValue; [ObservableProperty] private bool isProgressIndeterminate; [ObservableProperty] private bool showProgressBar; [ObservableProperty] private string? currentVersionText; [ObservableProperty] private string? newVersionText; [GeneratedRegex( @"(##\s*(v[0-9]+\.[0-9]+\.[0-9]+(?:-(?:[0-9A-Za-z-.]+))?)((?:\n|.)+?))(?=(##\s*v[0-9]+\.[0-9]+\.[0-9]+)|\z)" )] private static partial Regex RegexChangelog(); public UpdateViewModel( ILogger logger, ISettingsManager settingsManager, IHttpClientFactory httpClientFactory, IUpdateHelper updateHelper ) { this.logger = logger; this.settingsManager = settingsManager; this.httpClientFactory = httpClientFactory; this.updateHelper = updateHelper; EventManager.Instance.UpdateAvailable += (_, info) => { IsUpdateAvailable = true; UpdateInfo = info; }; } public async Task Preload() { if (UpdateInfo is null) return; ReleaseNotes = await GetReleaseNotes(UpdateInfo.Changelog.ToString()); } partial void OnUpdateInfoChanged(UpdateInfo? value) { CurrentVersionText = $"v{Compat.AppVersion.ToDisplayString()}"; NewVersionText = $"v{value?.Version.ToDisplayString()}"; } public override async Task OnLoadedAsync() { if (!isLoaded) { await Preload(); } } /// public override void OnUnloaded() { base.OnUnloaded(); isLoaded = false; } [RelayCommand] private async Task InstallUpdate() { if (UpdateInfo == null) { return; } ShowProgressBar = true; IsProgressIndeterminate = true; UpdateText = string.Format(Resources.TextTemplate_UpdatingPackage, Resources.Label_StabilityMatrix); try { await updateHelper.DownloadUpdate( UpdateInfo, new Progress(report => { ProgressValue = Convert.ToInt32(report.Percentage); IsProgressIndeterminate = report.IsIndeterminate; }) ); } catch (Exception e) { logger.LogWarning(e, "Failed to download update"); var dialog = DialogHelper.CreateMarkdownDialog( $"{e.GetType().Name}: {e.Message}", Resources.Label_UnexpectedErrorOccurred ); await dialog.ShowAsync(); return; } // On unix, we need to set the executable bit if (Compat.IsUnix) { File.SetUnixFileMode( UpdateHelper.ExecutablePath.FullPath, // 0755 UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute ); } // Handle Linux AppImage update if (Compat.IsLinux && Environment.GetEnvironmentVariable("APPIMAGE") is { } appImage) { try { var updateScriptPath = UpdateHelper.UpdateFolder.JoinFile("update_script.sh").FullPath; var newAppImage = UpdateHelper.ExecutablePath.FullPath; var scriptContent = """ #!/bin/bash set -e # Wait for the process to exit while kill -0 "$PID" 2>/dev/null; do sleep 0.5 done # Move the new AppImage over the old one mv -f "$NEW_APPIMAGE" "$OLD_APPIMAGE" chmod +x "$OLD_APPIMAGE" # Launch the new AppImage detached "$OLD_APPIMAGE" > /dev/null 2>&1 & disown """; await File.WriteAllTextAsync(updateScriptPath, scriptContent); File.SetUnixFileMode( updateScriptPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute ); var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = "/usr/bin/env", Arguments = $"bash \"{updateScriptPath}\"", UseShellExecute = false, CreateNoWindow = true, }; startInfo.EnvironmentVariables["PID"] = Environment.ProcessId.ToString(); startInfo.EnvironmentVariables["NEW_APPIMAGE"] = newAppImage; startInfo.EnvironmentVariables["OLD_APPIMAGE"] = appImage; System.Diagnostics.Process.Start(startInfo); App.Shutdown(); return; } catch (Exception ex) { logger.LogError(ex, "Failed to execute AppImage update script"); var dialog = DialogHelper.CreateMarkdownDialog( "AppImage update script failed. \nCould not replace old AppImage with new version. Please check directory permissions. \nFalling back to standard update process. User intervention required: \nAfter program closes, \nplease move the new AppImage extracted in the '.StabilityMatrixUpdate' hidden directory to the old AppImage overwriting it. \n\nClose this dialog to continue with standard update process.", Resources.Label_UnexpectedErrorOccurred ); await dialog.ShowAsync(); } } // Set current version for update messages settingsManager.Transaction( s => s.UpdatingFromVersion = Compat.AppVersion, ignoreMissingLibraryDir: true ); UpdateText = "Getting a few things ready..."; await using (new MinimumDelay(500, 1000)) { await Task.Run(() => { var args = new[] { "--wait-for-exit-pid", $"{Environment.ProcessId}" }; if (Program.Args.NoSentry) { args = args.Append("--no-sentry").ToArray(); } ProcessRunner.StartApp(UpdateHelper.ExecutablePath.FullPath, args); }); } UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds..."; await Task.Delay(1000); UpdateText = "Update complete. Restarting Stability Matrix in 2 seconds..."; await Task.Delay(1000); UpdateText = "Update complete. Restarting Stability Matrix in 1 second..."; await Task.Delay(1000); UpdateText = "Update complete. Restarting Stability Matrix..."; App.Shutdown(); } internal async Task GetReleaseNotes(string changelogUrl) { using var client = httpClientFactory.CreateClient(); try { var response = await client.GetAsync(changelogUrl); if (response.IsSuccessStatusCode) { var changelog = await response.Content.ReadAsStringAsync(); // Formatting for new changelog format // https://keepachangelog.com/en/1.1.0/ if (changelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) { return FormatChangelog( changelog, Compat.AppVersion, settingsManager.Settings.PreferredUpdateChannel ) ?? "## Unable to format release notes"; } return changelog; } return "## Unable to load release notes"; } catch (HttpRequestException e) { return $"## Unable to fetch release notes ({e.StatusCode})\n\n[{changelogUrl}]({changelogUrl})"; } catch (TaskCanceledException) { } return $"## Unable to fetch release notes\n\n[{changelogUrl}]({changelogUrl})"; } /// /// Formats changelog markdown including up to the current version /// /// Markdown to format /// Versions equal or below this are excluded /// Maximum channel level to include internal static string? FormatChangelog( string markdown, SemVersion currentVersion, UpdateChannel maxChannel = UpdateChannel.Stable ) { var pattern = RegexChangelog(); var results = pattern .Matches(markdown) .Select(m => new { Block = m.Groups[1].Value.Trim(), Version = SemVersion.TryParse( m.Groups[2].Value.Trim(), SemVersionStyles.AllowV, out var version ) ? version : null, Content = m.Groups[3].Value.Trim(), }) .Where(x => x.Version is not null) .ToList(); // Join all blocks until and excluding the current version // If we're on a pre-release, include the current release var currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutMetadata()); // For mismatching build metadata, add one if ( currentVersionBlock != -1 && results[currentVersionBlock].Version?.Metadata != currentVersion.Metadata ) { currentVersionBlock++; } // Support for previous pre-release without changelogs if (currentVersionBlock == -1) { currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutPrereleaseOrMetadata() ); // Add 1 if found to include the current release if (currentVersionBlock != -1) { currentVersionBlock++; } } // Still not found, just include all if (currentVersionBlock == -1) { currentVersionBlock = results.Count; } // Filter out pre-releases var blocks = results .Take(currentVersionBlock) .Where(x => x.Version!.PrereleaseIdentifiers.Count == 0 || x.Version.PrereleaseIdentifiers[0].Value switch { "pre" when maxChannel >= UpdateChannel.Preview => true, "dev" when maxChannel >= UpdateChannel.Development => true, _ => false, } ) .Select(x => x.Block); return string.Join(Environment.NewLine + Environment.NewLine, blocks); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs ================================================ using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(FirstLaunchSetupWindow))] [ManagedService] [RegisterSingleton] public partial class FirstLaunchSetupViewModel : DisposableViewModelBase { [ObservableProperty] private bool eulaAccepted; [ObservableProperty] private string gpuInfoText = string.Empty; [ObservableProperty] private RefreshBadgeViewModel checkHardwareBadge = new() { WorkingToolTipText = Resources.Label_CheckingHardware, SuccessToolTipText = Resources.Label_EverythingLooksGood, FailToolTipText = Resources.Label_NvidiaGpuRecommended, FailColorBrush = ThemeColors.ThemeYellow, }; [ObservableProperty] private bool selectDifferentGpu; [ObservableProperty] private ObservableCollection gpuInfoCollection = []; [ObservableProperty] private GpuInfo? selectedGpu; public string YouCanChangeThis => string.Format( Resources.TextTemplate_YouCanChangeThisBehavior, "Settings > System Settings > Default GPU" ); public FirstLaunchSetupViewModel(ISettingsManager settingsManager) { CheckHardwareBadge.RefreshFunc = SetGpuInfo; AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.SelectedGpu, settings => settings.PreferredGpu, true ) ); } private async Task SetGpuInfo() { GpuInfo[] gpuInfo; await using (new MinimumDelay(800, 1200)) { // Query GPU info gpuInfo = await Task.Run(() => HardwareHelper.IterGpuInfo().ToArray()); GpuInfoCollection = new ObservableCollection(gpuInfo); } // First Nvidia GPU var activeGpu = gpuInfo.FirstOrDefault( gpu => gpu.Name?.Contains("nvidia", StringComparison.InvariantCultureIgnoreCase) ?? false ); var isNvidia = activeGpu is not null; // Otherwise first GPU activeGpu ??= gpuInfo.FirstOrDefault(); SelectedGpu = activeGpu; GpuInfoText = activeGpu is null ? "No GPU detected" : $"{activeGpu.Name} ({Size.FormatBytes(activeGpu.MemoryBytes)})"; // Always succeed for macos arm if (Compat.IsMacOS && Compat.IsArm) { return true; } return isNvidia; } public override void OnLoaded() { base.OnLoaded(); CheckHardwareBadge.RefreshCommand.ExecuteAsync(null).SafeFireAndForget(); } [RelayCommand] private void ToggleManualGpu() { SelectDifferentGpu = !SelectDifferentGpu; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/CategoryViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Threading; 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 items = new ObservableCollectionExtended(); public SourceCache ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName); [ObservableProperty] private string? title; [ObservableProperty] private bool isChecked; [ObservableProperty] private int numSelected; public CategoryViewModel(IEnumerable items, string modelsDir) { ItemsCache .Connect() .DeferUntilLoaded() .Transform(i => new HuggingfaceItemViewModel { Item = i, ModelsDir = modelsDir }) .Bind(Items) .WhenPropertyChanged(p => p.IsSelected) .ObserveOn(SynchronizationContext.Current) .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) { if (item.Exists) continue; item.IsSelected = value; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/HuggingfaceItemViewModel.cs ================================================ using System.IO; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using StabilityMatrix.Avalonia.Models.HuggingFace; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; 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}"; public required string? ModelsDir { get; init; } public bool Exists => ModelsDir != null && File.Exists( Path.Combine( ModelsDir, Item.ModelCategory.ConvertTo().ToString(), Item.Subfolder ?? string.Empty, Path.GetFileName(Item.Files[0]).Equals("ae.safetensors") && Item.ModelName.Equals("HiDream I1 VAE") ? "hidream_vae.safetensors" : Path.GetFileName(Item.Files[0]) ) ); [RelayCommand] private void ToggleSelected() { IsSelected = !IsSelected; } public void NotifyExistsChanged() { OnPropertyChanged(nameof(Exists)); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs ================================================ using Avalonia.Input; namespace StabilityMatrix.Avalonia.ViewModels; public interface IDropTarget { void DragOver(object? sender, DragEventArgs e); void Drop(object? sender, DragEventArgs e); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(BatchSizeCard))] [ManagedService] [RegisterTransient] public partial class BatchSizeCardViewModel : LoadableViewModelBase, IComfyStep { [NotifyDataErrorInfo] [ObservableProperty] [Range(1, 1024)] private int batchSize = 1; [NotifyDataErrorInfo] [ObservableProperty] [Range(1, int.MaxValue)] private int batchCount = 1; [NotifyDataErrorInfo] [ObservableProperty] [Required] private bool isBatchIndexEnabled; [NotifyDataErrorInfo] [ObservableProperty] [Range(1, 1024)] private int batchIndex = 1; /// /// Sets batch size to connections. /// Provides: /// /// /// /// /// public void ApplyStep(ModuleApplyStepEventArgs e) { e.Builder.Connections.BatchSize = BatchSize; e.Builder.Connections.BatchIndex = IsBatchIndexEnabled ? BatchIndex : null; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/CfzCudnnToggleCardViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(CfzCudnnToggleCard))] [ManagedService] [RegisterTransient] public partial class CfzCudnnToggleCardViewModel : LoadableViewModelBase { public const string ModuleKey = "CfzCudnnToggle"; [ObservableProperty] private bool disableCudnn = true; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs ================================================ using System; using System.ComponentModel.DataAnnotations; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ControlNetCard))] [ManagedService] [RegisterTransient] public partial class ControlNetCardViewModel : LoadableViewModelBase { public const string ModuleKey = "ControlNet"; private readonly IServiceManager vmFactory; [ObservableProperty] [Required] private HybridModelFile? selectedModel; [ObservableProperty] [Required] private ComfyAuxPreprocessor? selectedPreprocessor; [ObservableProperty] [Required] [Range(0, 2048)] private int width; [ObservableProperty] [Required] [Range(0, 2048)] private int height; [ObservableProperty] [Required] [Range(0d, 10d)] private double strength = 1.0; [ObservableProperty] [Required] [Range(0d, 1d)] private double startPercent; [ObservableProperty] [Required] [Range(0d, 1d)] private double endPercent = 1.0; public SelectImageCardViewModel SelectImageCardViewModel { get; } public IInferenceClientManager ClientManager { get; } public ControlNetCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory ) { this.vmFactory = vmFactory; ClientManager = clientManager; SelectImageCardViewModel = vmFactory.Get(); // Update our width and height when the image changes SelectImageCardViewModel .WhenPropertyChanged(card => card.CurrentBitmapSize) .ObserveOn(SynchronizationContext.Current) .Subscribe(propertyValue => { if (!propertyValue.Value.IsEmpty) { Width = propertyValue.Value.Width; Height = propertyValue.Value.Height; } }); } [RelayCommand] private async Task PreviewPreprocessor(ComfyAuxPreprocessor? preprocessor) { if ( preprocessor is null || SelectImageCardViewModel.ImageSource is not { } imageSource || SelectImageCardViewModel.IsImageFileNotFound ) return; var args = new InferenceQueueCustomPromptEventArgs(); var images = SelectImageCardViewModel.GetInputImages(); await ClientManager.UploadInputImageAsync(imageSource); var image = args.Nodes.AddTypedNode( new ComfyNodeBuilder.LoadImage { Name = args.Nodes.GetUniqueName("Preprocessor_LoadImage"), Image = SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") ?? throw new ValidationException("No ImageSource") } ).Output1; var aioPreprocessor = args.Nodes.AddTypedNode( new ComfyNodeBuilder.AIOPreprocessor { Name = args.Nodes.GetUniqueName("Preprocessor"), Image = image, Preprocessor = preprocessor.ToString(), // AIO wants the lower of the two resolutions. who knows why. // also why can't we put in the low/high thresholds? // Or any of the other parameters for the other preprocessors? Resolution = Math.Min(Width, Height) } ); args.Builder.Connections.OutputNodes.Add( args.Nodes.AddTypedNode( new ComfyNodeBuilder.PreviewImage { Name = args.Nodes.GetUniqueName("Preprocessor_OutputImage"), Images = aioPreprocessor.Output } ) ); // Queue Dispatcher.UIThread.Post(() => EventManager.Instance.OnInferenceQueueCustomPrompt(args)); // We don't know when it's done so wait a bit? await Task.Delay(1000); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/DiscreteModelSamplingCardViewModel.cs ================================================ using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(DiscreteModelSamplingCard))] [ManagedService] [RegisterTransient] public partial class DiscreteModelSamplingCardViewModel : LoadableViewModelBase { public const string ModuleKey = "DiscreteModelSampling"; public List SamplingMethods => ["eps", "v_prediction", "lcm", "x0"]; [ObservableProperty] private bool isZsnrEnabled; [ObservableProperty] private string selectedSamplingMethod = "eps"; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ExtraNetworkCardViewModel.cs ================================================ using System.ComponentModel; using System.Reactive; using System.Reactive.Linq; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Services; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ExtraNetworkCard))] [ManagedService] [RegisterTransient] public partial class ExtraNetworkCardViewModel : DisposableLoadableViewModelBase { private readonly ISettingsManager settingsManager; private readonly ModelCompatChecker modelCompatChecker = new(); public const string ModuleKey = "ExtraNetwork"; /// /// Whether user can toggle model weight visibility /// [JsonIgnore] public bool IsModelWeightToggleEnabled { get; set; } /// /// Whether user can toggle clip weight visibility /// [JsonIgnore] public bool IsClipWeightToggleEnabled { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(TriggerWords), nameof(ShowTriggerWords))] private HybridModelFile? selectedModel; [ObservableProperty] private bool isModelWeightEnabled; [ObservableProperty] [property: Category("Settings")] [property: DisplayName("CLIP Strength Adjustment")] private bool isClipWeightEnabled; [ObservableProperty] private double modelWeight = 1.0; [ObservableProperty] private double clipWeight = 1.0; [ObservableProperty] private HybridModelFile? selectedBaseModel; /// public ExtraNetworkCardViewModel(IInferenceClientManager clientManager, ISettingsManager settingsManager) { this.settingsManager = settingsManager; ClientManager = clientManager; // Observable signal when SelectedBaseModel changes var baseModelChangedSignal = this.WhenPropertyChanged(vm => vm.SelectedBaseModel) .Throttle(TimeSpan.FromMilliseconds(50)) .Select(_ => Unit.Default); // Observable signal when the FilterExtraNetworksByBaseModel setting changes var settingChangedSignal = settingsManager .ObservePropertyChanged(s => s.FilterExtraNetworksByBaseModel) .Select(_ => Unit.Default); // Combine signals var reapplyFilterSignal = Observable .Merge([baseModelChangedSignal, settingChangedSignal]) // StartWith ensures the filter is applied at least once initially .StartWith(Unit.Default); var filterPredicate = reapplyFilterSignal .ObserveOn(SynchronizationContext.Current!) .Select(_ => { if (!settingsManager.Settings.FilterExtraNetworksByBaseModel) return (Func)(_ => true); return (Func)FilterCompatibleLoras; }); AddDisposable( ClientManager .LoraModelsChangeSet.DeferUntilLoaded() .Filter(filterPredicate) .SortAndBind( LoraModels, SortExpressionComparer .Ascending(f => f.Type) .ThenByAscending(f => f.SortKey) ) .ObserveOn(SynchronizationContext.Current!) .Subscribe() ); } public IObservableCollection LoraModels { get; } = new ObservableCollectionExtended(); public string TriggerWords => SelectedModel?.Local?.ConnectedModelInfo?.TrainedWordsString ?? string.Empty; public bool ShowTriggerWords => !string.IsNullOrWhiteSpace(TriggerWords); public IInferenceClientManager ClientManager { get; } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel( new ExtraNetworkCardModel { SelectedModelName = SelectedModel?.RelativePath, IsModelWeightEnabled = IsModelWeightEnabled, IsClipWeightEnabled = IsClipWeightEnabled, ModelWeight = ModelWeight, ClipWeight = ClipWeight, } ); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); SelectedModel = model.SelectedModelName is null ? null : ClientManager.LoraModels.FirstOrDefault(x => x.RelativePath == model.SelectedModelName); IsModelWeightEnabled = model.IsModelWeightEnabled; IsClipWeightEnabled = model.IsClipWeightEnabled; ModelWeight = model.ModelWeight; ClipWeight = model.ClipWeight; } [RelayCommand] private void CopyTriggerWords() { if (!ShowTriggerWords) return; App.Clipboard.SetTextAsync(TriggerWords); } private bool FilterCompatibleLoras(HybridModelFile? lora) { if (!settingsManager.Settings.FilterExtraNetworksByBaseModel) return true; return modelCompatChecker.IsLoraCompatibleWithBaseModel(lora, SelectedBaseModel) ?? true; } internal class ExtraNetworkCardModel { public string? SelectedModelName { get; init; } public bool IsModelWeightEnabled { get; init; } public bool IsClipWeightEnabled { get; init; } public double ModelWeight { get; init; } public double ClipWeight { get; init; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/FaceDetailerViewModel.cs ================================================ using System.Collections.ObjectModel; using System.ComponentModel; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(FaceDetailerCard))] [ManagedService] [RegisterTransient] public partial class FaceDetailerViewModel : LoadableViewModelBase { private readonly IServiceManager vmFactory; public const string ModuleKey = "FaceDetailer"; [ObservableProperty] private bool guideSizeFor = true; [ObservableProperty] private int guideSize = 256; [ObservableProperty] private int maxSize = 768; [ObservableProperty] [property: Category("Settings"), DisplayName("Step Count Selection")] private bool isStepsEnabled; [ObservableProperty] private int steps = 20; [ObservableProperty] [property: Category("Settings"), DisplayName("CFG Scale Selection")] private bool isCfgScaleEnabled; [ObservableProperty] private double cfg = 8; [ObservableProperty] [property: Category("Settings"), DisplayName("Sampler Selection")] private bool isSamplerSelectionEnabled; [ObservableProperty] private ComfySampler? sampler = ComfySampler.Euler; [ObservableProperty] [property: Category("Settings"), DisplayName("Scheduler Selection")] private bool isSchedulerSelectionEnabled; [ObservableProperty] private ComfyScheduler? scheduler = ComfyScheduler.Normal; [ObservableProperty] private double denoise = 0.5d; [ObservableProperty] private int feather = 5; [ObservableProperty] private bool noiseMask = true; [ObservableProperty] private bool forceInpaint = false; [ObservableProperty] private double bboxThreshold = 0.5d; [ObservableProperty] private int bboxDilation = 10; [ObservableProperty] private int bboxCropFactor = 3; [ObservableProperty] private string samDetectionHint = "center-1"; [ObservableProperty] private int samDilation = 0; [ObservableProperty] private double samThreshold = 0.93d; [ObservableProperty] private int samBboxExpansion = 0; [ObservableProperty] private double samMaskHintThreshold = 0.7d; [ObservableProperty] private string samMaskHintUseNegative = "False"; [ObservableProperty] private int dropSize = 10; [ObservableProperty] private int cycle = 1; [ObservableProperty] private HybridModelFile? bboxModel; [ObservableProperty] private HybridModelFile? segmModel; [ObservableProperty] private HybridModelFile? samModel; [ObservableProperty] private bool showSamModelSelector = true; [ObservableProperty] private bool useSeparatePrompt; [ObservableProperty] private string positivePrompt = string.Empty; [ObservableProperty] private string negativePrompt = string.Empty; [ObservableProperty] [property: Category("Settings"), DisplayName("Inherit Seed")] private bool inheritSeed = true; [ObservableProperty] public partial bool UseTiledEncode { get; set; } [ObservableProperty] public partial bool UseTiledDecode { get; set; } public IReadOnlyList AvailableSchedulers => ComfyScheduler.FaceDetailerDefaults; /// public FaceDetailerViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory ) { this.vmFactory = vmFactory; ClientManager = clientManager; SeedCardViewModel = vmFactory.Get(); SeedCardViewModel.GenerateNewSeed(); PromptCardViewModel = vmFactory.Get(); WildcardViewModel = vmFactory.Get(vm => { vm.IsNegativePromptEnabled = false; vm.IsStackCardEnabled = false; }); } [JsonPropertyName("DetailerSeed")] public SeedCardViewModel SeedCardViewModel { get; } [JsonPropertyName("DetailerPrompt")] public PromptCardViewModel PromptCardViewModel { get; } [JsonPropertyName("DetailerWildcard")] public PromptCardViewModel WildcardViewModel { get; } public ObservableCollection SamDetectionHints { get; set; } = [ "center-1", "horizontal-2", "vertical-2", "rect-4", "diamond-4", "mask-area", "mask-points", "mask-point-bbox", "none", ]; public ObservableCollection SamMaskHintUseNegatives { get; set; } = ["False", "Small", "Outter"]; public IInferenceClientManager ClientManager { get; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(FreeUCard))] [ManagedService] [RegisterTransient] public partial class FreeUCardViewModel : LoadableViewModelBase { public const string ModuleKey = "FreeU"; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0D, 10D)] private double b1 = 1.5; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0D, 10D)] private double b2 = 1.6; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0D, 10D)] private double s1 = 0.9; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0D, 10D)] private double s2 = 0.2; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/IImageGalleryComponent.cs ================================================ using System.Linq; using Avalonia.Threading; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.ViewModels.Inference; public interface IImageGalleryComponent { ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } /// /// Clears existing images and loads new ones /// public void LoadImagesToGallery(params ImageSource[] imageSources) { Dispatcher.UIThread.Post(() => { ImageGalleryCardViewModel.SelectedImage = null; ImageGalleryCardViewModel.SelectedImageIndex = -1; ImageGalleryCardViewModel.ImageSources.Clear(); foreach (var imageSource in imageSources) { ImageGalleryCardViewModel.ImageSources.Add(imageSource); } ImageGalleryCardViewModel.SelectedImageIndex = imageSources.Length > 0 ? 0 : -1; ImageGalleryCardViewModel.SelectedImage = imageSources.FirstOrDefault(); }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardItemViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Avalonia.ViewModels.Inference; public partial class ImageFolderCardItemViewModel : ViewModelBase { [ObservableProperty] private LocalImageFile? localImageFile; [ObservableProperty] private string? imagePath; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs ================================================ using System; using System.IO; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FuzzySharp; using FuzzySharp.PreProcess; using Injectio.Attributes; using Microsoft.Extensions.Logging; using SkiaSharp; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using StabilityMatrix.Native; using SortDirection = DynamicData.Binding.SortDirection; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ImageFolderCard))] [ManagedService] [RegisterTransient] public partial class ImageFolderCardViewModel : DisposableViewModelBase { private readonly ILogger logger; private readonly IImageIndexService imageIndexService; private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly IServiceManager vmFactory; [ObservableProperty] private string? searchQuery; [ObservableProperty] private Size imageSize = new(150, 190); /// /// Collection of local image files /// public IObservableCollection LocalImages { get; } = new ObservableCollectionExtended(); public ImageFolderCardViewModel( ILogger logger, IImageIndexService imageIndexService, ISettingsManager settingsManager, INotificationService notificationService, IServiceManager vmFactory ) { this.logger = logger; this.imageIndexService = imageIndexService; this.settingsManager = settingsManager; this.notificationService = notificationService; this.vmFactory = vmFactory; var searcher = new ImageSearcher(); // Observable predicate from SearchQuery changes var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(50))! .Select(property => searcher.GetPredicate(property.Value)) .ObserveOn(SynchronizationContext.Current) .AsObservable(); imageIndexService .InferenceImages.ItemsSource.Connect() .DeferUntilLoaded() .Filter(searchPredicate) .SortBy(file => file.LastModifiedAt, SortDirection.Descending) .Bind(LocalImages) .ObserveOn(SynchronizationContext.Current) .Subscribe(); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.ImageSize, settings => settings.InferenceImageSize, delay: TimeSpan.FromMilliseconds(250) ) ); } private static bool SearchPredicate(LocalImageFile file, string? query) { if ( string.IsNullOrWhiteSpace(query) || file.FileName.Contains(query, StringComparison.OrdinalIgnoreCase) ) { return true; } // File name var filenameScore = Fuzz.WeightedRatio(query, file.FileName, PreprocessMode.Full); if (filenameScore > 80) { return true; } // Generation params if (file.GenerationParameters is { } parameters) { if ( parameters.Seed.ToString().StartsWith(query, StringComparison.OrdinalIgnoreCase) || parameters.Sampler is { } sampler && sampler.StartsWith(query, StringComparison.OrdinalIgnoreCase) || parameters.ModelName is { } modelName && modelName.StartsWith(query, StringComparison.OrdinalIgnoreCase) ) { return true; } } return false; } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); ImageSize = settingsManager.Settings.InferenceImageSize; imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget(); } /// /// Gets the image path if it exists, returns null. /// If the image path is resolved but the file doesn't exist, it will be removed from the index. /// private FilePath? GetImagePathIfExists(LocalImageFile item) { var imageFile = new FilePath(item.AbsolutePath); if (!imageFile.Exists) { // Remove from index imageIndexService.InferenceImages.Remove(item); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) { loader.RemoveAllNamesFromCache(imageFile.Name); } return null; } return imageFile; } /// /// Handles image clicks to show preview /// [RelayCommand] private async Task OnImageClick(LocalImageFile item) { if (GetImagePathIfExists(item) is not { } imageFile) { return; } var currentIndex = LocalImages.IndexOf(item); var image = new ImageSource(imageFile); // Preload await image.GetBitmapAsync(); var vm = vmFactory.Get(); vm.ImageSource = image; vm.LocalImageFile = item; using var onNext = Observable .FromEventPattern( vm, nameof(ImageViewerViewModel.NavigationRequested) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(ctx => { Dispatcher .UIThread.InvokeAsync(async () => { var sender = (ImageViewerViewModel)ctx.Sender!; var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1); if (newIndex >= 0 && newIndex < LocalImages.Count) { var newImage = LocalImages[newIndex]; var newImageSource = new ImageSource(newImage.AbsolutePath); // Preload await newImageSource.GetBitmapAsync(); await newImageSource.GetOrRefreshTemplateKeyAsync(); // var oldImageSource = sender.ImageSource; sender.ImageSource = newImageSource; sender.LocalImageFile = newImage; // oldImageSource?.Dispose(); currentIndex = newIndex; } }) .SafeFireAndForget(); }); await vm.GetDialog().ShowAsync(); } /// /// Handles clicks to the image delete button /// [RelayCommand] private async Task OnImageDelete(LocalImageFile? item) { if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } // Delete the file var isRecycle = settingsManager.Settings.IsInferenceImageBrowserUseRecycleBinForDelete && NativeFileOperations.IsRecycleBinAvailable; var result = isRecycle ? await notificationService.TryAsync( NativeFileOperations.RecycleBin!.MoveFileToRecycleBinAsync(imageFile) ) : await notificationService.TryAsync(imageFile.DeleteAsync()); if (result.IsSuccessful) { // Remove from index imageIndexService.InferenceImages.Remove(item); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) { loader.RemoveAllNamesFromCache(imageFile.Name); } } else { logger.LogWarning(result.Exception, "Failed to delete image"); } } /// /// Handles clicks to the image delete button /// [RelayCommand] private async Task OnImageCopy(LocalImageFile? item) { if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } var clipboard = App.Clipboard; await clipboard.SetFileDataObjectAsync(imageFile.FullPath); } /// /// Handles clicks to the image open-in-explorer button /// [RelayCommand] private async Task OnImageOpen(LocalImageFile? item) { if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } await ProcessRunner.OpenFileBrowser(imageFile); } /// /// Handles clicks to the image export button /// private async Task ImageExportImpl( LocalImageFile? item, SKEncodedImageFormat format, bool includeMetadata = false ) { if (item is null || GetImagePathIfExists(item) is not { } sourceFile) { return; } var formatName = format.ToString(); var storageFile = await App.StorageProvider.SaveFilePickerAsync( new FilePickerSaveOptions { Title = "Export Image", ShowOverwritePrompt = true, SuggestedFileName = item.FileNameWithoutExtension, DefaultExtension = formatName.ToLowerInvariant(), FileTypeChoices = new FilePickerFileType[] { new(formatName) { Patterns = new[] { $"*.{formatName.ToLowerInvariant()}" }, MimeTypes = new[] { $"image/{formatName.ToLowerInvariant()}" }, }, }, } ); if (storageFile?.TryGetLocalPath() is not { } targetPath) { return; } var targetFile = new FilePath(targetPath); try { if (format is SKEncodedImageFormat.Png) { // For include metadata, just copy the file if (includeMetadata) { await sourceFile.CopyToAsync(targetFile, true); } else { // Otherwise read and strip the metadata var imageBytes = await sourceFile.ReadAllBytesAsync(); imageBytes = PngDataHelper.RemoveMetadata(imageBytes); await targetFile.WriteAllBytesAsync(imageBytes); } } else { await Task.Run(() => { using var fs = sourceFile.Info.OpenRead(); var image = SKImage.FromEncodedData(fs); fs.Dispose(); using var targetStream = targetFile.Info.OpenWrite(); image.Encode(format, 100).SaveTo(targetStream); }); } } catch (IOException e) { logger.LogWarning(e, "Failed to export image"); notificationService.ShowPersistent("Failed to export image", e.Message, NotificationType.Error); return; } notificationService.Show("Image Exported", $"Saved to {targetPath}", NotificationType.Success); } [RelayCommand] private async Task CopySeedToClipboard(LocalImageFile? item) { if (item?.GenerationParameters is null || App.Clipboard is null) { return; } await App.Clipboard.SetTextAsync(item.GenerationParameters.Seed.ToString()); } [RelayCommand] private async Task CopyPromptToClipboard(LocalImageFile? item) { if (item?.GenerationParameters is null || App.Clipboard is null) { return; } await App.Clipboard.SetTextAsync(item.GenerationParameters.PositivePrompt); } [RelayCommand] private async Task CopyNegativePromptToClipboard(LocalImageFile? item) { if (item?.GenerationParameters is null || App.Clipboard is null) { return; } await App.Clipboard.SetTextAsync(item.GenerationParameters.NegativePrompt); } [RelayCommand] private async Task CopyModelNameToClipboard(LocalImageFile? item) { if (item?.GenerationParameters is null || App.Clipboard is null) { return; } await App.Clipboard.SetTextAsync(item.GenerationParameters.ModelName); } [RelayCommand] private async Task CopyModelHashToClipboard(LocalImageFile? item) { if (item?.GenerationParameters is null || App.Clipboard is null) { return; } await App.Clipboard.SetTextAsync(item.GenerationParameters.ModelHash); } [RelayCommand] private Task OnImageExportPng(LocalImageFile? item) => ImageExportImpl(item, SKEncodedImageFormat.Png); [RelayCommand] private Task OnImageExportPngWithMetadata(LocalImageFile? item) => ImageExportImpl(item, SKEncodedImageFormat.Png, true); [RelayCommand] private Task OnImageExportJpeg(LocalImageFile? item) => ImageExportImpl(item, SKEncodedImageFormat.Jpeg); [RelayCommand] private Task OnImageExportWebp(LocalImageFile? item) => ImageExportImpl(item, SKEncodedImageFormat.Webp); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs ================================================ using System; using System.Collections.Specialized; using System.IO; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ImageGalleryCard))] [ManagedService] [RegisterTransient] public partial class ImageGalleryCardViewModel : ViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IServiceManager vmFactory; [ObservableProperty] private bool isPreviewOverlayEnabled; [ObservableProperty] private Bitmap? previewImage; [ObservableProperty] private AvaloniaList imageSources = new(); [ObservableProperty] private ImageSource? selectedImage; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanNavigateBack), nameof(CanNavigateForward))] private int selectedImageIndex; [ObservableProperty] private bool isPixelGridEnabled; public bool HasMultipleImages => ImageSources.Count > 1; public bool CanNavigateBack => SelectedImageIndex > 0; public bool CanNavigateForward => SelectedImageIndex < ImageSources.Count - 1; public ImageGalleryCardViewModel( IServiceManager vmFactory, ISettingsManager settingsManager ) { this.vmFactory = vmFactory; IsPixelGridEnabled = settingsManager.Settings.IsImageViewerPixelGridEnabled; settingsManager.RegisterPropertyChangedHandler( s => s.IsImageViewerPixelGridEnabled, newValue => { IsPixelGridEnabled = newValue; } ); ImageSources.CollectionChanged += OnImageSourcesItemsChanged; } public void SetPreviewImage(byte[] imageBytes) { Dispatcher.UIThread.Post(() => { using var stream = new MemoryStream(imageBytes); var bitmap = new Bitmap(stream); var currentImage = PreviewImage; PreviewImage = bitmap; IsPreviewOverlayEnabled = true; // currentImage?.Dispose(); }); } private void OnImageSourcesItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (sender is AvaloniaList sources) { if ( e.Action is NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Remove or NotifyCollectionChangedAction.Reset ) { if (sources.Count == 0) { SelectedImageIndex = -1; } else if (SelectedImageIndex == -1) { SelectedImageIndex = 0; } // Clamp the selected index to the new range else { SelectedImageIndex = Math.Clamp(SelectedImageIndex, 0, sources.Count - 1); } OnPropertyChanged(nameof(CanNavigateBack)); OnPropertyChanged(nameof(CanNavigateForward)); OnPropertyChanged(nameof(HasMultipleImages)); } } } [RelayCommand] // ReSharper disable once UnusedMember.Local private async Task FlyoutCopy(IImage? image) { if (image is null) { Logger.Trace("FlyoutCopy: image is null"); return; } Logger.Trace($"FlyoutCopy is copying bitmap..."); await Task.Run(() => { if (Compat.IsWindows) { WindowsClipboard.SetBitmap((Bitmap)image); } }); } [RelayCommand] // ReSharper disable once UnusedMember.Local private async Task FlyoutPreview(IImage? image) { if (image is null) { Logger.Trace("FlyoutPreview: image is null"); return; } Logger.Trace($"FlyoutPreview opening..."); var viewerVm = vmFactory.Get(); viewerVm.ImageSource = new ImageSource((Bitmap)image); var dialog = new BetterContentDialog { Content = new ImageViewerDialog { DataContext = viewerVm } }; await dialog.ShowAsync(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceFluxTextToImageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Injectio.Attributes; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceTextToImageView), IsPersistent = true)] [ManagedService] [RegisterScoped] public class InferenceFluxTextToImageViewModel : InferenceGenerationViewModelBase, IParametersLoadableState { private readonly INotificationService notificationService; private readonly IModelIndexService modelIndexService; [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } [JsonPropertyName("Modules")] public StackEditableCardViewModel ModulesCardViewModel { get; } [JsonPropertyName("Model")] public UnetModelCardViewModel ModelCardViewModel { get; } [JsonPropertyName("Sampler")] public SamplerCardViewModel SamplerCardViewModel { get; } [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } [JsonPropertyName("BatchSize")] public BatchSizeCardViewModel BatchSizeCardViewModel { get; } [JsonPropertyName("Seed")] public SeedCardViewModel SeedCardViewModel { get; } public InferenceFluxTextToImageViewModel( IServiceManager vmFactory, IInferenceClientManager inferenceClientManager, INotificationService notificationService, ISettingsManager settingsManager, RunningPackageService runningPackageService ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { this.notificationService = notificationService; this.modelIndexService = modelIndexService; SeedCardViewModel = vmFactory.Get(); SeedCardViewModel.GenerateNewSeed(); ModelCardViewModel = vmFactory.Get(); SamplerCardViewModel = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.DenoiseStrength = 1.0d; samplerCard.EnableAddons = false; samplerCard.SelectedSampler = ComfySampler.Euler; samplerCard.SelectedScheduler = ComfyScheduler.Simple; samplerCard.CfgScale = 3.5d; samplerCard.Width = 1024; samplerCard.Height = 1024; }); PromptCardViewModel = vmFactory.Get(promptCard => { promptCard.IsNegativePromptEnabled = false; }); BatchSizeCardViewModel = vmFactory.Get(); ModulesCardViewModel = vmFactory.Get(modulesCard => { modulesCard.AvailableModules = new[] { typeof(CfzCudnnToggleModule), typeof(FaceDetailerModule), typeof(FluxHiresFixModule), typeof(SaveImageModule), typeof(UpscalerModule), }; modulesCard.DefaultModules = new[] { typeof(FluxHiresFixModule), typeof(UpscalerModule) }; modulesCard.InitializeDefaults(); }); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( ModelCardViewModel, SamplerCardViewModel, ModulesCardViewModel, SeedCardViewModel, BatchSizeCardViewModel ); } protected override void BuildPrompt(BuildPromptEventArgs args) { base.BuildPrompt(args); var builder = args.Builder; builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed), }; var applyArgs = args.ToModuleApplyStepEventArgs(); BatchSizeCardViewModel.ApplyStep(applyArgs); ModelCardViewModel.ApplyStep(applyArgs); builder.SetupEmptyLatentSource( SamplerCardViewModel.Width, SamplerCardViewModel.Height, BatchSizeCardViewModel.BatchSize, BatchSizeCardViewModel.BatchIndex, latentType: LatentType.Sd3 ); PromptCardViewModel.ApplyStep(applyArgs); // Do custom Sampler setup SamplerCardViewModel.ApplyStepsInitialCustomSampler(applyArgs, true); // Apply steps from our modules foreach (var module in ModulesCardViewModel.Cards.Cast()) { module.ApplyStep(applyArgs); } applyArgs.InvokeAllPreOutputActions(); builder.SetupOutputImage(); } /// protected override async Task GenerateImageImpl( GenerateOverrides overrides, CancellationToken cancellationToken ) { // Validate the prompts if (!await PromptCardViewModel.ValidatePrompts()) return; if (!await ModelCardViewModel.ValidateModel()) return; foreach (var module in ModulesCardViewModel.Cards.OfType()) { if (!module.IsEnabled) continue; if (module is not IValidatableModule validatableModule) continue; if (!await validatableModule.Validate()) { return; } } if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) return; // If enabled, randomize the seed var seedCard = StackCardViewModel.GetCard(); if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled) { seedCard.GenerateNewSeed(); } var batches = BatchSizeCardViewModel.BatchCount; var batchArgs = new List(); for (var i = 0; i < batches; i++) { var seed = seedCard.Seed + i; var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed }; BuildPrompt(buildPromptArgs); // update seed in project for batches var inferenceProject = InferenceProjectDocument.FromLoadable(this); if (inferenceProject.State?["Seed"]?["Seed"] is not null) { inferenceProject = inferenceProject.WithState(x => x["Seed"]["Seed"] = seed); } var generationArgs = new ImageGenerationEventArgs { Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), Parameters = SaveStateToParameters(new GenerationParameters()), Project = inferenceProject, FilesToTransfer = buildPromptArgs.FilesToTransfer, BatchIndex = i, // Only clear output images on the first batch ClearOutputImages = i == 0, }; batchArgs.Add(generationArgs); } // Run batches foreach (var args in batchArgs) { await RunGeneration(args, cancellationToken); } } public void LoadStateFromParameters(GenerationParameters parameters) { PromptCardViewModel.LoadStateFromParameters(parameters); SamplerCardViewModel.LoadStateFromParameters(parameters); ModelCardViewModel.LoadStateFromParameters(parameters); SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); if (Math.Abs(SamplerCardViewModel.DenoiseStrength - 1.0d) > 0.01d) { SamplerCardViewModel.DenoiseStrength = 1.0d; } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { parameters = PromptCardViewModel.SaveStateToParameters(parameters); parameters = SamplerCardViewModel.SaveStateToParameters(parameters); parameters = ModelCardViewModel.SaveStateToParameters(parameters); parameters.Seed = (ulong)SeedCardViewModel.Seed; return parameters; } // Deserialization overrides public override void LoadStateFromJsonObject(JsonObject state, int version) { // For v2 and below, do migration if (version <= 2) { ModulesCardViewModel.Clear(); // Add by default the original cards as steps - HiresFix, Upscaler ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsHiresFixEnabled"); if (state.TryGetPropertyValue("HiresSampler", out var hiresSamplerState)) { module .GetCard() .LoadStateFromJsonObject(hiresSamplerState!.AsObject()); } if (state.TryGetPropertyValue("HiresUpscaler", out var hiresUpscalerState)) { module .GetCard() .LoadStateFromJsonObject(hiresUpscalerState!.AsObject()); } }); ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsUpscaleEnabled"); if (state.TryGetPropertyValue("Upscaler", out var upscalerState)) { module .GetCard() .LoadStateFromJsonObject(upscalerState!.AsObject()); } }); // Add FreeU to sampler SamplerCardViewModel.ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsFreeUEnabled"); }); } base.LoadStateFromJsonObject(state); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToImageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Injectio.Attributes; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceImageToImageView), IsPersistent = true)] [RegisterScoped, ManagedService] public class InferenceImageToImageViewModel : InferenceTextToImageViewModel { [JsonPropertyName("SelectImage")] public SelectImageCardViewModel SelectImageCardViewModel { get; } /// public InferenceImageToImageViewModel( IServiceManager vmFactory, IInferenceClientManager inferenceClientManager, INotificationService notificationService, ISettingsManager settingsManager, IModelIndexService modelIndexService, RunningPackageService runningPackageService, TabContext tabContext ) : base( notificationService, inferenceClientManager, settingsManager, vmFactory, modelIndexService, runningPackageService, tabContext ) { SelectImageCardViewModel = vmFactory.Get(vm => { vm.IsMaskEditorEnabled = true; }); SamplerCardViewModel.IsDenoiseStrengthEnabled = true; } /// protected override void BuildPrompt(BuildPromptEventArgs args) { var builder = args.Builder; // Setup constants builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed) }; var applyArgs = args.ToModuleApplyStepEventArgs(); BatchSizeCardViewModel.ApplyStep(applyArgs); // Load models ModelCardViewModel.ApplyStep(applyArgs); // Setup image latent source SelectImageCardViewModel.ApplyStep(applyArgs); // Prompts and loras PromptCardViewModel.ApplyStep(applyArgs); // Setup Sampler and Refiner if enabled var isUnetLoader = ModelCardViewModel.SelectedModelLoader is ModelLoader.Unet || ModelCardViewModel.IsGguf; if (isUnetLoader) { SamplerCardViewModel.ApplyStepsInitialCustomSampler(applyArgs, true); } else if (SamplerCardViewModel.SelectedScheduler?.Name is "align_your_steps") { SamplerCardViewModel.ApplyStepsInitialCustomSampler(applyArgs, false); } else { SamplerCardViewModel.ApplyStep(applyArgs); } // Apply module steps foreach (var module in ModulesCardViewModel.Cards.OfType()) { module.ApplyStep(applyArgs); } applyArgs.InvokeAllPreOutputActions(); builder.SetupOutputImage(); } /// protected override IEnumerable GetInputImages() { var mainImages = SelectImageCardViewModel.GetInputImages(); var samplerImages = SamplerCardViewModel .ModulesCardViewModel.Cards.OfType() .SelectMany(m => m.GetInputImages()); var moduleImages = ModulesCardViewModel .Cards.OfType() .SelectMany(m => m.GetInputImages()); return mainImages.Concat(samplerImages).Concat(moduleImages); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Video; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceImageToVideoView), persistent: true)] [ManagedService] [RegisterScoped] public partial class InferenceImageToVideoViewModel : InferenceGenerationViewModelBase, IParametersLoadableState { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; private readonly IModelIndexService modelIndexService; [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } [JsonPropertyName("Model")] public ImgToVidModelCardViewModel ModelCardViewModel { get; } [JsonPropertyName("Sampler")] public SamplerCardViewModel SamplerCardViewModel { get; } [JsonPropertyName("BatchSize")] public BatchSizeCardViewModel BatchSizeCardViewModel { get; } [JsonPropertyName("Seed")] public SeedCardViewModel SeedCardViewModel { get; } [JsonPropertyName("ImageLoader")] public SelectImageCardViewModel SelectImageCardViewModel { get; } [JsonPropertyName("Conditioning")] public SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel { get; } [JsonPropertyName("VideoOutput")] public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; } public InferenceImageToVideoViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, IServiceManager vmFactory, IModelIndexService modelIndexService, RunningPackageService runningPackageService ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { this.notificationService = notificationService; this.modelIndexService = modelIndexService; // Get sub view models from service manager SeedCardViewModel = vmFactory.Get(); SeedCardViewModel.GenerateNewSeed(); ModelCardViewModel = vmFactory.Get( vm => vm.EnableModelLoaderSelection = false ); SamplerCardViewModel = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.CfgScale = 2.5d; samplerCard.SelectedSampler = ComfySampler.Euler; samplerCard.SelectedScheduler = ComfyScheduler.Karras; samplerCard.IsDenoiseStrengthEnabled = true; samplerCard.DenoiseStrength = 1.0f; }); BatchSizeCardViewModel = vmFactory.Get(); SelectImageCardViewModel = vmFactory.Get(); SvdImgToVidConditioningViewModel = vmFactory.Get(); VideoOutputSettingsCardViewModel = vmFactory.Get(); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( ModelCardViewModel, SvdImgToVidConditioningViewModel, SamplerCardViewModel, SeedCardViewModel, VideoOutputSettingsCardViewModel, BatchSizeCardViewModel ); } /// protected override void BuildPrompt(BuildPromptEventArgs args) { base.BuildPrompt(args); var builder = args.Builder; builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed) }; // Load models ModelCardViewModel.ApplyStep(args); // Setup latent from image var imageLoad = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.LoadImage { Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"), Image = SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") ?? throw new ValidationException() } ); builder.Connections.Primary = imageLoad.Output1; builder.Connections.PrimarySize = SelectImageCardViewModel.CurrentBitmapSize; // Setup img2vid stuff // Set width & height from SamplerCard SvdImgToVidConditioningViewModel.Width = SamplerCardViewModel.Width; SvdImgToVidConditioningViewModel.Height = SamplerCardViewModel.Height; SvdImgToVidConditioningViewModel.ApplyStep(args); // Setup Sampler and Refiner if enabled SamplerCardViewModel.ApplyStep(args); // Animated webp output VideoOutputSettingsCardViewModel.ApplyStep(args); } /// protected override IEnumerable GetInputImages() { if (SelectImageCardViewModel.ImageSource is { } image) { yield return image; } } /// protected override async Task GenerateImageImpl( GenerateOverrides overrides, CancellationToken cancellationToken ) { if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) { return; } if (!await ModelCardViewModel.ValidateModel()) return; // If enabled, randomize the seed var seedCard = StackCardViewModel.GetCard(); if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled) { seedCard.GenerateNewSeed(); } var batches = BatchSizeCardViewModel.BatchCount; var batchArgs = new List(); for (var i = 0; i < batches; i++) { var seed = seedCard.Seed + i; var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed }; BuildPrompt(buildPromptArgs); // update seed in project for batches var inferenceProject = InferenceProjectDocument.FromLoadable(this); if (inferenceProject.State?["Seed"]?["Seed"] is not null) { inferenceProject = inferenceProject.WithState(x => x["Seed"]["Seed"] = seed); } var generationArgs = new ImageGenerationEventArgs { Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), Parameters = SaveStateToParameters(new GenerationParameters()) with { Seed = Convert.ToUInt64(seed) }, Project = inferenceProject, FilesToTransfer = buildPromptArgs.FilesToTransfer, BatchIndex = i, // Only clear output images on the first batch ClearOutputImages = i == 0 }; batchArgs.Add(generationArgs); } // Run batches foreach (var args in batchArgs) { await RunGeneration(args, cancellationToken); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { SamplerCardViewModel.LoadStateFromParameters(parameters); ModelCardViewModel.LoadStateFromParameters(parameters); SvdImgToVidConditioningViewModel.LoadStateFromParameters(parameters); VideoOutputSettingsCardViewModel.LoadStateFromParameters(parameters); SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { parameters = SamplerCardViewModel.SaveStateToParameters(parameters); parameters = ModelCardViewModel.SaveStateToParameters(parameters); parameters = SvdImgToVidConditioningViewModel.SaveStateToParameters(parameters); parameters = VideoOutputSettingsCardViewModel.SaveStateToParameters(parameters); parameters.Seed = (ulong)SeedCardViewModel.Seed; return parameters; } // Migration for v2 deserialization public override void LoadStateFromJsonObject(JsonObject state, int version) { if (version > 2) { LoadStateFromJsonObject(state); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs ================================================ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Services; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceImageUpscaleView), persistent: true)] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] [ManagedService] [RegisterScoped] public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } [JsonPropertyName("Upscaler")] public UpscalerCardViewModel UpscalerCardViewModel { get; } [JsonPropertyName("Sharpen")] public SharpenCardViewModel SharpenCardViewModel { get; } [JsonPropertyName("SelectImage")] public SelectImageCardViewModel SelectImageCardViewModel { get; } public bool IsUpscaleEnabled { get => StackCardViewModel.GetCard().IsEnabled; set => StackCardViewModel.GetCard().IsEnabled = value; } public bool IsSharpenEnabled { get => StackCardViewModel.GetCard(1).IsEnabled; set => StackCardViewModel.GetCard(1).IsEnabled = value; } public InferenceImageUpscaleViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, IServiceManager vmFactory, RunningPackageService runningPackageService ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { this.notificationService = notificationService; UpscalerCardViewModel = vmFactory.Get(); SharpenCardViewModel = vmFactory.Get(); SelectImageCardViewModel = vmFactory.Get(); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( vmFactory.Get(stackExpander => { stackExpander.Title = "Upscale"; stackExpander.AddCards(UpscalerCardViewModel); }), vmFactory.Get(stackExpander => { stackExpander.Title = "Sharpen"; stackExpander.AddCards(SharpenCardViewModel); }) ); } /// protected override IEnumerable GetInputImages() { if (SelectImageCardViewModel.ImageSource is { } imageSource) { yield return imageSource; } } /// protected override void BuildPrompt(BuildPromptEventArgs args) { base.BuildPrompt(args); var builder = args.Builder; var nodes = builder.Nodes; // Setup image source SelectImageCardViewModel.ApplyStep(args); // If upscale is enabled, add another upscale group if (IsUpscaleEnabled) { var upscaleSize = builder.Connections.PrimarySize.WithScale(UpscalerCardViewModel.Scale); // Build group builder.Connections.Primary = builder .Group_UpscaleToImage( "Upscale", builder.GetPrimaryAsImage(), UpscalerCardViewModel.SelectedUpscaler!.Value, upscaleSize.Width, upscaleSize.Height ) .Output; } // If sharpen is enabled, add another sharpen group if (IsSharpenEnabled) { builder.Connections.Primary = nodes .AddTypedNode( new ComfyNodeBuilder.ImageSharpen { Name = "Sharpen", Image = builder.GetPrimaryAsImage(), SharpenRadius = SharpenCardViewModel.SharpenRadius, Sigma = SharpenCardViewModel.Sigma, Alpha = SharpenCardViewModel.Alpha } ) .Output; } builder.SetupOutputImage(); } /// protected override async Task GenerateImageImpl( GenerateOverrides overrides, CancellationToken cancellationToken ) { if (!ClientManager.IsConnected) { notificationService.Show("Client not connected", "Please connect first"); return; } if (SelectImageCardViewModel.ImageSource?.LocalFile?.FullPath is not { } path) { notificationService.Show("No image selected", "Please select an image first"); return; } foreach (var image in GetInputImages()) { await ClientManager.UploadInputImageAsync(image, cancellationToken); } var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides }; BuildPrompt(buildPromptArgs); var generationArgs = new ImageGenerationEventArgs { Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), Parameters = new GenerationParameters { ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name, }, Project = InferenceProjectDocument.FromLoadable(this) }; await RunGeneration(generationArgs, cancellationToken); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using DynamicData.Binding; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Services; using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceTextToImageView), IsPersistent = true)] [ManagedService] [RegisterScoped] public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, IParametersLoadableState { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; private readonly IModelIndexService modelIndexService; private readonly TabContext tabContext; [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } [JsonPropertyName("Modules")] public StackEditableCardViewModel ModulesCardViewModel { get; } [JsonPropertyName("Model")] public ModelCardViewModel ModelCardViewModel { get; } [JsonPropertyName("Sampler")] public SamplerCardViewModel SamplerCardViewModel { get; } [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } [JsonPropertyName("BatchSize")] public BatchSizeCardViewModel BatchSizeCardViewModel { get; } [JsonPropertyName("Seed")] public SeedCardViewModel SeedCardViewModel { get; } public InferenceTextToImageViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, IServiceManager vmFactory, IModelIndexService modelIndexService, RunningPackageService runningPackageService, TabContext tabContext ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { this.notificationService = notificationService; this.modelIndexService = modelIndexService; this.tabContext = tabContext; // Get sub view models from service manager SeedCardViewModel = vmFactory.Get(); SeedCardViewModel.GenerateNewSeed(); ModelCardViewModel = vmFactory.Get(); // When the model changes in the ModelCardViewModel, we'll have access to it in the TabContext SamplerCardViewModel = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.DenoiseStrength = 1.0d; }); PromptCardViewModel = AddDisposable(vmFactory.Get()); BatchSizeCardViewModel = vmFactory.Get(); ModulesCardViewModel = vmFactory.Get(modulesCard => { modulesCard.AvailableModules = new[] { typeof(CfzCudnnToggleModule), typeof(FaceDetailerModule), typeof(HiresFixModule), typeof(SaveImageModule), typeof(UpscalerModule), }; modulesCard.DefaultModules = new[] { typeof(HiresFixModule), typeof(UpscalerModule) }; modulesCard.InitializeDefaults(); }); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( ModelCardViewModel, SamplerCardViewModel, ModulesCardViewModel, SeedCardViewModel, BatchSizeCardViewModel ); // When refiner is provided in model card, enable for sampler AddDisposable( ModelCardViewModel .WhenPropertyChanged(x => x.IsRefinerSelectionEnabled) .ObserveOn(SynchronizationContext.Current) .Subscribe(e => { SamplerCardViewModel.IsRefinerStepsEnabled = e.Sender is { IsRefinerSelectionEnabled: true, SelectedRefiner: not null }; }) ); AddDisposable( SamplerCardViewModel .WhenPropertyChanged(x => x.SelectedScheduler) .ObserveOn(SynchronizationContext.Current) .Subscribe(next => { var isAlignYourSteps = next.Value is { Name: "align_your_steps" }; ModelCardViewModel.ShowRefinerOption = !isAlignYourSteps; if (isAlignYourSteps) { ModelCardViewModel.IsRefinerSelectionEnabled = false; } }) ); } /// protected override void BuildPrompt(BuildPromptEventArgs args) { base.BuildPrompt(args); var builder = args.Builder; // Load constants builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed), }; var applyArgs = args.ToModuleApplyStepEventArgs(); BatchSizeCardViewModel.ApplyStep(applyArgs); // Load models ModelCardViewModel.ApplyStep(applyArgs); var isUnetLoader = ModelCardViewModel.SelectedModelLoader is ModelLoader.Unet; var useSd3Latent = SamplerCardViewModel.ModulesCardViewModel.IsModuleEnabled() || isUnetLoader; var usePlasmaNoise = SamplerCardViewModel.ModulesCardViewModel.IsModuleEnabled(); if (useSd3Latent) { builder.SetupEmptyLatentSource( SamplerCardViewModel.Width, SamplerCardViewModel.Height, BatchSizeCardViewModel.BatchSize, BatchSizeCardViewModel.IsBatchIndexEnabled ? BatchSizeCardViewModel.BatchIndex : null, latentType: LatentType.Sd3 ); } else if (usePlasmaNoise) { var plasmaVm = SamplerCardViewModel .ModulesCardViewModel.GetCard() .GetCard(); builder.SetupPlasmaLatentSource( SamplerCardViewModel.Width, SamplerCardViewModel.Height, builder.Connections.Seed, plasmaVm.SelectedNoiseType, plasmaVm.ValueMin, plasmaVm.ValueMax, plasmaVm.RedMin, plasmaVm.RedMax, plasmaVm.GreenMin, plasmaVm.GreenMax, plasmaVm.BlueMin, plasmaVm.BlueMax, plasmaVm.PlasmaTurbulence ); } else { // Setup empty latent builder.SetupEmptyLatentSource( SamplerCardViewModel.Width, SamplerCardViewModel.Height, BatchSizeCardViewModel.BatchSize, BatchSizeCardViewModel.IsBatchIndexEnabled ? BatchSizeCardViewModel.BatchIndex : null ); } // Prompts and loras PromptCardViewModel.ApplyStep(applyArgs); // Setup Sampler and Refiner if enabled if (isUnetLoader) { SamplerCardViewModel.ApplyStepsInitialCustomSampler(applyArgs, true); } else if (SamplerCardViewModel.SelectedScheduler?.Name is "align_your_steps") { SamplerCardViewModel.ApplyStepsInitialCustomSampler(applyArgs, false); } else { SamplerCardViewModel.ApplyStep(applyArgs); } // Hires fix if enabled foreach (var module in ModulesCardViewModel.Cards.OfType()) { module.ApplyStep(applyArgs); } applyArgs.InvokeAllPreOutputActions(); builder.SetupOutputImage(); } /// protected override IEnumerable GetInputImages() { var samplerImages = SamplerCardViewModel .ModulesCardViewModel.Cards.OfType() .SelectMany(m => m.GetInputImages()); var moduleImages = ModulesCardViewModel .Cards.OfType() .SelectMany(m => m.GetInputImages()); return samplerImages.Concat(moduleImages); } /// protected override async Task GenerateImageImpl( GenerateOverrides overrides, CancellationToken cancellationToken ) { // Validate the prompts if (!await PromptCardViewModel.ValidatePrompts()) return; if (!await ModelCardViewModel.ValidateModel()) return; foreach (var module in ModulesCardViewModel.Cards.OfType()) { if (!module.IsEnabled) continue; if (module is not IValidatableModule validatableModule) continue; if (!await validatableModule.Validate()) { return; } } if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) return; // If enabled, randomize the seed var seedCard = StackCardViewModel.GetCard(); if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled) { seedCard.GenerateNewSeed(); } var batches = BatchSizeCardViewModel.BatchCount; var batchArgs = new List(); for (var i = 0; i < batches; i++) { var seed = seedCard.Seed + i; var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed }; BuildPrompt(buildPromptArgs); // update seed in project for batches var inferenceProject = InferenceProjectDocument.FromLoadable(this); if (inferenceProject.State?["Seed"]?["Seed"] is not null) { inferenceProject = inferenceProject.WithState(x => x["Seed"]["Seed"] = seed); } var generationArgs = new ImageGenerationEventArgs { Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), Parameters = SaveStateToParameters(new GenerationParameters()) with { Seed = Convert.ToUInt64(seed), }, Project = inferenceProject, FilesToTransfer = buildPromptArgs.FilesToTransfer, BatchIndex = i, // Only clear output images on the first batch ClearOutputImages = i == 0, }; batchArgs.Add(generationArgs); } // Run batches foreach (var args in batchArgs) { await RunGeneration(args, cancellationToken); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { PromptCardViewModel.LoadStateFromParameters(parameters); SamplerCardViewModel.LoadStateFromParameters(parameters); ModelCardViewModel.LoadStateFromParameters(parameters); SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); if (Math.Abs(SamplerCardViewModel.DenoiseStrength - 1.0d) > 0.01d) { SamplerCardViewModel.DenoiseStrength = 1.0d; } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { parameters = PromptCardViewModel.SaveStateToParameters(parameters); parameters = SamplerCardViewModel.SaveStateToParameters(parameters); parameters = ModelCardViewModel.SaveStateToParameters(parameters); parameters.Seed = (ulong)SeedCardViewModel.Seed; return parameters; } // Deserialization overrides public override void LoadStateFromJsonObject(JsonObject state, int version) { // For v2 and below, do migration if (version <= 2) { ModulesCardViewModel.Clear(); // Add by default the original cards as steps - HiresFix, Upscaler ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsHiresFixEnabled"); if (state.TryGetPropertyValue("HiresSampler", out var hiresSamplerState)) { module .GetCard() .LoadStateFromJsonObject(hiresSamplerState!.AsObject()); } if (state.TryGetPropertyValue("HiresUpscaler", out var hiresUpscalerState)) { module .GetCard() .LoadStateFromJsonObject(hiresUpscalerState!.AsObject()); } }); ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsUpscaleEnabled"); if (state.TryGetPropertyValue("Upscaler", out var upscalerState)) { module .GetCard() .LoadStateFromJsonObject(upscalerState!.AsObject()); } }); // Add FreeU to sampler SamplerCardViewModel.ModulesCardViewModel.AddModule(module => { module.IsEnabled = state.GetPropertyValueOrDefault("IsFreeUEnabled"); }); } base.LoadStateFromJsonObject(state); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceWanImageToVideoViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceWanImageToVideoView), IsPersistent = true)] [RegisterScoped, ManagedService] public class InferenceWanImageToVideoViewModel : InferenceWanTextToVideoViewModel { public InferenceWanImageToVideoViewModel( IServiceManager vmFactory, IInferenceClientManager inferenceClientManager, INotificationService notificationService, ISettingsManager settingsManager, RunningPackageService runningPackageService ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { SelectImageCardViewModel = vmFactory.Get(); SamplerCardViewModel.IsDenoiseStrengthEnabled = true; SamplerCardViewModel.Width = 512; SamplerCardViewModel.Height = 512; ModelCardViewModel.IsClipVisionEnabled = true; } [JsonPropertyName("ImageLoader")] public SelectImageCardViewModel SelectImageCardViewModel { get; } /// protected override void BuildPrompt(BuildPromptEventArgs args) { var applyArgs = args.ToModuleApplyStepEventArgs(); var builder = args.Builder; builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed), }; // Load models ModelCardViewModel.ApplyStep(applyArgs); // Setup latent from image var imageLoad = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.LoadImage { Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"), Image = SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") ?? throw new ValidationException(), } ); builder.Connections.Primary = imageLoad.Output1; builder.Connections.PrimarySize = SelectImageCardViewModel.CurrentBitmapSize; BatchSizeCardViewModel.ApplyStep(applyArgs); SelectImageCardViewModel.ApplyStep(applyArgs); PromptCardViewModel.ApplyStep(applyArgs); SamplerCardViewModel.ApplyStep(applyArgs); applyArgs.InvokeAllPreOutputActions(); // Animated webp output VideoOutputSettingsCardViewModel.ApplyStep(applyArgs); } /// protected override IEnumerable GetInputImages() { if (SelectImageCardViewModel.ImageSource is { } image) { yield return image; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/InferenceWanTextToVideoViewModel.cs ================================================ using System.Text.Json.Serialization; using Injectio.Attributes; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Video; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceWanTextToVideoView), IsPersistent = true)] [RegisterScoped, ManagedService] public class InferenceWanTextToVideoViewModel : InferenceGenerationViewModelBase, IParametersLoadableState { [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } [JsonPropertyName("Model")] public WanModelCardViewModel ModelCardViewModel { get; } [JsonPropertyName("Sampler")] public SamplerCardViewModel SamplerCardViewModel { get; } [JsonPropertyName("BatchSize")] public BatchSizeCardViewModel BatchSizeCardViewModel { get; } [JsonPropertyName("Seed")] public SeedCardViewModel SeedCardViewModel { get; } [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } [JsonPropertyName("VideoOutput")] public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; } public InferenceWanTextToVideoViewModel( IServiceManager vmFactory, IInferenceClientManager inferenceClientManager, INotificationService notificationService, ISettingsManager settingsManager, RunningPackageService runningPackageService ) : base(vmFactory, inferenceClientManager, notificationService, settingsManager, runningPackageService) { SeedCardViewModel = vmFactory.Get(); SeedCardViewModel.GenerateNewSeed(); ModelCardViewModel = vmFactory.Get(); SamplerCardViewModel = vmFactory.Get(samplerCard => { samplerCard.IsDimensionsEnabled = true; samplerCard.IsCfgScaleEnabled = true; samplerCard.IsSamplerSelectionEnabled = true; samplerCard.IsSchedulerSelectionEnabled = true; samplerCard.DenoiseStrength = 1.0d; samplerCard.EnableAddons = true; samplerCard.IsLengthEnabled = true; samplerCard.Width = 832; samplerCard.Height = 480; samplerCard.Length = 33; }); PromptCardViewModel = AddDisposable(vmFactory.Get()); BatchSizeCardViewModel = vmFactory.Get(); VideoOutputSettingsCardViewModel = vmFactory.Get(vm => vm.Fps = 16.0d ); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( ModelCardViewModel, SamplerCardViewModel, SeedCardViewModel, BatchSizeCardViewModel, VideoOutputSettingsCardViewModel ); } /// protected override void BuildPrompt(BuildPromptEventArgs args) { base.BuildPrompt(args); var applyArgs = args.ToModuleApplyStepEventArgs(); var builder = args.Builder; builder.Connections.Seed = args.SeedOverride switch { { } seed => Convert.ToUInt64(seed), _ => Convert.ToUInt64(SeedCardViewModel.Seed), }; // Load models ModelCardViewModel.ApplyStep(applyArgs); builder.SetupEmptyLatentSource( SamplerCardViewModel.Width, SamplerCardViewModel.Height, BatchSizeCardViewModel.BatchSize, BatchSizeCardViewModel.IsBatchIndexEnabled ? BatchSizeCardViewModel.BatchIndex : null, SamplerCardViewModel.Length, LatentType.Hunyuan ); BatchSizeCardViewModel.ApplyStep(applyArgs); PromptCardViewModel.ApplyStep(applyArgs); SamplerCardViewModel.ApplyStep(applyArgs); applyArgs.InvokeAllPreOutputActions(); // Animated webp output VideoOutputSettingsCardViewModel.ApplyStep(applyArgs); } /// protected override async Task GenerateImageImpl( GenerateOverrides overrides, CancellationToken cancellationToken ) { if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) { return; } if (!await ModelCardViewModel.ValidateModel()) return; // If enabled, randomize the seed var seedCard = StackCardViewModel.GetCard(); if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled) { seedCard.GenerateNewSeed(); } var batches = BatchSizeCardViewModel.BatchCount; var batchArgs = new List(); for (var i = 0; i < batches; i++) { var seed = seedCard.Seed + i; var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed }; BuildPrompt(buildPromptArgs); // update seed in project for batches var inferenceProject = InferenceProjectDocument.FromLoadable(this); if (inferenceProject.State?["Seed"]?["Seed"] is not null) { inferenceProject = inferenceProject.WithState(x => x["Seed"]["Seed"] = seed); } var generationArgs = new ImageGenerationEventArgs { Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), Parameters = SaveStateToParameters(new GenerationParameters()) with { Seed = Convert.ToUInt64(seed), }, Project = inferenceProject, FilesToTransfer = buildPromptArgs.FilesToTransfer, BatchIndex = i, // Only clear output images on the first batch ClearOutputImages = i == 0, }; batchArgs.Add(generationArgs); } // Run batches foreach (var args in batchArgs) { await RunGeneration(args, cancellationToken); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { SamplerCardViewModel.LoadStateFromParameters(parameters); ModelCardViewModel.LoadStateFromParameters(parameters); PromptCardViewModel.LoadStateFromParameters(parameters); VideoOutputSettingsCardViewModel.LoadStateFromParameters(parameters); SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { parameters = SamplerCardViewModel.SaveStateToParameters(parameters); parameters = ModelCardViewModel.SaveStateToParameters(parameters); parameters = PromptCardViewModel.SaveStateToParameters(parameters); parameters = VideoOutputSettingsCardViewModel.SaveStateToParameters(parameters); parameters.Seed = (ulong)SeedCardViewModel.Seed; return parameters; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/LayerDiffuseCardViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using KGySoft.CoreLibraries; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [RegisterTransient] [ManagedService] [View(typeof(LayerDiffuseCard))] public partial class LayerDiffuseCardViewModel : LoadableViewModelBase, IComfyStep { public const string ModuleKey = "LayerDiffuse"; [ObservableProperty] private LayerDiffuseMode selectedMode = LayerDiffuseMode.None; public IEnumerable AvailableModes => Enum.GetValues(); [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(-1d, 3d)] private double weight = 1; /// public void ApplyStep(ModuleApplyStepEventArgs e) { if (SelectedMode == LayerDiffuseMode.None) return; var sdType = SelectedMode switch { LayerDiffuseMode.GenerateForegroundWithTransparencySD15 => "SD15", LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL", LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException() }; // Choose config based on mode var config = SelectedMode switch { LayerDiffuseMode.GenerateForegroundWithTransparencySD15 => "SD15, Attention Injection, attn_sharing", LayerDiffuseMode.GenerateForegroundWithTransparencySDXL => "SDXL, Conv Injection", LayerDiffuseMode.None => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException() }; foreach (var modelConnections in e.Temp.Models.Values) { var layerDiffuseApply = e.Nodes.AddTypedNode( new ComfyNodeBuilder.LayeredDiffusionApply { Name = e.Nodes.GetUniqueName($"LayerDiffuseApply_{modelConnections.Name}"), Model = modelConnections.Model, Config = config, Weight = Weight, } ); modelConnections.Model = layerDiffuseApply.Output; } // Add pre output action e.PreOutputActions.Add(applyArgs => { // Use last latent for decode var latent = applyArgs.Builder.Connections.LastPrimaryLatent ?? throw new InvalidOperationException("Connections.LastPrimaryLatent not set"); // Convert primary to image if not already var primaryImage = applyArgs.Builder.GetPrimaryAsImage(); applyArgs.Builder.Connections.Primary = primaryImage; // Add a Layer Diffuse Decode var decode = applyArgs.Nodes.AddTypedNode( new ComfyNodeBuilder.LayeredDiffusionDecodeRgba { Name = applyArgs.Nodes.GetUniqueName("LayerDiffuseDecode"), Samples = latent, Images = primaryImage, SdVersion = sdType } ); // Set primary to decode output applyArgs.Builder.Connections.Primary = decode.Output; }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Text.Json.Nodes; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ModelCard))] [ManagedService] [RegisterTransient] public partial class ModelCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory, TabContext tabContext ) : LoadableViewModelBase, IParametersLoadableState, IComfyStep { [ObservableProperty] private HybridModelFile? selectedModel; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsGguf), nameof(ShowPrecisionSelection))] private HybridModelFile? selectedUnetModel; [ObservableProperty] private bool isRefinerSelectionEnabled; [ObservableProperty] private bool showRefinerOption = true; [ObservableProperty] private HybridModelFile? selectedRefiner = HybridModelFile.None; [ObservableProperty] private HybridModelFile? selectedVae = HybridModelFile.Default; [ObservableProperty] private bool isVaeSelectionEnabled; [ObservableProperty] private bool disableSettings; [ObservableProperty] private bool isClipSkipEnabled; [NotifyDataErrorInfo] [ObservableProperty] [Range(1, 24)] private int clipSkip = 1; [ObservableProperty] private bool isExtraNetworksEnabled; [ObservableProperty] private bool isModelLoaderSelectionEnabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsStandaloneModelLoader))] private ModelLoader selectedModelLoader; [ObservableProperty] private HybridModelFile? selectedClip1; [ObservableProperty] private HybridModelFile? selectedClip2; [ObservableProperty] private HybridModelFile? selectedClip3; [ObservableProperty] private HybridModelFile? selectedClip4; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsSd3Clip), nameof(IsHiDreamClip))] private string? selectedClipType; [ObservableProperty] private string? selectedDType; [ObservableProperty] private bool enableModelLoaderSelection = true; [ObservableProperty] private bool isClipModelSelectionEnabled; [ObservableProperty] private double shift = 3.0d; public List WeightDTypes { get; set; } = ["default", "fp8_e4m3fn", "fp8_e5m2"]; public List ClipTypes { get; set; } = ["flux", "sd3", "HiDream"]; public StackEditableCardViewModel ExtraNetworksStackCardViewModel { get; } = new(vmFactory) { Title = Resources.Label_ExtraNetworks, AvailableModules = [typeof(LoraModule)] }; public IInferenceClientManager ClientManager { get; } = clientManager; public List ModelLoaders { get; } = Enum.GetValues().Except([ModelLoader.Gguf]).ToList(); public bool IsStandaloneModelLoader => SelectedModelLoader is ModelLoader.Unet; public bool ShowPrecisionSelection => SelectedModelLoader is ModelLoader.Unet && !IsGguf; public bool IsSd3Clip => SelectedClipType == "sd3"; public bool IsHiDreamClip => SelectedClipType == "HiDream"; public bool IsGguf => SelectedUnetModel?.RelativePath.EndsWith("gguf") ?? false; protected override void OnInitialLoaded() { base.OnInitialLoaded(); ExtraNetworksStackCardViewModel.CardAdded += ExtraNetworksStackCardViewModelOnCardAdded; } public override void OnUnloaded() { base.OnUnloaded(); ExtraNetworksStackCardViewModel.CardAdded -= ExtraNetworksStackCardViewModelOnCardAdded; } private void ExtraNetworksStackCardViewModelOnCardAdded(object? sender, LoadableViewModelBase e) { OnSelectedModelChanged(SelectedModel); } [RelayCommand] private static async Task OnConfigClickAsync() { await DialogHelper .CreateMarkdownDialog( """ You can use a config (.yaml) file to load a model with specific settings. Place the config file next to the model file with the same name: ```md Models/ StableDiffusion/ my_model.safetensors my_model.yaml <- ``` """, "Using Model Configs", TextEditorPreset.Console ) .ShowAsync(); } public async Task ValidateModel() { if (IsStandaloneModelLoader && SelectedUnetModel != null) return true; if (!IsStandaloneModelLoader && SelectedModel != null) return true; var dialog = DialogHelper.CreateMarkdownDialog( "Please select a model to continue.", "No Model Selected" ); await dialog.ShowAsync(); return false; } private static ComfyTypedNodeBase< ModelNodeConnection, ClipNodeConnection, VAENodeConnection > GetDefaultModelLoader(ModuleApplyStepEventArgs e, string nodeName, HybridModelFile model) { // Check if config if (model.Local?.ConfigFullPath is { } configPath) { // We'll need to upload the config file to `models/configs` later var uploadConfigPath = e.AddFileTransferToConfigs(configPath); return new ComfyNodeBuilder.CheckpointLoader { Name = nodeName, // Only the file name is needed ConfigName = Path.GetFileName(uploadConfigPath), CkptName = model.RelativePath, }; } // Simple loader if no config return new ComfyNodeBuilder.CheckpointLoaderSimple { Name = nodeName, CkptName = model.RelativePath }; } /// public virtual void ApplyStep(ModuleApplyStepEventArgs e) { if (SelectedModelLoader is ModelLoader.Default or ModelLoader.Nf4) { SetupDefaultModelLoader(e); } else // UNET/GGUF UNET workflow { SetupStandaloneModelLoader(e); } // Clip skip all models if enabled if (IsClipSkipEnabled) { foreach (var (modelName, model) in e.Builder.Connections.Models) { if (model.Clip is not { } modelClip) continue; var clipSetLastLayer = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPSetLastLayer { Name = $"CLIP_Skip_{modelName}", Clip = modelClip, // Need to convert to negative indexing from (1 to 24) to (-1 to -24) StopAtClipLayer = -ClipSkip, } ); model.Clip = clipSetLastLayer.Output; } } // Load extra networks if enabled if (IsExtraNetworksEnabled) { ExtraNetworksStackCardViewModel.ApplyStep(e); } } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel( new ModelCardModel { SelectedModelName = IsStandaloneModelLoader ? SelectedUnetModel?.RelativePath : SelectedModel?.RelativePath, SelectedVaeName = SelectedVae?.RelativePath, SelectedRefinerName = SelectedRefiner?.RelativePath, ClipSkip = ClipSkip, IsVaeSelectionEnabled = IsVaeSelectionEnabled, IsRefinerSelectionEnabled = IsRefinerSelectionEnabled, IsClipSkipEnabled = IsClipSkipEnabled, IsExtraNetworksEnabled = IsExtraNetworksEnabled, IsModelLoaderSelectionEnabled = IsModelLoaderSelectionEnabled, SelectedClip1Name = SelectedClip1?.RelativePath, SelectedClip2Name = SelectedClip2?.RelativePath, SelectedClip3Name = SelectedClip3?.RelativePath, SelectedClip4Name = SelectedClip4?.RelativePath, SelectedClipType = SelectedClipType, IsClipModelSelectionEnabled = IsClipModelSelectionEnabled, ModelLoader = SelectedModelLoader, ShowRefinerOption = ShowRefinerOption, ExtraNetworks = ExtraNetworksStackCardViewModel.SaveStateToJsonObject(), } ); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); // uwu 123 // :thinknom: // :thinkcode: SelectedModelLoader = model.ModelLoader is ModelLoader.Gguf ? ModelLoader.Unet : model.ModelLoader; if (SelectedModelLoader is ModelLoader.Unet) { SelectedUnetModel = model.SelectedModelName is null ? null : ClientManager.UnetModels.FirstOrDefault(x => x.RelativePath == model.SelectedModelName); } else { SelectedModel = model.SelectedModelName is null ? null : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedModelName); } SelectedVae = model.SelectedVaeName is null ? HybridModelFile.Default : ClientManager.VaeModels.FirstOrDefault(x => x.RelativePath == model.SelectedVaeName); SelectedRefiner = model.SelectedRefinerName is null ? HybridModelFile.None : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedRefinerName); SelectedClip1 = model.SelectedClip1Name is null ? HybridModelFile.None : ClientManager.ClipModels.FirstOrDefault(x => x.RelativePath == model.SelectedClip1Name); SelectedClip2 = model.SelectedClip2Name is null ? HybridModelFile.None : ClientManager.ClipModels.FirstOrDefault(x => x.RelativePath == model.SelectedClip2Name); SelectedClip3 = model.SelectedClip3Name is null ? HybridModelFile.None : ClientManager.ClipModels.FirstOrDefault(x => x.RelativePath == model.SelectedClip3Name); SelectedClip4 = model.SelectedClip4Name is null ? HybridModelFile.None : ClientManager.ClipModels.FirstOrDefault(x => x.RelativePath == model.SelectedClip4Name); SelectedClipType = model.SelectedClipType; ClipSkip = model.ClipSkip; IsVaeSelectionEnabled = model.IsVaeSelectionEnabled; IsRefinerSelectionEnabled = model.IsRefinerSelectionEnabled; ShowRefinerOption = model.ShowRefinerOption; IsClipSkipEnabled = model.IsClipSkipEnabled; IsExtraNetworksEnabled = model.IsExtraNetworksEnabled; IsModelLoaderSelectionEnabled = model.IsModelLoaderSelectionEnabled; IsClipModelSelectionEnabled = model.IsClipModelSelectionEnabled; if (model.ExtraNetworks is not null) { ExtraNetworksStackCardViewModel.LoadStateFromJsonObject(model.ExtraNetworks); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { if (parameters.ModelName is not { } paramsModelName) return; var currentModels = ClientManager.Models.Concat(ClientManager.UnetModels).ToList(); var currentExtraNetworks = ClientManager.LoraModels.ToList(); HybridModelFile? model; // First try hash match if (parameters.ModelHash is not null) { model = currentModels.FirstOrDefault(m => m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256 && sha256.StartsWith(parameters.ModelHash, StringComparison.InvariantCultureIgnoreCase) ); } else if (parameters.ModelVersionId is not null) { model = currentModels.FirstOrDefault(m => m.Local?.ConnectedModelInfo?.VersionId == parameters.ModelVersionId ); } else { // Name matches model = currentModels.FirstOrDefault(m => m.RelativePath.EndsWith(paramsModelName)); model ??= currentModels.FirstOrDefault(m => m.ShortDisplayName.StartsWith(paramsModelName)); } ExtraNetworksStackCardViewModel.Clear(); if (parameters.ExtraNetworkModelVersionIds is not null) { IsExtraNetworksEnabled = true; foreach (var versionId in parameters.ExtraNetworkModelVersionIds) { var module = ExtraNetworksStackCardViewModel.AddModule(); module.GetCard().SelectedModel = currentExtraNetworks.FirstOrDefault(m => m.Local?.ConnectedModelInfo?.VersionId == versionId ); module.IsEnabled = true; } } if (model is null) return; if (model.Local?.SharedFolderType is SharedFolderType.DiffusionModels) { SelectedUnetModel = model; } else { SelectedModel = model; } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { if (IsStandaloneModelLoader) { return parameters with { ModelName = SelectedUnetModel?.FileName, ModelHash = SelectedUnetModel?.Local?.ConnectedModelInfo?.Hashes.SHA256, }; } return parameters with { ModelName = SelectedModel?.FileName, ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256, }; } partial void OnSelectedModelLoaderChanged(ModelLoader value) { if (value is ModelLoader.Unet) { if (!IsVaeSelectionEnabled) IsVaeSelectionEnabled = true; if (!IsClipModelSelectionEnabled) IsClipModelSelectionEnabled = true; } } partial void OnSelectedModelChanged(HybridModelFile? value) { // Update TabContext with the selected model tabContext.SelectedModel = value; if (!IsExtraNetworksEnabled) return; foreach (var card in ExtraNetworksStackCardViewModel.Cards) { if (card is not LoraModule loraModule) continue; if (loraModule.GetCard() is not { } cardViewModel) continue; cardViewModel.SelectedBaseModel = value; } } partial void OnSelectedUnetModelChanged(HybridModelFile? value) => OnSelectedModelChanged(value); private void SetupStandaloneModelLoader(ModuleApplyStepEventArgs e) { if (SelectedModelLoader is ModelLoader.Unet && IsGguf) { var checkpointLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.UnetLoaderGGUF { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UNETLoader)), UnetName = SelectedUnetModel?.RelativePath ?? throw new ValidationException("Model not selected"), } ); e.Builder.Connections.Base.Model = checkpointLoader.Output; } else { var checkpointLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.UNETLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UNETLoader)), UnetName = SelectedUnetModel?.RelativePath ?? throw new ValidationException("Model not selected"), WeightDtype = SelectedDType ?? "default", } ); e.Builder.Connections.Base.Model = checkpointLoader.Output; } if (SelectedModelLoader is ModelLoader.Unet && IsHiDreamClip) { var modelSamplingSd3 = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ModelSamplingSD3 { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.ModelSamplingSD3)), Model = e.Builder.Connections.Base.Model, Shift = Shift, } ); e.Builder.Connections.Base.Model = modelSamplingSd3.Output; } var vaeLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.VAELoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.VAELoader)), VaeName = SelectedVae?.RelativePath ?? throw new ValidationException("No VAE Selected"), } ); e.Builder.Connections.Base.VAE = vaeLoader.Output; if (SelectedClipType == "flux") { // DualCLIPLoader var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.DualCLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.DualCLIPLoader)), ClipName1 = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), ClipName2 = SelectedClip2?.RelativePath ?? throw new ValidationException("No Clip2 Selected"), Type = SelectedClipType ?? throw new ValidationException("No Clip Type Selected"), } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } else { SetupClipLoaders(e); } } private void SetupDefaultModelLoader(ModuleApplyStepEventArgs e) { // Load base checkpoint var loaderNode = SelectedModelLoader is ModelLoader.Default ? GetDefaultModelLoader( e, "CheckpointLoader_Base", SelectedModel ?? throw new ValidationException("Model not selected") ) : new ComfyNodeBuilder.CheckpointLoaderNF4 { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CheckpointLoaderNF4)), CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected"), }; var baseLoader = e.Nodes.AddTypedNode(loaderNode); e.Builder.Connections.Base.Model = baseLoader.Output1; e.Builder.Connections.Base.VAE = baseLoader.Output3; if (IsClipModelSelectionEnabled) { SetupClipLoaders(e); } else { e.Builder.Connections.Base.Clip = baseLoader.Output2; } // Load refiner if enabled if (IsRefinerSelectionEnabled && SelectedRefiner is { IsNone: false }) { var refinerLoader = e.Nodes.AddTypedNode( GetDefaultModelLoader( e, "CheckpointLoader_Refiner", SelectedRefiner ?? throw new ValidationException("Refiner Model enabled but not selected") ) ); e.Builder.Connections.Refiner.Model = refinerLoader.Output1; e.Builder.Connections.Refiner.Clip = refinerLoader.Output2; e.Builder.Connections.Refiner.VAE = refinerLoader.Output3; } // Load VAE override if enabled if (IsVaeSelectionEnabled && SelectedVae is { IsNone: false, IsDefault: false }) { var vaeLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.VAELoader { Name = "VAELoader", VaeName = SelectedVae?.RelativePath ?? throw new ValidationException("VAE enabled but not selected"), } ); e.Builder.Connections.PrimaryVAE = vaeLoader.Output; } } private void SetupClipLoaders(ModuleApplyStepEventArgs e) { if ( SelectedClip4 is { IsNone: false } && SelectedClip3 is { IsNone: false } && SelectedClip2 is { IsNone: false } && SelectedClip1 is { IsNone: false } ) { var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.QuadrupleCLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.QuadrupleCLIPLoader)), ClipName1 = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), ClipName2 = SelectedClip2?.RelativePath ?? throw new ValidationException("No Clip2 Selected"), ClipName3 = SelectedClip3?.RelativePath ?? throw new ValidationException("No Clip3 Selected"), ClipName4 = SelectedClip4?.RelativePath ?? throw new ValidationException("No Clip4 Selected"), } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } else if ( SelectedClip3 is { IsNone: false } && SelectedClip2 is { IsNone: false } && SelectedClip1 is { IsNone: false } ) { var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.TripleCLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.TripleCLIPLoader)), ClipName1 = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), ClipName2 = SelectedClip2?.RelativePath ?? throw new ValidationException("No Clip2 Selected"), ClipName3 = SelectedClip3?.RelativePath ?? throw new ValidationException("No Clip3 Selected"), } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } else if (SelectedClip2 is { IsNone: false } && SelectedClip1 is { IsNone: false }) { var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.DualCLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.DualCLIPLoader)), ClipName1 = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), ClipName2 = SelectedClip2?.RelativePath ?? throw new ValidationException("No Clip2 Selected"), Type = SelectedClipType ?? throw new ValidationException("No Clip Type Selected"), } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } else if (SelectedClip1 is { IsNone: false }) { var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPLoader() { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPLoader)), ClipName = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), Type = SelectedClipType ?? throw new ValidationException("No Clip Type Selected"), } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } } internal class ModelCardModel { public string? SelectedModelName { get; init; } public string? SelectedRefinerName { get; init; } public string? SelectedVaeName { get; init; } public string? SelectedClip1Name { get; init; } public string? SelectedClip2Name { get; init; } public string? SelectedClip3Name { get; init; } public string? SelectedClip4Name { get; init; } public string? SelectedClipType { get; init; } public ModelLoader ModelLoader { get; init; } public int ClipSkip { get; init; } = 1; public bool IsVaeSelectionEnabled { get; init; } public bool IsRefinerSelectionEnabled { get; init; } public bool IsClipSkipEnabled { get; init; } public bool IsExtraNetworksEnabled { get; init; } public bool IsModelLoaderSelectionEnabled { get; init; } public bool IsClipModelSelectionEnabled { get; init; } public bool ShowRefinerOption { get; init; } public JsonObject? ExtraNetworks { get; init; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/CfzCudnnToggleModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class CfzCudnnToggleModule : ModuleBase { /// public CfzCudnnToggleModule(IServiceManager vmFactory) : base(vmFactory) { Title = "CUDNN Toggle (ComfyUI-Zluda)"; AddCards(vmFactory.Get()); } /// /// Applies CUDNN Toggle node between sampler latent output and VAE decode /// This prevents "GET was unable to find an engine" errors on AMD cards with Zluda /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { // Get the primary connection (can be latent or image) var primary = e.Builder.Connections.Primary; if (primary == null) { return; // No primary connection to process } // Check if primary is a latent (from sampler output) if (primary.IsT0) // T0 is LatentNodeConnection { var card = GetCard(); var latentConnection = primary.AsT0; // Insert CUDNN toggle node between sampler and VAE decode var cudnnToggleOutput = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CUDNNToggleAutoPassthrough { Name = e.Nodes.GetUniqueName("CUDNNToggle"), Model = null, Conditioning = null, Latent = latentConnection, // Pass through the latent from sampler EnableCudnn = !card.DisableCudnn, CudnnBenchmark = false, } ); // Update the primary connection to use the CUDNN toggle latent output (Output3) // This ensures VAE decode receives latent from CUDNN toggle instead of directly from sampler e.Builder.Connections.Primary = cudnnToggleOutput.Output3; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class ControlNetModule : ModuleBase { /// public ControlNetModule(IServiceManager vmFactory) : base(vmFactory) { Title = "ControlNet"; AddCards(vmFactory.Get()); } protected override IEnumerable GetInputImages() { if ( IsEnabled && GetCard().SelectImageCardViewModel is { ImageSource: { } image, IsImageFileNotFound: false } ) { yield return image; } } /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); var image = e.Nodes.AddTypedNode( new ComfyNodeBuilder.LoadImage { Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"), Image = card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference") ?? throw new ValidationException("No ImageSource") } ).Output1; if (card.SelectedPreprocessor is { } preprocessor && preprocessor != ComfyAuxPreprocessor.None) { var aioPreprocessor = e.Nodes.AddTypedNode( new ComfyNodeBuilder.AIOPreprocessor { Name = e.Nodes.GetUniqueName("ControlNet_Preprocessor"), Image = image, Preprocessor = preprocessor.ToString(), // Use lower of width and height for resolution Resolution = Math.Min(card.Width, card.Height) } ); image = aioPreprocessor.Output; } // If ReferenceOnly is selected, use special node if (card.SelectedModel == RemoteModels.ControlNetReferenceOnlyModel) { // We need to rescale image to be the current primary size if it's not already var originalPrimary = e.Temp.Primary!.Unwrap(); var originalPrimarySize = e.Builder.Connections.PrimarySize; if (card.SelectImageCardViewModel.CurrentBitmapSize != originalPrimarySize) { var scaled = e.Builder.Group_Upscale( e.Nodes.GetUniqueName("ControlNet_Rescale"), image, e.Temp.GetDefaultVAE(), ComfyUpscaler.NearestExact, originalPrimarySize.Width, originalPrimarySize.Height ); e.Temp.Primary = scaled; } else { e.Temp.Primary = image; } // Set image as new latent source, add reference only node var model = e.Temp.GetRefinerOrBaseModel(); var controlNetReferenceOnly = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ReferenceOnlySimple { Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly"), Reference = e.Builder.GetPrimaryAsLatent( e.Temp.Primary, e.Builder.Connections.GetDefaultVAE() ), Model = model } ); var referenceOnlyModel = controlNetReferenceOnly.Output1; // If ControlNet strength is not 1, add Model Merge if (Math.Abs(card.Strength - 1) > 0.01) { var modelBlend = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ModelMergeSimple { Name = e.Nodes.GetUniqueName("ControlNet_ReferenceOnly_ModelMerge"), Model1 = referenceOnlyModel, Model2 = e.Temp.GetRefinerOrBaseModel(), // Where 0 is full reference only, 1 is full original Ratio = 1 - card.Strength } ); referenceOnlyModel = modelBlend.Output; } // Set output as new primary and model source if (model == e.Temp.Refiner.Model) { e.Temp.Refiner.Model = referenceOnlyModel; } else { e.Temp.Base.Model = referenceOnlyModel; } e.Temp.Primary = controlNetReferenceOnly.Output2; // Indicate that the Primary latent has been temp batched // https://github.com/comfyanonymous/ComfyUI_experiments/issues/11 e.Temp.IsPrimaryTempBatched = true; // Index 0 is the original image, index 1 is the reference only latent e.Temp.PrimaryTempBatchPickIndex = 1; return; } var controlNetLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ControlNetLoader { Name = e.Nodes.GetUniqueName("ControlNetLoader"), ControlNetName = card.SelectedModel?.RelativePath ?? throw new ValidationException("No SelectedModel"), } ); var controlNetApply = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ControlNetApplyAdvanced { Name = e.Nodes.GetUniqueName("ControlNetApply"), Image = image, ControlNet = controlNetLoader.Output, Positive = e.Temp.Base.Conditioning!.Unwrap().Positive, Negative = e.Temp.Base.Conditioning.Negative, Strength = card.Strength, StartPercent = card.StartPercent, EndPercent = card.EndPercent, } ); e.Temp.Base.Conditioning = (controlNetApply.Output1, controlNetApply.Output2); // Refiner if available if (e.Temp.Refiner.Conditioning is not null) { var controlNetRefinerApply = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ControlNetApplyAdvanced { Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"), Image = image, ControlNet = controlNetLoader.Output, Positive = e.Temp.Refiner.Conditioning!.Unwrap().Positive, Negative = e.Temp.Refiner.Conditioning.Negative, Strength = card.Strength, StartPercent = card.StartPercent, EndPercent = card.EndPercent, } ); e.Temp.Refiner.Conditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/DiscreteModelSamplingModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class DiscreteModelSamplingModule : ModuleBase { public DiscreteModelSamplingModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Discrete Model Sampling"; AddCards(vmFactory.Get()); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var vm = GetCard(); foreach (var modelConnections in e.Builder.Connections.Models.Values) { if (modelConnections.Model is not { } model) continue; var modelSamplingDiscrete = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ModelSamplingDiscrete { Name = e.Nodes.GetUniqueName("ModelSamplingDiscrete"), Model = model, Sampling = vm.SelectedSamplingMethod, Zsnr = vm.IsZsnrEnabled } ); modelConnections.Model = modelSamplingDiscrete.Output; e.Temp.Base.Model = modelSamplingDiscrete.Output; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FaceDetailerModule.cs ================================================ using System; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public partial class FaceDetailerModule : ModuleBase, IValidatableModule { /// public override bool IsSettingsEnabled => true; /// public override IRelayCommand SettingsCommand => OpenSettingsDialogCommand; public FaceDetailerModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Face Detailer"; AddCards(vmFactory.Get()); } [RelayCommand] private async Task OpenSettingsDialog() { var gridVm = VmFactory.Get(vm => { vm.Title = $"{Title} {Resources.Label_Settings}"; vm.SelectedObject = Cards.ToArray(); vm.IncludeCategories = ["Settings"]; }); await gridVm.GetDialog().ShowAsync(); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var faceDetailerCard = GetCard(); if (faceDetailerCard is { InheritSeed: false, SeedCardViewModel.IsRandomizeEnabled: true }) { faceDetailerCard.SeedCardViewModel.GenerateNewSeed(); } var bboxLoader = new ComfyNodeBuilder.UltralyticsDetectorProvider { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UltralyticsDetectorProvider)), ModelName = GetModelName(faceDetailerCard.BboxModel) ?? throw new ArgumentException("No BboxModel"), }; var samplerName = ( faceDetailerCard.IsSamplerSelectionEnabled ? faceDetailerCard.Sampler?.Name : e.Builder.Connections.PrimarySampler?.Name ) ?? throw new ArgumentException("No PrimarySampler"); var schedulerName = ( faceDetailerCard.IsSchedulerSelectionEnabled ? faceDetailerCard.Scheduler?.Name : e.Builder.Connections.PrimaryScheduler?.Name ) ?? throw new ArgumentException("No PrimaryScheduler"); if (schedulerName == "align_your_steps") { if (e.Builder.Connections.PrimaryModelType is null) { throw new ArgumentException("No Model Type for AYS"); } schedulerName = e.Builder.Connections.PrimaryModelType == "SDXL" ? ComfyScheduler.FaceDetailerAlignYourStepsSDXL.Name : ComfyScheduler.FaceDetailerAlignYourStepsSD1.Name; } var cfg = faceDetailerCard.IsCfgScaleEnabled ? faceDetailerCard.Cfg : e.Builder.Connections.PrimaryCfg ?? throw new ArgumentException("No CFG"); var steps = faceDetailerCard.IsStepsEnabled ? faceDetailerCard.Steps : e.Builder.Connections.PrimarySteps ?? throw new ArgumentException("No Steps"); var faceDetailer = new ComfyNodeBuilder.FaceDetailer { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.FaceDetailer)), GuideSize = faceDetailerCard.GuideSize, GuideSizeFor = faceDetailerCard.GuideSizeFor, MaxSize = faceDetailerCard.MaxSize, Seed = faceDetailerCard.InheritSeed ? e.Builder.Connections.Seed : Convert.ToUInt64(faceDetailerCard.SeedCardViewModel.Seed), Steps = steps, Cfg = cfg, SamplerName = samplerName, Scheduler = schedulerName, Denoise = faceDetailerCard.Denoise, Feather = faceDetailerCard.Feather, NoiseMask = faceDetailerCard.NoiseMask, ForceInpaint = faceDetailerCard.ForceInpaint, BboxThreshold = faceDetailerCard.BboxThreshold, BboxDilation = faceDetailerCard.BboxDilation, BboxCropFactor = faceDetailerCard.BboxCropFactor, SamDetectionHint = faceDetailerCard.SamDetectionHint, SamDilation = faceDetailerCard.SamDilation, SamThreshold = faceDetailerCard.SamThreshold, SamBboxExpansion = faceDetailerCard.SamBboxExpansion, SamMaskHintThreshold = faceDetailerCard.SamMaskHintThreshold, SamMaskHintUseNegative = faceDetailerCard.SamMaskHintUseNegative, DropSize = faceDetailerCard.DropSize, Cycle = faceDetailerCard.Cycle, Image = e.Builder.GetPrimaryAsImage(), Model = e.Builder.Connections.GetRefinerOrBaseModel(), Clip = e.Builder.Connections.Base.Clip ?? throw new ArgumentException("No BaseClip"), Vae = e.Builder.Connections.GetDefaultVAE(), Positive = GetPositiveConditioning(faceDetailerCard, e), Negative = GetNegativeConditioning(faceDetailerCard, e), BboxDetector = e.Nodes.AddTypedNode(bboxLoader).Output1, Wildcard = faceDetailerCard.WildcardViewModel.GetPrompt().ProcessedText ?? string.Empty, TiledDecode = faceDetailerCard.UseTiledDecode, TiledEncode = faceDetailerCard.UseTiledEncode, }; var segmModelName = GetModelName(faceDetailerCard.SegmModel); if (!string.IsNullOrWhiteSpace(segmModelName)) { var segmLoader = new ComfyNodeBuilder.UltralyticsDetectorProvider { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UltralyticsDetectorProvider)), ModelName = segmModelName, }; faceDetailer.SegmDetectorOpt = e.Nodes.AddTypedNode(segmLoader).Output2; } var samModelName = GetModelName(faceDetailerCard.SamModel); if (!string.IsNullOrWhiteSpace(samModelName)) { var samLoader = new ComfyNodeBuilder.SamLoader { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.SamLoader)), ModelName = samModelName, DeviceMode = "AUTO", }; faceDetailer.SamModelOpt = e.Nodes.AddTypedNode(samLoader).Output; } e.Builder.Connections.Primary = e.Nodes.AddTypedNode(faceDetailer).Output; } private string? GetModelName(HybridModelFile? model) => model switch { null => null, { FileName: "@none" } => null, { RemoteName: "@none" } => null, { Local: not null } => model.RelativePath.NormalizePathSeparators(), { RemoteName: not null } => model.RemoteName, _ => null, }; private ConditioningNodeConnection GetPositiveConditioning( FaceDetailerViewModel viewModel, ModuleApplyStepEventArgs e ) { if (!viewModel.UseSeparatePrompt) { return e.Builder.Connections.GetRefinerOrBaseConditioning().Positive; } var prompt = viewModel.PromptCardViewModel.GetPrompt(); prompt.Process(); var positiveClip = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPTextEncode { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPTextEncode)), Clip = e.Builder.Connections.Base.Clip!, Text = prompt.ProcessedText, } ); return positiveClip.Output; } private ConditioningNodeConnection GetNegativeConditioning( FaceDetailerViewModel viewModel, ModuleApplyStepEventArgs e ) { if (!viewModel.UseSeparatePrompt) { return e.Builder.Connections.GetRefinerOrBaseConditioning().Negative; } var prompt = viewModel.PromptCardViewModel.GetNegativePrompt(); prompt.Process(); var negativeClip = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPTextEncode { Name = e.Builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPTextEncode)), Clip = e.Builder.Connections.Base.Clip!, Text = prompt.ProcessedText, } ); return negativeClip.Output; } public async Task Validate() { var faceDetailerCard = GetCard(); if (!string.IsNullOrWhiteSpace(GetModelName(faceDetailerCard.BboxModel))) return true; var dialog = DialogHelper.CreateMarkdownDialog( "Please select a BBox Model to continue.", "No Model Selected" ); await dialog.ShowAsync(); return false; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FluxGuidanceModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class FluxGuidanceModule : ModuleBase { public FluxGuidanceModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Use Flux Guidance"; } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FluxHiresFixModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class FluxHiresFixModule(IServiceManager vmFactory) : HiresFixModule(vmFactory) { protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var builder = e.Builder; var upscaleCard = GetCard(); var samplerCard = GetCard(); // Get new latent size var hiresSize = builder.Connections.PrimarySize.WithScale(upscaleCard.Scale); // Select between latent upscale and normal upscale based on the upscale method var selectedUpscaler = upscaleCard.SelectedUpscaler!.Value; // If upscaler selected, upscale latent image first if (selectedUpscaler.Type != ComfyUpscalerType.None) { builder.Connections.Primary = builder.Group_Upscale( builder.Nodes.GetUniqueName("HiresFix"), builder.Connections.Primary.Unwrap(), builder.Connections.GetDefaultVAE(), selectedUpscaler, hiresSize.Width, hiresSize.Height ); } // If we need to inherit primary sampler addons, use their temp args if (samplerCard.InheritPrimarySamplerAddons) { e.Temp = e.Builder.Connections.BaseSamplerTemporaryArgs ?? e.CreateTempFromBuilder(); } else { // otherwise just use new ones e.Temp = e.CreateTempFromBuilder(); } var hiresSampler = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.SamplerCustomAdvanced { Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"), Guider = builder.Connections.PrimaryGuider, Noise = builder.Connections.PrimaryNoise, Sampler = builder.Connections.PrimarySamplerNode, Sigmas = builder.Connections.PrimarySigmas, LatentImage = builder.GetPrimaryAsLatent() } ); // Set as primary builder.Connections.Primary = hiresSampler.Output1; builder.Connections.PrimarySize = hiresSize; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs ================================================ using System.Linq; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class FreeUModule : ModuleBase { /// public FreeUModule(IServiceManager vmFactory) : base(vmFactory) { Title = "FreeU"; AddCards(vmFactory.Get()); } /// /// Applies FreeU to the Model property /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); // Currently applies to all models // TODO: Add option to apply to either base or refiner foreach (var modelConnections in e.Builder.Connections.Models.Values.Where(m => m.Model is not null)) { var freeUOutput = e.Nodes.AddTypedNode( new ComfyNodeBuilder.FreeU { Name = e.Nodes.GetUniqueName($"FreeU_{modelConnections.Name}"), Model = modelConnections.Model!, B1 = card.B1, B2 = card.B2, S1 = card.S1, S2 = card.S2 } ).Output; modelConnections.Model = freeUOutput; e.Temp.Base.Model = freeUOutput; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public partial class HiresFixModule : ModuleBase { /// public override bool IsSettingsEnabled => true; /// public override IRelayCommand SettingsCommand => OpenSettingsDialogCommand; /// public HiresFixModule(IServiceManager vmFactory) : base(vmFactory) { Title = "HiresFix"; AddCards( vmFactory.Get(), vmFactory.Get(vmSampler => { vmSampler.IsDenoiseStrengthEnabled = true; }) ); } [RelayCommand] private async Task OpenSettingsDialog() { var gridVm = VmFactory.Get(vm => { vm.Title = $"{Title} {Resources.Label_Settings}"; vm.SelectedObject = Cards.ToArray(); vm.IncludeCategories = ["Settings"]; }); await gridVm.GetDialog().ShowAsync(); } /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var builder = e.Builder; var upscaleCard = GetCard(); var samplerCard = GetCard(); // Get new latent size var hiresSize = builder.Connections.PrimarySize.WithScale(upscaleCard.Scale); // Select between latent upscale and normal upscale based on the upscale method var selectedUpscaler = upscaleCard.SelectedUpscaler!.Value; // If upscaler selected, upscale latent image first if (selectedUpscaler.Type != ComfyUpscalerType.None) { builder.Connections.Primary = builder.Group_Upscale( builder.Nodes.GetUniqueName("HiresFix"), builder.Connections.Primary.Unwrap(), builder.Connections.GetDefaultVAE(), selectedUpscaler, hiresSize.Width, hiresSize.Height ); } // If we need to inherit primary sampler addons, use their temp args if (samplerCard.InheritPrimarySamplerAddons) { e.Temp = e.Builder.Connections.BaseSamplerTemporaryArgs ?? e.CreateTempFromBuilder(); } else { // otherwise just use new ones e.Temp = e.CreateTempFromBuilder(); } var samplerName = ( samplerCard.IsSamplerSelectionEnabled ? samplerCard.SelectedSampler?.Name : e.Builder.Connections.PrimarySampler?.Name ) ?? throw new ArgumentException("No PrimarySampler"); var schedulerName = ( samplerCard.IsSchedulerSelectionEnabled ? samplerCard.SelectedScheduler?.Name : e.Builder.Connections.PrimaryScheduler?.Name ) ?? throw new ArgumentException("No PrimaryScheduler"); var cfg = samplerCard.IsCfgScaleEnabled ? samplerCard.CfgScale : e.Builder.Connections.PrimaryCfg ?? throw new ArgumentException("No CFG"); if (schedulerName == "align_your_steps") { var samplerSelect = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.KSamplerSelect { Name = builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.KSamplerSelect)), SamplerName = samplerName } ); var alignYourStepsScheduler = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.AlignYourStepsScheduler { Name = builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.AlignYourStepsScheduler)), ModelType = samplerCard.SelectedModelType, Denoise = samplerCard.DenoiseStrength, Steps = samplerCard.Steps } ); var hiresCustomSampler = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.SamplerCustom { Name = builder.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.SamplerCustom)), Model = builder.Connections.GetRefinerOrBaseModel(), AddNoise = true, NoiseSeed = builder.Connections.Seed, Cfg = cfg, Sampler = samplerSelect.Output, Sigmas = alignYourStepsScheduler.Output, Positive = e.Temp.GetRefinerOrBaseConditioning().Positive, Negative = e.Temp.GetRefinerOrBaseConditioning().Negative, LatentImage = builder.GetPrimaryAsLatent(), } ); builder.Connections.Primary = hiresCustomSampler.Output1; } else { var hiresSampler = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.KSampler { Name = builder.Nodes.GetUniqueName("HiresFix_Sampler"), Model = builder.Connections.GetRefinerOrBaseModel(), Seed = builder.Connections.Seed, Steps = samplerCard.Steps, Cfg = cfg, SamplerName = samplerName ?? throw new ArgumentException("No PrimarySampler"), Scheduler = schedulerName ?? throw new ArgumentException("No PrimaryScheduler"), Positive = e.Temp.GetRefinerOrBaseConditioning().Positive, Negative = e.Temp.GetRefinerOrBaseConditioning().Negative, LatentImage = builder.GetPrimaryAsLatent(), Denoise = samplerCard.DenoiseStrength } ); // Set as primary builder.Connections.Primary = hiresSampler.Output; } builder.Connections.PrimarySize = hiresSize; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LayerDiffuseModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class LayerDiffuseModule : ModuleBase { /// public LayerDiffuseModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Layer Diffuse"; AddCards(vmFactory.Get()); } /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); card.ApplyStep(e); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/LoraModule.cs ================================================ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public partial class LoraModule : ModuleBase { /// public override bool IsSettingsEnabled => true; /// public override IRelayCommand SettingsCommand => OpenSettingsDialogCommand; public LoraModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Lora"; var extraNetworksVm = vmFactory.Get(card => { card.IsModelWeightEnabled = true; // Disable clip weight by default, but allow user to enable it card.IsClipWeightToggleEnabled = true; card.IsClipWeightEnabled = false; }); AddCards(extraNetworksVm); AddDisposable( extraNetworksVm .WhenPropertyChanged(vm => vm.SelectedModel) .Throttle(TimeSpan.FromMilliseconds(50)) .Subscribe(next => { var model = next.Value; if (model is null) { Title = Resources.Label_ExtraNetworks; return; } if (model.Local?.HasConnectedModel ?? false) { Title = model.Local.ConnectedModelInfo.ModelName; } else { Title = model.ShortDisplayName; } }) ); } [RelayCommand] private async Task OpenSettingsDialog() { var gridVm = VmFactory.Get(vm => { vm.Title = $"{Title} {Resources.Label_Settings}"; vm.SelectedObject = Cards.ToArray(); vm.IncludeCategories = ["Settings"]; }); await gridVm.GetDialog().ShowAsync(); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); // Skip if no lora model if (card.SelectedModel is not { } selectedLoraModel) return; // Add lora conditioning to all models foreach (var modelConnections in e.Builder.Connections.Models.Values) { if (modelConnections.Model is not { } model || modelConnections.Clip is not { } clip) continue; var loraLoader = e.Nodes.AddNamedNode( ComfyNodeBuilder.LoraLoader( e.Nodes.GetUniqueName($"Loras_{modelConnections.Name}"), model, clip, selectedLoraModel.RelativePath, card.ModelWeight, card.ClipWeight ) ); // Replace current model and clip with lora loaded model and clip modelConnections.Model = loraLoader.Output1; modelConnections.Clip = loraLoader.Output2; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ModuleBase.cs ================================================ using System.Collections.Generic; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; public abstract class ModuleBase : StackExpanderViewModel, IComfyStep, IInputImageProvider { protected readonly IServiceManager VmFactory; /// protected ModuleBase(IServiceManager vmFactory) : base(vmFactory) { VmFactory = vmFactory; } /// public void ApplyStep(ModuleApplyStepEventArgs e) { if (e.IsEnabledOverrides.TryGetValue(GetType(), out var isEnabledOverride)) { if (isEnabledOverride) { OnApplyStep(e); } return; } if (!IsEnabled) { return; } OnApplyStep(e); } protected abstract void OnApplyStep(ModuleApplyStepEventArgs e); /// IEnumerable IInputImageProvider.GetInputImages() => GetInputImages(); protected virtual IEnumerable GetInputImages() { yield break; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/NRSModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class NRSModule : ModuleBase { /// public NRSModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Negative Rejection Steering (NRS)"; AddCards(vmFactory.Get()); } /// /// Applies NRS to the Model property /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); // Currently applies to all models // TODO: Add option to apply to either base or refiner foreach (var modelConnections in e.Builder.Connections.Models.Values.Where(m => m.Model is not null)) { var nrsOutput = e .Nodes.AddTypedNode( new ComfyNodeBuilder.NRS { Name = e.Nodes.GetUniqueName($"NRS_{modelConnections.Name}"), Model = modelConnections.Model!, Skew = card.Skew, Stretch = card.Stretch, Squash = card.Squash, } ) .Output; modelConnections.Model = nrsOutput; e.Temp.Base.Model = nrsOutput; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/PlasmaNoiseModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class PlasmaNoiseModule : ModuleBase { public PlasmaNoiseModule(ServiceManager vmFactory) : base(vmFactory) { Title = "Plasma Noise"; AddCards(vmFactory.Get()); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { // handled elsewhere } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/PromptExpansionModule.cs ================================================ using System; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class PromptExpansionModule : ModuleBase { public PromptExpansionModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Prompt Expansion"; AddCards(vmFactory.Get()); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var promptExpansionCard = GetCard(); var model = promptExpansionCard.SelectedModel ?? throw new InvalidOperationException($"{Title}: Model not selected"); e.Builder.Connections.PositivePrompt = e.Nodes.AddTypedNode( new ComfyNodeBuilder.PromptExpansion { Name = e.Nodes.GetUniqueName("PromptExpansion_Positive"), ModelName = model.RelativePath, Text = e.Builder.Connections.PositivePrompt, Seed = e.Builder.Connections.Seed, LogPrompt = promptExpansionCard.IsLogOutputEnabled } ).Output; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/RescaleCfgModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class RescaleCfgModule : ModuleBase { public RescaleCfgModule(IServiceManager vmFactory) : base(vmFactory) { Title = "CFG Rescale"; AddCards(vmFactory.Get()); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var vm = GetCard(); foreach (var modelConnections in e.Builder.Connections.Models.Values) { if (modelConnections.Model is not { } model) continue; var rescaleCfg = e.Nodes.AddTypedNode( new ComfyNodeBuilder.RescaleCFG { Name = e.Nodes.GetUniqueName("RescaleCFG"), Model = model, Multiplier = vm.Multiplier } ); modelConnections.Model = rescaleCfg.Output; switch (modelConnections.Name) { case "Base": e.Temp.Base.Model = rescaleCfg.Output; break; case "Refiner": e.Temp.Refiner.Model = rescaleCfg.Output; break; } } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/SaveImageModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class SaveImageModule : ModuleBase { /// public SaveImageModule(IServiceManager vmFactory) : base(vmFactory) { Title = Resources.Label_SaveIntermediateImage; } /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var preview = e.Builder.Nodes.AddTypedNode( new ComfyNodeBuilder.PreviewImage { Name = e.Builder.Nodes.GetUniqueName("SaveIntermediateImage"), Images = e.Builder.GetPrimaryAsImage() } ); e.Builder.Connections.OutputNodes.Add(preview); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/TiledVAEModule.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class TiledVAEModule : ModuleBase { public TiledVAEModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Tiled VAE Decode"; AddCards(vmFactory.Get()); } protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); // Register a pre-output action that replaces standard VAE decode with tiled decode e.PreOutputActions.Add(args => { var builder = args.Builder; // Only apply if primary is in latent space if (builder.Connections.Primary?.IsT0 != true) return; var latent = builder.Connections.Primary.AsT0; var vae = builder.Connections.GetDefaultVAE(); // Use tiled VAE decode instead of standard decode var tiledDecode = builder.Nodes.AddTypedNode( new ComfyNodeBuilder.TiledVAEDecode { Name = builder.Nodes.GetUniqueName("TiledVAEDecode"), Samples = latent, Vae = vae, TileSize = card.TileSize, Overlap = card.Overlap, TemporalSize = card.TemporalSize, TemporalOverlap = card.TemporalOverlap } ); // Update primary connection to the decoded image builder.Connections.Primary = tiledDecode.Output; }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Modules/UpscalerModule.cs ================================================ using System; using System.ComponentModel.DataAnnotations; using Injectio.Attributes; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules; [ManagedService] [RegisterTransient] public class UpscalerModule : ModuleBase { /// public UpscalerModule(IServiceManager vmFactory) : base(vmFactory) { Title = "Upscaler"; AddCards(vmFactory.Get()); } /// protected override void OnApplyStep(ModuleApplyStepEventArgs e) { var card = GetCard(); // Skip if scale is close to 1 if (Math.Abs(card.Scale - 1) < 0.005) { return; } var upscaleSize = e.Builder.Connections.PrimarySize.WithScale(card.Scale); var upscaleResult = e.Builder.Group_Upscale( e.Builder.Nodes.GetUniqueName("PostUpscale"), e.Builder.Connections.Primary ?? throw new ArgumentException("No Primary"), e.Builder.Connections.GetDefaultVAE(), card.SelectedUpscaler ?? throw new ValidationException("Upscaler is required"), upscaleSize.Width, upscaleSize.Height ); e.Builder.Connections.Primary = upscaleResult; e.Builder.Connections.PrimarySize = upscaleSize; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/NrsCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(NrsCard))] [ManagedService] [RegisterTransient] public partial class NrsCardViewModel : LoadableViewModelBase { public const string ModuleKey = "NRS"; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(-30.0d, 30.0d)] public partial double Skew { get; set; } = 4; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(-30.0d, 30.0d)] public partial double Stretch { get; set; } = 2; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0d, 1d)] public partial double Squash { get; set; } = 0; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/PlasmaNoiseCardViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PlasmaNoiseCard))] [ManagedService] [RegisterTransient] public partial class PlasmaNoiseCardViewModel : LoadableViewModelBase { public const string ModuleKey = "PlasmaNoise"; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowPlasmaTurbulence))] private NoiseType selectedNoiseType = NoiseType.Plasma; [ObservableProperty] private double plasmaTurbulence = 2.75; [ObservableProperty] private int valueMin = -1; [ObservableProperty] private int valueMax = -1; [ObservableProperty] private bool isPerChannelClampingEnabled; [ObservableProperty] private bool isPlasmaSamplerEnabled; [ObservableProperty] private int redMin = -1; [ObservableProperty] private int redMax = -1; [ObservableProperty] private int greenMin = -1; [ObservableProperty] private int greenMax = -1; [ObservableProperty] private int blueMin = -1; [ObservableProperty] private int blueMax = -1; [ObservableProperty] private double plasmaSamplerLatentNoise = 0.05; public List NoiseTypes => Enum.GetValues().ToList(); public bool ShowPlasmaTurbulence => SelectedNoiseType == NoiseType.Plasma; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs ================================================ using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; using AvaloniaEdit; using AvaloniaEdit.Document; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using Injectio.Attributes; using Microsoft.Extensions.Logging; using OpenIddict.Client; using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api.PromptGenApi; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Models.PromptSyntax; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using YamlDotNet.Core.Tokens; using Prompt = StabilityMatrix.Avalonia.Models.Inference.Prompt; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PromptCard))] [ManagedService] [RegisterTransient] public partial class PromptCardViewModel : DisposableLoadableViewModelBase, IParametersLoadableState, IComfyStep { private readonly IModelIndexService modelIndexService; private readonly ISettingsManager settingsManager; private readonly TabContext tabContext; private readonly IPromptGenApi promptGenApi; private readonly INotificationService notificationService; private readonly ILogger logger; private readonly IAccountsService accountsService; private readonly IServiceManager vmFactory; /// /// Cache of prompt text to tokenized Prompt /// private LRUCache PromptCache { get; } = new(4); public ICompletionProvider CompletionProvider { get; } public ITokenizerProvider TokenizerProvider { get; } public SharedState SharedState { get; } public TextDocument PromptDocument { get; } = new(); public TextDocument NegativePromptDocument { get; } = new(); public StackEditableCardViewModel ModulesCardViewModel { get; } [ObservableProperty] private bool isAutoCompletionEnabled; [ObservableProperty] private bool isHelpButtonTeachingTipOpen; [ObservableProperty] private bool isPromptAmplifyTeachingTipOpen; [ObservableProperty] private bool isNegativePromptEnabled = true; [ObservableProperty] private bool isThinkingEnabled; [ObservableProperty] private bool isFocused; [ObservableProperty] private bool isBalanced = true; [ObservableProperty] private bool isImaginative; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowLowTokenWarning), nameof(LowTokenWarningText))] private int tokensRemaining = -1; [ObservableProperty] private bool isFlyoutOpen; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowLowTokenWarning))] private int lowTokenThreshold = 25; [ObservableProperty] public partial bool IsStackCardEnabled { get; set; } = true; public bool ShowLowTokenWarning => TokensRemaining <= LowTokenThreshold && TokensRemaining >= 0; public string LowTokenWarningText => $"{TokensRemaining} amplification{(TokensRemaining == 1 ? "" : "s")} remaining (resets in {Utilities.GetNumDaysTilBeginningOfNextMonth()} days)"; /// public PromptCardViewModel( ICompletionProvider completionProvider, ITokenizerProvider tokenizerProvider, ISettingsManager settingsManager, IModelIndexService modelIndexService, IServiceManager vmFactory, IPromptGenApi promptGenApi, INotificationService notificationService, ILogger logger, IAccountsService accountsService, SharedState sharedState, TabContext tabContext ) { this.modelIndexService = modelIndexService; this.settingsManager = settingsManager; this.tabContext = tabContext; this.promptGenApi = promptGenApi; this.notificationService = notificationService; this.logger = logger; this.accountsService = accountsService; this.vmFactory = vmFactory; CompletionProvider = completionProvider; TokenizerProvider = tokenizerProvider; SharedState = sharedState; // Subscribe to tab context state changes tabContext.StateChanged += OnTabContextStateChanged; ModulesCardViewModel = vmFactory.Get(vm => { vm.Title = "Styles"; vm.AvailableModules = [typeof(PromptExpansionModule)]; }); AddDisposable( settingsManager.RelayPropertyFor( this, vm => vm.IsAutoCompletionEnabled, settings => settings.IsPromptCompletionEnabled, true ) ); } private void OnTabContextStateChanged(object? sender, TabContext.TabStateChangedEventArgs e) { if (e.PropertyName == nameof(TabContext.SelectedModel)) { // Handle selected model change // Could use this to update prompt suggestions based on the model } } public override void OnUnloaded() { base.OnUnloaded(); // Unsubscribe from events when view model is unloaded tabContext.StateChanged -= OnTabContextStateChanged; } partial void OnIsHelpButtonTeachingTipOpenChanging(bool oldValue, bool newValue) { // If the teaching tip is being closed, save the setting if (oldValue && !newValue) { settingsManager.Transaction(settings => { settings.SeenTeachingTips.Add(TeachingTip.InferencePromptHelpButtonTip); }); if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.InferencePromptAmplifyTip)) { IsPromptAmplifyTeachingTipOpen = true; } } } partial void OnIsPromptAmplifyTeachingTipOpenChanging(bool oldValue, bool newValue) { if (oldValue && !newValue) { settingsManager.Transaction(settings => { settings.SeenTeachingTips.Add(TeachingTip.InferencePromptAmplifyTip); }); } } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); // Show teaching tip for help button if not seen if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.InferencePromptHelpButtonTip)) { IsHelpButtonTeachingTipOpen = true; } if ( !IsHelpButtonTeachingTipOpen && !settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.InferencePromptAmplifyTip) ) { IsPromptAmplifyTeachingTipOpen = true; } } protected override Task OnInitialLoadedAsync() { Task.Run(async () => { try { var isLoggedIn = await accountsService.HasStoredLykosAccountAsync(); if (!isLoggedIn) { return; } SetTokenThreshold(); } catch (Exception ex) { logger.LogError(ex, "Error refreshing account data"); } try { var result = await promptGenApi.AccountMeTokens(); TokensRemaining = result.Available; } catch (ApiException e) { if ( e.StatusCode != HttpStatusCode.Unauthorized && e.StatusCode != HttpStatusCode.NotFound ) { notificationService.Show( "Error retrieving prompt amplifier data", e.Message, NotificationType.Error ); return; } TokensRemaining = -1; } }) .SafeFireAndForget(onException: ex => logger.LogError(ex, "Error getting prompt amplifier data")); return Task.CompletedTask; } private void SetTokenThreshold() { if (accountsService.LykosStatus is not { User: not null } status) return; if (status.User.Roles.Count is 1 && status.User.Roles.Contains(LykosRole.Basic.ToString())) { LowTokenThreshold = 25; } } /// /// Applies the prompt step. /// Requires: /// /// - Model, Clip /// /// Provides: /// /// - Conditioning /// /// public void ApplyStep(ModuleApplyStepEventArgs e) { // Load prompts var positivePrompt = GetPrompt(); positivePrompt.Process(); e.Builder.Connections.PositivePrompt = positivePrompt.ProcessedText; var negativePrompt = GetNegativePrompt(); negativePrompt.Process(); e.Builder.Connections.NegativePrompt = negativePrompt.ProcessedText; // Apply modules / styles that may modify the prompt ModulesCardViewModel.ApplyStep(e); foreach (var modelConnections in e.Builder.Connections.Models.Values) { if (modelConnections.Model is not { } model || modelConnections.Clip is not { } clip) continue; // If need to load loras, add a group if (positivePrompt.ExtraNetworks.Count > 0) { var loras = positivePrompt.GetExtraNetworksAsLocalModels(modelIndexService).ToList(); // Add group to load loras onto model and clip in series var lorasGroup = e.Builder.Group_LoraLoadMany( $"Loras_{modelConnections.Name}", model, clip, loras ); // Set last outputs as model and clip modelConnections.Model = lorasGroup.Output1; modelConnections.Clip = lorasGroup.Output2; } // Clips var positiveClip = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPTextEncode { Name = $"PositiveCLIP_{modelConnections.Name}", Clip = e.Builder.Connections.Base.Clip!, Text = e.Builder.Connections.PositivePrompt, } ); var negativeClip = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPTextEncode { Name = $"NegativeCLIP_{modelConnections.Name}", Clip = e.Builder.Connections.Base.Clip!, Text = e.Builder.Connections.NegativePrompt, } ); // Set conditioning from Clips modelConnections.Conditioning = (positiveClip.Output, negativeClip.Output); } } /// /// Gets the tokenized Prompt for given text and caches it /// private Prompt GetOrCachePrompt(string text) { // Try get from cache if (PromptCache.Get(text, out var cachedPrompt)) { return cachedPrompt!; } var prompt = Prompt.FromRawText(text, TokenizerProvider); PromptCache.Add(text, prompt); return prompt; } /// /// Processes current positive prompt text into a Prompt object /// public Prompt GetPrompt() => GetOrCachePrompt(PromptDocument.Text); /// /// Processes current negative prompt text into a Prompt object /// public Prompt GetNegativePrompt() => GetOrCachePrompt(NegativePromptDocument.Text); /// /// Validates both prompts, shows an error dialog if invalid /// public async Task ValidatePrompts() { var promptText = PromptDocument.Text; var negPromptText = NegativePromptDocument.Text; try { var prompt = GetOrCachePrompt(promptText); prompt.Process(processWildcards: false); prompt.ValidateExtraNetworks(modelIndexService); } catch (PromptError e) { var dialog = DialogHelper.CreatePromptErrorDialog(e, promptText, modelIndexService); await dialog.ShowAsync(); return false; } try { var negPrompt = GetOrCachePrompt(negPromptText); negPrompt.Process(); } catch (PromptError e) { var dialog = DialogHelper.CreatePromptErrorDialog(e, negPromptText, modelIndexService); await dialog.ShowAsync(); return false; } return true; } [RelayCommand] private async Task ShowHelpDialog() { var md = $$""" ## {{Resources.Label_Emphasis}} You can also use (`Ctrl+Up`/`Ctrl+Down`) in the editor to adjust the weight emphasis of the token under the caret or the currently selected text. ```prompt (keyword) (keyword:1.0) ``` ## {{Resources.Label_Deemphasis}} ```prompt [keyword] ``` ## {{Resources.Label_EmbeddingsOrTextualInversion}} They may be used in either the positive or negative prompts. Essentially they are text presets, so the position where you place them could make a difference. ```prompt ``` ## {{Resources.Label_NetworksLoraOrLycoris}} Unlike embeddings, network tags do not get tokenized to the model, so the position in the prompt where you place them does not matter. ```prompt ``` ## {{Resources.Label_Comments}} Inline comments can be marked by a hashtag ' # '. All text after a ' # ' on a line will be disregarded during generation. ```prompt # comments a red cat # also comments detailed ``` ## {{Resources.Label_Wildcards}} Wildcards can be used to select a random value from a list of options. ```prompt {red|green|blue} cat ``` In this example, a color will be randomly chosen at the start of each generation. The final output could be "red cat", "green cat", or "blue cat". You can also use networks and embeddings in wildcards. For example: ```prompt {|} cat ``` """; var dialog = DialogHelper.CreateMarkdownDialog(md, "Prompt Syntax", TextEditorPreset.Prompt); dialog.MinDialogWidth = 800; dialog.MaxDialogHeight = 1000; dialog.MaxDialogWidth = 1000; await dialog.ShowAsync(); } [RelayCommand] private async Task DebugShowTokens() { var prompt = GetPrompt(); try { prompt.Process(); } catch (PromptError e) { await DialogHelper.CreatePromptErrorDialog(e, prompt.RawText, modelIndexService).ShowAsync(); return; } var tokens = prompt.TokenizeResult.Tokens; var builder = new StringBuilder(); try { var astBuilder = new PromptSyntaxBuilder(prompt.TokenizeResult, prompt.RawText); var ast = astBuilder.BuildAST(); builder.AppendLine("## AST:"); builder.AppendLine("```csharp"); builder.AppendLine(ast.ToDebugString()); builder.AppendLine("```"); } catch (PromptError e) { builder.AppendLine($"## AST (Error)"); builder.AppendLine($"({e.GetType().Name} - {e.Message})"); builder.AppendLine("```csharp"); builder.AppendLine(e.StackTrace); builder.AppendLine("```"); throw; } builder.AppendLine($"## Tokens ({tokens.Length}):"); builder.AppendLine("```csharp"); builder.AppendLine(prompt.GetDebugText()); builder.AppendLine("```"); try { if (prompt.ExtraNetworks is { } networks) { builder.AppendLine($"## Networks ({networks.Count}):"); builder.AppendLine("```csharp"); builder.AppendLine( JsonSerializer.Serialize(networks, new JsonSerializerOptions() { WriteIndented = true }) ); builder.AppendLine("```"); } builder.AppendLine("## Formatted for server:"); builder.AppendLine("```csharp"); builder.AppendLine(prompt.ProcessedText); builder.AppendLine("```"); } catch (PromptError e) { builder.AppendLine($"##{e.GetType().Name} - {e.Message}"); builder.AppendLine("```csharp"); builder.AppendLine(e.StackTrace); builder.AppendLine("```"); throw; } var dialog = DialogHelper.CreateMarkdownDialog(builder.ToString(), "Prompt Tokens"); dialog.MinDialogWidth = 800; dialog.MaxDialogHeight = 1000; dialog.MaxDialogWidth = 1000; await dialog.ShowAsync(); } [RelayCommand] private void EditorCopy(TextEditor? textEditor) { textEditor?.Copy(); } [RelayCommand] private void EditorPaste(TextEditor? textEditor) { textEditor?.Paste(); } [RelayCommand] private void EditorCut(TextEditor? textEditor) { textEditor?.Cut(); } [RelayCommand] private async Task AmplifyPrompt() { if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.PromptAmplifyDisclaimer)) { var dialog = DialogHelper.CreateMarkdownDialog(Resources.PromptAmplifier_Disclaimer); dialog.PrimaryButtonText = "Continue"; dialog.CloseButtonText = "Back"; dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { settingsManager.Transaction(settings => { settings.SeenTeachingTips.Add(TeachingTip.PromptAmplifyDisclaimer); }); } else { return; } } var valid = await ValidatePrompts(); if (!valid) return; var prompt = GetPrompt(); if (string.IsNullOrWhiteSpace(prompt.RawText)) { notificationService.Show("Prompt Amplifier Error", "Prompt is empty", NotificationType.Error); return; } var negativePrompt = GetNegativePrompt(); var selectedModel = tabContext.SelectedModel; var modelTags = selectedModel?.Local?.ConnectedModelInfo?.BaseModel?.ToLower() switch { { } baseModel when baseModel.Contains("flux") => new List { ModelTags.Flux }, { } baseModel when baseModel.Contains("sdxl") => [ModelTags.Sdxl], "pony" => [ModelTags.Pony], "noobai" => [ModelTags.Illustrious], "illustrious" => [ModelTags.Illustrious], _ => [], }; var mode = IsFocused ? PromptExpansionRequestMode.Focused : IsImaginative ? PromptExpansionRequestMode.Imaginative : PromptExpansionRequestMode.Balanced; try { var expandedPrompt = await promptGenApi.ExpandPrompt( new PromptExpansionRequest { Prompt = new PromptToEnhance { PositivePrompt = prompt.ProcessedText ?? prompt.RawText, NegativePrompt = negativePrompt.ProcessedText ?? negativePrompt.RawText, Model = selectedModel?.Local?.DisplayModelName, }, Model = IsThinkingEnabled ? "PromptV1ThinkingDev" : "PromptV1Dev", Mode = mode, ModelTags = modelTags, } ); PromptDocument.Text = expandedPrompt.Response.PositivePrompt; NegativePromptDocument.Text = expandedPrompt.Response.NegativePrompt; TokensRemaining = expandedPrompt.AvailableTokens; } catch (ApiException e) { logger.LogError(e, "Error amplifying prompt"); switch (e.StatusCode) { case HttpStatusCode.PaymentRequired: { var dialog = DialogHelper.CreateMarkdownDialog( $"You have no Prompt Amplifier usage left this month. Usage resets on the 1st of each month. ({Utilities.GetNumDaysTilBeginningOfNextMonth()} days left)", "Rate Limit Reached" ); dialog.PrimaryButtonText = "Upgrade"; dialog.PrimaryButtonCommand = new RelayCommand(() => ProcessRunner.OpenUrl("https://patreon.com/join/StabilityMatrix") ); dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; await dialog.ShowAsync(); break; } case HttpStatusCode.BadRequest: notificationService.Show( "Moderation Error", "Your prompt was flagged by the moderation system. Please try again with a different prompt.", NotificationType.Error ); break; case HttpStatusCode.Unauthorized: if (await ShowLoginDialog()) { await AmplifyPrompt(); } else { notificationService.Show( "Prompt Amplifier Error", "You need to be logged in to use this feature.", NotificationType.Error ); } break; default: notificationService.Show( "Prompt Amplifier Error", "There was an error processing your request.", NotificationType.Error ); break; } } catch (Exception e) { logger.LogError(e, "Error amplifying prompt"); notificationService.Show("Prompt Amplifier Error", e.Message, NotificationType.Error); } } [RelayCommand] private Task ShowAmplifierDisclaimer() => DialogHelper.CreateMarkdownDialog(Resources.PromptAmplifier_Disclaimer).ShowAsync(); partial void OnIsBalancedChanged(bool value) { switch (value) { case false when !IsFocused && !IsImaginative: IsBalanced = true; return; case false: return; default: IsFocused = false; IsImaginative = false; break; } } partial void OnIsFocusedChanged(bool value) { if (!value) return; IsBalanced = false; IsImaginative = false; } partial void OnIsImaginativeChanged(bool value) { if (!value) return; IsBalanced = false; IsFocused = false; } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel( new PromptCardModel { Prompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text, ModulesCardState = ModulesCardViewModel.SaveStateToJsonObject(), } ); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); PromptDocument.Text = model.Prompt ?? ""; NegativePromptDocument.Text = model.NegativePrompt ?? ""; if (model.ModulesCardState is not null) { ModulesCardViewModel.LoadStateFromJsonObject(model.ModulesCardState); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { PromptDocument.Text = parameters.PositivePrompt ?? ""; NegativePromptDocument.Text = parameters.NegativePrompt ?? ""; } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { return parameters with { PositivePrompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text, }; } private async Task ShowLoginDialog() { var dialog = DialogHelper.CreateTaskDialog( "Lykos Account Required", "You need to be logged in to use this feature. Please log in to your Lykos account." ); dialog.Buttons = [ new TaskDialogButton(Resources.Action_Login, TaskDialogStandardResult.OK), TaskDialogButton.CloseButton, ]; if (await dialog.ShowAsync(true) is not TaskDialogStandardResult.OK) return false; var vm = vmFactory.Get(); vm.ChallengeRequest = new OpenIddictClientModels.DeviceChallengeRequest { ProviderName = OpenIdClientConstants.LykosAccount.ProviderName, }; await vm.ShowDialogAsync(); if (vm.AuthenticationResult is not { } result) return false; await accountsService.LykosAccountV2LoginAsync( new LykosAccountV2Tokens(result.AccessToken, result.RefreshToken, result.IdentityToken) ); var tokens = await promptGenApi.AccountMeTokens(); TokensRemaining = tokens.Available; SetTokenThreshold(); return true; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/PromptExpansionCardViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PromptExpansionCard))] [ManagedService] [RegisterTransient] public partial class PromptExpansionCardViewModel(IInferenceClientManager clientManager) : LoadableViewModelBase { public const string ModuleKey = "PromptExpansion"; public IInferenceClientManager ClientManager { get; } = clientManager; [ObservableProperty] private HybridModelFile? selectedModel; [ObservableProperty] private bool isLogOutputEnabled = true; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/RescaleCfgCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(RescaleCfgCard))] [ManagedService] [RegisterTransient] public partial class RescaleCfgCardViewModel : LoadableViewModelBase { public const string ModuleKey = "RescaleCFG"; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0d, 1d)] private double multiplier = 0.7d; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs ================================================ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Services; using Size = System.Drawing.Size; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SamplerCard))] [ManagedService] [RegisterTransient] public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep { private ISettingsManager settingsManager; private readonly TabContext tabContext; public const string ModuleKey = "Sampler"; [ObservableProperty] private bool isRefinerStepsEnabled; [ObservableProperty] private int steps = 20; [ObservableProperty] private int refinerSteps = 10; [ObservableProperty] private bool isDenoiseStrengthEnabled; /// /// Temporary enable for denoise strength, used for SDTurbo. /// Denoise will be enabled if either this or is true. /// public bool IsDenoiseStrengthTempEnabled => SelectedScheduler == ComfyScheduler.SDTurbo; [ObservableProperty] private double denoiseStrength = 0.7f; [ObservableProperty] [property: Category("Settings")] [property: DisplayName("CFG Scale Selection")] private bool isCfgScaleEnabled; [ObservableProperty] private double cfgScale = 5; [ObservableProperty] private bool isDimensionsEnabled; [ObservableProperty] private int width = 1024; [ObservableProperty] private int height = 1024; [ObservableProperty] [property: Category("Settings")] [property: DisplayName("Sampler Selection")] private bool isSamplerSelectionEnabled; [ObservableProperty] [Required] private ComfySampler? selectedSampler = ComfySampler.EulerAncestral; [ObservableProperty] [property: Category("Settings")] [property: DisplayName("Scheduler Selection")] private bool isSchedulerSelectionEnabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDenoiseStrengthTempEnabled), nameof(IsModelTypeSelectionEnabled))] [Required] private ComfyScheduler? selectedScheduler = ComfyScheduler.Normal; [ObservableProperty] [property: Category("Settings")] [property: DisplayName("Inherit Primary Sampler Addons")] private bool inheritPrimarySamplerAddons = true; [ObservableProperty] private bool enableAddons = true; [ObservableProperty] private string selectedModelType = "SDXL"; [ObservableProperty] private bool isLengthEnabled; [ObservableProperty] private int length; [ObservableProperty] public partial List AvailableResolutions { get; set; } [ObservableProperty] public partial Dictionary> GroupedResolutionsByAspectRatio { get; set; } = new(); [ObservableProperty] public partial int DimensionStepChange { get; set; } [JsonPropertyName("Modules")] public StackEditableCardViewModel ModulesCardViewModel { get; } [JsonIgnore] public bool IsModelTypeSelectionEnabled => SelectedScheduler?.Name == ComfyScheduler.AlignYourSteps.Name; [JsonIgnore] public List ModelTypes => ["SD1", "SDXL"]; [JsonIgnore] public IInferenceClientManager ClientManager { get; } private int TotalSteps => Steps + RefinerSteps; public SamplerCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory, ISettingsManager settingsManager, TabContext tabContext ) { this.settingsManager = settingsManager; this.tabContext = tabContext; ClientManager = clientManager; ModulesCardViewModel = vmFactory.Get(modulesCard => { modulesCard.Title = Resources.Label_Addons; modulesCard.AvailableModules = [ typeof(FreeUModule), typeof(ControlNetModule), typeof(LayerDiffuseModule), typeof(FluxGuidanceModule), typeof(DiscreteModelSamplingModule), typeof(RescaleCfgModule), typeof(PlasmaNoiseModule), typeof(NRSModule), typeof(TiledVAEModule), ]; }); } public override void OnLoaded() { base.OnLoaded(); DimensionStepChange = settingsManager.Settings.InferenceDimensionStepChange; AvailableResolutions = settingsManager.Settings.SavedInferenceDimensions.ToList(); LoadAvailableResolutions(); tabContext.StateChanged += TabContextOnStateChanged; } public override void OnUnloaded() { base.OnUnloaded(); tabContext.StateChanged -= TabContextOnStateChanged; } private void TabContextOnStateChanged(object? sender, TabContext.TabStateChangedEventArgs e) { if (e.PropertyName != nameof(tabContext.SelectedModel)) return; if (tabContext.SelectedModel?.Local?.ConnectedModelInfo?.InferenceDefaults is not { } defaults) return; Width = defaults.Width; Height = defaults.Height; Steps = defaults.Steps; CfgScale = defaults.CfgScale; SelectedSampler = defaults.Sampler; SelectedScheduler = defaults.Scheduler; } [RelayCommand] private void SwapDimensions() { (Width, Height) = (Height, Width); } [RelayCommand] private void SetResolution(string resolution) { if (string.IsNullOrWhiteSpace(resolution)) { return; } // split on 'x' or 'X' var parts = resolution .ToLowerInvariant() .Split('x', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length != 2 || !int.TryParse(parts[0], out var w) || !int.TryParse(parts[1], out var h)) { return; } Width = w; Height = h; } [RelayCommand] private void SaveDimensionsToFavorites() { var dimension = $"{Width}x{Height}"; var dimensionWithSpace = $"{Width} x {Height}"; // Check if already exists if ( settingsManager.Settings.SavedInferenceDimensions.Any(d => d == dimension || d == dimensionWithSpace ) ) { return; } // Add to favorites settingsManager.Transaction(s => s.SavedInferenceDimensions.Add(dimensionWithSpace)); var orientation = Width > Height ? "Landscape" : Width < Height ? "Portrait" : "Square"; GroupedResolutionsByAspectRatio[orientation].Add(dimensionWithSpace); } /// public virtual void ApplyStep(ModuleApplyStepEventArgs e) { // Resample the current primary if size does not match the selected size if ( e.Builder.Connections.PrimarySize.Width != Width || e.Builder.Connections.PrimarySize.Height != Height ) { e.Builder.Connections.Primary = e.Builder.Group_Upscale( e.Nodes.GetUniqueName("Sampler_ScalePrimary"), e.Builder.Connections.Primary ?? throw new ArgumentException("No Primary"), e.Builder.Connections.GetDefaultVAE(), ComfyUpscaler.NearestExact, Width, Height ); e.Builder.Connections.PrimarySize = new Size(Width, Height); } // Provide temp values e.Temp = e.CreateTempFromBuilder(); // Apply steps from our addons ApplyAddonSteps(e); // If "Sampler" is not yet a node, do initial setup // otherwise do hires setup if (!e.Nodes.ContainsKey("Sampler")) { ApplyStepsInitialSampler(e); // Save temp e.Builder.Connections.BaseSamplerTemporaryArgs = e.Temp; } else { // Hires does its own sampling so just throw I guess throw new InvalidOperationException( "Sampler ApplyStep was called when Sampler node already exists" ); } } public void ApplyStepsInitialCustomSampler(ModuleApplyStepEventArgs e, bool useFluxGuidance) { // Provide temp values e.Temp = e.CreateTempFromBuilder(); // Apply steps from our addons ApplyAddonSteps(e); // Get primary as latent using vae var primaryLatent = e.Builder.GetPrimaryAsLatent( e.Temp.Primary!.Unwrap(), e.Builder.Connections.GetDefaultVAE() ); // Set primary sampler and scheduler var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected"); e.Builder.Connections.PrimarySampler = primarySampler; var primaryScheduler = SelectedScheduler ?? throw new ValidationException("Scheduler not selected"); e.Builder.Connections.PrimaryScheduler = primaryScheduler; // for later inheritance if needed e.Builder.Connections.PrimaryCfg = CfgScale; e.Builder.Connections.PrimarySteps = Steps; e.Builder.Connections.PrimaryModelType = SelectedModelType; // KSamplerSelect var kSamplerSelect = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSamplerSelect { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.KSamplerSelect)), SamplerName = e.Builder.Connections.PrimarySampler?.Name!, } ); e.Builder.Connections.PrimarySamplerNode = kSamplerSelect.Output; // Scheduler/Sigmas if (e.Builder.Connections.PrimaryScheduler?.Name is "align_your_steps") { var alignYourSteps = e.Nodes.AddTypedNode( new ComfyNodeBuilder.AlignYourStepsScheduler { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.AlignYourStepsScheduler)), ModelType = SelectedModelType, Steps = Steps, Denoise = IsDenoiseStrengthEnabled ? DenoiseStrength : 1.0d, } ); e.Builder.Connections.PrimarySigmas = alignYourSteps.Output; } else { var basicScheduler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.BasicScheduler { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.BasicScheduler)), Model = e.Temp.Base.Model.Unwrap(), Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!, Denoise = IsDenoiseStrengthEnabled ? DenoiseStrength : 1.0d, Steps = Steps, } ); e.Builder.Connections.PrimarySigmas = basicScheduler.Output; } // Noise var randomNoise = e.Nodes.AddTypedNode( new ComfyNodeBuilder.RandomNoise { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.RandomNoise)), NoiseSeed = e.Builder.Connections.Seed, } ); e.Builder.Connections.PrimaryNoise = randomNoise.Output; if (useFluxGuidance) { // Guidance var fluxGuidance = e.Nodes.AddTypedNode( new ComfyNodeBuilder.FluxGuidance { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.FluxGuidance)), Conditioning = e.Builder.Connections.GetRefinerOrBaseConditioning().Positive, Guidance = CfgScale, } ); e.Builder.Connections.Base.Conditioning = new ConditioningConnections( fluxGuidance.Output, e.Builder.Connections.GetRefinerOrBaseConditioning().Negative ); // Guider var basicGuider = e.Nodes.AddTypedNode( new ComfyNodeBuilder.BasicGuider { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.BasicGuider)), Model = e.Builder.Connections.Base.Model.Unwrap(), Conditioning = e.Builder.Connections.GetRefinerOrBaseConditioning().Positive, } ); e.Builder.Connections.PrimaryGuider = basicGuider.Output; } else { e.Builder.Connections.Base.Conditioning = e.Builder.Connections.GetRefinerOrBaseConditioning(); var cfgGuider = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CFGGuider { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CFGGuider)), Model = e.Temp.Base.Model.Unwrap(), Positive = e.Builder.Connections.Base.Conditioning.Positive, Negative = e.Builder.Connections.Base.Conditioning.Negative, Cfg = CfgScale, } ); e.Builder.Connections.PrimaryGuider = cfgGuider.Output; } // SamplerCustomAdvanced var sampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.SamplerCustomAdvanced { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.SamplerCustomAdvanced)), Guider = e.Builder.Connections.PrimaryGuider, Noise = e.Builder.Connections.PrimaryNoise, Sampler = e.Builder.Connections.PrimarySamplerNode, Sigmas = e.Builder.Connections.PrimarySigmas, LatentImage = primaryLatent, } ); e.Builder.Connections.Primary = sampler.Output1; e.Builder.Connections.BaseSamplerTemporaryArgs = e.Temp; } private void ApplyStepsInitialSampler(ModuleApplyStepEventArgs e) { // Get primary as latent using vae var primaryLatent = e.Builder.GetPrimaryAsLatent( e.Temp.Primary!.Unwrap(), e.Builder.Connections.GetDefaultVAE() ); // Set primary sampler and scheduler var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected"); e.Builder.Connections.PrimarySampler = primarySampler; var primaryScheduler = SelectedScheduler ?? throw new ValidationException("Scheduler not selected"); e.Builder.Connections.PrimaryScheduler = primaryScheduler; // for later inheritance if needed e.Builder.Connections.PrimaryCfg = CfgScale; e.Builder.Connections.PrimarySteps = Steps; e.Builder.Connections.PrimaryModelType = SelectedModelType; // Use Temp Conditioning that may be modified by addons var conditioning = e.Temp.Base.Conditioning.Unwrap(); var refinerConditioning = e.Temp.Refiner.Conditioning; var useFluxGuidance = ModulesCardViewModel.IsModuleEnabled(); var isPlasmaEnabled = ModulesCardViewModel.IsModuleEnabled(); var usePlasmaSampler = false; if (isPlasmaEnabled) { var plasmaViewModel = ModulesCardViewModel .GetCard() .GetCard(); usePlasmaSampler = plasmaViewModel.IsPlasmaSamplerEnabled; } if (useFluxGuidance) { // Flux guidance var fluxGuidance = e.Nodes.AddTypedNode( new ComfyNodeBuilder.FluxGuidance { Name = e.Nodes.GetUniqueName("FluxGuidance"), Conditioning = conditioning.Positive, Guidance = CfgScale, } ); conditioning = conditioning with { Positive = fluxGuidance.Output }; } // Use custom sampler if SDTurbo scheduler is selected if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo) { // Error if using refiner if (e.Builder.Connections.Refiner.Model is not null) { throw new ValidationException("SDTurbo Scheduler cannot be used with Refiner Model"); } var kSamplerSelect = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSamplerSelect { Name = "KSamplerSelect", SamplerName = e.Builder.Connections.PrimarySampler?.Name!, } ); var turboScheduler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.SDTurboScheduler { Name = "SDTurboScheduler", Model = e.Builder.Connections.Base.Model.Unwrap(), Steps = Steps, Denoise = DenoiseStrength, } ); var sampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.SamplerCustom { Name = "Sampler", Model = e.Builder.Connections.Base.Model, AddNoise = true, NoiseSeed = e.Builder.Connections.Seed, Cfg = useFluxGuidance ? 1.0d : CfgScale, Positive = conditioning.Positive, Negative = conditioning.Negative, Sampler = kSamplerSelect.Output, Sigmas = turboScheduler.Output, LatentImage = primaryLatent, } ); e.Builder.Connections.Primary = sampler.Output1; } else if (usePlasmaSampler) { var plasmaViewModel = ModulesCardViewModel .GetCard() .GetCard(); var sampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.PlasmaSampler { Name = "PlasmaSampler", Model = e.Temp.Base.Model!.Unwrap(), NoiseSeed = e.Builder.Connections.Seed, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Steps = Steps, Cfg = useFluxGuidance ? 1.0d : CfgScale, Positive = conditioning.Positive, Negative = conditioning.Negative, LatentImage = primaryLatent, Denoise = DenoiseStrength, DistributionType = "rand", LatentNoise = plasmaViewModel.PlasmaSamplerLatentNoise, } ); e.Builder.Connections.Primary = sampler.Output; } // Use KSampler if no refiner, otherwise need KSamplerAdvanced else if (e.Builder.Connections.Refiner.Model is null) { // No refiner var sampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSampler { Name = "Sampler", Model = e.Temp.Base.Model!.Unwrap(), Seed = e.Builder.Connections.Seed, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Steps = Steps, Cfg = useFluxGuidance ? 1.0d : CfgScale, Positive = conditioning.Positive, Negative = conditioning.Negative, LatentImage = primaryLatent, Denoise = DenoiseStrength, } ); e.Builder.Connections.Primary = sampler.Output; } else { // Advanced base sampler for refiner var sampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSamplerAdvanced { Name = "Sampler", Model = e.Temp.Base.Model!.Unwrap(), AddNoise = true, NoiseSeed = e.Builder.Connections.Seed, Steps = TotalSteps, Cfg = useFluxGuidance ? 1.0d : CfgScale, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Positive = conditioning.Positive, Negative = conditioning.Negative, LatentImage = primaryLatent, StartAtStep = 0, EndAtStep = Steps, ReturnWithLeftoverNoise = true, } ); e.Builder.Connections.Primary = sampler.Output; } // If temp batched, add a LatentFromBatch to pick the temp batch right after first sampler if (e.Temp.IsPrimaryTempBatched) { e.Builder.Connections.Primary = e .Nodes.AddTypedNode( new ComfyNodeBuilder.LatentFromBatch { Name = e.Nodes.GetUniqueName("ControlNet_LatentFromBatch"), Samples = e.Builder.GetPrimaryAsLatent(), BatchIndex = e.Temp.PrimaryTempBatchPickIndex, // Use max length here as recommended // https://github.com/comfyanonymous/ComfyUI_experiments/issues/11 Length = 64, } ) .Output; } // Refiner if (e.Builder.Connections.Refiner.Model is not null) { // Add refiner sampler e.Builder.Connections.Primary = e .Nodes.AddTypedNode( new ComfyNodeBuilder.KSamplerAdvanced { Name = "Sampler_Refiner", Model = e.Builder.Connections.Refiner.Model, AddNoise = false, NoiseSeed = e.Builder.Connections.Seed, Steps = TotalSteps, Cfg = CfgScale, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Positive = refinerConditioning!.Positive, Negative = refinerConditioning.Negative, // Connect to previous sampler LatentImage = e.Builder.GetPrimaryAsLatent(), StartAtStep = Steps, EndAtStep = TotalSteps, ReturnWithLeftoverNoise = false, } ) .Output; } } /// /// Applies each step of our addons /// /// private void ApplyAddonSteps(ModuleApplyStepEventArgs e) { // Apply steps from our modules foreach (var module in ModulesCardViewModel.Cards.Cast()) { module.ApplyStep(e); } } /// public void LoadStateFromParameters(GenerationParameters parameters) { Width = parameters.Width; Height = parameters.Height; Steps = parameters.Steps; CfgScale = parameters.CfgScale; if ( !string.IsNullOrEmpty(parameters.Sampler) && GenerationParametersConverter.TryGetSamplerScheduler( parameters.Sampler, out var samplerScheduler ) ) { SelectedSampler = ClientManager.Samplers.FirstOrDefault(s => s == samplerScheduler.Sampler); SelectedScheduler = ClientManager.Schedulers.FirstOrDefault(s => s == samplerScheduler.Scheduler); } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { var sampler = GenerationParametersConverter.TryGetParameters( new ComfySamplerScheduler(SelectedSampler ?? default, SelectedScheduler ?? default), out var res ) ? res : null; if (sampler is null && SelectedSampler is not null && SelectedScheduler is not null) { sampler = $"{SelectedSampler?.DisplayName} {SelectedScheduler?.DisplayName}"; } return parameters with { Width = Width, Height = Height, Steps = Steps, CfgScale = CfgScale, Sampler = sampler, }; } private void LoadAvailableResolutions() { GroupedResolutionsByAspectRatio.Clear(); foreach (var res in AvailableResolutions) { // split on 'x' or 'X' var parts = res.ToLowerInvariant() .Split('x', StringSplitOptions.RemoveEmptyEntries) .Select(p => p.Trim()) .ToArray(); if (parts.Length != 2 || !int.TryParse(parts[0], out var w) || !int.TryParse(parts[1], out var h)) { continue; } var category = "Square"; if (w > h) { category = "Landscape"; } else if (h > w) { category = "Portrait"; } if (!GroupedResolutionsByAspectRatio.TryGetValue(category, out var list)) { list = []; GroupedResolutionsByAspectRatio[category] = list; } list.Add(res.Trim()); } // Sort the resolutions by width and height foreach (var key in GroupedResolutionsByAspectRatio.Keys.ToList()) { if (key == "Portrait") { GroupedResolutionsByAspectRatio[key] = GroupedResolutionsByAspectRatio[key] .Order(DimensionStringComparer.Instance) .ToList(); } else { GroupedResolutionsByAspectRatio[key] = GroupedResolutionsByAspectRatio[key] .OrderDescending(DimensionStringComparer.Instance) .ToList(); } } // fire that off just in case OnPropertyChanged(nameof(GroupedResolutionsByAspectRatio)); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/SeedCardViewModel.cs ================================================ using System; using System.Text.Json.Nodes; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SeedCard))] [ManagedService] [RegisterTransient] public partial class SeedCardViewModel : LoadableViewModelBase { [ObservableProperty, NotifyPropertyChangedFor(nameof(RandomizeButtonToolTip))] private bool isRandomizeEnabled = true; [ObservableProperty] private long seed; public string RandomizeButtonToolTip => IsRandomizeEnabled ? "Randomizing Seed on each run" : "Seed is locked"; [RelayCommand] public void GenerateNewSeed() { Seed = Random.Shared.NextInt64(0, int.MaxValue); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); Seed = model.Seed; IsRandomizeEnabled = model.IsRandomizeEnabled; } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel(new SeedCardModel { Seed = Seed, IsRandomizeEnabled = IsRandomizeEnabled }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Database; using Size = System.Drawing.Size; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SelectImageCard))] [ManagedService] [RegisterTransient] public partial class SelectImageCardViewModel( INotificationService notificationService, IServiceManager vmFactory ) : LoadableViewModelBase, IDropTarget, IComfyStep, IInputImageProvider { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static FilePickerFileType SupportedImages { get; } = new("Supported Images") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg" }, AppleUniformTypeIdentifiers = new[] { "public.jpeg", "public.png" }, MimeTypes = new[] { "image/jpeg", "image/png" } }; private readonly Lazy _lazyMaskEditorViewModel = new(vmFactory.Get); /// /// When true, enables a button to open a mask editor for the image. /// This is not saved or loaded from state. /// [ObservableProperty] [property: JsonIgnore] [property: MemberNotNull(nameof(MaskEditorViewModel))] private bool isMaskEditorEnabled; /// /// Toggles whether the mask overlay is shown over the image. /// [ObservableProperty] private bool isMaskOverlayEnabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsSelectionAvailable))] [NotifyPropertyChangedFor(nameof(IsImageFileNotFound))] private ImageSource? imageSource; [ObservableProperty] [property: JsonIgnore] [NotifyPropertyChangedFor(nameof(IsSelectionAvailable))] private bool isSelectionEnabled = true; /// /// Set by when the image is loaded. /// [ObservableProperty] private Size currentBitmapSize = Size.Empty; /// /// True if the image file is set but the local file does not exist. /// [MemberNotNullWhen(true, nameof(NotFoundImagePath))] public bool IsImageFileNotFound => ImageSource?.LocalFile?.Exists == false; public bool IsSelectionAvailable => IsSelectionEnabled && ImageSource == null; /// /// Path of the not found image /// public string? NotFoundImagePath => ImageSource?.LocalFile?.FullPath; [JsonInclude] public MaskEditorViewModel? MaskEditorViewModel => IsMaskEditorEnabled ? _lazyMaskEditorViewModel.Value : null; [JsonIgnore] public ImageSource? LastMaskImage { get; private set; } /// public void ApplyStep(ModuleApplyStepEventArgs e) { // With Mask image if (IsMaskEditorEnabled && MaskEditorViewModel.IsMaskEnabled) { MaskEditorViewModel.PaintCanvasViewModel.CanvasSize = CurrentBitmapSize; e.Builder.SetupImagePrimarySourceWithMask( ImageSource ?? throw new ValidationException("Input Image is required"), !CurrentBitmapSize.IsEmpty ? CurrentBitmapSize : throw new ValidationException("CurrentBitmapSize is null"), MaskEditorViewModel.GetCachedOrNewMaskRenderInverseAlphaImage(), MaskEditorViewModel.PaintCanvasViewModel.CanvasSize, e.Builder.Connections.BatchIndex ); } // Normal image only else { e.Builder.SetupImagePrimarySource( ImageSource ?? throw new ValidationException("Input Image is required"), !CurrentBitmapSize.IsEmpty ? CurrentBitmapSize : throw new ValidationException("CurrentBitmapSize is null"), e.Builder.Connections.BatchIndex ); } } /// public IEnumerable GetInputImages() { // Main image if (ImageSource is { } image && !IsImageFileNotFound) { yield return image; } // Mask image if (IsMaskEditorEnabled && MaskEditorViewModel.IsMaskEnabled) { using var timer = CodeTimer.StartDebug("MaskImage"); MaskEditorViewModel.PaintCanvasViewModel.CanvasSize = CurrentBitmapSize; var maskImage = MaskEditorViewModel.GetCachedOrNewMaskRenderInverseAlphaImage(); timer.Dispose(); yield return maskImage; } } partial void OnImageSourceChanged(ImageSource? value) { // Cache the hash for later upload use if (value?.LocalFile is { Exists: true } localFile) { value .GetBlake3HashAsync() .SafeFireAndForget(ex => { Logger.Warn(ex, "Error getting hash for image {Path}", localFile.Name); notificationService.ShowPersistent( $"Error getting hash for image {localFile.Name}", $"{ex.GetType().Name}: {ex.Message}" ); }); } } [RelayCommand] private async Task SelectImageFromFilePickerAsync() { var files = await App.StorageProvider.OpenFilePickerAsync( new FilePickerOpenOptions { FileTypeFilter = [FilePickerFileTypes.ImagePng, FilePickerFileTypes.ImageJpg, SupportedImages] } ); if (files.FirstOrDefault()?.TryGetLocalPath() is { } path) { Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(path))); } } [RelayCommand] private async Task OpenEditMaskDialogAsync() { if (!IsMaskEditorEnabled || ImageSource is null) { return; } // Make a backup to restore if not saving later var maskEditorStateBackup = MaskEditorViewModel.SaveStateToJsonObject(); // Set the background image if (await ImageSource.GetBitmapAsync() is not { } currentBitmap) { Logger.Warn("GetBitmapAsync returned null for image {Path}", ImageSource.LocalFile?.FullPath); notificationService.ShowPersistent( "Error Loading Image", "Could not load mask editor for the provided image.", NotificationType.Error ); return; } MaskEditorViewModel.PaintCanvasViewModel.BackgroundImage = currentBitmap.ToSKBitmap(); if (await MaskEditorViewModel.GetDialog().ShowAsync() == ContentDialogResult.Primary) { MaskEditorViewModel.InvalidateCachedMaskRenderImage(); } else { // Restore the backup MaskEditorViewModel.LoadStateFromJsonObject(maskEditorStateBackup); } } /// /// Supports LocalImageFile Context or OS Files /// public void DragOver(object? sender, DragEventArgs e) { if ( e.Data.GetDataFormats().Contains(DataFormats.Files) || e.Data.GetContext() is not null ) { e.Handled = true; return; } e.DragEffects = DragDropEffects.None; } /// public void Drop(object? sender, DragEventArgs e) { // 1. Context drop for LocalImageFile if (e.Data.GetContext() is { } imageFile) { e.Handled = true; Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(imageFile.AbsolutePath))); return; } // 2. OS Files if ( e.Data.GetFiles() is { } files && files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path ) { e.Handled = true; Dispatcher.UIThread.Post(() => LoadUserImageSafe(new ImageSource(path))); } } /// /// Calls with notification error handling. /// private void LoadUserImageSafe(ImageSource image) { try { LoadUserImage(image); } catch (Exception e) { Logger.Warn(e, "Error loading image"); notificationService.Show("Error loading image", e.Message); } } /// /// Loads the user image from the given ImageSource. /// /// The ImageSource object representing the user image. [MethodImpl(MethodImplOptions.Synchronized)] private void LoadUserImage(ImageSource image) { var current = ImageSource; ImageSource = image; // current?.Dispose(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/SharpenCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SharpenCard))] [ManagedService] [RegisterTransient] public partial class SharpenCardViewModel : LoadableViewModelBase { [Range(1, 31)] [ObservableProperty] private int sharpenRadius = 1; [Range(0.1, 10)] [ObservableProperty] private double sigma = 1; [Range(0, 5)] [ObservableProperty] private double alpha = 1; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/StackCardViewModel.cs ================================================ using System.Linq; using System.Text.Json.Nodes; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(StackCard))] [ManagedService] [RegisterTransient] public class StackCardViewModel : StackViewModelBase { /// public StackCardViewModel(IServiceManager vmFactory) : base(vmFactory) { } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); if (model.Cards is null) return; foreach (var (i, card) in model.Cards.Enumerate()) { // Ignore if more than cards than we have if (i > Cards.Count - 1) break; Cards[i].LoadStateFromJsonObject(card); } } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel( new StackCardModel { Cards = Cards.Select(x => x.SaveStateToJsonObject()).ToList() } ); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/StackEditableCardViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(StackEditableCard))] [ManagedService] [RegisterTransient] public partial class StackEditableCardViewModel : StackViewModelBase, IComfyStep { private readonly IServiceManager vmFactory; [ObservableProperty] [property: JsonIgnore] private string? title = Languages.Resources.Label_Steps; [ObservableProperty] [property: JsonIgnore] private bool isEditEnabled; /// /// Available module types for user creation /// [JsonIgnore] public IReadOnlyList AvailableModules { get; set; } = Array.Empty(); /// /// Default modules that are used when no modules are loaded /// This is a subset of /// [JsonIgnore] public IReadOnlyList DefaultModules { get; set; } = Array.Empty(); /// public StackEditableCardViewModel(IServiceManager vmFactory) : base(vmFactory) { this.vmFactory = vmFactory; } /// /// Populate with new instances of types /// public void InitializeDefaults() { foreach (var module in DefaultModules) { AddModule(module); } } partial void OnIsEditEnabledChanged(bool value) { // Propagate edit state to children foreach (var module in Cards.OfType()) { module.IsEditEnabled = value; } } /// public void ApplyStep(ModuleApplyStepEventArgs e) { foreach (var module in Cards.OfType()) { module.ApplyStep(e); } } /// protected override void OnCardAdded(LoadableViewModelBase item) { base.OnCardAdded(item); if (item is StackExpanderViewModel module) { // Inherit our edit state module.IsEditEnabled = IsEditEnabled; } } public T AddModule() where T : ModuleBase { var card = vmFactory.Get(); AddCards(card); return card; } public T AddModule(Action initializer) where T : ModuleBase { var card = vmFactory.Get(initializer); AddCards(card); return card; } [RelayCommand] private void AddModule(Type type) { if (!type.IsSubclassOf(typeof(ModuleBase))) { throw new ArgumentException($"Type {type} must be subclass of {nameof(ModuleBase)}"); } var card = vmFactory.Get(type) as LoadableViewModelBase; AddCards(card!); } public bool IsModuleEnabled(int index = 0) where T : ModuleBase { var card = Cards.OfType().ElementAtOrDefault(index); return card is { IsEnabled: true }; } /*/// public override void LoadStateFromJsonObject(JsonObject state) { var derivedTypes = ViewModelSerializer.GetDerivedTypes(typeof(LoadableViewModelBase)); Clear(); var stateArray = state.AsArray(); foreach (var node in stateArray) { } var cards = ViewModelSerializer.DeserializeJsonObject>(state); AddCards(cards!); } /// public override JsonObject SaveStateToJsonObject() { return ViewModelSerializer.SerializeToJsonObject(Cards.ToList()); }*/ } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/StackExpanderViewModel.cs ================================================ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(StackExpander))] [ManagedService] [RegisterTransient] public partial class StackExpanderViewModel : StackViewModelBase { public const string ModuleKey = "StackExpander"; [ObservableProperty] [property: JsonIgnore] private string? title; [ObservableProperty] [property: JsonIgnore] private string? titleExtra; [ObservableProperty] private bool isEnabled; /// /// True if parent StackEditableCard is in edit mode (can drag to reorder) /// [ObservableProperty] [property: JsonIgnore] private bool isEditEnabled; /// /// True to show the settings button, invokes when clicked /// public virtual bool IsSettingsEnabled { get; set; } public virtual IRelayCommand? SettingsCommand { get; set; } /// public StackExpanderViewModel(IServiceManager vmFactory) : base(vmFactory) { } public override void OnContainerIndexChanged(int value) { TitleExtra = $"{value + 1}."; } /// public override void LoadStateFromJsonObject(JsonObject state) { base.LoadStateFromJsonObject(state); if ( state.TryGetPropertyValue(nameof(IsEnabled), out var isEnabledNode) && isEnabledNode is JsonValue jsonValue && jsonValue.TryGetValue(out bool isEnabledBool) ) { IsEnabled = isEnabledBool; } } /// public override JsonObject SaveStateToJsonObject() { var state = base.SaveStateToJsonObject(); state.Add(nameof(IsEnabled), IsEnabled); return state; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/StackViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Nodes; using Nito.Disposables.Internals; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.ViewModels.Inference; public abstract class StackViewModelBase : DisposableLoadableViewModelBase { private readonly IServiceManager vmFactory; public AdvancedObservableList Cards { get; } = new(); protected StackViewModelBase(IServiceManager vmFactory) { this.vmFactory = vmFactory; Cards.CollectionChanged += (sender, args) => { if (args.NewItems != null) { var itemIndex = args.NewStartingIndex; foreach (var item in args.NewItems.OfType()) { item.OnContainerIndexChanged(itemIndex); itemIndex++; } } }; } public virtual void OnContainerIndexChanged(int value) { } /// /// Event raised when a card is added /// public event EventHandler? CardAdded; protected virtual void OnCardAdded(LoadableViewModelBase item) { CardAdded?.Invoke(this, item); } public void AddCards(params LoadableViewModelBase[] cards) { AddCards((IEnumerable)cards); } /// /// Register new cards /// public void AddCards(IEnumerable cards) { foreach (var card in cards) { Cards.Add(card); OnCardAdded(card); } } /// /// Registers new cards and returns self /// public StackViewModelBase WithCards(IEnumerable cards) { AddCards(cards); return this; } /// /// Gets a card by type at specified index /// public T GetCard(int index = 0) where T : LoadableViewModelBase { return Cards.OfType().ElementAtOrDefault(index) ?? throw new InvalidOperationException( $"Card of type {typeof(T).Name} at index {index} not found" ); } public void Clear() { Cards.Clear(); } /// public override void LoadStateFromJsonObject(JsonObject state) { Clear(); var derivedTypes = ViewModelSerializer.GetDerivedTypes(typeof(LoadableViewModelBase)); if (!state.TryGetPropertyValue("$values", out var values) || values is not JsonArray nodesArray) { return; } foreach (var node in nodesArray.Select(n => n as JsonObject).WhereNotNull()) { // Get $type key if ( !node.TryGetPropertyValue("$type", out var typeValue) || typeValue is not JsonValue jsonValue || jsonValue.ToString() is not { } typeKey ) { continue; } // Get type from key if (!derivedTypes.TryGetValue(typeKey, out var type)) { continue; } if (vmFactory.Get(type) is not LoadableViewModelBase vm) { continue; } vm.LoadStateFromJsonObject(node); AddCards(vm); } } /// public override JsonObject SaveStateToJsonObject() { var derivedTypeNames = ViewModelSerializer .GetDerivedTypes(typeof(LoadableViewModelBase)) .ToDictionary(x => x.Value, x => x.Key); var nodes = new JsonArray( Cards .Select(x => { var typeKey = derivedTypeNames[x.GetType()]; var node = x.SaveStateToJsonObject(); node.Add("$type", typeKey); return (JsonNode)node; }) .ToArray() ); return new JsonObject { ["$values"] = nodes }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/TiledVAECardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(TiledVAECard))] [ManagedService] [RegisterTransient] public partial class TiledVAECardViewModel : LoadableViewModelBase { public const string ModuleKey = "TiledVAE"; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(64, 4096)] private int tileSize = 512; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(0, 4096)] private int overlap = 64; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(8, 4096)] private int temporalSize = 64; [ObservableProperty] [NotifyDataErrorInfo] [Required] [Range(4, 4096)] private int temporalOverlap = 8; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/UnetModelCardViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json.Nodes; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(UnetModelCard))] [ManagedService] [RegisterTransient] public partial class UnetModelCardViewModel(IInferenceClientManager clientManager) : LoadableViewModelBase, IParametersLoadableState, IComfyStep { [ObservableProperty] private HybridModelFile? selectedModel; [ObservableProperty] private HybridModelFile? selectedVae; [ObservableProperty] private HybridModelFile? selectedClip1; [ObservableProperty] private HybridModelFile? selectedClip2; [ObservableProperty] private string selectedDType = "default"; public List WeightDTypes { get; set; } = ["default", "fp8_e4m3fn", "fp8_e5m2"]; public IInferenceClientManager ClientManager { get; } = clientManager; public async Task ValidateModel() { if (SelectedModel != null) return true; var dialog = DialogHelper.CreateMarkdownDialog( "Please select a model to continue.", "No Model Selected" ); await dialog.ShowAsync(); return false; } public void ApplyStep(ModuleApplyStepEventArgs e) { var checkpointLoader = e.Nodes.AddTypedNode(GetModelLoader(e, SelectedModel!, SelectedDType)); e.Builder.Connections.Base.Model = checkpointLoader.Output; var vaeLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.VAELoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.VAELoader)), VaeName = SelectedVae?.RelativePath ?? throw new ValidationException("No VAE Selected") } ); e.Builder.Connections.Base.VAE = vaeLoader.Output; // DualCLIPLoader var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.DualCLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.DualCLIPLoader)), ClipName1 = SelectedClip1?.RelativePath ?? throw new ValidationException("No Clip1 Selected"), ClipName2 = SelectedClip2?.RelativePath ?? throw new ValidationException("No Clip2 Selected"), Type = "flux" } ); e.Builder.Connections.Base.Clip = clipLoader.Output; } private static ComfyTypedNodeBase GetModelLoader( ModuleApplyStepEventArgs e, HybridModelFile model, string selectedDType ) { // Simple loader for UNET return new ComfyNodeBuilder.UNETLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UNETLoader)), UnetName = model.RelativePath, WeightDtype = selectedDType }; } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel( new UnetModelCardModel { SelectedModelName = SelectedModel?.RelativePath, SelectedVaeName = SelectedVae?.RelativePath, SelectedClip1Name = SelectedClip1?.RelativePath, SelectedClip2Name = SelectedClip2?.RelativePath } ); } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); SelectedModel = model.SelectedModelName is null ? null : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedModelName); SelectedVae = model.SelectedVaeName is null ? HybridModelFile.Default : ClientManager.VaeModels.FirstOrDefault(x => x.RelativePath == model.SelectedVaeName); SelectedClip1 = model.SelectedClip1Name is null ? HybridModelFile.None : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedClip1Name); SelectedClip2 = model.SelectedClip2Name is null ? HybridModelFile.None : ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedClip2Name); } internal class UnetModelCardModel { public string? SelectedModelName { get; set; } public string? SelectedVaeName { get; set; } public string? SelectedClip1Name { get; set; } public string? SelectedClip2Name { get; set; } } /// public void LoadStateFromParameters(GenerationParameters parameters) { if (parameters.ModelName is not { } paramsModelName) return; var currentModels = ClientManager.Models; HybridModelFile? model; // First try hash match if (parameters.ModelHash is not null) { model = currentModels.FirstOrDefault( m => m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256 && sha256.StartsWith(parameters.ModelHash, StringComparison.InvariantCultureIgnoreCase) ); } else { // Name matches model = currentModels.FirstOrDefault(m => m.RelativePath.EndsWith(paramsModelName)); model ??= currentModels.FirstOrDefault(m => m.ShortDisplayName.StartsWith(paramsModelName)); } if (model is not null) { SelectedModel = model; } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { return parameters with { ModelName = SelectedModel?.FileName, ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256 }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs ================================================ using System.Text.Json.Nodes; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(UpscalerCard))] [ManagedService] [RegisterTransient] public partial class UpscalerCardViewModel : LoadableViewModelBase { public const string ModuleKey = "Upscaler"; private readonly INotificationService notificationService; private readonly IServiceManager vmFactory; [ObservableProperty] private double scale = 2; [ObservableProperty] private ComfyUpscaler? selectedUpscaler = ComfyUpscaler.Defaults[0]; public IInferenceClientManager ClientManager { get; } public UpscalerCardViewModel( IInferenceClientManager clientManager, INotificationService notificationService, IServiceManager vmFactory ) { this.notificationService = notificationService; this.vmFactory = vmFactory; ClientManager = clientManager; } /// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); Scale = model.Scale; SelectedUpscaler = model.SelectedUpscaler; } /// public override JsonObject SaveStateToJsonObject() { return SerializeModel(new UpscalerCardModel { Scale = Scale, SelectedUpscaler = SelectedUpscaler }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; [View(typeof(ModelCard))] [ManagedService] [RegisterTransient] public class ImgToVidModelCardViewModel : ModelCardViewModel { public ImgToVidModelCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory, TabContext tabContext ) : base(clientManager, vmFactory, tabContext) { DisableSettings = true; } public override void ApplyStep(ModuleApplyStepEventArgs e) { var imgToVidLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ImageOnlyCheckpointLoader { Name = "ImageOnlyCheckpointLoader", CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected"), } ); e.Builder.Connections.Base.Model = imgToVidLoader.Output1; e.Builder.Connections.BaseClipVision = imgToVidLoader.Output2; e.Builder.Connections.Base.VAE = imgToVidLoader.Output3; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; [View(typeof(VideoGenerationSettingsCard))] [ManagedService] [RegisterTransient] public partial class SvdImgToVidConditioningViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep { [ObservableProperty] private int width = 1024; [ObservableProperty] private int height = 576; [ObservableProperty] private int numFrames = 14; [ObservableProperty] private int motionBucketId = 127; [ObservableProperty] private int fps = 6; [ObservableProperty] private double augmentationLevel; [ObservableProperty] private double minCfg = 1.0d; public void LoadStateFromParameters(GenerationParameters parameters) { Width = parameters.Width; Height = parameters.Height; NumFrames = parameters.FrameCount; MotionBucketId = parameters.MotionBucketId; Fps = parameters.Fps; AugmentationLevel = parameters.AugmentationLevel; MinCfg = parameters.MinCfg; } public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { return parameters with { FrameCount = NumFrames, MotionBucketId = MotionBucketId, Fps = Fps, AugmentationLevel = AugmentationLevel, MinCfg = MinCfg, }; } public void ApplyStep(ModuleApplyStepEventArgs e) { // do VideoLinearCFGGuidance stuff first var cfgGuidanceNode = e.Nodes.AddTypedNode( new ComfyNodeBuilder.VideoLinearCFGGuidance { Name = e.Nodes.GetUniqueName("LinearCfgGuidance"), Model = e.Builder.Connections.Base.Model ?? throw new ValidationException("Model not selected"), MinCfg = MinCfg } ); e.Builder.Connections.Base.Model = cfgGuidanceNode.Output; // then do the SVD stuff var svdImgToVidConditioningNode = e.Nodes.AddTypedNode( new ComfyNodeBuilder.SVD_img2vid_Conditioning { ClipVision = e.Builder.Connections.BaseClipVision!, InitImage = e.Builder.GetPrimaryAsImage(), Vae = e.Builder.Connections.Base.VAE!, Name = e.Nodes.GetUniqueName("SvdImgToVidConditioning"), Width = Width, Height = Height, VideoFrames = NumFrames, MotionBucketId = MotionBucketId, Fps = Fps, AugmentationLevel = AugmentationLevel } ); e.Builder.Connections.Base.Conditioning = new ConditioningConnections( svdImgToVidConditioningNode.Output1, svdImgToVidConditioningNode.Output2 ); e.Builder.Connections.Primary = svdImgToVidConditioningNode.Output3; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video; [View(typeof(VideoOutputSettingsCard))] [ManagedService] [RegisterTransient] public partial class VideoOutputSettingsCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep { [ObservableProperty] private double fps = 6; [ObservableProperty] private bool lossless = true; [ObservableProperty] private int quality = 85; [ObservableProperty] private VideoOutputMethod selectedMethod = VideoOutputMethod.Default; [ObservableProperty] private List availableMethods = Enum.GetValues().ToList(); public void LoadStateFromParameters(GenerationParameters parameters) { Fps = parameters.OutputFps; Lossless = parameters.Lossless; Quality = parameters.VideoQuality; if (string.IsNullOrWhiteSpace(parameters.VideoOutputMethod)) return; SelectedMethod = Enum.TryParse(parameters.VideoOutputMethod, true, out var method) ? method : VideoOutputMethod.Default; } public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { return parameters with { OutputFps = Fps, Lossless = Lossless, VideoQuality = Quality, VideoOutputMethod = SelectedMethod.ToString(), }; } public void ApplyStep(ModuleApplyStepEventArgs e) { if (e.Builder.Connections.Primary is null) throw new ArgumentException("No Primary"); var image = e.Builder.Connections.Primary.Match( _ => e.Builder.GetPrimaryAsImage( e.Builder.Connections.PrimaryVAE ?? e.Builder.Connections.Refiner.VAE ?? e.Builder.Connections.Base.VAE ?? throw new ArgumentException("No Primary, Refiner, or Base VAE") ), image => image ); var outputStep = e.Nodes.AddTypedNode( new ComfyNodeBuilder.SaveAnimatedWEBP { Name = e.Nodes.GetUniqueName("SaveAnimatedWEBP"), Images = image, FilenamePrefix = "InferenceVideo", Fps = Fps, Lossless = Lossless, Quality = Quality, Method = SelectedMethod.ToString().ToLowerInvariant() } ); e.Builder.Connections.OutputNodes.Add(outputStep); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/WanModelCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using CommunityToolkit.Mvvm.ComponentModel; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(WanModelCard))] [ManagedService] [RegisterTransient] public partial class WanModelCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory ) : LoadableViewModelBase, IParametersLoadableState, IComfyStep { [ObservableProperty] private HybridModelFile? selectedModel; [ObservableProperty] private HybridModelFile? selectedClipModel; [ObservableProperty] private HybridModelFile? selectedClipVisionModel; [ObservableProperty] private HybridModelFile? selectedVae; [ObservableProperty] private string? selectedDType = "fp8_e4m3fn_fast"; [ObservableProperty] private bool isClipVisionEnabled; [ObservableProperty] private double shift = 8.0d; public IInferenceClientManager ClientManager { get; } = clientManager; public StackEditableCardViewModel ExtraNetworksStackCardViewModel { get; } = new(vmFactory) { Title = Resources.Label_ExtraNetworks, AvailableModules = [typeof(LoraModule)] }; public List WeightDTypes { get; set; } = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"]; public async Task ValidateModel() { if (SelectedModel == null) { var dialog = DialogHelper.CreateMarkdownDialog( "Please select a model to continue.", "No Model Selected" ); await dialog.ShowAsync(); return false; } if (SelectedVae == null) { var dialog = DialogHelper.CreateMarkdownDialog( "Please select a VAE model to continue.", "No VAE Model Selected" ); await dialog.ShowAsync(); return false; } if (SelectedClipModel == null) { var dialog = DialogHelper.CreateMarkdownDialog( "Please select a CLIP model to continue.", "No CLIP Model Selected" ); await dialog.ShowAsync(); return false; } if (IsClipVisionEnabled && SelectedClipVisionModel == null) { var dialog = DialogHelper.CreateMarkdownDialog( "Please select a CLIP Vision model to continue.", "No CLIP Vision Model Selected" ); await dialog.ShowAsync(); return false; } return true; } public void ApplyStep(ModuleApplyStepEventArgs e) { ComfyTypedNodeBase modelLoader; if (SelectedModel?.RelativePath.EndsWith("gguf", StringComparison.OrdinalIgnoreCase) is true) { modelLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.UnetLoaderGGUF { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UnetLoaderGGUF)), UnetName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected") } ); } else { modelLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.UNETLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.UNETLoader)), UnetName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected"), WeightDtype = SelectedDType ?? "fp8_e4m3fn_fast" } ); } var modelSamplingSd3 = e.Nodes.AddTypedNode( new ComfyNodeBuilder.ModelSamplingSD3 { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.ModelSamplingSD3)), Model = modelLoader.Output, Shift = Shift } ); e.Builder.Connections.Base.Model = modelSamplingSd3.Output; var clipLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPLoader)), ClipName = SelectedClipModel?.RelativePath ?? throw new ValidationException("No Clip Model Selected"), Type = "wan" } ); e.Builder.Connections.Base.Clip = clipLoader.Output; var vaeLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.VAELoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.VAELoader)), VaeName = SelectedVae?.RelativePath ?? throw new ValidationException("No VAE Selected") } ); e.Builder.Connections.Base.VAE = vaeLoader.Output; e.Builder.Connections.PrimaryVAE = vaeLoader.Output; if (ExtraNetworksStackCardViewModel.Cards.OfType().Any(x => x.IsEnabled)) { ExtraNetworksStackCardViewModel.ApplyStep(e); } if (!IsClipVisionEnabled) return; var clipVisionLoader = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPVisionLoader { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPVisionLoader)), ClipName = SelectedClipVisionModel?.RelativePath ?? throw new ValidationException("No Clip Vision Model Selected") } ); e.Builder.Connections.BaseClipVision = clipVisionLoader.Output; e.Builder.Connections.Base.ClipVision = clipVisionLoader.Output; } public void LoadStateFromParameters(GenerationParameters parameters) { if (parameters.ModelName is not { } paramsModelName) return; var currentModels = ClientManager.UnetModels; HybridModelFile? model; // First try hash match if (parameters.ModelHash is not null) { model = currentModels.FirstOrDefault( m => m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256 && sha256.StartsWith(parameters.ModelHash, StringComparison.InvariantCultureIgnoreCase) ); } else { // Name matches model = currentModels.FirstOrDefault(m => m.RelativePath.EndsWith(paramsModelName)); model ??= currentModels.FirstOrDefault(m => m.ShortDisplayName.StartsWith(paramsModelName)); } if (model is null) return; SelectedModel = model; } public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { return parameters with { ModelName = SelectedModel?.FileName, ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256 }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Inference/WanSamplerCardViewModel.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Linq; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SamplerCard))] [ManagedService] [RegisterTransient] public class WanSamplerCardViewModel : SamplerCardViewModel { public WanSamplerCardViewModel( IInferenceClientManager clientManager, IServiceManager vmFactory, ISettingsManager settingsManager, TabContext tabContext ) : base(clientManager, vmFactory, settingsManager, tabContext) { EnableAddons = false; IsLengthEnabled = true; SelectedSampler = ComfySampler.UniPC; SelectedScheduler = ComfyScheduler.Simple; Length = 33; } public override void ApplyStep(ModuleApplyStepEventArgs e) { if (EnableAddons) { foreach (var module in ModulesCardViewModel.Cards.OfType()) { module.ApplyStep(e); } } // Set primary sampler and scheduler var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected"); e.Builder.Connections.PrimarySampler = primarySampler; var primaryScheduler = SelectedScheduler ?? throw new ValidationException("Scheduler not selected"); e.Builder.Connections.PrimaryScheduler = primaryScheduler; // for later inheritance if needed e.Builder.Connections.PrimaryCfg = CfgScale; e.Builder.Connections.PrimarySteps = Steps; e.Builder.Connections.PrimaryModelType = SelectedModelType; e.Temp = e.CreateTempFromBuilder(); var conditioning = e.Temp.Base.Conditioning.Unwrap(); var isImgToVid = IsDenoiseStrengthEnabled; if (isImgToVid) { var clipVisionEncode = e.Nodes.AddTypedNode( new ComfyNodeBuilder.CLIPVisionEncode { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.CLIPVisionEncode)), ClipVision = e.Builder.Connections.BaseClipVision ?? throw new ValidationException("BaseClipVision not set"), Image = e.Builder.GetPrimaryAsImage(), Crop = "none", } ); var wanImageToVideo = e.Nodes.AddTypedNode( new ComfyNodeBuilder.WanImageToVideo { Name = e.Nodes.GetUniqueName(nameof(ComfyNodeBuilder.WanImageToVideo)), Positive = conditioning.Positive, Negative = conditioning.Negative, Vae = e.Builder.Connections.GetDefaultVAE(), ClipVisionOutput = clipVisionEncode.Output, StartImage = e.Builder.GetPrimaryAsImage(), Width = Width, Height = Height, Length = Length, BatchSize = e.Builder.Connections.BatchSize, } ); var kSampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSampler { Name = "Sampler", Model = e.Temp.Base.Model!.Unwrap(), Seed = e.Builder.Connections.Seed, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Steps = Steps, Cfg = CfgScale, Positive = wanImageToVideo.Output1, Negative = wanImageToVideo.Output2, LatentImage = wanImageToVideo.Output3, Denoise = DenoiseStrength, } ); e.Builder.Connections.Primary = kSampler.Output; } else { var primaryLatent = e.Builder.GetPrimaryAsLatent( e.Temp.Primary!.Unwrap(), e.Builder.Connections.GetDefaultVAE() ); var kSampler = e.Nodes.AddTypedNode( new ComfyNodeBuilder.KSampler { Name = "Sampler", Model = e.Temp.Base.Model!.Unwrap(), Seed = e.Builder.Connections.Seed, SamplerName = primarySampler.Name, Scheduler = primaryScheduler.Name, Steps = Steps, Cfg = CfgScale, Positive = conditioning.Positive, Negative = conditioning.Negative, LatentImage = primaryLatent, Denoise = DenoiseStrength, } ); e.Builder.Connections.Primary = kSampler.Output; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Reactive.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using Microsoft.Extensions.DependencyInjection; using NLog; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; using InferenceTabViewModelBase = StabilityMatrix.Avalonia.ViewModels.Base.InferenceTabViewModelBase; using Path = System.IO.Path; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [Preload] [View(typeof(InferencePage))] [RegisterSingleton] public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly IServiceManager vmFactory; private readonly IModelIndexService modelIndexService; private readonly ILiteDbContext liteDbContext; private readonly RunningPackageService runningPackageService; private Guid? selectedPackageId; private List scopes = []; public override string Title => Resources.Label_Inference; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.AppGeneric, IconVariant = IconVariant.Filled }; public RefreshBadgeViewModel ConnectionBadge { get; } = new() { State = ProgressState.Failed, FailToolTipText = "Not connected", FailIcon = FluentAvalonia.UI.Controls.Symbol.Refresh, SuccessToolTipText = Resources.Label_Connected, }; public IInferenceClientManager ClientManager { get; } public SharedState SharedState { get; } public ObservableCollection Tabs { get; } = new(); [ObservableProperty] private InferenceTabViewModelBase? selectedTab; [ObservableProperty] private int selectedTabIndex; [ObservableProperty] private bool isWaitingForConnection; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsComfyRunning))] private PackagePair? runningPackage; public bool IsComfyRunning => RunningPackage?.BasePackage is ComfyUI; private IDisposable? onStartupComplete; public InferenceViewModel( IServiceManager vmFactory, INotificationService notificationService, IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, IModelIndexService modelIndexService, ILiteDbContext liteDbContext, RunningPackageService runningPackageService, SharedState sharedState ) { this.vmFactory = vmFactory; this.notificationService = notificationService; this.settingsManager = settingsManager; this.modelIndexService = modelIndexService; this.liteDbContext = liteDbContext; this.runningPackageService = runningPackageService; ClientManager = inferenceClientManager; SharedState = sharedState; // Keep RunningPackage updated with the current package pair runningPackageService.RunningPackages.CollectionChanged += RunningPackagesOnCollectionChanged; // "Send to Inference" EventManager.Instance.InferenceProjectRequested += InstanceOnInferenceProjectRequested; // Global requests for custom prompt queueing EventManager.Instance.InferenceQueueCustomPrompt += OnInferenceQueueCustomPromptRequested; MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService); MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService); } private Task InstanceOnInferenceProjectRequested( object? sender, LocalImageFile imageFile, InferenceProjectType type ) => Dispatcher.UIThread.InvokeAsync(async () => await AddTabFromFileAsync(imageFile, type)); private void DisconnectFromComfy() { RunningPackage = null; // Cancel any pending connection if (ConnectCancelCommand.CanExecute(null)) { ConnectCancelCommand.Execute(null); } onStartupComplete?.Dispose(); onStartupComplete = null; IsWaitingForConnection = false; // Disconnect Logger.Trace("On package close - disconnecting"); DisconnectCommand.Execute(null); } /// /// Updates the RunningPackage property when the running package changes. /// Also starts a connection to the backend if a new ComfyUI package is running. /// And disconnects if the package is closed. /// private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if ( e.NewItems?.OfType>().Select(x => x.Value) is not { } newItems ) { if (RunningPackage != null) { DisconnectFromComfy(); } return; } var comfyViewModel = newItems.FirstOrDefault( vm => vm.RunningPackage.InstalledPackage.Id == selectedPackageId || vm.RunningPackage.BasePackage is ComfyUI ); if (comfyViewModel is null && RunningPackage?.BasePackage is ComfyUI) { DisconnectFromComfy(); } else if (comfyViewModel != null && RunningPackage == null) { IsWaitingForConnection = true; RunningPackage = comfyViewModel.RunningPackage; onStartupComplete = Observable .FromEventPattern( comfyViewModel.RunningPackage.BasePackage, nameof(comfyViewModel.RunningPackage.BasePackage.StartupComplete) ) .Take(1) .Subscribe(_ => { Dispatcher.UIThread.Post(() => { if (ConnectCommand.CanExecute(null)) { Logger.Trace("On package launch - starting connection"); ConnectCommand.Execute(null); } IsWaitingForConnection = false; }); }); } } private void OnInferenceQueueCustomPromptRequested(object? sender, InferenceQueueCustomPromptEventArgs e) { // Get currently selected tab var currentTab = SelectedTab; if (currentTab is InferenceGenerationViewModelBase generationViewModel) { Dispatcher .UIThread.InvokeAsync(async () => { await generationViewModel.RunCustomGeneration(e); }) .SafeFireAndForget(ex => { Logger.Error(ex, "Failed to queue prompt"); Dispatcher.UIThread.Post(() => { notificationService.ShowPersistent( "Failed to queue prompt", $"{ex.GetType().Name}: {ex.Message}", NotificationType.Error ); }); }); } } public override void OnLoaded() { base.OnLoaded(); modelIndexService.BackgroundRefreshIndex(); } protected override async Task OnInitialLoadedAsync() { await base.OnInitialLoadedAsync(); if (Design.IsDesignMode) return; // Load any open projects var openProjects = await liteDbContext.InferenceProjects.FindAsync(p => p.IsOpen); if (openProjects is not null) { foreach (var project in openProjects.OrderBy(p => p.CurrentTabIndex)) { var file = new FilePath(project.FilePath); if (!file.Exists) { // Remove from database await liteDbContext.InferenceProjects.DeleteAsync(project.Id); } try { if (file.Exists) { await AddTabFromFile(project.FilePath); } } catch (Exception e) { Logger.Warn(e, "Failed to open project file {FilePath}", project.FilePath); notificationService.Show( "Failed to open project file", $"[{e.GetType().Name}] {e.Message}", NotificationType.Error ); // Set not open await liteDbContext.InferenceProjects.UpdateAsync( project with { IsOpen = false, IsSelected = false, CurrentTabIndex = -1 } ); } } } if (Tabs.Count == 0) { AddTab(InferenceProjectType.TextToImage); } } /// /// On exit, sync tab states to database /// public async ValueTask DisposeAsync() { await SyncTabStatesWithDatabase(); foreach (var scope in scopes) { scope.Dispose(); } GC.SuppressFinalize(this); } /// /// Update the database with current tabs /// private async Task SyncTabStatesWithDatabase() { // Update the database with the current tabs foreach (var (i, tab) in Tabs.ToImmutableArray().Enumerate()) { if (tab.ProjectFile is not { } projectFile) { continue; } var projectPath = projectFile.ToString(); var entry = await liteDbContext.InferenceProjects.FindOneAsync(p => p.FilePath == projectPath); // Create if not found entry ??= new InferenceProjectEntry { Id = Guid.NewGuid(), FilePath = projectFile.ToString() }; entry.IsOpen = tab == SelectedTab; entry.CurrentTabIndex = i; Logger.Trace( "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", tab.TabTitle, entry ); await liteDbContext.InferenceProjects.UpsertAsync(entry); } } /// /// Update the database with given tab /// private async Task SyncTabStateWithDatabase(InferenceTabViewModelBase tab) { if (tab.ProjectFile is not { } projectFile) { return; } var entry = await liteDbContext.InferenceProjects.FindOneAsync( p => p.FilePath == projectFile.ToString() ); // Create if not found entry ??= new InferenceProjectEntry { Id = Guid.NewGuid(), FilePath = projectFile.ToString() }; entry.IsOpen = tab == SelectedTab; entry.CurrentTabIndex = Tabs.IndexOf(tab); Logger.Trace( "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", tab.TabTitle, entry ); await liteDbContext.InferenceProjects.UpsertAsync(entry); } /// /// When the + button on the tab control is clicked, add a new tab. /// [RelayCommand] private void AddTab(InferenceProjectType type) { if (type.ToViewModelType() is not { } vmType) { return; } // Create a new scope for this tab var scope = vmFactory.CreateScope(); // Get the view model using the scope's service provider var tab = scope.ServiceManager.Get(vmType) as InferenceTabViewModelBase ?? throw new NullReferenceException($"Could not create view model of type {vmType}"); Tabs.Add(tab); // Set as new selected tab SelectedTabIndex = Tabs.Count - 1; // Update the database with the current tab SyncTabStateWithDatabase(tab).SafeFireAndForget(); } /// /// When the close button on the tab is clicked, remove the tab. /// public void OnTabCloseRequested(TabViewTabCloseRequestedEventArgs e) { if (e.Item is not InferenceTabViewModelBase vm) { Logger.Warn("Tab close requested for unknown item {@Item}", e); return; } Logger.Trace("Closing tab {Title}", vm.TabTitle); // Set the selected tab to the next tab if there is one, then previous, then null lock (Tabs) { var index = Tabs.IndexOf(vm); if (index < Tabs.Count - 1) { SelectedTabIndex = index + 1; } else if (index > 0) { SelectedTabIndex = index - 1; } // Remove the tab Tabs.RemoveAt(index); // Dispose the scope for this tab if (index < scopes.Count) { scopes[index].Dispose(); scopes.RemoveAt(index); } } // Update the database with the current tab SyncTabStateWithDatabase(vm).SafeFireAndForget(); // Dispose the view model vm.Dispose(); } /// /// Show the connection help dialog. /// [RelayCommand] private async Task ShowConnectionHelp() { var vm = vmFactory.Get(); var result = await vm.CreateDialog().ShowAsync(); if (result != ContentDialogResult.Primary) return; selectedPackageId = vm.SelectedPackage?.Id; } /// /// Connect to the inference server. /// [RelayCommand(IncludeCancelCommand = true)] private async Task Connect(CancellationToken cancellationToken = default) { if (ClientManager.IsConnected) return; if (Design.IsDesignMode) { await ClientManager.ConnectAsync(cancellationToken); return; } if (RunningPackage is not null) { var result = await notificationService.TryAsync( ClientManager.ConnectAsync(RunningPackage, cancellationToken), "Could not connect to backend" ); if (result.Exception is { } exception) { Logger.Error(exception, "Failed to connect to Inference backend"); } } } /// /// Disconnect from the inference server. /// [RelayCommand] private async Task Disconnect() { if (!ClientManager.IsConnected) return; if (Design.IsDesignMode) { await ClientManager.CloseAsync(); return; } await notificationService.TryAsync( ClientManager.CloseAsync(), "Could not disconnect from ComfyUI backend" ); } /// /// Menu "Save As" command. /// [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task MenuSaveAs() { var currentTab = SelectedTab; if (currentTab == null) { Logger.Warn("MenuSaveAs: currentTab is null"); return; } // Prompt for save file dialog var provider = App.StorageProvider; var projectDir = new DirectoryPath(settingsManager.LibraryDir, "Projects"); projectDir.Create(); var startDir = await provider.TryGetFolderFromPathAsync(projectDir); var result = await provider.SaveFilePickerAsync( new FilePickerSaveOptions { Title = "Save As", SuggestedFileName = "Untitled", FileTypeChoices = new FilePickerFileType[] { new("StabilityMatrix Project") { Patterns = new[] { "*.smproj" }, MimeTypes = new[] { "application/json" }, } }, SuggestedStartLocation = startDir, DefaultExtension = ".smproj", ShowOverwritePrompt = true, } ); if (result is null) { Logger.Trace("MenuSaveAs: user cancelled"); return; } var document = InferenceProjectDocument.FromLoadable(currentTab); // Save to file try { await using var stream = await result.OpenWriteAsync(); stream.SetLength(0); // Overwrite fully await JsonSerializer.SerializeAsync( stream, document, new JsonSerializerOptions { WriteIndented = true } ); } catch (Exception e) { notificationService.ShowPersistent( "Could not save to file", $"[{e.GetType().Name}] {e.Message}", NotificationType.Error ); return; } // Update project file currentTab.ProjectFile = new FilePath(result.TryGetLocalPath()!); await SyncTabStatesWithDatabase(); notificationService.Show("Saved", $"Saved project to {result.Name}", NotificationType.Success); } /// /// Menu "Save Project" command. /// [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task MenuSave() { if (SelectedTab is not { } currentTab) { Logger.Info("MenuSaveProject: currentTab is null"); return; } // If the tab has no project file, prompt for save as if (currentTab.ProjectFile is not { } projectFile) { await MenuSaveAs(); return; } // Otherwise, save to the current project file var document = InferenceProjectDocument.FromLoadable(currentTab); // Save to file try { await using var stream = projectFile.Info.OpenWrite(); stream.SetLength(0); // Overwrite fully await JsonSerializer.SerializeAsync( stream, document, new JsonSerializerOptions { WriteIndented = true } ); } catch (Exception e) { notificationService.ShowPersistent( "Could not save to file", $"[{e.GetType().Name}] {e.Message}", NotificationType.Error ); return; } notificationService.Show("Saved", $"Saved project to {projectFile.Name}", NotificationType.Success); } [RelayCommand] private void GoForwardTabWithLooping() { if (SelectedTabIndex == Tabs.Count - 1) { SelectedTabIndex = 0; } else { SelectedTabIndex++; } } [RelayCommand] private void GoBackwardsTabWithLooping() { if (SelectedTabIndex == 0) { SelectedTabIndex = Tabs.Count - 1; } else { SelectedTabIndex--; } } private async Task AddTabFromFile(FilePath file) { await using var stream = file.Info.OpenRead(); var document = await JsonSerializer.DeserializeAsync(stream); if (document is null) { throw new ApplicationException("MenuOpenProject: Deserialize project file returned null"); } if (document.State is null) { throw new ApplicationException("Project file does not have 'State' key"); } document.VerifyVersion(); if (document.ProjectType.ToViewModelType() is not { } vmType) { throw new InvalidOperationException($"Unsupported project type: {document.ProjectType}"); } // Create a new scope for this tab var scope = vmFactory.CreateScope(); scopes.Add(scope); // Get the view model using the scope's service provider var vm = scope.ServiceManager.Get(vmType) as InferenceTabViewModelBase ?? throw new NullReferenceException($"Could not create view model of type {vmType}"); vm.LoadStateFromJsonObject(document.State); vm.ProjectFile = file; Tabs.Add(vm); SelectedTab = vm; await SyncTabStatesWithDatabase(); } private async Task AddTabFromFileAsync(LocalImageFile imageFile, InferenceProjectType projectType) { // Create a new scope for this tab var scope = vmFactory.CreateScope(); scopes.Add(scope); // Get the appropriate view model from the scope InferenceTabViewModelBase vm = projectType switch { InferenceProjectType.TextToImage => scope.ServiceManager.Get(), InferenceProjectType.ImageToImage => scope.ServiceManager.Get(), InferenceProjectType.ImageToVideo => scope.ServiceManager.Get(), InferenceProjectType.Upscale => scope.ServiceManager.Get(), InferenceProjectType.FluxTextToImage => scope.ServiceManager.Get(), }; switch (vm) { case InferenceImageToImageViewModel imgToImgVm: imgToImgVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath); vm.LoadImageMetadata(imageFile.AbsolutePath); break; case InferenceTextToImageViewModel _: vm.LoadImageMetadata(imageFile.AbsolutePath); break; case InferenceImageUpscaleViewModel upscaleVm: upscaleVm.IsUpscaleEnabled = true; upscaleVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath); break; case InferenceImageToVideoViewModel imgToVidVm: imgToVidVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath); break; case InferenceFluxTextToImageViewModel _: vm.LoadImageMetadata(imageFile.AbsolutePath); break; } Tabs.Add(vm); SelectedTab = vm; await SyncTabStatesWithDatabase(); } /// /// Menu "Open Project" command. /// [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task MenuOpenProject() { // Prompt for open file dialog var provider = App.StorageProvider; var projectDir = new DirectoryPath(settingsManager.LibraryDir, "Projects"); projectDir.Create(); var startDir = await provider.TryGetFolderFromPathAsync(projectDir); var results = await provider.OpenFilePickerAsync( new FilePickerOpenOptions { Title = "Open Project File", FileTypeFilter = new FilePickerFileType[] { new("StabilityMatrix Project") { Patterns = new[] { "*.smproj" }, MimeTypes = new[] { "application/json" }, } }, SuggestedStartLocation = startDir, } ); if (results.Count == 0) { Logger.Trace("MenuOpenProject: No files selected"); return; } // Load from file var file = results[0].TryGetLocalPath()!; try { await AddTabFromFile(file); } catch (NotSupportedException e) { notificationService.ShowPersistent( $"Unsupported Project Version", $"[{Path.GetFileName(file)}] {e.Message}", NotificationType.Error ); } catch (Exception e) { notificationService.ShowPersistent( $"Failed to load Project", $"[{Path.GetFileName(file)}] {e.Message}", NotificationType.Error ); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs ================================================ using System.Reactive.Linq; using System.Text.Json; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(InstalledWorkflowsPage))] [RegisterSingleton] public partial class InstalledWorkflowsViewModel( ISettingsManager settingsManager, INotificationService notificationService ) : TabViewModelBase { public override string Header => Resources.TabLabel_InstalledWorkflows; private readonly SourceCache workflowsCache = new(x => x.Workflow?.Id ?? Guid.NewGuid().ToString()); [ObservableProperty] private IObservableCollection displayedWorkflows = new ObservableCollectionExtended(); [ObservableProperty] private string searchQuery = string.Empty; protected override async Task OnInitialLoadedAsync() { await base.OnInitialLoadedAsync(); var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(100)) .DistinctUntilChanged() .Select(_ => (Func)FilterWorkflows); AddDisposable( workflowsCache .Connect() .DeferUntilLoaded() .Filter(searchPredicate) .SortBy(x => x.Index) .Bind(DisplayedWorkflows) .ObserveOn(SynchronizationContext.Current!) .Subscribe() ); if (Design.IsDesignMode) return; await LoadInstalledWorkflowsAsync(); EventManager.Instance.WorkflowInstalled += OnWorkflowInstalled; } [RelayCommand] private async Task LoadInstalledWorkflowsAsync() { workflowsCache.Clear(); if (!Directory.Exists(settingsManager.WorkflowDirectory)) { Directory.CreateDirectory(settingsManager.WorkflowDirectory); } var count = 0; foreach ( var workflowPath in Directory.EnumerateFiles( settingsManager.WorkflowDirectory, "*.json", EnumerationOptionConstants.AllDirectories ) ) { try { var json = await File.ReadAllTextAsync(workflowPath); var metadata = JsonSerializer.Deserialize(json); if (metadata?.Workflow == null) { metadata = new OpenArtMetadata { Workflow = new OpenArtSearchResult { Id = Guid.NewGuid().ToString(), Name = Path.GetFileNameWithoutExtension(workflowPath), }, Index = count++, }; } metadata.FilePath = [await App.StorageProvider.TryGetFileFromPathAsync(workflowPath)]; workflowsCache.AddOrUpdate(metadata); } catch (Exception e) { Console.WriteLine(e); } } } [RelayCommand] private async Task OpenInExplorer(OpenArtMetadata metadata) { if (metadata.FilePath == null) return; var path = metadata.FilePath.FirstOrDefault()?.Path.ToString(); if (string.IsNullOrWhiteSpace(path)) return; await ProcessRunner.OpenFileBrowser(path); } [RelayCommand] private void OpenOnOpenArt(OpenArtMetadata metadata) { if (metadata.Workflow == null) return; ProcessRunner.OpenUrl($"https://openart.ai/workflows/{metadata.Workflow.Id}"); } [RelayCommand] private async Task DeleteAsync(OpenArtMetadata metadata) { var confirmationDialog = new BetterContentDialog { Title = Resources.Label_AreYouSure, Content = Resources.Label_ActionCannotBeUndone, PrimaryButtonText = Resources.Action_Delete, SecondaryButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, IsSecondaryButtonEnabled = true, }; var dialogResult = await confirmationDialog.ShowAsync(); if (dialogResult != ContentDialogResult.Primary) return; await using var delay = new MinimumDelay(200, 500); var path = metadata?.FilePath?.FirstOrDefault()?.Path.ToString().Replace("file:///", ""); if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) { await notificationService.TryAsync( Task.Run(() => File.Delete(path)), message: "Error deleting workflow" ); var id = metadata?.Workflow?.Id; if (!string.IsNullOrWhiteSpace(id)) { workflowsCache.Remove(id); } } notificationService.Show( Resources.Label_WorkflowDeleted, string.Format(Resources.Label_WorkflowDeletedSuccessfully, metadata?.Workflow?.Name) ); } private bool FilterWorkflows(OpenArtMetadata metadata) { if (string.IsNullOrWhiteSpace(SearchQuery)) return true; if (metadata.HasMetadata) { return metadata.Workflow.Creator.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) || metadata.Workflow.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase); } return metadata.Workflow?.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ?? false; } private void OnWorkflowInstalled(object? sender, EventArgs e) { LoadInstalledWorkflowsAsync().SafeFireAndForget(); } protected override void Dispose(bool disposing) { if (disposing) { EventManager.Instance.WorkflowInstalled -= OnWorkflowInstalled; } base.Dispose(disposing); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(LaunchPageView))] public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyncDisposable { private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly IPyRunner pyRunner; private readonly INotificationService notificationService; private readonly ISharedFolders sharedFolders; private readonly IServiceManager dialogFactory; protected readonly IPackageFactory PackageFactory; // Regex to match if input contains a yes/no prompt, // i.e "Y/n", "yes/no". Case insensitive. // Separated by / or |. [GeneratedRegex(@"y(/|\|)n|yes(/|\|)no", RegexOptions.IgnoreCase)] private static partial Regex InputYesNoRegex(); public override string Title => "Launch"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Rocket, IconVariant = IconVariant.Filled }; public ConsoleViewModel Console { get; } = new(); [ObservableProperty] private bool launchButtonVisibility; [ObservableProperty] private bool stopButtonVisibility; [ObservableProperty] private bool isLaunchTeachingTipsOpen; [ObservableProperty] private bool showWebUiButton; [ ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedBasePackage), nameof(SelectedPackageExtraCommands)) ] private InstalledPackage? selectedPackage; [ObservableProperty] private ObservableCollection installedPackages = new(); [ObservableProperty] private PackagePair? runningPackage; [ObservableProperty] private bool autoScrollToEnd = true; public virtual BasePackage? SelectedBasePackage => PackageFactory.FindPackageByName(SelectedPackage?.PackageName); public IReadOnlyDictionary SelectedPackageExtraCommands => SelectedBasePackage?.ExtraLaunchCommands ?? new Dictionary(); // private bool clearingPackages; private string webUiUrl = string.Empty; // Input info-bars [ObservableProperty] private bool showManualInputPrompt; [ObservableProperty] private bool showConfirmInputPrompt; public LaunchPageViewModel( ILogger logger, ISettingsManager settingsManager, IPackageFactory packageFactory, IPyRunner pyRunner, INotificationService notificationService, ISharedFolders sharedFolders, IServiceManager dialogFactory ) { this.logger = logger; this.settingsManager = settingsManager; this.PackageFactory = packageFactory; this.pyRunner = pyRunner; this.notificationService = notificationService; this.sharedFolders = sharedFolders; this.dialogFactory = dialogFactory; settingsManager.RelayPropertyFor( this, vm => vm.SelectedPackage, settings => settings.ActiveInstalledPackage ); settingsManager.RelayPropertyFor( this, vm => vm.AutoScrollToEnd, settings => settings.AutoScrollLaunchConsoleToEnd ); EventManager.Instance.PackageLaunchRequested += OnPackageLaunchRequested; EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; EventManager.Instance.TeachingTooltipNeeded += OnTeachingTooltipNeeded; // Handler for console input Console.ApcInput += (_, message) => { if (InputYesNoRegex().IsMatch(message.Data)) { ShowConfirmInputPrompt = true; } else { ShowManualInputPrompt = true; } }; } private void OnTeachingTooltipNeeded(object? sender, EventArgs e) => IsLaunchTeachingTipsOpen = true; private void OnInstalledPackagesChanged(object? sender, EventArgs e) => OnLoaded(); private void OnPackageLaunchRequested(object? sender, Guid e) { if (RunningPackage is not null) { notificationService.Show( "A package is already running", "Please stop the current package before launching another.", NotificationType.Error ); return; } SelectedPackage = InstalledPackages.FirstOrDefault(x => x.Id == e); LaunchAsync().SafeFireAndForget(); } partial void OnAutoScrollToEndChanged(bool value) { if (value) { EventManager.Instance.OnScrollToBottomRequested(); } } protected override Task OnInitialLoadedAsync() { if (string.IsNullOrWhiteSpace(Program.Args.LaunchPackageName)) return base.OnInitialLoadedAsync(); var package = InstalledPackages.FirstOrDefault(x => x.DisplayName == Program.Args.LaunchPackageName); if (package is not null) { SelectedPackage = package; return LaunchAsync(); } package = InstalledPackages.FirstOrDefault(x => x.Id.ToString() == Program.Args.LaunchPackageName); if (package is null) return base.OnInitialLoadedAsync(); SelectedPackage = package; return LaunchAsync(); } public override void OnLoaded() { // Load installed packages InstalledPackages = new ObservableCollection( settingsManager.Settings.InstalledPackages ); // Ensure active package either exists or is null if (SelectedPackage?.Id is { } id && InstalledPackages.All(x => x.Id != id)) { settingsManager.Transaction( s => { s.UpdateActiveInstalledPackage(); }, ignoreMissingLibraryDir: true ); } // Load active package SelectedPackage = settingsManager.Settings.ActiveInstalledPackage; AutoScrollToEnd = settingsManager.Settings.AutoScrollLaunchConsoleToEnd; base.OnLoaded(); } [RelayCommand] public async Task LaunchAsync(string? command = null) { await notificationService.TryAsync(LaunchImpl(command)); } protected virtual async Task LaunchImpl(string? command) { IsLaunchTeachingTipsOpen = false; var activeInstall = SelectedPackage; if (activeInstall == null) { // No selected package: error notification notificationService.Show( new Notification( message: "You must install and select a package before launching", title: "No package selected", type: NotificationType.Error ) ); return; } var activeInstallName = activeInstall.PackageName; var basePackage = string.IsNullOrWhiteSpace(activeInstallName) ? null : PackageFactory.FindPackageByName(activeInstallName); if (basePackage == null) { logger.LogWarning( "During launch, package name '{PackageName}' did not match a definition", activeInstallName ); notificationService.Show( new Notification( "Package name invalid", "Install package name did not match a definition. Please reinstall and let us know about this issue.", NotificationType.Error ) ); return; } // If this is the first launch (LaunchArgs is null), // load and save a launch options dialog vm // so that dynamic initial values are saved. if (activeInstall.LaunchArgs == null) { var definitions = basePackage.LaunchOptions; // Create config cards and save them var cards = LaunchOptionCard .FromDefinitions(definitions, Array.Empty()) .ToImmutableArray(); var args = cards.SelectMany(c => c.Options).ToList(); logger.LogDebug( "Setting initial launch args: {Args}", string.Join(", ", args.Select(o => o.ToArgString()?.ToRepr())) ); settingsManager.SaveLaunchArgs(activeInstall.Id, args); } if (basePackage is not StableSwarm) { await pyRunner.Initialize(); } // Get path from package var packagePath = new DirectoryPath(settingsManager.LibraryDir, activeInstall.LibraryPath!); if (basePackage is not StableSwarm) { // Unpack sitecustomize.py to venv await UnpackSiteCustomize(packagePath.JoinDir("venv")); } basePackage.Exited += OnProcessExited; basePackage.StartupComplete += RunningPackageOnStartupComplete; // Clear console and start update processing await Console.StopUpdatesAsync(); await Console.Clear(); Console.StartUpdates(); // Update shared folder links (in case library paths changed) await basePackage.UpdateModelFolders( packagePath, activeInstall.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod ); // Load user launch args from settings and convert to string var userArgs = activeInstall.LaunchArgs ?? []; var userArgsString = string.Join(" ", userArgs.Select(opt => opt.ToArgString())); // Join with extras, if any userArgsString = string.Join(" ", userArgsString, basePackage.ExtraLaunchArguments); // Use input command if provided, otherwise use package launch command command ??= basePackage.LaunchCommand; // await basePackage.RunPackage(packagePath, command, userArgsString, OnProcessOutputReceived); RunningPackage = new PackagePair(activeInstall, basePackage); EventManager.Instance.OnRunningPackageStatusChanged(RunningPackage); } // Unpacks sitecustomize.py to the target venv private static async Task UnpackSiteCustomize(DirectoryPath venvPath) { var sitePackages = venvPath.JoinDir(PyVenvRunner.RelativeSitePackagesPath); var file = sitePackages.JoinFile("sitecustomize.py"); file.Directory?.Create(); await Assets.PyScriptSiteCustomize.ExtractTo(file, true); } [RelayCommand] private async Task Config() { var activeInstall = SelectedPackage; var name = activeInstall?.PackageName; if (name == null || activeInstall == null) { logger.LogWarning($"Selected package is null"); return; } var package = PackageFactory.FindPackageByName(name); if (package == null) { logger.LogWarning("Package {Name} not found", name); return; } // Check if package supports IArgParsable // Use dynamic parsed args over static /*if (package is IArgParsable parsable) { var rootPath = activeInstall.FullPath!; var moduleName = parsable.RelativeArgsDefinitionScriptPath; var parser = new ArgParser(pyRunner, rootPath, moduleName); definitions = await parser.GetArgsAsync(); }*/ // Open a config page var viewModel = dialogFactory.Get(); viewModel.Cards = LaunchOptionCard .FromDefinitions(package.LaunchOptions, activeInstall.LaunchArgs ?? []) .ToImmutableArray(); logger.LogDebug("Launching config dialog with cards: {CardsCount}", viewModel.Cards.Count); var dialog = new BetterContentDialog { ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, IsPrimaryButtonEnabled = true, PrimaryButtonText = Resources.Action_Save, CloseButtonText = Resources.Action_Cancel, FullSizeDesired = true, DefaultButton = ContentDialogButton.Primary, ContentMargin = new Thickness(32, 16), Padding = new Thickness(0, 16), Content = new LaunchOptionsDialog { DataContext = viewModel }, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { // Save config var args = viewModel.AsLaunchArgs(); settingsManager.SaveLaunchArgs(activeInstall.Id, args); } } // Send user input to running package public async Task SendInput(string input) { if (RunningPackage?.BasePackage is BaseGitPackage gitPackage) { var venv = gitPackage.VenvRunner; var process = venv?.Process; if (process is not null) { await process.StandardInput.WriteLineAsync(input); } else { logger.LogWarning("Attempted to write input but Process is null"); } } } [RelayCommand] private async Task SendConfirmInput(bool value) { // This must be on the UI thread Dispatcher.UIThread.CheckAccess(); // Also send input to our own console if (value) { Console.Post("y\n"); await SendInput("y\n"); } else { Console.Post("n\n"); await SendInput("n\n"); } ShowConfirmInputPrompt = false; } [RelayCommand] private async Task SendManualInput(string input) { // Also send input to our own console Console.PostLine(input); await SendInput(input); } public virtual async Task Stop() { if (RunningPackage is null) return; await RunningPackage.BasePackage.WaitForShutdown(); RunningPackage = null; ShowWebUiButton = false; Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}"); } public void OpenWebUi() { if (string.IsNullOrEmpty(webUiUrl)) return; notificationService.TryAsync( Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)), "Failed to open URL", $"{webUiUrl}" ); } private void OnProcessExited(object? sender, int exitCode) { EventManager.Instance.OnRunningPackageStatusChanged(null); Dispatcher .UIThread.InvokeAsync(async () => { logger.LogTrace("Process exited ({Code}) at {Time:g}", exitCode, DateTimeOffset.Now); // Need to wait for streams to finish before detaching handlers if (sender is BaseGitPackage { VenvRunner: not null } package) { var process = package.VenvRunner.Process; if (process is not null) { // Max 5 seconds var ct = new CancellationTokenSource(5000).Token; try { await process.WaitUntilOutputEOF(ct); } catch (OperationCanceledException e) { logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message); } } } // Detach handlers if (sender is BasePackage basePackage) { basePackage.Exited -= OnProcessExited; basePackage.StartupComplete -= RunningPackageOnStartupComplete; } RunningPackage = null; ShowWebUiButton = false; await Console.StopUpdatesAsync(); // Need to reset cursor in case its in some weird position // from progress bars await Console.ResetWriteCursor(); Console.PostLine($"{Environment.NewLine}Process finished with exit code {exitCode}"); }) .SafeFireAndForget(); } // Callback for processes private void OnProcessOutputReceived(ProcessOutput output) { Console.Post(output); if (AutoScrollToEnd) { EventManager.Instance.OnScrollToBottomRequested(); } } private void OnOneClickInstallFinished(object? sender, bool e) { OnLoaded(); } private void RunningPackageOnStartupComplete(object? sender, string e) { webUiUrl = e.Replace("0.0.0.0", "127.0.0.1"); ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl); } public void OnMainWindowClosing(WindowClosingEventArgs e) { if (RunningPackage != null) { // Show confirmation if (e.CloseReason is WindowCloseReason.WindowClosing) { e.Cancel = true; var dialog = CreateExitConfirmDialog(); Dispatcher .UIThread.InvokeAsync(async () => { if ( (TaskDialogStandardResult)await dialog.ShowAsync(true) == TaskDialogStandardResult.Yes ) { App.Services.GetRequiredService().Hide(); App.Shutdown(); } }) .SafeFireAndForget(); } } } private static TaskDialog CreateExitConfirmDialog() { var dialog = DialogHelper.CreateTaskDialog( "Confirm Exit", "Are you sure you want to exit? This will also close the currently running package." ); dialog.ShowProgressBar = false; dialog.FooterVisibility = TaskDialogFooterVisibility.Never; dialog.Buttons = new List { new("Exit", TaskDialogStandardResult.Yes), TaskDialogButton.CancelButton, }; dialog.Buttons[0].IsDefault = true; return dialog; } protected override void Dispose(bool disposing) { if (disposing) { RunningPackage?.BasePackage.Shutdown(); RunningPackage = null; Console.Dispose(); } base.Dispose(disposing); } public async ValueTask DisposeAsync() { if (RunningPackage is not null) { await RunningPackage.BasePackage.WaitForShutdown(); RunningPackage = null; } await Console.DisposeAsync(); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Analytics; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Lykos.Analytics; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(MainWindow))] public partial class MainWindowViewModel : ViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly ISettingsManager settingsManager; private readonly IServiceManager dialogFactory; private readonly ITrackedDownloadService trackedDownloadService; private readonly IDiscordRichPresenceService discordRichPresenceService; private readonly IModelIndexService modelIndexService; private readonly Lazy modelDownloadLinkHandler; private readonly INotificationService notificationService; private readonly IAnalyticsHelper analyticsHelper; private readonly IUpdateHelper updateHelper; private readonly ISecretsManager secretsManager; private readonly INavigationService navigationService; private readonly INavigationService settingsNavService; public string Greeting => "Welcome to Avalonia!"; [ObservableProperty] private PageViewModelBase? currentPage; [ObservableProperty] private object? selectedCategory; [ObservableProperty] public partial bool IsPaneOpen { get; set; } [ObservableProperty] private List pages = new(); [ObservableProperty] private List footerPages = new(); public ProgressManagerViewModel ProgressManagerViewModel { get; init; } public UpdateViewModel UpdateViewModel { get; init; } public double PaneWidth => (Compat.IsWindows ? 0 : 20) + Cultures.Current switch { { Name: "it-IT" } => 250, { Name: "fr-FR" } => 250, { Name: "es" } => 250, { Name: "ru-RU" } => 250, { Name: "tr-TR" } => 235, { Name: "de" } => 250, { Name: "pt-PT" } => 300, { Name: "pt-BR" } => 260, { Name: "ko-KR" } => 235, { Name: "cs-CZ" } => 250, _ => 200, }; public MainWindowViewModel( ISettingsManager settingsManager, IDiscordRichPresenceService discordRichPresenceService, IServiceManager dialogFactory, ITrackedDownloadService trackedDownloadService, IModelIndexService modelIndexService, Lazy modelDownloadLinkHandler, INotificationService notificationService, IAnalyticsHelper analyticsHelper, IUpdateHelper updateHelper, ISecretsManager secretsManager, INavigationService navigationService, INavigationService settingsNavService ) { this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.discordRichPresenceService = discordRichPresenceService; this.trackedDownloadService = trackedDownloadService; this.modelIndexService = modelIndexService; this.modelDownloadLinkHandler = modelDownloadLinkHandler; this.notificationService = notificationService; this.analyticsHelper = analyticsHelper; this.updateHelper = updateHelper; this.secretsManager = secretsManager; this.navigationService = navigationService; this.settingsNavService = settingsNavService; ProgressManagerViewModel = dialogFactory.Get(); UpdateViewModel = dialogFactory.Get(); } public override void OnLoaded() { base.OnLoaded(); // Set only if null, since this may be called again when content dialogs open CurrentPage ??= Pages.FirstOrDefault(); SelectedCategory ??= Pages.FirstOrDefault(); } protected override async Task OnInitialLoadedAsync() { await base.OnLoadedAsync(); // Skip if design mode if (Design.IsDesignMode) return; if (!await EnsureDataDirectory()) { // False if user exited dialog, shutdown app App.Shutdown(); return; } Task.Run(() => SharedFolders.SetupSharedModelFolders(settingsManager.ModelsDirectory)) .SafeFireAndForget(ex => { Logger.Error(ex, "Error setting up shared model folders"); }); try { await modelDownloadLinkHandler.Value.StartListening(); } catch (IOException) { var dialog = new BetterContentDialog { Title = Resources.Label_StabilityMatrixAlreadyRunning, Content = Resources.Label_AnotherInstanceAlreadyRunning, IsPrimaryButtonEnabled = true, PrimaryButtonText = Resources.Action_Close, DefaultButton = ContentDialogButton.Primary, }; await dialog.ShowAsync(); App.Shutdown(); return; } settingsManager.RelayPropertyFor( this, vm => vm.IsPaneOpen, settings => settings.IsMainWindowSidebarOpen, true ); // Initialize Discord Rich Presence (this needs LibraryDir so is set here) discordRichPresenceService.UpdateState(); // Ensure GPU compute capability is populated before other components check it // This must run early and be awaited to prevent race conditions with package initialization await Task.Run(AddComputeCapabilityIfNecessary); // Load in-progress downloads ProgressManagerViewModel.AddDownloads(trackedDownloadService.Downloads); // Index checkpoints if we dont have // Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget(); // Disable preload for now, might be causing https://github.com/LykosAI/StabilityMatrix/issues/249 /*if (!App.IsHeadlessMode) { PreloadPages(); }*/ Program.StartupTimer.Stop(); var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed); Logger.Info($"App started ({startupTime})"); // Show analytics notice if not seen if ( settingsManager.Settings.Analytics.LastSeenConsentVersion is null || settingsManager.Settings.Analytics.LastSeenConsentAccepted is null ) { var vm = dialogFactory.Get(); var result = await vm.GetDialog().ShowAsync(); settingsManager.Transaction(s => { s.Analytics.LastSeenConsentVersion = Compat.AppVersion; s.Analytics.LastSeenConsentAccepted = result == ContentDialogResult.Secondary; }); if (result == ContentDialogResult.Secondary) { settingsManager.Transaction(s => s.Analytics.IsUsageDataEnabled, true); } else if (result is ContentDialogResult.Primary) { settingsManager.Transaction(s => s.Analytics.IsUsageDataEnabled, false); } } if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Count == 0) { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, FullSizeDesired = true, MinDialogHeight = 775, Content = new NewOneClickInstallDialog { DataContext = viewModel }, }; var firstDialogResult = await dialog.ShowAsync(App.TopLevel); if (firstDialogResult != ContentDialogResult.Primary) { analyticsHelper.TrackFirstTimeInstallAsync(null, null, true).SafeFireAndForget(); return; } var recommendedModelsViewModel = dialogFactory.Get(); dialog = new BetterContentDialog { IsPrimaryButtonEnabled = true, FullSizeDesired = true, MinDialogHeight = 900, PrimaryButtonText = Resources.Action_Download, CloseButtonText = Resources.Action_Close, DefaultButton = ContentDialogButton.Primary, PrimaryButtonCommand = recommendedModelsViewModel.DoImportCommand, Content = new RecommendedModelsDialog { DataContext = recommendedModelsViewModel }, }; await dialog.ShowAsync(App.TopLevel); EventManager.Instance.OnRecommendedModelsDialogClosed(); EventManager.Instance.OnDownloadsTeachingTipRequested(); var installedPackageNameMaybe = settingsManager.PackageInstallsInProgress.FirstOrDefault() ?? settingsManager.Settings.InstalledPackages.FirstOrDefault()?.PackageName; analyticsHelper .TrackFirstTimeInstallAsync( installedPackageNameMaybe, recommendedModelsViewModel .RecommendedModels.Where(x => x.IsSelected) .Select(x => x.CivitModel.Name) .ToList(), false ) .SafeFireAndForget(); } // Show what's new for updates if (settingsManager.Settings.UpdatingFromVersion is { } updatingFromVersion) { var currentVersion = Compat.AppVersion; notificationService.Show( "Update Successful", $"Stability Matrix has been updated from {updatingFromVersion.ToDisplayString()} to {currentVersion.ToDisplayString()}." ); settingsManager.Transaction(s => s.UpdatingFromVersion = null); } // Start checking for updates await updateHelper.StartCheckingForUpdates(); // Periodic launch stats if ( settingsManager.Settings.Analytics.IsUsageDataEnabled && ( settingsManager.Settings.Analytics.LaunchDataLastSentAt is null || (DateTimeOffset.UtcNow - settingsManager.Settings.Analytics.LaunchDataLastSentAt) > AnalyticsSettings.DefaultLaunchDataSendInterval ) ) { analyticsHelper .TrackAsync( new LaunchAnalyticsRequest { Version = Compat.AppVersion.ToString(), RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier, OsDescription = RuntimeInformation.OSDescription, } ) .ContinueWith(task => { if (!task.IsFaulted) { settingsManager.Transaction(s => s.Analytics.LaunchDataLastSentAt = DateTimeOffset.UtcNow ); } }) .SafeFireAndForget(); } // Account migration notice Task.Run(async () => { if (settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.LykosAccountMigrateTip)) { return; } var secrets = await secretsManager.LoadAsync(); if (!secrets.HasLegacyLykosAccount() || secrets.LykosAccountV2 is not null) { return; } // Show dialog await Dispatcher.UIThread.InvokeAsync(async () => { var dialog = DialogHelper.CreateMarkdownDialog(Resources.Text_LykosAccountUpgradeNotice); dialog.MaxDialogWidth = 600; dialog.ContentMargin = new Thickness(32, 0); dialog.PrimaryButtonText = Resources.Action_GoToSettings; dialog.IsPrimaryButtonEnabled = true; dialog.CloseButtonText = Resources.Action_Close; dialog.DefaultButton = ContentDialogButton.Primary; // Show dialog, nav to settings if primary if (await dialog.ShowAsync() is ContentDialogResult.Primary) { navigationService.NavigateTo( new SuppressNavigationTransitionInfo() ); await Task.Delay(100); settingsNavService.NavigateTo( new SuppressNavigationTransitionInfo() ); } }); // Mark as seen settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.LykosAccountMigrateTip)); }) .SafeFireAndForget(ex => { Logger.Error(ex, "Error during account migration notice check"); }); await ShowMigrationTipIfNecessaryAsync(); } private void PreloadPages() { // Preload pages with Preload attribute foreach ( var page in Pages .Concat(FooterPages) .Where(p => p.GetType().GetCustomAttributes(typeof(PreloadAttribute), true).Any()) ) { Dispatcher .UIThread.InvokeAsync( async () => { var stopwatch = Stopwatch.StartNew(); // ReSharper disable once MethodHasAsyncOverload page.OnLoaded(); await page.OnLoadedAsync(); // Get view new ViewLocator().Build(page); Logger.Trace( $"Preloaded page {page.GetType().Name} in {stopwatch.Elapsed.TotalMilliseconds:F1}ms" ); }, DispatcherPriority.Background ) .ContinueWith(task => { if (task.Exception is { } exception) { Logger.Error(exception, "Error preloading page"); Debug.Fail(exception.Message); } }); } } /// /// Check if the data directory exists, if not, show the select data directory dialog. /// private async Task EnsureDataDirectory() { // If we can't find library, show selection dialog var foundInitially = settingsManager.TryFindLibrary(); if (!foundInitially) { var result = await ShowSelectDataDirectoryDialog(); if (!result) return false; } // Try to find library again, should be found now if (!settingsManager.TryFindLibrary()) { throw new Exception("Could not find library after setting path"); } // Tell LaunchPage to load any packages if they selected an existing directory if (!foundInitially) { EventManager.Instance.OnInstalledPackagesChanged(); } // Check if there are old packages, if so show migration dialog // TODO: Migration dialog return true; } /// /// Return true if we should show the update available teaching tip /// public bool ShouldShowUpdateAvailableTeachingTip([NotNullWhen(true)] UpdateInfo? info) { if (info is null) { return false; } // If matching settings seen version, don't show if (info.Version == settingsManager.Settings.LastSeenUpdateVersion) { return false; } // Save that we have dismissed this update settingsManager.Transaction( s => s.LastSeenUpdateVersion = info.Version, ignoreMissingLibraryDir: true ); return true; } /// /// Shows the select data directory dialog. /// /// true if path set successfully, false if user exited dialog. private async Task ShowSelectDataDirectoryDialog() { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, Content = new SelectDataDirectoryDialog { DataContext = viewModel }, }; var result = await dialog.ShowAsync(App.TopLevel); if (result == ContentDialogResult.Primary) { // 1. For portable mode, call settings.SetPortableMode() if (viewModel.IsPortableMode) { settingsManager.SetPortableMode(); } // 2. For custom path, call settings.SetLibraryPath(path) else { settingsManager.SetLibraryPath(viewModel.DataDirectory); } // Indicate success return true; } return false; } public async Task ShowUpdateDialog() { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog { ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, DefaultButton = ContentDialogButton.Close, IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, Content = new UpdateDialog { DataContext = viewModel }, }; await viewModel.Preload(); await dialog.ShowAsync(); } private async Task ShowMigrationTipIfNecessaryAsync() { if ( settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.SharedFolderMigrationTip) || settingsManager.Settings.InstalledPackages.Count == 0 ) { return; } var folderReference = DialogHelper.CreateMarkdownDialog(MarkdownSnippets.SharedFolderMigration); folderReference.CloseButtonText = Resources.Action_OK; await folderReference.ShowAsync(); settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.SharedFolderMigrationTip)); } private void AddComputeCapabilityIfNecessary() { try { if (settingsManager.Settings.PreferredGpu is not { IsNvidia: true, ComputeCapability: null }) return; var newGpuInfos = HardwareHelper.IterGpuInfoNvidiaSmi(); var matchedGpuInfo = newGpuInfos?.FirstOrDefault(x => x.Name?.Equals(settingsManager.Settings.PreferredGpu.Name) ?? false ); if (matchedGpuInfo is null) { return; } using var transaction = settingsManager.BeginTransaction(); transaction.Settings.PreferredGpu = matchedGpuInfo; } catch (Exception) { // ignored } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/OpenArtBrowserViewModel.cs ================================================ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using Resources = StabilityMatrix.Avalonia.Languages.Resources; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(OpenArtBrowserPage))] [RegisterSingleton] public partial class OpenArtBrowserViewModel( IOpenArtApi openArtApi, INotificationService notificationService, ISettingsManager settingsManager, IPackageFactory packageFactory ) : TabViewModelBase, IInfinitelyScroll { private const int PageSize = 20; public override string Header => Resources.Label_OpenArtBrowser; private readonly SourceCache searchResultsCache = new(x => x.Id); [ObservableProperty] [NotifyPropertyChangedFor(nameof(PageCount), nameof(CanGoBack), nameof(CanGoForward), nameof(CanGoToEnd))] private OpenArtSearchResponse? latestSearchResponse; [ObservableProperty] private IObservableCollection searchResults = new ObservableCollectionExtended(); [ObservableProperty] private string searchQuery = string.Empty; [ObservableProperty] private bool isLoading; [ObservableProperty] [NotifyPropertyChangedFor(nameof(InternalPageNumber), nameof(CanGoBack))] private int displayedPageNumber = 1; public int InternalPageNumber => DisplayedPageNumber - 1; public int PageCount => Math.Max( 1, Convert.ToInt32(Math.Ceiling((LatestSearchResponse?.Total ?? 0) / Convert.ToDouble(PageSize))) ); public bool CanGoBack => string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && InternalPageNumber > 0; public bool CanGoForward => !string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) || PageCount > InternalPageNumber + 1; public bool CanGoToEnd => string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor) && PageCount > InternalPageNumber + 1; public IEnumerable AllSortModes => ["Trending", "Latest", "Most Downloaded", "Most Liked"]; [ObservableProperty] private string? selectedSortMode; protected override void OnInitialLoaded() { searchResultsCache .Connect() .DeferUntilLoaded() .Bind(SearchResults) .ObserveOn(SynchronizationContext.Current) .Subscribe(); SelectedSortMode = AllSortModes.First(); DoSearch().SafeFireAndForget(); } [RelayCommand] private async Task FirstPage() { DisplayedPageNumber = 1; searchResultsCache.Clear(); await DoSearch(); } [RelayCommand] private async Task PreviousPage() { DisplayedPageNumber--; searchResultsCache.Clear(); await DoSearch(InternalPageNumber); } [RelayCommand] private async Task NextPage() { if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { DisplayedPageNumber++; } searchResultsCache.Clear(); await DoSearch(InternalPageNumber); } [RelayCommand] private async Task LastPage() { if (string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { DisplayedPageNumber = PageCount; } searchResultsCache.Clear(); await DoSearch(PageCount - 1); } [Localizable(false)] [RelayCommand] private void OpenModel(OpenArtSearchResult workflow) { ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); } [RelayCommand] private async Task SearchButton() { DisplayedPageNumber = 1; LatestSearchResponse = null; searchResultsCache.Clear(); await DoSearch(); } [RelayCommand] private async Task OpenWorkflow(OpenArtSearchResult workflow) { var vm = new OpenArtWorkflowViewModel(settingsManager, packageFactory) { Workflow = workflow }; var dialog = new BetterContentDialog { IsPrimaryButtonEnabled = true, IsSecondaryButtonEnabled = true, PrimaryButtonText = Resources.Action_Import, SecondaryButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, IsFooterVisible = true, MaxDialogWidth = 750, MaxDialogHeight = 850, CloseOnClickOutside = true, Content = vm }; var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; List steps = [ new DownloadOpenArtWorkflowStep(openArtApi, vm.Workflow, settingsManager) ]; // Add install steps if missing nodes and preferred if ( vm is { InstallRequiredNodes: true, MissingNodes: { Count: > 0 } missingNodes, SelectedPackage: not null, SelectedPackagePair: not null } ) { var extensionManager = vm.SelectedPackagePair.BasePackage.ExtensionManager!; steps.AddRange( missingNodes.Select( extension => new InstallExtensionStep( extensionManager, vm.SelectedPackagePair.InstalledPackage, extension ) ) ); } var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = Resources.Label_WorkflowImported, ModificationCompleteMessage = Resources.Label_FinishedImportingWorkflow }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); notificationService.Show( Resources.Label_WorkflowImported, Resources.Label_WorkflowImportComplete, NotificationType.Success ); EventManager.Instance.OnWorkflowInstalled(); } [RelayCommand] private void OpenOnOpenArt(OpenArtSearchResult? workflow) { if (workflow?.Id == null) return; ProcessRunner.OpenUrl($"https://openart.ai/workflows/{workflow.Id}"); } private async Task DoSearch(int page = 0) { IsLoading = true; try { OpenArtSearchResponse? response = null; if (string.IsNullOrWhiteSpace(SearchQuery)) { var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { request.Cursor = LatestSearchResponse.NextCursor; } response = await openArtApi.GetFeedAsync(request); } else { response = await openArtApi.SearchAsync( new OpenArtSearchRequest { Keyword = SearchQuery, PageSize = PageSize, CurrentPage = page } ); } foreach (var item in response.Items) { searchResultsCache.AddOrUpdate(item); } LatestSearchResponse = response; } catch (ApiException e) { notificationService.Show(Resources.Label_ErrorRetrievingWorkflows, e.Message); } finally { IsLoading = false; } } partial void OnSelectedSortModeChanged(string? value) { if (value is null || SearchResults.Count == 0) return; searchResultsCache.Clear(); LatestSearchResponse = null; DoSearch().SafeFireAndForget(); } public async Task LoadNextPageAsync() { if (!CanGoForward) return; try { OpenArtSearchResponse? response = null; if (string.IsNullOrWhiteSpace(SearchQuery)) { var request = new OpenArtFeedRequest { Sort = GetSortMode(SelectedSortMode) }; if (!string.IsNullOrWhiteSpace(LatestSearchResponse?.NextCursor)) { request.Cursor = LatestSearchResponse.NextCursor; } response = await openArtApi.GetFeedAsync(request); } else { DisplayedPageNumber++; response = await openArtApi.SearchAsync( new OpenArtSearchRequest { Keyword = SearchQuery, PageSize = PageSize, CurrentPage = InternalPageNumber } ); } foreach (var item in response.Items) { searchResultsCache.AddOrUpdate(item); } LatestSearchResponse = response; } catch (ApiException e) { notificationService.Show("Unable to load the next page", e.Message); } } private static string GetSortMode(string? sortMode) { return sortMode switch { "Trending" => "trending", "Latest" => "latest", "Most Downloaded" => "most_downloaded", "Most Liked" => "most_liked", _ => "trending" }; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs ================================================ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage; public class OutputImageViewModel : SelectableViewModelBase { public OutputImageViewModel(LocalImageFile imageFile) { ImageFile = imageFile; } public LocalImageFile ImageFile { get; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Media; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Nito.Disposables.Internals; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.VendorLabs; using StabilityMatrix.Avalonia.Controls.VendorLabs.Cache; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.OutputsPage; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(Views.OutputsPage))] [RegisterSingleton] public partial class OutputsPageViewModel : PageViewModelBase { private readonly ISettingsManager settingsManager; private readonly IPackageFactory packageFactory; private readonly INotificationService notificationService; private readonly INavigationService navigationService; private readonly ILogger logger; private readonly List cancellationTokenSources = []; private readonly IServiceManager vmFactory; public override string Title => Resources.Label_OutputsPageTitle; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IconVariant = IconVariant.Filled }; public SourceCache OutputsCache { get; } = new(file => file.AbsolutePath); private SourceCache categoriesCache = new(category => category.Path); public IObservableCollection Outputs { get; set; } = new ObservableCollectionExtended(); public IObservableCollection Categories { get; set; } = new ObservableCollectionExtended(); [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanShowOutputTypes))] private TreeViewDirectory? selectedCategory; [ObservableProperty] private SharedOutputType? selectedOutputType; [ObservableProperty] [NotifyPropertyChangedFor(nameof(NumImagesSelected))] private int numItemsSelected; [ObservableProperty] private string searchQuery; [ObservableProperty] private bool isConsolidating; [ObservableProperty] private bool isLoading; [ObservableProperty] private bool showFolders; [ObservableProperty] private bool isChangingCategory; [ObservableProperty] private double resizeFactor; public bool CanShowOutputTypes => SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false; public string NumImagesSelected => NumItemsSelected == 1 ? Resources.Label_OneImageSelected : string.Format(Resources.Label_NumImagesSelected, NumItemsSelected); private string[] allowedExtensions = [".png", ".webp", ".jpg", ".jpeg", ".gif"]; private TreeViewDirectory? lastOutputCategory; public OutputsPageViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory, INotificationService notificationService, INavigationService navigationService, ILogger logger, IServiceManager vmFactory ) { this.settingsManager = settingsManager; this.packageFactory = packageFactory; this.notificationService = notificationService; this.navigationService = navigationService; this.logger = logger; this.vmFactory = vmFactory; var searcher = new ImageSearcher(); // Observable predicate from SearchQuery changes var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(100))! .Select(property => searcher.GetPredicate(property.Value)) .ObserveOn(SynchronizationContext.Current) .AsObservable(); OutputsCache .Connect() .DeferUntilLoaded() .Filter(searchPredicate) .Transform(file => new OutputImageViewModel(file)) .Sort( SortExpressionComparer .Descending(vm => vm.ImageFile.CreatedAt) .ThenByDescending(vm => vm.ImageFile.FileName) ) .Bind(Outputs) .WhenPropertyChanged(p => p.IsSelected) .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { NumItemsSelected = Outputs.Count(o => o.IsSelected); }); categoriesCache .Connect() .DeferUntilLoaded() .Bind(Categories) .ObserveOn(SynchronizationContext.Current) .Subscribe(); settingsManager.RelayPropertyFor( this, vm => vm.ResizeFactor, settings => settings.OutputsPageResizeFactor, true, delay: TimeSpan.FromMilliseconds(250) ); settingsManager.RelayPropertyFor( this, vm => vm.ShowFolders, settings => settings.IsOutputsTreeViewEnabled, true ); } protected override void OnInitialLoaded() { if (Design.IsDesignMode) return; if (!settingsManager.IsLibraryDirSet) return; Directory.CreateDirectory(settingsManager.ImagesDirectory); RefreshCategories(); SelectedCategory ??= Categories.First(); SelectedOutputType ??= SharedOutputType.All; SearchQuery = string.Empty; lastOutputCategory = SelectedCategory; IsChangingCategory = true; var path = CanShowOutputTypes && SelectedOutputType != SharedOutputType.All ? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString()) : SelectedCategory.Path; GetOutputs(path); } public override void OnUnloaded() { base.OnUnloaded(); logger.LogTrace("OutputsPageViewModel Unloaded"); logger.LogTrace("Clearing memory cache"); var items = ImageLoaders.OutputsPageImageCache.ClearMemoryCache(); logger.LogTrace("Cleared {Items} items from memory cache", items); } partial void OnSelectedCategoryChanged(TreeViewDirectory? oldValue, TreeViewDirectory? newValue) { if (oldValue == newValue || oldValue == null || newValue == null) return; var path = CanShowOutputTypes && SelectedOutputType != SharedOutputType.All ? Path.Combine(newValue.Path, SelectedOutputType.ToString()) : SelectedCategory.Path; GetOutputs(path); lastOutputCategory = newValue; } partial void OnSelectedOutputTypeChanged(SharedOutputType? oldValue, SharedOutputType? newValue) { if (oldValue == newValue || oldValue == null || newValue == null) return; var path = newValue == SharedOutputType.All ? SelectedCategory?.Path : Path.Combine(SelectedCategory.Path, newValue.ToString()); GetOutputs(path); } public Task OnImageClick(OutputImageViewModel item) { // Select image if we're in "select mode" if (NumItemsSelected > 0) { item.IsSelected = !item.IsSelected; } else { return ShowImageDialog(item); } return Task.CompletedTask; } public async Task ShowImageDialog(OutputImageViewModel item) { var currentIndex = Outputs.IndexOf(item); var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath)); // Preload await image.GetBitmapAsync(); var vm = vmFactory.Get(); vm.ImageSource = image; vm.LocalImageFile = item.ImageFile; using var onNext = Observable .FromEventPattern( vm, nameof(ImageViewerViewModel.NavigationRequested) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(ctx => { Dispatcher .UIThread.InvokeAsync(async () => { var sender = (ImageViewerViewModel)ctx.Sender!; var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1); if (newIndex >= 0 && newIndex < Outputs.Count) { var newImage = Outputs[newIndex]; var newImageSource = new ImageSource( new FilePath(newImage.ImageFile.AbsolutePath) ); // Preload await newImageSource.GetBitmapAsync(); await newImageSource.GetOrRefreshTemplateKeyAsync(); sender.ImageSource = newImageSource; sender.LocalImageFile = newImage.ImageFile; currentIndex = newIndex; } }) .SafeFireAndForget(); }); await vm.GetDialog().ShowAsync(); } public Task CopyImage(string imagePath) { var clipboard = App.Clipboard; return clipboard.SetFileDataObjectAsync(imagePath); } public Task OpenImage(string imagePath) => ProcessRunner.OpenFileBrowser(imagePath); public void Refresh() { Dispatcher.UIThread.Post(RefreshCategories); var path = CanShowOutputTypes && SelectedOutputType != SharedOutputType.All ? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString()) : SelectedCategory.Path; GetOutputs(path); } public async Task DeleteImage(OutputImageViewModel? item) { if (item is null) return; var itemPath = item.ImageFile.AbsolutePath; var pathsToDelete = new List { itemPath }; // Add .txt sidecar to paths if they exist var sideCar = Path.ChangeExtension(itemPath, ".txt"); if (File.Exists(sideCar)) { pathsToDelete.Add(sideCar); } var vm = vmFactory.Get(); vm.PathsToDelete = pathsToDelete; if (await vm.GetDialog().ShowAsync() != ContentDialogResult.Primary) { return; } try { await vm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting files", e.Message, NotificationType.Error); Refresh(); return; } OutputsCache.Remove(item.ImageFile); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) { loader.RemoveAllNamesFromCache(itemPath); } } public void SendToTextToImage(OutputImageViewModel vm) { navigationService.NavigateTo(); EventManager.Instance.OnInferenceProjectRequested(vm.ImageFile, InferenceProjectType.TextToImage); } public void SendToUpscale(OutputImageViewModel vm) { navigationService.NavigateTo(); EventManager.Instance.OnInferenceProjectRequested(vm.ImageFile, InferenceProjectType.Upscale); } public void SendToImageToImage(OutputImageViewModel vm) { navigationService.NavigateTo(); EventManager.Instance.OnInferenceProjectRequested(vm.ImageFile, InferenceProjectType.ImageToImage); } public void SendToImageToVideo(OutputImageViewModel vm) { navigationService.NavigateTo(); EventManager.Instance.OnInferenceProjectRequested(vm.ImageFile, InferenceProjectType.ImageToVideo); } public void ClearSelection() { foreach (var output in Outputs) { output.IsSelected = false; } } public void SelectAll() { foreach (var output in Outputs) { output.IsSelected = true; } } public async Task DeleteAllSelected() { var pathsToDelete = new List(); foreach (var path in Outputs.Where(o => o.IsSelected).Select(o => o.ImageFile.AbsolutePath)) { pathsToDelete.Add(path); // Add .txt sidecars to paths if they exist var sideCar = Path.ChangeExtension(path, ".txt"); if (File.Exists(sideCar)) { pathsToDelete.Add(sideCar); } } var vm = vmFactory.Get(); vm.PathsToDelete = pathsToDelete; if (await vm.GetDialog().ShowAsync() != ContentDialogResult.Primary) { return; } try { await vm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting files", e.Message, NotificationType.Error); Refresh(); return; } OutputsCache.Remove(pathsToDelete); NumItemsSelected = 0; ClearSelection(); } public async Task ConsolidateImages() { var stackPanel = new StackPanel(); stackPanel.Children.Add( new TextBlock { Text = Resources.Label_ConsolidateExplanation, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 8, 0, 16), } ); foreach (var category in Categories) { if (category.Name == "Shared Output Folder") { continue; } stackPanel.Children.Add( new CheckBox { Content = $"{category.Name} ({category.Path})", IsChecked = true, Margin = new Thickness(0, 8, 0, 0), Tag = category.Path, } ); } var confirmationDialog = new BetterContentDialog { Title = Resources.Label_AreYouSure, Content = stackPanel, PrimaryButtonText = Resources.Action_Yes, SecondaryButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, IsSecondaryButtonEnabled = true, }; var dialogResult = await confirmationDialog.ShowAsync(); if (dialogResult != ContentDialogResult.Primary) return; IsConsolidating = true; Directory.CreateDirectory(settingsManager.ConsolidatedImagesDirectory); foreach (var category in stackPanel.Children.OfType().Where(c => c.IsChecked == true)) { if ( string.IsNullOrWhiteSpace(category.Tag?.ToString()) || !Directory.Exists(category.Tag?.ToString()) ) continue; var directory = category.Tag.ToString(); foreach ( var path in Directory.EnumerateFiles( directory, "*", EnumerationOptionConstants.AllDirectories ) ) { try { var file = new FilePath(path); if (!allowedExtensions.Contains(file.Extension)) continue; var newPath = settingsManager.ConsolidatedImagesDirectory + file.Name; if (file.FullPath == newPath) continue; // ignore inference if not in inference directory if ( file.FullPath.Contains(settingsManager.ImagesInferenceDirectory) && directory != settingsManager.ImagesInferenceDirectory ) { continue; } await file.MoveToWithIncrementAsync(newPath); var sideCar = new FilePath(Path.ChangeExtension(file, ".txt")); //If a .txt sidecar file exists, and the image was moved successfully, try to move the sidecar along with the image if (File.Exists(newPath) && File.Exists(sideCar)) { var newSidecar = new FilePath(Path.ChangeExtension(newPath, ".txt")); await sideCar.MoveToWithIncrementAsync(newSidecar); } } catch (Exception e) { logger.LogError(e, "Error when consolidating: "); } } } Refresh(); IsConsolidating = false; } public void ClearSearchQuery() { SearchQuery = string.Empty; } private void GetOutputs(string directory) { if (!settingsManager.IsLibraryDirSet) return; if ( !Directory.Exists(directory) && ( SelectedCategory.Path != settingsManager.ImagesDirectory || SelectedOutputType != SharedOutputType.All ) ) { Directory.CreateDirectory(directory); return; } if (lastOutputCategory?.Path.Equals(directory) is not true) { OutputsCache.Clear(); IsChangingCategory = true; } IsLoading = true; cancellationTokenSources.ForEach(cts => cts.Cancel()); Task.Run(() => { var getOutputsTokenSource = new CancellationTokenSource(); cancellationTokenSources.Add(getOutputsTokenSource); if (getOutputsTokenSource.IsCancellationRequested) { cancellationTokenSources.Remove(getOutputsTokenSource); return; } var files = Directory .EnumerateFiles(directory, "*", EnumerationOptionConstants.AllDirectories) .Where(file => allowedExtensions.Contains(new FilePath(file).Extension) && new FilePath(file).Info.DirectoryName?.EndsWith( "thumbnails", StringComparison.OrdinalIgnoreCase ) is false ) .Select(file => LocalImageFile.FromPath(file)) .ToList(); if (getOutputsTokenSource.IsCancellationRequested) { cancellationTokenSources.Remove(getOutputsTokenSource); return; } Dispatcher.UIThread.Post(() => { if (files.Count == 0 && OutputsCache.Count == 0) { IsLoading = false; IsChangingCategory = false; return; } OutputsCache.EditDiff( files, (oldItem, newItem) => oldItem.AbsolutePath == newItem.AbsolutePath ); IsLoading = false; IsChangingCategory = false; }); cancellationTokenSources.Remove(getOutputsTokenSource); }); } private void RefreshCategories() { if (Design.IsDesignMode) return; if (!settingsManager.IsLibraryDirSet) return; var previouslySelectedCategory = SelectedCategory; var packageCategories = settingsManager .Settings.InstalledPackages.Where(x => !x.UseSharedOutputFolder) .Select(packageFactory.GetPackagePair) .WhereNotNull() .Where(p => p.BasePackage.SharedOutputFolders is { Count: > 0 } && p.InstalledPackage is { FullPath: not null } ) .Select(pair => new TreeViewDirectory { Path = Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName), Name = pair.InstalledPackage.DisplayName ?? "", SubDirectories = GetSubfolders( Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName) ), }) .OrderBy(d => d.Name) .ToList(); packageCategories.Insert( 0, new TreeViewDirectory { Path = settingsManager.ImagesDirectory, Name = "Shared Output Folder", SubDirectories = GetSubfolders(settingsManager.ImagesDirectory), } ); categoriesCache.Edit(updater => updater.Load(packageCategories)); if (!string.IsNullOrWhiteSpace(previouslySelectedCategory?.Path)) { FindAndExpandPathToCategory(Categories, previouslySelectedCategory.Path); } SelectedCategory = FindCategoryByPath(Categories, previouslySelectedCategory?.Path) ?? Categories.FirstOrDefault(); } private ObservableCollection GetSubfolders(string strPath) { var subfolders = new ObservableCollection(); if (!Directory.Exists(strPath)) return subfolders; var directories = Directory.EnumerateDirectories( strPath, "*", EnumerationOptionConstants.TopLevelOnly ); foreach (var dir in directories) { var category = new TreeViewDirectory { Name = Path.GetFileName(dir), Path = dir }; if (Directory.GetDirectories(dir, "*", EnumerationOptionConstants.TopLevelOnly).Length > 0) { category.SubDirectories = GetSubfolders(dir); } subfolders.Add(category); } subfolders = new ObservableCollection(subfolders.OrderBy(d => d.Name)); return subfolders; } private TreeViewDirectory? FindCategoryByPath(IEnumerable nodes, string? path) { // Can't find a category if the path is null or empty if (string.IsNullOrEmpty(path)) { return null; } foreach (var node in nodes) { // Check if the current node is the one we're looking for if (node.Path == path) { return node; } if (node.SubDirectories is not { Count: > 0 }) continue; // If not, and if this node has children, search within its children var foundInChildren = FindCategoryByPath(node.SubDirectories, path); if (foundInChildren != null) { // Found it in a sub-directory, so return it up the call stack return foundInChildren; } } // If we've searched all nodes at this level and their children without success return null; } /// /// Recursively searches for a category by its path and expands all parent nodes along the way. /// /// The collection of nodes to search within. /// The path of the node to find. /// True if the target node was found in this branch; otherwise, false. private bool FindAndExpandPathToCategory(IEnumerable nodes, string? targetPath) { if (string.IsNullOrEmpty(targetPath)) { return false; } foreach (var node in nodes) { // First, check if the target is a direct descendant of this node. // This is the recursive part. if ( node.SubDirectories is { Count: > 0 } && FindAndExpandPathToCategory(node.SubDirectories, targetPath) ) { // If the recursive call returns true, it means the target was found // in this node's children. Therefore, this node is an ancestor and // must be expanded. node.IsExpanded = true; return true; // Bubble "true" up to the parent. } // After checking children, check if the current node itself is the target. if (node.Path == targetPath) { return true; // Found it. Start the "bubble up" of true values. } } return false; // Target was not found in this collection of nodes. } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManager/MainPackageManagerViewModel.cs ================================================ using System.Collections.Immutable; using System.Reactive.Linq; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services; using MainPackageManagerView = StabilityMatrix.Avalonia.Views.PackageManager.MainPackageManagerView; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; /// /// This is our ViewModel for the second page /// [View(typeof(MainPackageManagerView))] [ManagedService] [RegisterSingleton] public partial class MainPackageManagerViewModel : PageViewModelBase { private readonly ISettingsManager settingsManager; private readonly IServiceManager dialogFactory; private readonly INotificationService notificationService; private readonly INavigationService packageNavigationService; private readonly ILogger logger; private readonly RunningPackageService runningPackageService; public override string Title => Resources.Label_Packages; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IconVariant = IconVariant.Filled }; /// /// List of installed packages /// private readonly SourceCache installedPackages = new(p => p.Id); /// /// List of indexed packages without a corresponding installed package /// private readonly SourceCache unknownInstalledPackages = new(p => p.Id); public IObservableCollection Packages { get; } = new ObservableCollectionExtended(); public IObservableCollection PackageCards { get; } = new ObservableCollectionExtended(); private DispatcherTimer timer; public MainPackageManagerViewModel( ISettingsManager settingsManager, IServiceManager dialogFactory, INotificationService notificationService, INavigationService packageNavigationService, ILogger logger, RunningPackageService runningPackageService ) { this.settingsManager = settingsManager; this.dialogFactory = dialogFactory; this.notificationService = notificationService; this.packageNavigationService = packageNavigationService; this.logger = logger; this.runningPackageService = runningPackageService; EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged; EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; var installed = installedPackages.Connect(); var unknown = unknownInstalledPackages.Connect(); installed .Or(unknown) .DeferUntilLoaded() .Bind(Packages) .Transform(p => dialogFactory.Get(vm => { vm.Package = p; vm.OnLoadedAsync().SafeFireAndForget(); }) ) .Bind(PackageCards) .ObserveOn(SynchronizationContext.Current) .Subscribe(); timer = new DispatcherTimer { Interval = TimeSpan.FromMinutes(60), IsEnabled = true }; timer.Tick += async (_, _) => await CheckPackagesForUpdates(); } private void OnOneClickInstallFinished(object? sender, bool e) { Dispatcher.UIThread.Post(() => OnLoadedAsync().SafeFireAndForget()); } public void SetPackages(IEnumerable packages) { installedPackages.Edit(s => s.Load(packages)); } public void SetUnknownPackages(IEnumerable packages) { unknownInstalledPackages.Edit(s => s.Load(packages)); } protected override async Task OnInitialLoadedAsync() { if (string.IsNullOrWhiteSpace(Program.Args.LaunchPackageName)) { await base.OnInitialLoadedAsync(); return; } await LoadPackages(); var package = Packages.FirstOrDefault(x => x.DisplayName == Program.Args.LaunchPackageName); if (package is not null) { await runningPackageService.StartPackage(package); return; } package = Packages.FirstOrDefault(x => x.Id.ToString() == Program.Args.LaunchPackageName); if (package is null) { await base.OnInitialLoadedAsync(); return; } await runningPackageService.StartPackage(package); } public override async Task OnLoadedAsync() { if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet) return; await LoadPackages(); timer.Start(); } public override void OnUnloaded() { timer.Stop(); base.OnUnloaded(); } public void ShowInstallDialog(BasePackage? selectedPackage = null) { NavigateToSubPage(typeof(PackageInstallBrowserViewModel)); } private async Task LoadPackages() { installedPackages.EditDiff(settingsManager.Settings.InstalledPackages, InstalledPackage.Comparer); var currentUnknown = await Task.Run(IndexUnknownPackages); unknownInstalledPackages.Edit(s => s.Load(currentUnknown)); } private async Task CheckPackagesForUpdates() { foreach (var package in PackageCards) { try { await package.OnLoadedAsync(); } catch (Exception e) { logger.LogError( e, "Failed to check for updates for {Package}", package?.Package?.PackageName ); } } } private IEnumerable IndexUnknownPackages() { var packageDir = settingsManager.LibraryDir.JoinDir("Packages"); if (!packageDir.Exists) { yield break; } var currentPackages = settingsManager.Settings.InstalledPackages.ToImmutableArray(); foreach (var subDir in packageDir.Info.EnumerateDirectories().Select(info => new DirectoryPath(info))) { var expectedLibraryPath = $"Packages{Path.DirectorySeparatorChar}{subDir.Name}"; // Skip if the package is already installed if (currentPackages.Any(p => p.LibraryPath == expectedLibraryPath)) { continue; } if (settingsManager.PackageInstallsInProgress.Contains(subDir.Name)) { continue; } yield return UnknownInstalledPackage.FromDirectoryName(subDir.Name); } } [RelayCommand] private void NavigateToSubPage(Type viewModelType) { Dispatcher.UIThread.Post( () => packageNavigationService.NavigateTo( viewModelType, BetterSlideNavigationTransition.PageSlideFromRight ), DispatcherPriority.Send ); } private void OnInstalledPackagesChanged(object? sender, EventArgs e) => OnLoadedAsync().SafeFireAndForget(); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs ================================================ using System.Collections.Immutable; using System.Collections.Specialized; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [ManagedService] [RegisterTransient] public partial class PackageCardViewModel( ILogger logger, IPackageFactory packageFactory, INotificationService notificationService, ISettingsManager settingsManager, INavigationService navigationService, IServiceManager vmFactory, RunningPackageService runningPackageService ) : ProgressViewModel { private string webUiUrl = string.Empty; private string? lastLaunchCommand = null; [ObservableProperty] [NotifyPropertyChangedFor(nameof(PackageDisplayName))] private InstalledPackage? package; public string? PackageDisplayName => Package?.DisplayName; [ObservableProperty] private Uri? cardImageSource; [ObservableProperty] private bool isUpdateAvailable; [ObservableProperty] private string? installedVersion; [ObservableProperty] private bool isUnknownPackage; [ObservableProperty] private bool isSharedModelSymlink; [ObservableProperty] private bool isSharedModelConfig; [ObservableProperty] private bool isSharedModelDisabled; [ObservableProperty] private bool canUseConfigMethod; [ObservableProperty] private bool canUseSymlinkMethod; [ObservableProperty] private bool useSharedOutput; [ObservableProperty] private bool canUseSharedOutput; [ObservableProperty] private bool canUseExtensions; [ObservableProperty] private bool isRunning; [ObservableProperty] private bool showWebUiButton; [ObservableProperty] private DownloadPackageVersionOptions? updateVersion; [ObservableProperty] private bool dontCheckForUpdates; [ObservableProperty] private bool usesVenv; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowExtraCommands))] private List? extraCommands; [ObservableProperty] private IReadOnlyDictionary extraLaunchCommands = new Dictionary(); public bool ShowExtraCommands => ExtraCommands is { Count: > 0 }; private void RunningPackagesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (runningPackageService.RunningPackages.Select(x => x.Value) is not { } runningPackages) return; var runningViewModel = runningPackages.FirstOrDefault(x => x.RunningPackage.InstalledPackage.Id == Package?.Id ); if (runningViewModel is not null) { IsRunning = true; runningViewModel.RunningPackage.BasePackage.Exited += BasePackageOnExited; runningViewModel.RunningPackage.BasePackage.StartupComplete += RunningPackageOnStartupComplete; } else if (runningViewModel is null && IsRunning) { IsRunning = false; ShowWebUiButton = false; } } partial void OnPackageChanged(InstalledPackage? value) { if (string.IsNullOrWhiteSpace(value?.PackageName)) return; if ( value.PackageName == UnknownPackage.Key || packageFactory.FindPackageByName(value.PackageName) is null ) { IsUnknownPackage = true; CardImageSource = null; InstalledVersion = "Unknown"; } else { IsUnknownPackage = false; var basePackage = packageFactory[value.PackageName!]; CardImageSource = basePackage?.PreviewImageUri ?? Assets.NoImage; InstalledVersion = value.Version?.DisplayVersion ?? "Unknown"; CanUseConfigMethod = basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false; CanUseSymlinkMethod = basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Symlink) ?? false; UseSharedOutput = Package?.UseSharedOutputFolder ?? false; CanUseSharedOutput = basePackage?.SharedOutputFolders != null; CanUseExtensions = basePackage?.SupportsExtensions ?? false; DontCheckForUpdates = Package?.DontCheckForUpdates ?? false; UsesVenv = basePackage?.UsesVenv ?? true; ExtraLaunchCommands = basePackage?.ExtraLaunchCommands ?? new Dictionary(); // Set the extra commands if available from the package var packageExtraCommands = basePackage?.GetExtraCommands(); ExtraCommands = packageExtraCommands?.Count > 0 ? packageExtraCommands : null; runningPackageService.RunningPackages.CollectionChanged += RunningPackagesOnCollectionChanged; EventManager.Instance.PackageRelaunchRequested += InstanceOnPackageRelaunchRequested; } } private async Task InstanceOnPackageRelaunchRequested( object? sender, InstalledPackage e, RunPackageOptions options ) { if (e.Id != Package?.Id) return; navigationService.GoBack(); await Launch(options.Command); } public override async Task OnLoadedAsync() { if (Design.IsDesignMode && Package?.DisplayName == "Running Comfy") { IsRunning = true; IsUpdateAvailable = true; ShowWebUiButton = true; } if (Design.IsDesignMode || !settingsManager.IsLibraryDirSet || Package is not { } currentPackage) return; if ( packageFactory.FindPackageByName(currentPackage.PackageName) is { } basePackage and not UnknownPackage ) { // Migrate old packages with null preferred shared folder method currentPackage.PreferredSharedFolderMethod ??= basePackage.RecommendedSharedFolderMethod; switch (currentPackage.PreferredSharedFolderMethod) { case SharedFolderMethod.Configuration: IsSharedModelConfig = true; break; case SharedFolderMethod.Symlink: IsSharedModelSymlink = true; break; case SharedFolderMethod.None: IsSharedModelDisabled = true; break; default: throw new ArgumentOutOfRangeException(); } IsUpdateAvailable = await HasUpdate(); if (IsUpdateAvailable) { UpdateVersion = await basePackage.GetUpdate(currentPackage); } if ( Package != null && !IsRunning && runningPackageService.RunningPackages.TryGetValue(Package.Id, out var runningPackageVm) ) { IsRunning = true; runningPackageVm.RunningPackage.BasePackage.Exited += BasePackageOnExited; runningPackageVm.RunningPackage.BasePackage.StartupComplete += RunningPackageOnStartupComplete; webUiUrl = runningPackageVm.WebUiUrl; ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl); } } } protected override void Dispose(bool disposing) { if (!disposing) return; EventManager.Instance.PackageRelaunchRequested -= InstanceOnPackageRelaunchRequested; runningPackageService.RunningPackages.CollectionChanged -= RunningPackagesOnCollectionChanged; // Cleanup any running package event handlers if ( Package?.Id != null && runningPackageService.RunningPackages.TryGetValue(Package.Id, out var runningPackageVm) ) { runningPackageVm.RunningPackage.BasePackage.Exited -= BasePackageOnExited; runningPackageVm.RunningPackage.BasePackage.StartupComplete -= RunningPackageOnStartupComplete; } } public async Task Launch(string? command = null) { if (Package == null) return; var packagePair = await runningPackageService.StartPackage(Package, command); if (packagePair != null) { IsRunning = true; lastLaunchCommand = command; packagePair.BasePackage.Exited += BasePackageOnExited; packagePair.BasePackage.StartupComplete += RunningPackageOnStartupComplete; var vm = runningPackageService.GetRunningPackageViewModel(packagePair.InstalledPackage.Id); if (vm != null) { navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition()); } } // settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id); // // navigationService.NavigateTo(new BetterDrillInNavigationTransition()); // EventManager.Instance.OnPackageLaunchRequested(Package.Id); } public void NavToConsole() { if (Package == null) return; var vm = runningPackageService.GetRunningPackageViewModel(Package.Id); if (vm != null) { navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition()); } } public void LaunchWebUi() { if (string.IsNullOrEmpty(webUiUrl)) return; notificationService.TryAsync( Task.Run(() => ProcessRunner.OpenUrl(webUiUrl)), "Failed to open URL", $"{webUiUrl}" ); } private void BasePackageOnExited(object? sender, int exitCode) { Dispatcher .UIThread.InvokeAsync(async () => { logger.LogTrace("Process exited ({Code}) at {Time:g}", exitCode, DateTimeOffset.Now); // Need to wait for streams to finish before detaching handlers if (sender is BaseGitPackage { VenvRunner: not null } package) { var process = package.VenvRunner.Process; if (process is not null) { // Max 5 seconds var ct = new CancellationTokenSource(5000).Token; try { await process.WaitUntilOutputEOF(ct); } catch (OperationCanceledException e) { logger.LogWarning("Waiting for process EOF timed out: {Message}", e.Message); } } } // Detach handlers if (sender is BasePackage basePackage) { basePackage.Exited -= BasePackageOnExited; basePackage.StartupComplete -= RunningPackageOnStartupComplete; } if (Package?.Id != null) { runningPackageService.RunningPackages.Remove(Package.Id); } IsRunning = false; ShowWebUiButton = false; }) .SafeFireAndForget(); } public async Task Uninstall() { if (Package?.LibraryPath == null) { return; } var dialogViewModel = vmFactory.Get(vm => { vm.Package = Package; }); var dialog = new BetterContentDialog { Content = dialogViewModel, IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { Text = Resources.Progress_UninstallingPackage; IsIndeterminate = true; Value = -1; var packagePath = new DirectoryPath(settingsManager.LibraryDir, Package.LibraryPath); var deleteTask = packagePath.DeleteVerboseAsync(logger); var taskResult = await notificationService.TryAsync( deleteTask, Resources.Text_SomeFilesCouldNotBeDeleted ); if (taskResult.IsSuccessful) { notificationService.Show( new Notification( Resources.Label_PackageUninstalled, Package.DisplayName, NotificationType.Success ) ); if (!IsUnknownPackage) { settingsManager.Transaction(settings => { settings.RemoveInstalledPackageAndUpdateActive(Package); }); } EventManager.Instance.OnInstalledPackagesChanged(); } Text = ""; IsIndeterminate = false; Value = 0; } } public async Task Update() { if (Package is null || IsUnknownPackage) return; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) { logger.LogWarning("Could not find package {SelectedPackagePackageName}", Package.PackageName); notificationService.Show( Resources.Label_InvalidPackageType, Package.PackageName.ToRepr(), NotificationType.Error ); return; } if (!await ShowPythonUpgradeDialogIfNeeded(basePackage, Package)) return; var packageName = Package.DisplayName ?? Package.PackageName ?? ""; Text = $"Updating {packageName}"; IsIndeterminate = true; try { var runner = new PackageModificationRunner { ModificationCompleteMessage = $"Updated {packageName}", ModificationFailedMessage = $"Could not update {packageName}", }; runner.Completed += (_, completedRunner) => { notificationService.OnPackageInstallCompleted(completedRunner); }; var versionOptions = new DownloadPackageVersionOptions { IsLatest = true }; if (Package.Version.IsReleaseMode) { versionOptions = await basePackage.GetLatestVersion(Package.Version.IsPrerelease); } else { var commits = await basePackage.GetAllCommits(Package.Version.InstalledBranch); var latest = commits?.FirstOrDefault(); if (latest == null) throw new Exception("Could not find latest commit"); versionOptions.BranchName = Package.Version.InstalledBranch; versionOptions.CommitHash = latest.Sha; } var updatePackageStep = new UpdatePackageStep( settingsManager, basePackage, Package.FullPath!.Unwrap(), Package, new UpdatePackageOptions { VersionOptions = versionOptions, PythonOptions = { TorchIndex = Package.PreferredTorchIndex, PythonVersion = PyVersion.TryParse(Package.PythonVersion, out var pv) ? pv : null, }, } ); var steps = new List { updatePackageStep }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); IsUpdateAvailable = false; InstalledVersion = Package.Version?.DisplayVersion ?? "Unknown"; } catch (Exception e) { logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name); notificationService.ShowPersistent( string.Format(Resources.TextTemplate_ErrorUpdatingPackage, packageName), e.Message, NotificationType.Error ); } finally { IsIndeterminate = false; Value = 0; Text = ""; } } public async Task Import() { if (!IsUnknownPackage || Design.IsDesignMode) return; var viewModel = vmFactory.Get(vm => { vm.PackagePath = new DirectoryPath(Package?.FullPath ?? throw new InvalidOperationException()); }); var dialog = new TaskDialog { Content = new PackageImportDialog { DataContext = viewModel }, ShowProgressBar = false, Buttons = new List { new(Resources.Action_Import, TaskDialogStandardResult.Yes) { IsDefault = true }, new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel), }, }; dialog.Closing += async (sender, e) => { // We only want to use the deferral on the 'Yes' Button if ((TaskDialogStandardResult)e.Result == TaskDialogStandardResult.Yes) { var deferral = e.GetDeferral(); sender.ShowProgressBar = true; sender.SetProgressBarState(0, TaskDialogProgressState.Indeterminate); await using (new MinimumDelay(200, 300)) { var result = await notificationService.TryAsync(viewModel.AddPackageWithCurrentInputs()); if (result.IsSuccessful) { EventManager.Instance.OnInstalledPackagesChanged(); } } deferral.Complete(); } }; dialog.XamlRoot = App.VisualRoot; await dialog.ShowAsync(true); } public async Task OpenFolder() { if (string.IsNullOrWhiteSpace(Package?.FullPath)) return; await ProcessRunner.OpenFolderBrowser(Package.FullPath); } [RelayCommand] private async Task ChangeVersion() { if (Package is null || IsUnknownPackage) return; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) { logger.LogWarning("Could not find package {SelectedPackagePackageName}", Package.PackageName); notificationService.Show( Resources.Label_InvalidPackageType, Package.PackageName.ToRepr(), NotificationType.Error ); return; } if (!await ShowPythonUpgradeDialogIfNeeded(basePackage, Package)) return; var packageName = Package.DisplayName ?? Package.PackageName ?? ""; Text = $"Updating {packageName}"; IsIndeterminate = true; try { var viewModel = vmFactory.Get(vm => { vm.PackagePath = new DirectoryPath( Package?.FullPath ?? throw new InvalidOperationException() ); }); viewModel.SelectedBasePackage = basePackage; viewModel.CanSelectBasePackage = false; viewModel.ShowPythonVersionSelection = false; viewModel.IsReleaseMode = Package.Version?.IsReleaseMode ?? false; var dialog = new TaskDialog { Content = new PackageImportDialog { DataContext = viewModel }, ShowProgressBar = false, Buttons = new List { new(Resources.Action_Update, TaskDialogStandardResult.Yes) { IsDefault = true }, new(Resources.Action_Cancel, TaskDialogStandardResult.Cancel), }, XamlRoot = App.VisualRoot, }; var result = await dialog.ShowAsync(true); if (result is not TaskDialogStandardResult.Yes) return; var runner = new PackageModificationRunner { ModificationCompleteMessage = $"Updated {packageName}", ModificationFailedMessage = $"Could not update {packageName}", }; var versionOptions = new DownloadPackageVersionOptions(); if (!string.IsNullOrWhiteSpace(viewModel.CustomCommitSha)) { versionOptions.BranchName = viewModel.SelectedVersion?.TagName; versionOptions.CommitHash = viewModel.CustomCommitSha; } else if (viewModel.SelectedVersionType == PackageVersionType.GithubRelease) { versionOptions.VersionTag = viewModel.SelectedVersion?.TagName; } else { versionOptions.BranchName = viewModel.SelectedVersion?.TagName; versionOptions.CommitHash = viewModel.SelectedCommit?.Sha; } var updatePackageStep = new UpdatePackageStep( settingsManager, basePackage, Package.FullPath!.Unwrap(), Package, new UpdatePackageOptions { VersionOptions = versionOptions, PythonOptions = { TorchIndex = Package.PreferredTorchIndex, PythonVersion = PyVersion.TryParse(Package.PythonVersion, out var pyVer) ? pyVer : null, }, } ); var steps = new List { updatePackageStep }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); EventManager.Instance.OnInstalledPackagesChanged(); IsUpdateAvailable = false; InstalledVersion = Package.Version?.DisplayVersion ?? "Unknown"; if (runner.Failed) { notificationService.Show( Resources.Progress_UpdateFailed, string.Format(runner.ModificationFailedMessage, packageName), NotificationType.Error ); } else { notificationService.Show( Resources.Progress_UpdateComplete, string.Format(Resources.TextTemplate_PackageUpdatedToSelected, packageName), NotificationType.Success ); } } catch (Exception e) { logger.LogError(e, "Error Updating Package ({PackageName})", basePackage.Name); notificationService.ShowPersistent( string.Format(Resources.TextTemplate_ErrorUpdatingPackage, packageName), e.Message, NotificationType.Error ); } finally { IsIndeterminate = false; Value = 0; Text = ""; } } [RelayCommand] public async Task OpenPythonPackagesDialog() { if (Package is not { FullPath: not null }) return; var vm = vmFactory.Get(vm => { vm.VenvPath = new DirectoryPath(Package.FullPath, "venv"); vm.PythonVersion = PyVersion.Parse(Package.PythonVersion); }); await vm.GetDialog().ShowAsync(); } [RelayCommand] public async Task OpenPythonDependenciesOverrideDialog() { if (Package is not { FullPath: not null }) return; var vm = vmFactory.Get(); vm.LoadSpecifiers(Package.PipOverrides ?? []); if (await vm.GetDialog().ShowAsync() is ContentDialogResult.Primary) { await using var st = settingsManager.BeginTransaction(); Package.PipOverrides = vm.GetSpecifiers().ToList(); } } [RelayCommand] public async Task OpenExtensionsDialog() { if ( Package is not { FullPath: not null } || packageFactory.GetPackagePair(Package) is not { } packagePair ) return; var vm = vmFactory.Get(vm => { vm.PackagePair = packagePair; }); var dialog = new BetterContentDialog { Content = vm, MinDialogWidth = 850, MaxDialogHeight = 1100, MaxDialogWidth = 850, ContentMargin = new Thickness(16, 32), CloseOnClickOutside = true, FullSizeDesired = true, IsFooterVisible = false, ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, }; await dialog.ShowAsync(); } [RelayCommand] private void OpenOnGitHub() { if (Package is null) return; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) { logger.LogWarning("Could not find package {SelectedPackagePackageName}", Package.PackageName); return; } ProcessRunner.OpenUrl(basePackage.GithubUrl); } [RelayCommand] private async Task Stop() { if (Package is null) return; await runningPackageService.StopPackage(Package.Id); IsRunning = false; ShowWebUiButton = false; } [RelayCommand] private async Task Restart() { await Stop(); await Launch(lastLaunchCommand); } [RelayCommand] private async Task ShowLaunchOptions() { var basePackage = packageFactory.FindPackageByName(Package?.PackageName); if (basePackage == null) { logger.LogWarning("Package {Name} not found", Package?.PackageName); return; } var viewModel = vmFactory.Get(); viewModel.Cards = LaunchOptionCard .FromDefinitions(basePackage.LaunchOptions, Package?.LaunchArgs ?? []) .ToImmutableArray(); logger.LogDebug("Launching config dialog with cards: {CardsCount}", viewModel.Cards.Count); var dialog = new BetterContentDialog { ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, IsPrimaryButtonEnabled = true, PrimaryButtonText = Resources.Action_Save, CloseButtonText = Resources.Action_Cancel, FullSizeDesired = true, DefaultButton = ContentDialogButton.Primary, ContentMargin = new Thickness(32, 16), Padding = new Thickness(0, 16), Content = new LaunchOptionsDialog { DataContext = viewModel }, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary && Package != null) { // Save config var args = viewModel.AsLaunchArgs(); settingsManager.SaveLaunchArgs(Package.Id, args); } } [RelayCommand] private async Task Rename() { if (Package is null || IsUnknownPackage) return; var currentName = Package.DisplayName ?? Package.PackageName ?? string.Empty; var field = new TextBoxField { Label = Resources.Label_DisplayName, Text = currentName, Watermark = Resources.Watermark_EnterPackageName, Validator = text => { if (string.IsNullOrWhiteSpace(text)) { throw new DataValidationException(Resources.Validation_PackageNameCannotBeEmpty); } var directoryPath = new DirectoryPath(Path.GetDirectoryName(Package.FullPath!)!, text); if (directoryPath.Exists) { throw new DataValidationException( string.Format(Resources.ValidationError_PackageExists, text) ); } }, }; var result = await DialogHelper.GetTextEntryDialogResultAsync( field, string.Format(Resources.Description_RenamePackage, currentName) ); if (result.Result == ContentDialogResult.Primary && field.IsValid && field.Text != currentName) { var newPackagePath = new DirectoryPath(Path.GetDirectoryName(Package.FullPath!)!, field.Text); var existingPath = new DirectoryPath(Package.FullPath!); if (existingPath.FullPath == newPackagePath.FullPath) return; try { await existingPath.MoveToAsync(newPackagePath); } catch (Exception ex) { logger.LogError( ex, "Failed to rename package directory from {OldPath} to {NewPath}", existingPath.FullPath, newPackagePath.FullPath ); notificationService.Show( Resources.Label_UnexpectedErrorOccurred, ex.Message, NotificationType.Error ); return; } Package.DisplayName = field.Text; OnPropertyChanged(nameof(PackageDisplayName)); settingsManager.Transaction(s => { var packageToUpdate = s.InstalledPackages.FirstOrDefault(p => p.Id == Package.Id); if (packageToUpdate != null) { packageToUpdate.DisplayName = field.Text; packageToUpdate.LibraryPath = Path.Combine("Packages", field.Text); } }); } } [RelayCommand] private async Task ExecuteExtraCommand(string commandName) { var command = ExtraCommands?.FirstOrDefault(cmd => cmd.CommandName == commandName); if (command == null) return; Text = $"Executing {commandName}..."; IsIndeterminate = true; Value = -1; try { await command.Command(Package!); notificationService.Show("Command executed successfully", commandName, NotificationType.Success); } catch (Exception ex) { logger.LogError(ex, "Error executing command {CommandName}", commandName); notificationService.ShowPersistent( $"Error during {commandName} operation", ex.Message, NotificationType.Error ); } finally { Text = ""; IsIndeterminate = false; Value = 0; } } private async Task HasUpdate() { if (Package == null || IsUnknownPackage || Design.IsDesignMode || Package.DontCheckForUpdates) return false; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) return false; var canCheckUpdate = Package.LastUpdateCheck == null || Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15); if (!canCheckUpdate) { return Package.UpdateAvailable; } try { var hasUpdate = await basePackage.CheckForUpdates(Package); UpdateVersion = await basePackage.GetUpdate(Package); await using (settingsManager.BeginTransaction()) { Package.UpdateAvailable = hasUpdate; Package.LastUpdateCheck = DateTimeOffset.Now; } return hasUpdate; } catch (Exception e) { logger.LogError(e, "Error checking {PackageName} for updates", Package.PackageName); return false; } } private static bool RequiresPythonUpgradeNotice( BasePackage basePackage, InstalledPackage installedPackage ) { if (basePackage.MinimumPythonVersion is not { } minimumVersion) return false; return PyVersion.TryParse(installedPackage.PythonVersion, out var currentVersion) && currentVersion < minimumVersion; } private async Task ShowPythonUpgradeDialogIfNeeded( BasePackage basePackage, InstalledPackage installedPackage ) { if (!RequiresPythonUpgradeNotice(basePackage, installedPackage)) return true; var dialog = new BetterContentDialog { Title = "Python Upgrade Required", Content = "This update will recreate the package venv to migrate from Python " + $"{installedPackage.PythonVersion} to {basePackage.MinimumPythonVersion}.\n\n" + "Any custom pip packages manually installed into the current venv may need to be reinstalled. " + "Your launch options, extensions, and generated files are not affected.\n\n" + "You can also install a fresh copy and migrate manually.\n\n" + "Continue with update?", PrimaryButtonText = "Continue", CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, }; return await dialog.ShowAsync() == ContentDialogResult.Primary; } public void ToggleSharedModelSymlink() => IsSharedModelSymlink = !IsSharedModelSymlink; public void ToggleSharedModelConfig() => IsSharedModelConfig = !IsSharedModelConfig; public void ToggleSharedModelNone() => IsSharedModelDisabled = !IsSharedModelDisabled; public void ToggleSharedOutput() => UseSharedOutput = !UseSharedOutput; public void ToggleDontCheckForUpdates() => DontCheckForUpdates = !DontCheckForUpdates; partial void OnUseSharedOutputChanged(bool value) { if (Package == null) return; if (value == Package.UseSharedOutputFolder) return; using var st = settingsManager.BeginTransaction(); Package.UseSharedOutputFolder = value; var basePackage = packageFactory[Package.PackageName!]; if (basePackage == null) return; if (value) { basePackage.SetupOutputFolderLinks(Package.FullPath!); } else { basePackage.RemoveOutputFolderLinks(Package.FullPath!); } } // fake radio button stuff partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue) { if (oldValue == newValue) return; if (newValue != Package!.PreferredSharedFolderMethod is SharedFolderMethod.Symlink) { using var st = settingsManager.BeginTransaction(); Package.PreferredSharedFolderMethod = SharedFolderMethod.Symlink; } if (newValue) { IsSharedModelConfig = false; IsSharedModelDisabled = false; } else { var basePackage = packageFactory[Package!.PackageName!]; basePackage!.RemoveModelFolderLinks(Package.FullPath!, SharedFolderMethod.Symlink); } } partial void OnIsSharedModelConfigChanged(bool oldValue, bool newValue) { if (oldValue == newValue) return; if (newValue != Package!.PreferredSharedFolderMethod is SharedFolderMethod.Configuration) { using var st = settingsManager.BeginTransaction(); Package.PreferredSharedFolderMethod = SharedFolderMethod.Configuration; } if (newValue) { IsSharedModelSymlink = false; IsSharedModelDisabled = false; } else { var basePackage = packageFactory[Package!.PackageName!]; basePackage!.RemoveModelFolderLinks(Package.FullPath!, SharedFolderMethod.Configuration); } } partial void OnIsSharedModelDisabledChanged(bool value) { if (value) { if (Package!.PreferredSharedFolderMethod is not SharedFolderMethod.None) { using var st = settingsManager.BeginTransaction(); Package.PreferredSharedFolderMethod = SharedFolderMethod.None; } IsSharedModelSymlink = false; IsSharedModelConfig = false; } } partial void OnDontCheckForUpdatesChanged(bool value) { if (value) { UpdateVersion = null; IsUpdateAvailable = false; if (Package == null) return; Package.UpdateAvailable = false; settingsManager.Transaction(s => { s.SetUpdateCheckDisabledForPackage(Package, value); }); } else if (Package != null) { Package.LastUpdateCheck = DateTimeOffset.MinValue; settingsManager.Transaction(s => { s.SetUpdateCheckDisabledForPackage(Package, value); }); OnLoadedAsync().SafeFireAndForget(); } } private void RunningPackageOnStartupComplete(object? sender, string e) { webUiUrl = e.Replace("0.0.0.0", "127.0.0.1"); ShowWebUiButton = !string.IsNullOrWhiteSpace(webUiUrl); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageExtensionBrowserViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Data; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Collections; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Controls; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.PackageManager; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Git; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [View(typeof(PackageExtensionBrowserView))] [RegisterTransient] [ManagedService] public partial class PackageExtensionBrowserViewModel : ViewModelBase, IDisposable { private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly IServiceManager vmFactory; private readonly IPrerequisiteHelper prerequisiteHelper; private readonly CompositeDisposable cleanUp; public PackagePair? PackagePair { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowNoExtensionsFoundMessage))] private bool isLoading; private SourceCache availableExtensionsSource = new(ext => ext.Author + ext.Title + ext.Reference ); public IObservableCollection> SelectedAvailableItems { get; } = new ObservableCollectionExtended>(); public SearchCollection< SelectableItem, string, string > AvailableItemsSearchCollection { get; } private SourceCache installedExtensionsSource = new(ext => ext.Paths.FirstOrDefault()?.ToString() ?? ext.GitRepositoryUrl ?? ext.GetHashCode().ToString() ); public IObservableCollection> SelectedInstalledItems { get; } = new ObservableCollectionExtended>(); public SearchCollection< SelectableItem, string, string > InstalledItemsSearchCollection { get; } private SourceCache extensionPackSource = new(ext => ext.Name); public IObservableCollection ExtensionPacks { get; } = new ObservableCollectionExtended(); private SourceCache extensionPackExtensionsSource = new(ext => ext.PackageExtension.Author + ext.PackageExtension.Title + ext.PackageExtension.Reference ); public IObservableCollection< SelectableItem > SelectedExtensionPackExtensions { get; } = new ObservableCollectionExtended>(); public SearchCollection< SelectableItem, string, string > ExtensionPackExtensionsSearchCollection { get; } [ObservableProperty] private ExtensionPack? selectedExtensionPack; [ObservableProperty] private bool showNoExtensionsFoundMessage; [ObservableProperty] private bool areExtensionPacksLoading; public PackageExtensionBrowserViewModel( INotificationService notificationService, ISettingsManager settingsManager, IServiceManager vmFactory, IPrerequisiteHelper prerequisiteHelper ) { this.notificationService = notificationService; this.settingsManager = settingsManager; this.vmFactory = vmFactory; this.prerequisiteHelper = prerequisiteHelper; var availableItemsChangeSet = availableExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .ObserveOn(SynchronizationContext.Current!) .Publish(); availableItemsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) .Bind(SelectedAvailableItems) .ObserveOn(SynchronizationContext.Current) .Subscribe(); var installedItemsChangeSet = installedExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .ObserveOn(SynchronizationContext.Current!) .Publish(); installedItemsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) .Bind(SelectedInstalledItems) .ObserveOn(SynchronizationContext.Current) .Subscribe(); extensionPackSource .Connect() .Bind(ExtensionPacks) .ObserveOn(SynchronizationContext.Current) .Subscribe(); var extensionPackExtensionsChangeSet = extensionPackExtensionsSource .Connect() .Transform(ext => new SelectableItem(ext)) .ObserveOn(SynchronizationContext.Current!) .Publish(); extensionPackExtensionsChangeSet .AutoRefresh(item => item.IsSelected) .Filter(item => item.IsSelected) .Bind(SelectedExtensionPackExtensions) .ObserveOn(SynchronizationContext.Current) .Subscribe(); cleanUp = new CompositeDisposable( AvailableItemsSearchCollection = new SearchCollection< SelectableItem, string, string >( availableItemsChangeSet, query => string.IsNullOrWhiteSpace(query) ? _ => true : x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ), availableItemsChangeSet.Connect(), InstalledItemsSearchCollection = new SearchCollection< SelectableItem, string, string >( installedItemsChangeSet, query => string.IsNullOrWhiteSpace(query) ? _ => true : x => x.Item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ), installedItemsChangeSet.Connect(), ExtensionPackExtensionsSearchCollection = new SearchCollection< SelectableItem, string, string >( extensionPackExtensionsChangeSet, query => string.IsNullOrWhiteSpace(query) ? _ => true : x => x.Item.PackageExtension.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ), extensionPackExtensionsChangeSet.Connect() ); } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); await LoadExtensionPacksAsync(); await Refresh(); } public void AddExtensions( IEnumerable packageExtensions, IEnumerable installedExtensions ) { availableExtensionsSource.AddOrUpdate(packageExtensions); installedExtensionsSource.AddOrUpdate(installedExtensions); } public void AddExtensionPacks(IEnumerable packs) { extensionPackSource.AddOrUpdate(packs); SelectedExtensionPack = packs.FirstOrDefault(); if (SelectedExtensionPack == null) return; extensionPackExtensionsSource.AddOrUpdate(SelectedExtensionPack.Extensions); } [RelayCommand] public async Task InstallSelectedExtensions() { if (!await BeforeInstallCheck()) return; var extensions = SelectedAvailableItems .Select(item => item.Item) .Where(extension => !extension.IsInstalled) .ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select(ext => new InstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext )) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = "Installed Extensions", ModificationCompleteMessage = "Finished installing extensions", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } [RelayCommand] public async Task UpdateSelectedExtensions() { var extensions = SelectedInstalledItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select(ext => new UpdateExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext )) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = "Updated Extensions", ModificationCompleteMessage = "Finished updating extensions", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } [RelayCommand] public async Task UninstallSelectedExtensions() { var extensions = SelectedInstalledItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var steps = extensions .Select(ext => new UninstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, ext )) .Cast() .ToArray(); var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = "Uninstalled Extensions", ModificationCompleteMessage = "Finished uninstalling extensions", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } [RelayCommand] public async Task OpenExtensionsSettingsDialog() { if (PackagePair is null) return; var grid = new ExtensionSettingsPropertyGrid { ManifestUrls = new BindingList( PackagePair?.InstalledPackage.ExtraExtensionManifestUrls ?? [] ), }; var dialog = vmFactory .Get(vm => { vm.Title = $"{Resources.Label_Settings}"; vm.SelectedObject = grid; vm.IncludeCategories = ["Base"]; }) .GetSaveDialog(); dialog.MinDialogWidth = 750; dialog.MaxDialogWidth = 750; if (await dialog.ShowAsync() == ContentDialogResult.Primary) { await using var _ = settingsManager.BeginTransaction(); PackagePair!.InstalledPackage.ExtraExtensionManifestUrls = grid.ManifestUrls.ToList(); } } [RelayCommand] private async Task InstallExtensionPack() { if (SelectedExtensionPack == null) return; var steps = new List(); foreach (var extension in SelectedExtensionPack.Extensions) { var installedExtension = installedExtensionsSource.Items.FirstOrDefault(x => x.Definition?.Title == extension.PackageExtension.Title && x.Definition.Reference == extension.PackageExtension.Reference ); if (installedExtension != null) { steps.Add( new UpdateExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, installedExtension, extension.Version ) ); } else { steps.Add( new InstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair!.InstalledPackage, extension.PackageExtension, extension.Version ) ); } } var runner = new PackageModificationRunner { ShowDialogOnStart = true, CloseWhenFinished = true, ModificationCompleteMessage = $"Extension Pack {SelectedExtensionPack.Name} installed", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); await Refresh(); } [RelayCommand] public async Task CreateExtensionPackFromInstalled() { var extensions = SelectedInstalledItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var (dialog, nameField) = GetNameEntryDialog(); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var name = nameField.Text; var newExtensionPack = new ExtensionPack { Name = name, PackageType = PackagePair!.InstalledPackage.PackageName, Extensions = SelectedInstalledItems .Where(x => x.Item.Definition != null) .Select(x => new SavedPackageExtension { PackageExtension = x.Item.Definition, Version = x.Item.Version, }) .ToList(), }; SaveExtensionPack(newExtensionPack, name); await LoadExtensionPacksAsync(); notificationService.Show("Extension Pack Created", "The extension pack has been created"); } } [RelayCommand] public async Task CreateExtensionPackFromAvailable() { var extensions = SelectedAvailableItems.Select(x => x.Item).ToArray(); if (extensions.Length == 0) return; var (dialog, nameField) = GetNameEntryDialog(); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var name = nameField.Text; var newExtensionPack = new ExtensionPack { Name = name, PackageType = PackagePair!.InstalledPackage.PackageName, Extensions = SelectedAvailableItems .Select(x => new SavedPackageExtension { PackageExtension = x.Item, Version = null }) .ToList(), }; SaveExtensionPack(newExtensionPack, name); await LoadExtensionPacksAsync(); notificationService.Show("Extension Pack Created", "The extension pack has been created"); } } [RelayCommand] public async Task AddInstalledExtensionToPack(ExtensionPack pack) { foreach (var extension in SelectedInstalledItems) { if ( pack.Extensions.Any(x => x.PackageExtension.Title == extension.Item.Definition?.Title && x.PackageExtension.Author == extension.Item.Definition?.Author && x.PackageExtension.Reference == extension.Item.Definition?.Reference ) ) { continue; } pack.Extensions.Add( new SavedPackageExtension { PackageExtension = extension.Item.Definition!, Version = extension.Item.Version, } ); } SaveExtensionPack(pack, pack.Name); ClearSelection(); await LoadExtensionPacksAsync(); notificationService.Show( "Extensions added to pack", "The selected extensions have been added to the pack" ); } [RelayCommand] public async Task AddExtensionToPack(ExtensionPack pack) { foreach (var extension in SelectedAvailableItems) { if ( pack.Extensions.Any(x => x.PackageExtension.Title == extension.Item.Title && x.PackageExtension.Author == extension.Item.Author && x.PackageExtension.Reference == extension.Item.Reference ) ) { continue; } pack.Extensions.Add( new SavedPackageExtension { PackageExtension = extension.Item, Version = null } ); } SaveExtensionPack(pack, pack.Name); ClearSelection(); await LoadExtensionPacksAsync(); notificationService.Show( "Extensions added to pack", "The selected extensions have been added to the pack" ); } [RelayCommand] public async Task RemoveExtensionFromPack() { if (SelectedExtensionPack is null) return; foreach (var extension in SelectedExtensionPackExtensions) { extensionPackExtensionsSource.Remove(extension.Item); SelectedExtensionPack.Extensions.Remove(extension.Item); } SaveExtensionPack(SelectedExtensionPack, SelectedExtensionPack.Name); ClearSelection(); await LoadExtensionPacksAsync(); } [RelayCommand] private async Task DeleteExtensionPackAsync(ExtensionPack pack) { var pathToDelete = settingsManager .ExtensionPackDirectory.JoinDir(pack.PackageType) .JoinFile($"{pack.Name}.json"); var confirmDeleteVm = vmFactory.Get(); confirmDeleteVm.PathsToDelete = [pathToDelete]; if (await confirmDeleteVm.GetDialog().ShowAsync() != ContentDialogResult.Primary) { return; } try { await confirmDeleteVm.ExecuteCurrentDeleteOperationAsync(failFast: true); } catch (Exception e) { notificationService.ShowPersistent("Error deleting files", e.Message, NotificationType.Error); return; } ClearSelection(); extensionPackSource.Remove(pack); } [RelayCommand] private async Task OpenExtensionPackFolder() { var extensionPackDir = settingsManager.ExtensionPackDirectory.JoinDir( PackagePair!.InstalledPackage.PackageName ); if (!extensionPackDir.Exists) { extensionPackDir.Create(); } if (SelectedExtensionPack is null || ExtensionPacks.Count <= 0) { await ProcessRunner.OpenFolderBrowser(extensionPackDir); } else { var extensionPackPath = extensionPackDir.JoinFile($"{SelectedExtensionPack.Name}.json"); await ProcessRunner.OpenFileBrowser(extensionPackPath); } } [RelayCommand] private async Task SetExtensionVersion(SavedPackageExtension selectedExtension) { if (SelectedExtensionPack is null) return; var vm = new GitVersionSelectorViewModel { GitVersionProvider = new CachedCommandGitVersionProvider( selectedExtension.PackageExtension.Reference.ToString(), prerequisiteHelper ), }; var dialog = vm.GetDialog(); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { if (string.IsNullOrWhiteSpace(vm.SelectedGitVersion.ToString())) return; // update the version and save pack selectedExtension.Version = new PackageExtensionVersion { Branch = vm.SelectedGitVersion.Branch, CommitSha = vm.SelectedGitVersion.CommitSha, Tag = vm.SelectedGitVersion.Tag, }; SaveExtensionPack(SelectedExtensionPack, SelectedExtensionPack.Name); await LoadExtensionPacksAsync(); } } [RelayCommand] public async Task Refresh() { if (PackagePair is null) return; IsLoading = true; try { if (Design.IsDesignMode) { var (availableExtensions, installedExtensions) = SynchronizeExtensions( availableExtensionsSource.Items, installedExtensionsSource.Items ); availableExtensionsSource.EditDiff(availableExtensions); installedExtensionsSource.EditDiff(installedExtensions); await Task.Delay(250); } else { await RefreshCore(); } } finally { IsLoading = false; ShowNoExtensionsFoundMessage = AvailableItemsSearchCollection.FilteredItems.Count == 0; } } [RelayCommand] private void SelectAllInstalledExtensions() { foreach (var item in InstalledItemsSearchCollection.FilteredItems) { item.IsSelected = true; } } [RelayCommand] private async Task InstallExtensionManualAsync() { var textField = new TextBoxField { Label = "Extension URL", Validator = text => { if (string.IsNullOrWhiteSpace(text)) throw new DataValidationException("URL is required"); if (!Uri.TryCreate(text, UriKind.Absolute, out _)) throw new DataValidationException("Invalid URL format"); }, }; var dialog = DialogHelper.CreateTextEntryDialog("Manual Extension Install", "", [textField]); if (await dialog.ShowAsync() != ContentDialogResult.Primary) return; var url = textField.Text.Trim(); if (string.IsNullOrWhiteSpace(url)) return; if ( !Uri.TryCreate(url, UriKind.Absolute, out var uri) || !uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase) ) { notificationService.Show("Invalid URL", "Please provide a valid GitHub repository URL."); return; } var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Length < 2) { notificationService.Show( "Invalid URL", "The URL does not appear to be a valid GitHub repository." ); return; } var author = segments[0]; var title = segments[1].Replace(".git", ""); // create a new PackageExtension var packageExtension = new PackageExtension { Author = author, Title = title, Reference = new Uri(url), IsInstalled = false, InstallType = "git-clone", Files = [new Uri(url)], }; var steps = new List { new InstallExtensionStep( PackagePair!.BasePackage.ExtensionManager!, PackagePair.InstalledPackage, packageExtension ), }; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteTitle = "Installed Extensions", ModificationCompleteMessage = "Finished installing extensions", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); ClearSelection(); RefreshBackground(); } public void RefreshBackground() { RefreshCore() .SafeFireAndForget(ex => { notificationService.ShowPersistent("Failed to refresh extensions", ex.ToString()); }); } private async Task RefreshCore() { using var _ = CodeTimer.StartDebug(); if (PackagePair?.BasePackage.ExtensionManager is not { } extensionManager) throw new NotSupportedException( $"The package {PackagePair?.BasePackage} does not support extensions." ); var availableExtensions = ( await extensionManager.GetManifestExtensionsAsync( extensionManager.GetManifests(PackagePair.InstalledPackage) ) ).ToArray(); var installedExtensions = ( await extensionManager.GetInstalledExtensionsAsync(PackagePair.InstalledPackage) ).ToArray(); // Synchronize SynchronizeExtensions(availableExtensions, installedExtensions); await Task.Run(() => { availableExtensionsSource.Edit(updater => { updater.Load(availableExtensions); }); installedExtensionsSource.Edit(updater => { updater.Load(installedExtensions); }); }); } public void ClearSelection() { foreach (var item in SelectedAvailableItems.ToImmutableArray()) item.IsSelected = false; foreach (var item in SelectedInstalledItems.ToImmutableArray()) item.IsSelected = false; foreach (var item in SelectedExtensionPackExtensions.ToImmutableArray()) item.IsSelected = false; } private (BetterContentDialog dialog, TextBoxField nameField) GetNameEntryDialog() { var textFields = new TextBoxField[] { new() { Label = "Name", Validator = text => { if (string.IsNullOrWhiteSpace(text)) throw new DataValidationException("Name is required"); if (ExtensionPacks.Any(pack => pack.Name == text)) throw new DataValidationException("Pack already exists"); }, }, }; return (DialogHelper.CreateTextEntryDialog("Pack Name", "", textFields), textFields[0]); } private async Task BeforeInstallCheck() { if ( !settingsManager.Settings.SeenTeachingTips.Contains( Core.Models.Settings.TeachingTip.PackageExtensionsInstallNotice ) ) { var dialog = new BetterContentDialog { Title = "Installing Extensions", Content = """ Extensions, the extension index, and their dependencies are community provided and not verified by the Stability Matrix team. The install process may invoke external programs and scripts. Please review the extension's source code and applicable licenses before installing. """, PrimaryButtonText = Resources.Action_Continue, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, MaxDialogWidth = 400, }; if (await dialog.ShowAsync() != ContentDialogResult.Primary) return false; settingsManager.Transaction(s => s.SeenTeachingTips.Add(Core.Models.Settings.TeachingTip.PackageExtensionsInstallNotice) ); } return true; } [Pure] private static ( IEnumerable extensions, IEnumerable installedExtensions ) SynchronizeExtensions( IEnumerable extensions, IEnumerable installedExtensions ) { var availableArr = extensions.ToArray(); var installedArr = installedExtensions.ToArray(); SynchronizeExtensions(availableArr, installedArr); return (availableArr, installedArr); } private static void SynchronizeExtensions( IList extensions, IList installedExtensions ) { // For extensions, map their file paths for lookup var repoToExtension = extensions .SelectMany(ext => ext.Files.Select(path => (path, ext))) .ToLookup(kv => kv.path.ToString().StripEnd(".git")) .ToDictionary(group => group.Key, x => x.First().ext); // For installed extensions, add remote repo if available var extensionsInstalled = new HashSet(); foreach (var (i, installedExt) in installedExtensions.Enumerate()) if ( installedExt.GitRepositoryUrl is not null && repoToExtension.TryGetValue( installedExt.GitRepositoryUrl.StripEnd(".git"), out var mappedExt ) ) { extensionsInstalled.Add(mappedExt); installedExtensions[i] = installedExt with { Definition = mappedExt }; } // For available extensions, add installed status if available foreach (var (i, ext) in extensions.Enumerate()) if (extensionsInstalled.Contains(ext)) extensions[i] = ext with { IsInstalled = true }; } private async Task LoadExtensionPacksAsync() { if (Design.IsDesignMode) return; try { AreExtensionPacksLoading = true; var packDir = settingsManager.ExtensionPackDirectory; if (!packDir.Exists) packDir.Create(); var jsonFiles = packDir.EnumerateFiles("*.json", SearchOption.AllDirectories); var packs = new List(); foreach (var jsonFile in jsonFiles) { var json = await jsonFile.ReadAllTextAsync(); try { var extensionPack = JsonSerializer.Deserialize(json); if ( extensionPack != null && extensionPack.PackageType == PackagePair!.InstalledPackage.PackageName ) packs.Add(extensionPack); } catch (JsonException) { // ignored for now, need to log } } extensionPackSource.AddOrUpdate(packs); } finally { AreExtensionPacksLoading = false; } } private void SaveExtensionPack(ExtensionPack newExtensionPack, string name) { var extensionPackDir = settingsManager.ExtensionPackDirectory.JoinDir(newExtensionPack.PackageType); if (!extensionPackDir.Exists) { extensionPackDir.Create(); } var path = extensionPackDir.JoinFile($"{name}.json"); var json = JsonSerializer.Serialize(newExtensionPack); path.WriteAllText(json); } partial void OnSelectedExtensionPackChanged(ExtensionPack? value) { if (value != null) { extensionPackExtensionsSource.Edit(updater => updater.Load(value.Extensions)); } else { extensionPackExtensionsSource.Clear(); } } /// public void Dispose() { availableExtensionsSource.Dispose(); installedExtensionsSource.Dispose(); cleanUp.Dispose(); GC.SuppressFinalize(this); } private class ExtensionSettingsPropertyGrid : AbstractNotifyPropertyChanged { [Category("Base")] [DisplayName("Extension Manifest Sources")] public BindingList ManifestUrls { get; init; } = []; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallBrowserViewModel.cs ================================================ using System; using System.Reactive.Linq; using System.Threading; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Alias; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.PackageManager; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Analytics; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [View(typeof(PackageInstallBrowserView))] [ManagedService] [RegisterTransient] public partial class PackageInstallBrowserViewModel( IPackageFactory packageFactory, INavigationService packageNavigationService, ISettingsManager settingsManager, INotificationService notificationService, ILogger logger, IPrerequisiteHelper prerequisiteHelper, IAnalyticsHelper analyticsHelper, IPyInstallationManager pyInstallationManager ) : PageViewModelBase { [ObservableProperty] private bool showIncompatiblePackages; [ObservableProperty] private string searchFilter = string.Empty; private SourceCache packageSource = new(p => p.Name); public IObservableCollection InferencePackages { get; } = new ObservableCollectionExtended(); public IObservableCollection TrainingPackages { get; } = new ObservableCollectionExtended(); public IObservableCollection LegacyPackages { get; } = new ObservableCollectionExtended(); public override string Title => "Add Package"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Add }; protected override void OnInitialLoaded() { base.OnInitialLoaded(); var incompatiblePredicate = this.WhenPropertyChanged(vm => vm.ShowIncompatiblePackages) .Select(_ => new Func(p => p.IsCompatible || ShowIncompatiblePackages)) .ObserveOn(SynchronizationContext.Current) .AsObservable(); var searchPredicate = this.WhenPropertyChanged(vm => vm.SearchFilter) .Select(_ => new Func(p => p.DisplayName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase) )) .ObserveOn(SynchronizationContext.Current) .AsObservable(); packageSource .Connect() .DeferUntilLoaded() .Filter(incompatiblePredicate) .Filter(searchPredicate) .Where(p => p is { PackageType: PackageType.SdInference }) .SortAndBind( InferencePackages, SortExpressionComparer .Ascending(p => p.InstallerSortOrder) .ThenByAscending(p => p.DisplayName) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(); packageSource .Connect() .DeferUntilLoaded() .Filter(incompatiblePredicate) .Filter(searchPredicate) .Where(p => p is { PackageType: PackageType.SdTraining }) .SortAndBind( TrainingPackages, SortExpressionComparer .Ascending(p => p.InstallerSortOrder) .ThenByAscending(p => p.DisplayName) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(); packageSource .Connect() .DeferUntilLoaded() .Filter(incompatiblePredicate) .Filter(searchPredicate) .Where(p => p is { PackageType: PackageType.Legacy }) .SortAndBind( LegacyPackages, SortExpressionComparer .Ascending(p => p.InstallerSortOrder) .ThenByAscending(p => p.DisplayName) ) .ObserveOn(SynchronizationContext.Current) .Subscribe(); packageSource.EditDiff(packageFactory.GetAllAvailablePackages(), (a, b) => a.Name == b.Name); } public void OnPackageSelected(BasePackage? package) { if (package is null) { return; } var vm = new PackageInstallDetailViewModel( package, settingsManager, notificationService, logger, prerequisiteHelper, packageNavigationService, packageFactory, analyticsHelper, pyInstallationManager ); Dispatcher.UIThread.Post( () => packageNavigationService.NavigateTo(vm, BetterSlideNavigationTransition.PageSlideFromRight), DispatcherPriority.Send ); } public void ClearSearchQuery() { SearchFilter = string.Empty; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models.PackageSteps; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Analytics; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using PackageInstallDetailView = StabilityMatrix.Avalonia.Views.PackageManager.PackageInstallDetailView; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; [View(typeof(PackageInstallDetailView))] public partial class PackageInstallDetailViewModel( BasePackage package, ISettingsManager settingsManager, INotificationService notificationService, ILogger logger, IPrerequisiteHelper prerequisiteHelper, INavigationService packageNavigationService, IPackageFactory packageFactory, IAnalyticsHelper analyticsHelper, IPyInstallationManager pyInstallationManager ) : PageViewModelBase { public BasePackage SelectedPackage { get; } = package; public override string Title { get; } = package.DisplayName; public override IconSource IconSource => new SymbolIconSource(); public string FullInstallPath => Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); public bool ShowReleaseMode => SelectedPackage.ShouldIgnoreReleases == false; public bool ShowBranchMode => SelectedPackage.ShouldIgnoreBranches == false; public string? ReleaseTooltipText => ShowReleaseMode ? null : Resources.Label_ReleasesUnavailableForThisPackage; public bool ShowTorchIndexOptions => SelectedTorchIndex != TorchIndex.None; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullInstallPath))] private string installName = package.DisplayName; [ObservableProperty] private bool showDuplicateWarning; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ReleaseTooltipText))] private bool isReleaseMode; [ObservableProperty] private IEnumerable availableVersions = new List(); [ObservableProperty] private PackageVersion? selectedVersion; [ObservableProperty] private SharedFolderMethod selectedSharedFolderMethod; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowTorchIndexOptions))] private TorchIndex selectedTorchIndex; [ObservableProperty] private ObservableCollection? availableCommits; [ObservableProperty] private GitCommit? selectedCommit; [ObservableProperty] private bool isOutputSharingEnabled = true; [ObservableProperty] private bool canInstall; [ObservableProperty] private ObservableCollection availablePythonVersions = new(); [ObservableProperty] private UvPythonInfo selectedPythonVersion; public PythonPackageSpecifiersViewModel PythonPackageSpecifiersViewModel { get; } = new() { Title = null }; private PackageVersionOptions? allOptions; public override async Task OnLoadedAsync() { if (Design.IsDesignMode) { return; } OnInstallNameChanged(InstallName); CanInstall = false; SelectedTorchIndex = SelectedPackage.GetRecommendedTorchVersion(); SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; // Initialize Python versions await prerequisiteHelper.UnpackResourcesIfNecessary(); await prerequisiteHelper.InstallUvIfNecessary(); var pythonVersions = await pyInstallationManager.GetAllAvailablePythonsAsync(); AvailablePythonVersions = new ObservableCollection(pythonVersions); SelectedPythonVersion = GetRecommendedPyVersion() ?? AvailablePythonVersions.LastOrDefault(); allOptions = await SelectedPackage.GetAllVersionOptions(); if (ShowReleaseMode) { IsReleaseMode = true; } else { UpdateVersions(); await UpdateCommits(SelectedPackage.MainBranch); } CanInstall = !ShowDuplicateWarning; } [RelayCommand] private async Task Install() { if (string.IsNullOrWhiteSpace(InstallName)) { notificationService.Show( new Notification( "Package name is empty", "Please enter a name for the package", NotificationType.Error ) ); return; } if (SelectedPackage.InstallRequiresAdmin) { var reason = $""" # **{SelectedPackage.DisplayName}** may require administrator privileges during the installation. If necessary, you will be prompted to allow the installer to run with elevated privileges. ## The reason for this requirement is: {SelectedPackage.AdminRequiredReason} ## Would you like to proceed? """; var dialog = DialogHelper.CreateMarkdownDialog(reason, string.Empty); dialog.PrimaryButtonText = Resources.Action_Yes; dialog.CloseButtonText = Resources.Action_Cancel; dialog.IsPrimaryButtonEnabled = true; dialog.DefaultButton = ContentDialogButton.Primary; var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) { return; } } if (SelectedPackage is StableSwarm) { var comfy = settingsManager.Settings.InstalledPackages.FirstOrDefault(x => x.PackageName is nameof(ComfyUI) or "ComfyUI-Zluda" ); if (comfy == null) { // show dialog to install comfy var dialog = new BetterContentDialog { Title = Resources.Label_ComfyRequiredTitle, Content = Resources.Label_ComfyRequiredDetail, PrimaryButtonText = Resources.Action_Yes, CloseButtonText = Resources.Label_No, DefaultButton = ContentDialogButton.Primary, }; var result = await dialog.ShowAsync(); if (result != ContentDialogResult.Primary) return; packageNavigationService.GoBack(); var comfyPackage = packageFactory.FindPackageByName(nameof(ComfyUI)); if (comfyPackage is null) return; var vm = new PackageInstallDetailViewModel( comfyPackage, settingsManager, notificationService, logger, prerequisiteHelper, packageNavigationService, packageFactory, analyticsHelper, pyInstallationManager ); packageNavigationService.NavigateTo(vm); return; } } InstallName = InstallName.Trim(); var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); if (Directory.Exists(installLocation)) { var installPath = new DirectoryPath(installLocation); await installPath.DeleteVerboseAsync(logger); } var downloadOptions = new DownloadPackageVersionOptions(); var installedVersion = new InstalledPackageVersion(); if (IsReleaseMode) { if (SelectedVersion is not null) { downloadOptions.VersionTag = SelectedVersion.TagName; downloadOptions.IsLatest = AvailableVersions?.FirstOrDefault()?.TagName == downloadOptions.VersionTag; downloadOptions.IsPrerelease = SelectedVersion.IsPrerelease; installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; installedVersion.IsPrerelease = SelectedVersion.IsPrerelease; } else { downloadOptions.IsLatest = true; downloadOptions.BranchName = SelectedPackage.MainBranch; installedVersion.InstalledBranch = SelectedPackage.MainBranch; } } else { if (SelectedCommit is not null && SelectedVersion is not null) { downloadOptions.CommitHash = SelectedCommit.Sha; downloadOptions.BranchName = SelectedVersion.TagName; downloadOptions.IsLatest = AvailableCommits?.FirstOrDefault()?.Sha == SelectedCommit.Sha; installedVersion.InstalledBranch = SelectedVersion.TagName; installedVersion.InstalledCommitSha = downloadOptions.CommitHash; } else { downloadOptions.IsLatest = true; downloadOptions.BranchName = SelectedPackage.MainBranch; installedVersion.InstalledBranch = SelectedPackage.MainBranch; } } var pipOverrides = PythonPackageSpecifiersViewModel.GetSpecifiers().ToList(); var package = new InstalledPackage { DisplayName = InstallName, LibraryPath = Path.Combine("Packages", InstallName), Id = Guid.NewGuid(), PackageName = SelectedPackage.Name, Version = installedVersion, LaunchCommand = SelectedPackage.LaunchCommand, LastUpdateCheck = DateTimeOffset.Now, PreferredTorchIndex = SelectedTorchIndex, PreferredSharedFolderMethod = SelectedSharedFolderMethod, UseSharedOutputFolder = IsOutputSharingEnabled, PipOverrides = pipOverrides.Count > 0 ? pipOverrides : null, PythonVersion = SelectedPythonVersion.Version.StringValue, }; var steps = new List { new SetPackageInstallingStep(settingsManager, InstallName), new SetupPrerequisitesStep(prerequisiteHelper, SelectedPackage, SelectedPythonVersion.Version), new DownloadPackageVersionStep( SelectedPackage, installLocation, new DownloadPackageOptions { VersionOptions = downloadOptions } ), new UnpackSiteCustomizeStep(Path.Combine(installLocation, "venv")), new InstallPackageStep( SelectedPackage, installLocation, package, new InstallPackageOptions { SharedFolderMethod = SelectedSharedFolderMethod, VersionOptions = downloadOptions, PythonOptions = { TorchIndex = SelectedTorchIndex, PythonVersion = SelectedPythonVersion.Version, }, } ), new SetupModelFoldersStep(SelectedPackage, SelectedSharedFolderMethod, installLocation), }; if (IsOutputSharingEnabled) { steps.Add(new SetupOutputSharingStep(SelectedPackage, installLocation)); } steps.Add(new AddInstalledPackageStep(settingsManager, package)); var packageName = SelectedPackage.Name; var runner = new PackageModificationRunner { ModificationCompleteMessage = $"Installed {packageName} at [{installLocation}]", ModificationFailedMessage = $"Could not install {packageName}", ShowDialogOnStart = true, }; runner.Completed += (_, completedRunner) => { notificationService.OnPackageInstallCompleted(completedRunner); }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps(steps); if (!runner.Failed) { if (ReferenceEquals(this, packageNavigationService.CurrentPageDataContext)) { packageNavigationService.GoBack(); packageNavigationService.GoBack(); await Task.Delay(100); } EventManager.Instance.OnInstalledPackagesChanged(); } analyticsHelper .TrackPackageInstallAsync(packageName, installedVersion.DisplayVersion, !runner.Failed) .SafeFireAndForget(); } private void UpdateVersions() { CanInstall = false; AvailableVersions = IsReleaseMode && ShowReleaseMode ? allOptions.AvailableVersions : allOptions.AvailableBranches; SelectedVersion = !IsReleaseMode ? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch) ?? AvailableVersions?.FirstOrDefault() : AvailableVersions?.FirstOrDefault(v => !v.IsPrerelease); CanInstall = !ShowDuplicateWarning; } private async Task UpdateCommits(string branchName) { CanInstall = false; var commits = await SelectedPackage.GetAllCommits(branchName); if (commits != null) { AvailableCommits = new ObservableCollection( [.. commits, new GitCommit { Sha = "Custom " }] ); } else { AvailableCommits = [new GitCommit { Sha = "Custom " }]; } SelectedCommit = AvailableCommits.FirstOrDefault(); CanInstall = !ShowDuplicateWarning; } partial void OnInstallNameChanged(string? value) { ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(p => p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}" ); CanInstall = !ShowDuplicateWarning; } partial void OnIsReleaseModeChanged(bool value) { UpdateVersions(); } partial void OnSelectedVersionChanged(PackageVersion? value) { if (IsReleaseMode) return; UpdateCommits(value?.TagName ?? SelectedPackage.MainBranch).SafeFireAndForget(); } async partial void OnSelectedCommitChanged(GitCommit? oldValue, GitCommit? newValue) { if (newValue is not { Sha: "Custom " }) return; List textBoxFields = [new() { Label = "Commit hash", MinWidth = 400 }]; var dialog = DialogHelper.CreateTextEntryDialog("Enter a commit hash", string.Empty, textBoxFields); var dialogResult = await dialog.ShowAsync(); if (dialogResult != ContentDialogResult.Primary) { SelectedCommit = oldValue; return; } var commitHash = textBoxFields[0].Text; if (string.IsNullOrWhiteSpace(commitHash)) { SelectedCommit = oldValue; return; } var commit = new GitCommit { Sha = commitHash }; AvailableCommits?.Insert(AvailableCommits.IndexOf(newValue), commit); SelectedCommit = commit; } private UvPythonInfo? GetRecommendedPyVersion() => AvailablePythonVersions.LastOrDefault(x => x.Version.Major.Equals(SelectedPackage.RecommendedPythonVersion.Major) && x.Version.Minor.Equals(SelectedPackage.RecommendedPythonVersion.Minor) ); } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs ================================================ using System.Collections.Generic; using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(PackageManagerPage))] [RegisterSingleton] public partial class PackageManagerViewModel : PageViewModelBase { public override string Title => Resources.Label_Packages; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IconVariant = IconVariant.Filled }; public IReadOnlyList SubPages { get; } [ObservableProperty] private ObservableCollection currentPagePath = []; [ObservableProperty] private PageViewModelBase? currentPage; public PackageManagerViewModel(IServiceManager vmFactory) { SubPages = new PageViewModelBase[] { vmFactory.Get(), vmFactory.Get(), }; CurrentPagePath.AddRange(SubPages); CurrentPage = SubPages[0]; } partial void OnCurrentPageChanged(PageViewModelBase? value) { if (value is null) { return; } if (value is MainPackageManagerViewModel) { CurrentPagePath.Clear(); CurrentPagePath.Add(value); } else if (value is PackageInstallDetailViewModel) { CurrentPagePath.Add(value); } else if (value is RunningPackageViewModel) { CurrentPagePath.Add(value); } else { CurrentPagePath.Clear(); CurrentPagePath.AddRange(new[] { SubPages[0], value }); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Progress/DownloadProgressItemViewModel.cs ================================================ using System; using System.Threading.Tasks; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Progress; public class DownloadProgressItemViewModel : PausableProgressItemViewModelBase { private readonly ITrackedDownloadService downloadService; private readonly TrackedDownload download; public DownloadProgressItemViewModel(ITrackedDownloadService downloadService, TrackedDownload download) { this.downloadService = downloadService; this.download = download; Id = download.Id; Name = download.FileName; State = download.ProgressState; OnProgressStateChanged(State); // If initial progress provided, load it if (download is { TotalBytes: > 0, DownloadedBytes: > 0 }) { var current = download.DownloadedBytes / (double)download.TotalBytes; Progress.Value = (float)Math.Ceiling(Math.Clamp(current, 0, 1) * 100); } download.ProgressUpdate += (s, e) => { Progress.Value = e.Percentage; Progress.IsIndeterminate = e.IsIndeterminate; Progress.DownloadSpeedInMBps = e.SpeedInMBps; }; download.ProgressStateChanged += (s, e) => { State = e; OnProgressStateChanged(e); }; } private void OnProgressStateChanged(ProgressState state) { if (state is ProgressState.Inactive or ProgressState.Paused) { Progress.Text = "Paused"; } else if (state == ProgressState.Working) { Progress.Text = "Downloading..."; } else if (state == ProgressState.Success) { Progress.Text = "Completed"; } else if (state == ProgressState.Cancelled) { Progress.Text = "Cancelled"; } else if (state == ProgressState.Failed) { Progress.Text = "Failed"; } else if (state == ProgressState.Pending) { Progress.Text = "Waiting for other downloads to finish"; } } /// public override Task Cancel() { download.Cancel(); return Task.CompletedTask; } /// public override Task Pause() { download.Pause(); State = ProgressState.Paused; return Task.CompletedTask; } /// public override Task Resume() { return downloadService.TryResumeDownload(download); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs ================================================ using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels.Progress; public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase { private readonly IPackageModificationRunner packageModificationRunner; private BetterContentDialog? dialog; public PackageInstallProgressItemViewModel(IPackageModificationRunner packageModificationRunner) { this.packageModificationRunner = packageModificationRunner; Id = packageModificationRunner.Id; Name = packageModificationRunner.CurrentStep?.ProgressTitle; Progress.Value = packageModificationRunner.CurrentProgress.Percentage; Progress.Text = packageModificationRunner.ConsoleOutput.LastOrDefault(); Progress.IsIndeterminate = packageModificationRunner.CurrentProgress.IsIndeterminate; Progress.HideCloseButton = packageModificationRunner.HideCloseButton; if (Design.IsDesignMode) return; Progress.Console.StartUpdates(); Progress.Console.Post(string.Join(Environment.NewLine, packageModificationRunner.ConsoleOutput)); packageModificationRunner.ProgressChanged += PackageModificationRunnerOnProgressChanged; } private void PackageModificationRunnerOnProgressChanged(object? sender, ProgressReport e) { Progress.Value = e.Percentage; Progress.Description = e.ProcessOutput?.Text ?? e.Message; Progress.IsIndeterminate = e.IsIndeterminate; Progress.Text = packageModificationRunner.CurrentStep?.ProgressTitle; Name = packageModificationRunner.CurrentStep?.ProgressTitle; Failed = packageModificationRunner.Failed; if (e.ProcessOutput == null && string.IsNullOrWhiteSpace(e.Message)) return; if (!string.IsNullOrWhiteSpace(e.Message) && e.Message.Contains("Downloading...")) return; if (e is { ProcessOutput: not null, PrintToConsole: true }) { Progress.Console.Post(e.ProcessOutput.Value); } else if (e.PrintToConsole) { Progress.Console.PostLine(e.Message); } if (Progress.AutoScrollToBottom) { EventManager.Instance.OnScrollToBottomRequested(); } if ( e is { Message: not null, Percentage: >= 100 } && e.Message.Contains( packageModificationRunner.ModificationCompleteMessage ?? "Package Install Complete" ) && Progress.CloseWhenFinished ) { Dispatcher.UIThread.Post(() => dialog?.Hide()); } if (Failed) { Progress.Text = "Package Modification Failed"; Name = "Package Modification Failed"; } } public async Task ShowProgressDialog() { Progress.CloseWhenFinished = packageModificationRunner.CloseWhenFinished; dialog = new BetterContentDialog { MaxDialogWidth = 900, MinDialogWidth = 900, DefaultButton = ContentDialogButton.Close, IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, Content = new PackageModificationDialog { DataContext = Progress }, }; EventManager.Instance.OnToggleProgressFlyout(); await dialog.ShowAsync(); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Progress/ProgressItemViewModel.cs ================================================ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels.Progress; public class ProgressItemViewModel : ProgressItemViewModelBase { public ProgressItemViewModel(ProgressItem progressItem) { Id = progressItem.ProgressId; Name = progressItem.Name; Progress.Value = progressItem.Progress.Percentage; Failed = progressItem.Failed; Progress.Text = GetProgressText(progressItem.Progress); Progress.IsIndeterminate = progressItem.Progress.IsIndeterminate; EventManager.Instance.ProgressChanged += OnProgressChanged; } private void OnProgressChanged(object? sender, ProgressItem e) { if (e.ProgressId != Id) return; Progress.Value = e.Progress.Percentage; Failed = e.Failed; Progress.Text = GetProgressText(e.Progress); Progress.IsIndeterminate = e.Progress.IsIndeterminate; } private string GetProgressText(ProgressReport report) { switch (report.Type) { case ProgressType.Generic: break; case ProgressType.Download: return Failed ? "Download Failed" : "Downloading..."; case ProgressType.Extract: return Failed ? "Extraction Failed" : "Extracting..."; case ProgressType.Update: return Failed ? "Update Failed" : "Updating..."; } if (Failed) { return "Failed"; } return string.IsNullOrWhiteSpace(report.Message) ? string.IsNullOrWhiteSpace(report.Title) ? string.Empty : report.Title : report.Message; } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Collections; using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using FluentIcons.Common; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; using Notification = DesktopNotifications.Notification; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.Progress; [View(typeof(ProgressManagerPage))] [ManagedService] [RegisterSingleton] public partial class ProgressManagerViewModel : PageViewModelBase { private readonly ITrackedDownloadService trackedDownloadService; private readonly INotificationService notificationService; private readonly INavigationService navigationService; private readonly INavigationService settingsNavService; public override string Title => "Download Manager"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.ArrowCircleDown, IconVariant = IconVariant.Filled }; public AvaloniaList ProgressItems { get; } = new(); [ObservableProperty] private bool isOpen; public ProgressManagerViewModel( ITrackedDownloadService trackedDownloadService, INotificationService notificationService, INavigationService navigationService, INavigationService settingsNavService ) { this.trackedDownloadService = trackedDownloadService; this.notificationService = notificationService; this.navigationService = navigationService; this.settingsNavService = settingsNavService; // Attach to the event trackedDownloadService.DownloadAdded += TrackedDownloadService_OnDownloadAdded; EventManager.Instance.ToggleProgressFlyout += (_, _) => IsOpen = !IsOpen; EventManager.Instance.PackageInstallProgressAdded += InstanceOnPackageInstallProgressAdded; EventManager.Instance.RecommendedModelsDialogClosed += InstanceOnRecommendedModelsDialogClosed; } private void InstanceOnRecommendedModelsDialogClosed(object? sender, EventArgs e) { var vm = ProgressItems.OfType().FirstOrDefault(); vm?.ShowProgressDialog().SafeFireAndForget(); } private void InstanceOnPackageInstallProgressAdded(object? sender, IPackageModificationRunner runner) { AddPackageInstall(runner).SafeFireAndForget(); } private void TrackedDownloadService_OnDownloadAdded(object? sender, TrackedDownload e) { // Attach notification handlers // Use Changing because Changed might be called after the download is removed e.ProgressStateChanged += (s, state) => { Debug.WriteLine($"Download {e.FileName} state changed to {state}"); var download = s as TrackedDownload; switch (state) { case ProgressState.Success: var imageFile = e .DownloadDirectory.EnumerateFiles( $"{Path.GetFileNameWithoutExtension(e.FileName)}.preview.*" ) .FirstOrDefault(); notificationService .ShowAsync( NotificationKey.Download_Completed, new Notification { Title = "Download Completed", Body = $"Download of {e.FileName} completed successfully.", BodyImagePath = imageFile?.FullPath, } ) .SafeFireAndForget(); break; case ProgressState.Failed: var msg = ""; if (download?.Exception is { } exception) { msg = $"({exception.GetType().Name}) {exception.InnerException?.Message ?? exception.Message}"; if ( exception is EarlyAccessException || exception.InnerException is EarlyAccessException ) { msg = "This asset is in Early Access. Please check the asset page for more information."; } else if ( exception is CivitLoginRequiredException || exception.InnerException is CivitLoginRequiredException ) { ShowCivitLoginRequiredDialog(); return; } else if ( exception is HuggingFaceLoginRequiredException || exception.InnerException is HuggingFaceLoginRequiredException ) { ShowHuggingFaceLoginRequiredDialog(); return; } else if ( exception is CivitDownloadDisabledException || exception.InnerException is CivitDownloadDisabledException ) { Dispatcher.UIThread.InvokeAsync(async () => await notificationService.ShowPersistentAsync( NotificationKey.Download_Failed, new Notification { Title = "Download Disabled", Body = $"The creator of {e.FileName} has disabled downloads on this file", } ) ); return; } } Dispatcher.UIThread.InvokeAsync(async () => await notificationService.ShowPersistentAsync( NotificationKey.Download_Failed, new Notification { Title = "Download Failed", Body = $"Download of {e.FileName} failed: {msg}", } ) ); break; case ProgressState.Cancelled: notificationService .ShowAsync( NotificationKey.Download_Canceled, new Notification { Title = "Download Cancelled", Body = $"Download of {e.FileName} was cancelled.", } ) .SafeFireAndForget(); break; } }; var vm = new DownloadProgressItemViewModel(trackedDownloadService, e); ProgressItems.Add(vm); } private void ShowCivitLoginRequiredDialog() { Dispatcher.UIThread.InvokeAsync(async () => { var errorDialog = new BetterContentDialog { Title = Resources.Label_DownloadFailed, Content = Resources.Label_CivitAiLoginRequired, PrimaryButtonText = "Go to Settings", SecondaryButtonText = "Close", DefaultButton = ContentDialogButton.Primary, }; var result = await errorDialog.ShowAsync(); if (result == ContentDialogResult.Primary) { navigationService.NavigateTo(new SuppressNavigationTransitionInfo()); await Task.Delay(100); settingsNavService.NavigateTo( new SuppressNavigationTransitionInfo() ); } }); } private void ShowHuggingFaceLoginRequiredDialog() { Dispatcher.UIThread.InvokeAsync(async () => { var errorDialog = new BetterContentDialog { Title = Resources.Label_DownloadFailed, Content = Resources.Label_HuggingFaceLoginRequired, PrimaryButtonText = "Go to Settings", SecondaryButtonText = "Close", DefaultButton = ContentDialogButton.Primary, }; var result = await errorDialog.ShowAsync(); if (result == ContentDialogResult.Primary) { navigationService.NavigateTo(new SuppressNavigationTransitionInfo()); await Task.Delay(100); settingsNavService.NavigateTo( new SuppressNavigationTransitionInfo() ); } }); } public void AddDownloads(IEnumerable downloads) { foreach (var download in downloads) { if (ProgressItems.Any(vm => vm.Id == download.Id)) continue; var vm = new DownloadProgressItemViewModel(trackedDownloadService, download); ProgressItems.Add(vm); } } private Task AddPackageInstall(IPackageModificationRunner packageModificationRunner) { if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id)) return Task.CompletedTask; var vm = new PackageInstallProgressItemViewModel(packageModificationRunner); ProgressItems.Add(vm); return packageModificationRunner.ShowDialogOnStart ? vm.ShowProgressDialog() : Task.CompletedTask; } private void ShowFailedNotification(string title, string message) { notificationService.ShowPersistent(title, message, NotificationType.Error); } public void StartEventListener() { EventManager.Instance.ProgressChanged += OnProgressChanged; } public void ClearDownloads() { ProgressItems.RemoveAll(ProgressItems.Where(x => x.IsCompleted)); } private void OnProgressChanged(object? sender, ProgressItem e) { if (ProgressItems.Any(x => x.Id == e.ProgressId)) return; ProgressItems.Add(new ProgressItemViewModel(e)); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(RefreshBadge))] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [ManagedService] [RegisterTransient] public partial class RefreshBadgeViewModel : ViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public string WorkingToolTipText { get; set; } = "Loading..."; public string SuccessToolTipText { get; set; } = "Success"; public string InactiveToolTipText { get; set; } = ""; public string FailToolTipText { get; set; } = "Failed"; public Symbol InactiveIcon { get; set; } = Symbol.Clear; public Symbol SuccessIcon { get; set; } = Symbol.Checkmark; public Symbol FailIcon { get; set; } = Symbol.AlertUrgent; public IBrush SuccessColorBrush { get; set; } = ThemeColors.ThemeGreen; public IBrush InactiveColorBrush { get; set; } = ThemeColors.ThemeYellow; public IBrush FailColorBrush { get; set; } = ThemeColors.ThemeYellow; public Func>? RefreshFunc { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsWorking))] [NotifyPropertyChangedFor(nameof(ColorBrush))] [NotifyPropertyChangedFor(nameof(CurrentToolTip))] [NotifyPropertyChangedFor(nameof(Icon))] private ProgressState state; public bool IsWorking => State == ProgressState.Working; /*public ControlAppearance Appearance => State switch { ProgressState.Working => ControlAppearance.Info, ProgressState.Success => ControlAppearance.Success, ProgressState.Failed => ControlAppearance.Danger, _ => ControlAppearance.Secondary };*/ public IBrush ColorBrush => State switch { ProgressState.Success => SuccessColorBrush, ProgressState.Inactive => InactiveColorBrush, ProgressState.Failed => FailColorBrush, _ => Brushes.Gray }; public string CurrentToolTip => State switch { ProgressState.Working => WorkingToolTipText, ProgressState.Success => SuccessToolTipText, ProgressState.Inactive => InactiveToolTipText, ProgressState.Failed => FailToolTipText, _ => "" }; public Symbol Icon => State switch { ProgressState.Success => SuccessIcon, ProgressState.Failed => FailIcon, _ => InactiveIcon }; [RelayCommand] private async Task Refresh() { Logger.Info("Running refresh command..."); if (RefreshFunc == null) return; State = ProgressState.Working; try { var result = await RefreshFunc.Invoke(); State = result ? ProgressState.Success : ProgressState.Failed; } catch (Exception ex) { State = ProgressState.Failed; Logger.Error(ex, "Refresh command failed: {Ex}", ex.Message); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/RunningPackageViewModel.cs ================================================ using System; using System.Reactive.Disposables; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(ConsoleOutputPage))] public partial class RunningPackageViewModel : PageViewModelBase, IDisposable, IAsyncDisposable { private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly RunningPackageService runningPackageService; private readonly RunPackageOptions runPackageOptions; private readonly CompositeDisposable subscriptions = new(); public PackagePair RunningPackage { get; } public ConsoleViewModel Console { get; } public override string Title => RunningPackage.InstalledPackage.DisplayName ?? "Running Package"; public override IconSource IconSource => new SymbolIconSource(); [ObservableProperty] private bool autoScrollToEnd; [ObservableProperty] private bool showWebUiButton; [ObservableProperty] private string webUiUrl = string.Empty; [ObservableProperty] private bool isRunning = true; [ObservableProperty] private string consoleInput = string.Empty; [ObservableProperty] private bool showWebUiTeachingTip; /// public RunningPackageViewModel( ISettingsManager settingsManager, INotificationService notificationService, RunningPackageService runningPackageService, PackagePair runningPackage, RunPackageOptions runPackageOptions, ConsoleViewModel console ) { this.settingsManager = settingsManager; this.notificationService = notificationService; this.runningPackageService = runningPackageService; this.runPackageOptions = runPackageOptions; RunningPackage = runningPackage; Console = console; Console.MaxLines = settingsManager.Settings.ConsoleLogHistorySize; Console.Document.LineCountChanged += DocumentOnLineCountChanged; RunningPackage.BasePackage.StartupComplete += BasePackageOnStartupComplete; RunningPackage.BasePackage.Exited += BasePackageOnExited; subscriptions.Add( settingsManager.RegisterPropertyChangedHandler( settings => settings.ConsoleLogHistorySize, newValue => { Console.MaxLines = newValue; } ) ); settingsManager.RelayPropertyFor( this, vm => vm.AutoScrollToEnd, settings => settings.AutoScrollLaunchConsoleToEnd, true ); } public override void OnLoaded() { if (AutoScrollToEnd) { EventManager.Instance.OnScrollToBottomRequested(); } } private void BasePackageOnExited(object? sender, int e) { IsRunning = false; ShowWebUiButton = false; Console.Document.LineCountChanged -= DocumentOnLineCountChanged; RunningPackage.BasePackage.StartupComplete -= BasePackageOnStartupComplete; RunningPackage.BasePackage.Exited -= BasePackageOnExited; runningPackageService.RunningPackages.Remove(RunningPackage.InstalledPackage.Id); } private void BasePackageOnStartupComplete(object? sender, string url) { WebUiUrl = url.Replace("0.0.0.0", "127.0.0.1"); ShowWebUiButton = !string.IsNullOrWhiteSpace(WebUiUrl); if (settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.WebUiButtonMovedTip)) return; ShowWebUiTeachingTip = true; settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.WebUiButtonMovedTip)); } private void DocumentOnLineCountChanged(object? sender, EventArgs e) { if (AutoScrollToEnd) { EventManager.Instance.OnScrollToBottomRequested(); } } partial void OnAutoScrollToEndChanged(bool value) { if (value) { EventManager.Instance.OnScrollToBottomRequested(); } } [RelayCommand] private async Task Restart() { await Stop(); await Task.Delay(100); LaunchPackage(); } [RelayCommand] private void LaunchPackage() { EventManager.Instance.OnPackageRelaunchRequested(RunningPackage.InstalledPackage, runPackageOptions); } [RelayCommand] private async Task Stop() { IsRunning = false; await runningPackageService.StopPackage(RunningPackage.InstalledPackage.Id); Console.PostLine($"{Environment.NewLine}Stopped process at {DateTimeOffset.Now}"); await Console.StopUpdatesAsync(); } [RelayCommand] private void LaunchWebUi() { if (string.IsNullOrEmpty(WebUiUrl)) return; notificationService.TryAsync( Task.Run(() => ProcessRunner.OpenUrl(WebUiUrl)), "Failed to open URL", $"{WebUiUrl}" ); } [RelayCommand] private async Task SendToConsole() { Console.PostLine(ConsoleInput); if (RunningPackage?.BasePackage is BaseGitPackage gitPackage) { var venv = gitPackage.VenvRunner; var process = venv?.Process; if (process is not null) { await process.StandardInput.WriteLineAsync(ConsoleInput); } } ConsoleInput = string.Empty; } protected override void Dispose(bool disposing) { if (disposing) { RunningPackage.BasePackage.Shutdown(); Console.Dispose(); subscriptions.Dispose(); } base.Dispose(disposing); } public async ValueTask DisposeAsync() { RunningPackage.BasePackage.Shutdown(); await Console.DisposeAsync(); subscriptions.Dispose(); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/AccountSettingsViewModel.cs ================================================ using System; using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Controls.Notifications; // Added this line using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using Microsoft.Extensions.Options; using OpenIddict.Client; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(AccountSettingsPage))] [ManagedService] [RegisterSingleton] public partial class AccountSettingsViewModel : PageViewModelBase { private readonly IAccountsService accountsService; private readonly ISettingsManager settingsManager; private readonly IServiceManager vmFactory; private readonly INotificationService notificationService; private readonly ILykosAuthApiV2 lykosAuthApi; private readonly IOptions apiOptions; /// public override string Title => "Accounts"; /// public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Person, IconVariant = IconVariant.Filled }; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConnectLykosCommand))] [NotifyCanExecuteChangedFor(nameof(ConnectPatreonCommand))] [NotifyCanExecuteChangedFor(nameof(ConnectCivitCommand))] [NotifyCanExecuteChangedFor(nameof(ConnectHuggingFaceCommand))] private bool isInitialUpdateFinished; [ObservableProperty] private string? lykosProfileImageUrl; [ObservableProperty] private bool isPatreonConnected; [ObservableProperty] [NotifyPropertyChangedFor(nameof(LykosProfileImageUrl))] private LykosAccountStatusUpdateEventArgs lykosStatus = LykosAccountStatusUpdateEventArgs.Disconnected; [ObservableProperty] private CivitAccountStatusUpdateEventArgs civitStatus = CivitAccountStatusUpdateEventArgs.Disconnected; // Assume HuggingFaceAccountStatusUpdateEventArgs will be created with at least these properties // For now, using a placeholder or assuming a structure like: // public record HuggingFaceAccountStatusUpdateEventArgs(bool IsConnected, string? Username); // Initialize with a disconnected state. [ObservableProperty] private HuggingFaceAccountStatusUpdateEventArgs huggingFaceStatus = new(false, null); [ObservableProperty] private bool isHuggingFaceConnected; [ObservableProperty] private string huggingFaceUsernameWithParentheses = string.Empty; public string LykosAccountManageUrl => apiOptions.Value.LykosAccountApiBaseUrl.Append("/manage").ToString(); public AccountSettingsViewModel( IAccountsService accountsService, ISettingsManager settingsManager, IServiceManager vmFactory, INotificationService notificationService, ILykosAuthApiV2 lykosAuthApi, IOptions apiOptions ) { this.accountsService = accountsService; this.settingsManager = settingsManager; this.vmFactory = vmFactory; this.notificationService = notificationService; this.lykosAuthApi = lykosAuthApi; this.apiOptions = apiOptions; accountsService.LykosAccountStatusUpdate += (_, args) => { Dispatcher.UIThread.Post(() => { IsInitialUpdateFinished = true; LykosStatus = args; IsPatreonConnected = args.IsPatreonConnected; }); }; accountsService.CivitAccountStatusUpdate += (_, args) => { Dispatcher.UIThread.Post(() => { IsInitialUpdateFinished = true; CivitStatus = args; }); }; accountsService.HuggingFaceAccountStatusUpdate += (_, args) => { Dispatcher.UIThread.Post(() => { IsInitialUpdateFinished = true; HuggingFaceStatus = args; // IsHuggingFaceConnected and HuggingFaceUsernameWithParentheses will be updated by OnHuggingFaceStatusChanged }); }; } /// public override void OnLoaded() { base.OnLoaded(); if (Design.IsDesignMode) { return; } accountsService.RefreshAsync().SafeFireAndForget(); } private async Task BeforeConnectCheck() { // Show credentials storage notice if not seen if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.AccountsCredentialsStorageNotice)) { var dialog = new BetterContentDialog { Title = "About Account Credentials", Content = """ Account credentials and tokens are stored locally on your computer, with at-rest AES encryption. If you make changes to your computer hardware, you may need to re-login to your accounts. Account tokens will not be viewable after saving, please make a note of them if you need to use them elsewhere. """, PrimaryButtonText = Resources.Action_Continue, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, MaxDialogWidth = 400, }; if (await dialog.ShowAsync() != ContentDialogResult.Primary) { return false; } settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.AccountsCredentialsStorageNotice) ); } return true; } [RelayCommand(CanExecute = nameof(IsInitialUpdateFinished))] private async Task ConnectLykos() { if (!await BeforeConnectCheck()) return; var vm = vmFactory.Get(); vm.ChallengeRequest = new OpenIddictClientModels.DeviceChallengeRequest { ProviderName = OpenIdClientConstants.LykosAccount.ProviderName, }; await vm.ShowDialogAsync(); if (vm.AuthenticationResult is { } result) { await accountsService.LykosAccountV2LoginAsync( new LykosAccountV2Tokens(result.AccessToken, result.RefreshToken, result.IdentityToken) ); } } [RelayCommand] private Task DisconnectLykos() { return accountsService.LykosAccountV2LogoutAsync(); } [RelayCommand(CanExecute = nameof(IsInitialUpdateFinished))] private async Task ConnectPatreon() { if (!await BeforeConnectCheck()) return; if (LykosStatus.User?.Id is null) return; var urlResult = await notificationService.TryAsync( lykosAuthApi.ApiV2OauthPatreonLink(Program.MessagePipeUri.Append("/oauth/patreon/callback")) ); if (!urlResult.IsSuccessful || urlResult.Result is not { } url) { return; } ProcessRunner.OpenUrl(urlResult.Result); var dialogVm = vmFactory.Get(); dialogVm.Title = "Connect Patreon Account"; dialogVm.Url = url.ToString(); if (await dialogVm.GetDialog().ShowAsync() == ContentDialogResult.Primary) { await accountsService.RefreshAsync(); // Bring main window to front since browser is probably covering var main = App.TopLevel as Window; main?.Activate(); } } [RelayCommand] private async Task DisconnectPatreon() { await notificationService.TryAsync(accountsService.LykosPatreonOAuthLogoutAsync()); } [RelayCommand(CanExecute = nameof(IsInitialUpdateFinished))] private async Task ConnectCivit() { if (!await BeforeConnectCheck()) return; var textFields = new TextBoxField[] { new() { Label = Resources.Label_ApiKey, IsPassword = true, // Added this line Validator = s => { if (string.IsNullOrWhiteSpace(s)) { throw new ValidationException("API key is required"); } }, }, }; var dialog = DialogHelper.CreateTextEntryDialog( "Connect CivitAI Account", """ Login to [CivitAI](https://civitai.com/) and head to your [Account](https://civitai.com/user/account) page Add a new API key and paste it below """, "avares://StabilityMatrix.Avalonia/Assets/guide-civitai-api.webp", textFields ); dialog.PrimaryButtonText = Resources.Action_Connect; if (await dialog.ShowAsync() != ContentDialogResult.Primary || textFields[0].Text is not { } apiToken) { return; } var result = await notificationService.TryAsync(accountsService.CivitLoginAsync(apiToken)); if (result.IsSuccessful) { await accountsService.RefreshAsync(); } } [RelayCommand] private Task DisconnectCivit() { return accountsService.CivitLogoutAsync(); } [RelayCommand(CanExecute = nameof(IsInitialUpdateFinished))] private async Task ConnectHuggingFace() { if (!await BeforeConnectCheck()) return; var field = new TextBoxField { Label = "Hugging Face Token", // Assuming Label is for the prompt IsPassword = true, // Assuming TextBoxField has an IsPassword property Validator = s => { if (string.IsNullOrWhiteSpace(s)) { throw new ValidationException("Token is required"); } }, }; var dialog = DialogHelper.CreateTextEntryDialog( "Connect Hugging Face Account", "Go to [Hugging Face settings](https://huggingface.co/settings/tokens) to create a new Access Token. Ensure it has read permissions. Paste the token below.", [field] ); var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(field.Text)) { await accountsService.HuggingFaceLoginAsync(field.Text); await accountsService.RefreshAsync(); } } [RelayCommand] private Task DisconnectHuggingFace() { // Assuming HuggingFaceLogoutAsync will be added to IAccountsService return accountsService.HuggingFaceLogoutAsync(); } /// /// Update the Lykos profile image URL when the user changes. /// partial void OnLykosStatusChanged(LykosAccountStatusUpdateEventArgs value) { if (value.Email is { } userEmail) { userEmail = userEmail.Trim().ToLowerInvariant(); var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(userEmail)); var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); LykosProfileImageUrl = $"https://gravatar.com/avatar/{hash}?s=512&d=retro"; } else { LykosProfileImageUrl = null; } } partial void OnHuggingFaceStatusChanged(HuggingFaceAccountStatusUpdateEventArgs value) { IsHuggingFaceConnected = value.IsConnected; if (value.IsConnected) { if (!string.IsNullOrWhiteSpace(value.Username)) { HuggingFaceUsernameWithParentheses = $"({value.Username})"; } else { HuggingFaceUsernameWithParentheses = "(Connected)"; // Fallback if no username } } else { HuggingFaceUsernameWithParentheses = string.Empty; if (!string.IsNullOrWhiteSpace(value.ErrorMessage)) { // Assuming INotificationService.Show takes these parameters and NotificationType.Error is valid. // Dispatcher.UIThread.Post might be needed if Show itself doesn't handle UI thread marshalling, // but usually notification services are designed to be called from any thread. // The event handler for HuggingFaceAccountStatusUpdate already posts to UIThread, // so this method (OnHuggingFaceStatusChanged) is already on the UI thread. notificationService.Show( "Hugging Face Connection Error", $"Failed to connect Hugging Face account: {value.ErrorMessage}. Please check your token and try again.", NotificationType.Error, // Assuming NotificationType.Error exists and is correct TimeSpan.FromSeconds(10) // Display for 10 seconds, or TimeSpan.Zero for persistent ); } } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/AnalyticsSettingsViewModel.cs ================================================ using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(AnalyticsSettingsPage))] [ManagedService] [RegisterSingleton] public partial class AnalyticsSettingsViewModel : PageViewModelBase { public override string Title => Resources.Label_Analytics; /// public override IconSource IconSource => new FASymbolIconSource { Symbol = @"fa-solid fa-chart-simple" }; [ObservableProperty] private bool isPackageInstallAnalyticsEnabled; public AnalyticsSettingsViewModel(ISettingsManager settingsManager) { settingsManager.RelayPropertyFor( this, vm => vm.IsPackageInstallAnalyticsEnabled, s => s.Analytics.IsUsageDataEnabled, true ); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs ================================================ using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Reactive.Linq; using Avalonia.Controls.Notifications; using Avalonia.Data; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using NLog; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(InferenceSettingsPage))] [ManagedService] [RegisterSingleton] public partial class InferenceSettingsViewModel : PageViewModelBase { private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly ICompletionProvider completionProvider; /// public override string Title => "Inference"; /// public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IconVariant = IconVariant.Filled }; [ObservableProperty] private bool isPromptCompletionEnabled = true; [ObservableProperty] private IReadOnlyList availableTagCompletionCsvs = Array.Empty(); [ObservableProperty] private string? selectedTagCompletionCsv; [ObservableProperty] private bool isCompletionRemoveUnderscoresEnabled = true; [ObservableProperty] [CustomValidation(typeof(InferenceSettingsViewModel), nameof(ValidateOutputImageFileNameFormat))] private string? outputImageFileNameFormat; [ObservableProperty] private string? outputImageFileNameFormatSample; [ObservableProperty] private bool isInferenceImageBrowserUseRecycleBinForDelete = true; [ObservableProperty] private bool filterExtraNetworksByBaseModel; private List ignoredFileNameFormatVars = [ "author", "model_version_name", "base_model", "file_name", "model_type", "model_id", "model_version_id", "file_id", ]; [ObservableProperty] public partial int InferenceDimensionStepChange { get; set; } [ObservableProperty] public partial ObservableHashSet FavoriteDimensions { get; set; } = []; public IEnumerable OutputImageFileNameFormatVars => FileNameFormatProvider .GetSample() .Substitutions.Where(kv => !ignoredFileNameFormatVars.Contains(kv.Key)) .Select(kv => new FileNameFormatVar { Variable = $"{{{kv.Key}}}", Example = kv.Value.Invoke() }); [ObservableProperty] private bool isImageViewerPixelGridEnabled = true; public InferenceSettingsViewModel( INotificationService notificationService, IPrerequisiteHelper prerequisiteHelper, IPyRunner pyRunner, IServiceManager dialogFactory, ICompletionProvider completionProvider, ITrackedDownloadService trackedDownloadService, IModelIndexService modelIndexService, INavigationService settingsNavigationService, IAccountsService accountsService, ISettingsManager settingsManager ) { this.settingsManager = settingsManager; this.notificationService = notificationService; this.completionProvider = completionProvider; settingsManager.RelayPropertyFor( this, vm => vm.SelectedTagCompletionCsv, settings => settings.TagCompletionCsv ); settingsManager.RelayPropertyFor( this, vm => vm.IsPromptCompletionEnabled, settings => settings.IsPromptCompletionEnabled, true ); settingsManager.RelayPropertyFor( this, vm => vm.IsCompletionRemoveUnderscoresEnabled, settings => settings.IsCompletionRemoveUnderscoresEnabled, true ); settingsManager.RelayPropertyFor( this, vm => vm.IsInferenceImageBrowserUseRecycleBinForDelete, settings => settings.IsInferenceImageBrowserUseRecycleBinForDelete, true ); settingsManager.RelayPropertyFor( this, vm => vm.FilterExtraNetworksByBaseModel, settings => settings.FilterExtraNetworksByBaseModel, true ); this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat) .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .Subscribe(formatProperty => { var provider = FileNameFormatProvider.GetSample(); var template = formatProperty.Value ?? string.Empty; if ( !string.IsNullOrEmpty(template) && provider.Validate(template) == ValidationResult.Success ) { var format = FileNameFormat.Parse(template, provider); OutputImageFileNameFormatSample = format.GetFileName() + ".png"; } else { // Use default format if empty var defaultFormat = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, provider); OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png"; } }); settingsManager.RelayPropertyFor( this, vm => vm.OutputImageFileNameFormat, settings => settings.InferenceOutputImageFileNameFormat, true ); settingsManager.RelayPropertyFor( this, vm => vm.IsImageViewerPixelGridEnabled, settings => settings.IsImageViewerPixelGridEnabled, true ); settingsManager.RelayPropertyFor( this, vm => vm.InferenceDimensionStepChange, settings => settings.InferenceDimensionStepChange, true ); FavoriteDimensions .ToObservableChangeSet() .Throttle(TimeSpan.FromMilliseconds(50)) .ObserveOn(SynchronizationContext.Current) .Subscribe(_ => { if ( FavoriteDimensions is not { Count: > 0 } || FavoriteDimensions.SetEquals(settingsManager.Settings.SavedInferenceDimensions) ) return; settingsManager.Transaction(s => s.SavedInferenceDimensions = FavoriteDimensions.ToHashSet()); }); ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); } /// /// Validator for /// public static ValidationResult ValidateOutputImageFileNameFormat( string? format, ValidationContext context ) { return FileNameFormatProvider.GetSample().Validate(format ?? string.Empty); } /// public override void OnLoaded() { base.OnLoaded(); FavoriteDimensions.Clear(); FavoriteDimensions.AddRange( settingsManager.Settings.SavedInferenceDimensions.OrderDescending( DimensionStringComparer.Instance ) ); UpdateAvailableTagCompletionCsvs(); } #region Commands [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task ImportTagCsv() { var storage = App.StorageProvider; var files = await storage.OpenFilePickerAsync( new FilePickerOpenOptions { FileTypeFilter = new List { new("CSV") { Patterns = ["*.csv"] } }, } ); if (files.Count == 0) return; var sourceFile = new FilePath(files[0].TryGetLocalPath()!); var tagsDir = settingsManager.TagsDirectory; tagsDir.Create(); // Copy to tags directory var targetFile = tagsDir.JoinFile(sourceFile.Name); await sourceFile.CopyToAsync(targetFile); // Update index UpdateAvailableTagCompletionCsvs(); // Trigger load completionProvider.BackgroundLoadFromFile(targetFile, true); notificationService.Show( $"Imported {sourceFile.Name}", $"The {sourceFile.Name} file has been imported.", NotificationType.Success ); } [RelayCommand] private async Task AddRow() { // FavoriteDimensions.Add(string.Empty); var textFields = new TextBoxField[] { new() { Label = "Width", Validator = text => { if (string.IsNullOrWhiteSpace(text)) throw new DataValidationException("Width is required"); if (!int.TryParse(text, out var width) || width <= 0) throw new DataValidationException("Width must be a positive integer"); }, Watermark = "1024", }, new() { Label = "Height", Validator = text => { if (string.IsNullOrWhiteSpace(text)) throw new DataValidationException("Height is required"); if (!int.TryParse(text, out var height) || height <= 0) throw new DataValidationException("Height must be a positive integer"); }, Watermark = "1024", }, }; var dialog = DialogHelper.CreateTextEntryDialog("Add Favorite Dimensions", "", textFields); if (await dialog.ShowAsync() != ContentDialogResult.Primary) return; var width = textFields[0].Text; var height = textFields[1].Text; if (string.IsNullOrWhiteSpace(width) || string.IsNullOrWhiteSpace(height)) return; FavoriteDimensions.Add($"{width} x {height}"); } [RelayCommand] private void RemoveSelectedRow(string item) { FavoriteDimensions.Remove(item); } #endregion private void UpdateAvailableTagCompletionCsvs() { if (!settingsManager.IsLibraryDirSet) return; if (settingsManager.TagsDirectory is not { Exists: true } tagsDir) return; var csvFiles = tagsDir.Info.EnumerateFiles("*.csv"); AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray(); // Set selected to current if exists var settingsCsv = settingsManager.Settings.TagCompletionCsv; if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv)) { SelectedTagCompletionCsv = settingsCsv; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Reflection; using System.Runtime.Versioning; using System.Text; using System.Text.Json; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using KGySoft.CoreLibraries; using Microsoft.Win32; using NLog; using SkiaSharp; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Controls; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Git; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(MainSettingsPage))] [ManagedService] [RegisterSingleton] public partial class MainSettingsViewModel : PageViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; private readonly ISettingsManager settingsManager; private readonly IPrerequisiteHelper prerequisiteHelper; private readonly IPyRunner pyRunner; private readonly IServiceManager dialogFactory; private readonly ICompletionProvider completionProvider; private readonly ITrackedDownloadService trackedDownloadService; private readonly IModelIndexService modelIndexService; private readonly INavigationService settingsNavigationService; private readonly IAccountsService accountsService; private readonly ICivitBaseModelTypeService baseModelTypeService; public SharedState SharedState { get; } public bool IsMacOS => Compat.IsMacOS; public override string Title => "Settings"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IconVariant = IconVariant.Filled }; // ReSharper disable once MemberCanBeMadeStatic.Global public string AppVersion => $"Version {Compat.AppVersion.ToDisplayString()}" + (Program.IsDebugBuild ? " (Debug)" : ""); // Theme section [ObservableProperty] private string? selectedTheme; public IReadOnlyList AvailableThemes { get; } = new[] { "Light", "Dark", "System" }; [ObservableProperty] private CultureInfo selectedLanguage; // ReSharper disable once MemberCanBeMadeStatic.Global public IReadOnlyList AvailableLanguages => Cultures.SupportedCultures; [ObservableProperty] private NumberFormatMode selectedNumberFormatMode; public IReadOnlyList NumberFormatModes { get; } = Enum.GetValues().Where(mode => mode != default).ToList(); public IReadOnlyList AnimationScaleOptions { get; } = new[] { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f }; public IReadOnlyList HolidayModes { get; } = Enum.GetValues().ToList(); [ObservableProperty] private float selectedAnimationScale; // Shared folder options [ObservableProperty] private bool removeSymlinksOnShutdown; // Integrations section [ObservableProperty] private bool isDiscordRichPresenceEnabled; // Console section [ObservableProperty] private int consoleLogHistorySize; // Debug section [ObservableProperty] private string? debugPaths; [ObservableProperty] private string? debugCompatInfo; [ObservableProperty] private string? debugGpuInfo; [ObservableProperty] private HolidayMode holidayModeSetting; [ObservableProperty] private bool infinitelyScrollWorkflowBrowser; [ObservableProperty] private bool autoLoadCivitModels; [ObservableProperty] private bool moveFilesOnImport; [ObservableProperty] private int maxConcurrentDownloads; [ObservableProperty] private bool showAllAvailablePythonVersions; [ObservableProperty] public partial List AllBaseModelTypes { get; set; } = []; private SourceCache BaseModelTypesCache { get; } = new(s => s); #region System Settings [ObservableProperty] private bool isWindowsLongPathsEnabled; [ObservableProperty] private ObservableCollection gpuInfoCollection = []; [ObservableProperty] private GpuInfo? preferredGpu; #endregion #region System Info private static Lazy> GpuInfosLazy { get; } = new(() => HardwareHelper.IterGpuInfo().ToImmutableArray()); public static IReadOnlyList GpuInfos => GpuInfosLazy.Value; [ObservableProperty] private MemoryInfo memoryInfo; private readonly DispatcherTimer hardwareInfoUpdateTimer = new() { Interval = TimeSpan.FromSeconds(2.627), }; public Task CpuInfoAsync => HardwareHelper.GetCpuInfoAsync(); #endregion // Info section private const int VersionTapCountThreshold = 7; [ObservableProperty, NotifyPropertyChangedFor(nameof(VersionFlyoutText))] private int versionTapCount; [ObservableProperty] private bool isVersionTapTeachingTipOpen; public string VersionFlyoutText => $"You are {VersionTapCountThreshold - VersionTapCount} clicks away from enabling Debug options."; public string DataDirectory => settingsManager.IsLibraryDirSet ? settingsManager.LibraryDir : "Not set"; public string ModelsDirectory => settingsManager.ModelsDirectory; public MainSettingsViewModel( INotificationService notificationService, ISettingsManager settingsManager, IPrerequisiteHelper prerequisiteHelper, IPyRunner pyRunner, IServiceManager dialogFactory, ITrackedDownloadService trackedDownloadService, SharedState sharedState, ICompletionProvider completionProvider, IModelIndexService modelIndexService, INavigationService settingsNavigationService, IAccountsService accountsService, ICivitBaseModelTypeService baseModelTypeService ) { this.notificationService = notificationService; this.settingsManager = settingsManager; this.prerequisiteHelper = prerequisiteHelper; this.pyRunner = pyRunner; this.dialogFactory = dialogFactory; this.trackedDownloadService = trackedDownloadService; this.completionProvider = completionProvider; this.modelIndexService = modelIndexService; this.settingsNavigationService = settingsNavigationService; this.accountsService = accountsService; this.baseModelTypeService = baseModelTypeService; SharedState = sharedState; if (Program.Args.DebugMode) { SharedState.IsDebugMode = true; } SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1]; SelectedLanguage = Cultures.GetSupportedCultureOrDefault(settingsManager.Settings.Language); RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown; SelectedAnimationScale = settingsManager.Settings.AnimationScale; HolidayModeSetting = settingsManager.Settings.HolidayModeSetting; settingsManager.RelayPropertyFor(this, vm => vm.SelectedTheme, settings => settings.Theme); settingsManager.RelayPropertyFor( this, vm => vm.IsDiscordRichPresenceEnabled, settings => settings.IsDiscordRichPresenceEnabled, true ); settingsManager.RelayPropertyFor( this, vm => vm.SelectedAnimationScale, settings => settings.AnimationScale ); settingsManager.RelayPropertyFor( this, vm => vm.HolidayModeSetting, settings => settings.HolidayModeSetting ); settingsManager.RelayPropertyFor( this, vm => vm.InfinitelyScrollWorkflowBrowser, settings => settings.IsWorkflowInfiniteScrollEnabled, true ); settingsManager.RelayPropertyFor( this, vm => vm.SelectedNumberFormatMode, settings => settings.NumberFormatMode, true ); settingsManager.RelayPropertyFor( this, vm => vm.AutoLoadCivitModels, settings => settings.AutoLoadCivitModels, true ); settingsManager.RelayPropertyFor( this, vm => vm.MoveFilesOnImport, settings => settings.MoveFilesOnImport, true ); settingsManager.RelayPropertyFor( this, vm => vm.ConsoleLogHistorySize, settings => settings.ConsoleLogHistorySize, true ); settingsManager.RelayPropertyFor( this, vm => vm.PreferredGpu, settings => settings.PreferredGpu, true ); settingsManager.RelayPropertyFor( this, vm => vm.MaxConcurrentDownloads, settings => settings.MaxConcurrentDownloads, true ); settingsManager.RelayPropertyFor( this, vm => vm.ShowAllAvailablePythonVersions, settings => settings.ShowAllAvailablePythonVersions, true ); AddDisposable( BaseModelTypesCache .Connect() .DeferUntilLoaded() .Transform(x => new BaseModelOptionViewModel { ModelType = x, IsSelected = !settingsManager.Settings.DisabledBaseModelTypes.Contains(x), }) .SortAndBind( AllBaseModelTypes, SortExpressionComparer.Ascending(x => x.ModelType) ) .WhenPropertyChanged(vm => vm.IsSelected) .ObserveOn(SynchronizationContext.Current) .Subscribe(next => { if (next.Sender.IsSelected) { settingsManager.Transaction(s => s.DisabledBaseModelTypes.TryRemove(next.Sender.ModelType) ); } else { settingsManager.Transaction(s => s.DisabledBaseModelTypes.TryAdd(next.Sender.ModelType) ); } }) ); DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); hardwareInfoUpdateTimer.Tick += OnHardwareInfoUpdateTimerTick; } /// public override void OnLoaded() { base.OnLoaded(); hardwareInfoUpdateTimer.Start(); if (Compat.IsWindows) { UpdateRegistrySettings(); } } /// public override void OnUnloaded() { base.OnUnloaded(); hardwareInfoUpdateTimer.Stop(); } /// public override async Task OnLoadedAsync() { await base.OnLoadedAsync(); await notificationService.TryAsync(completionProvider.Setup()); var gpuInfos = HardwareHelper.IterGpuInfo(); GpuInfoCollection = new ObservableCollection(gpuInfos); PreferredGpu ??= GpuInfos.FirstOrDefault(gpu => gpu.Name?.Contains("nvidia", StringComparison.InvariantCultureIgnoreCase) ?? false ) ?? GpuInfos.FirstOrDefault(); if (Design.IsDesignMode) return; var baseModelTypes = await baseModelTypeService.GetBaseModelTypes(includeAllOption: false); BaseModelTypesCache.Edit(updater => updater.Load(baseModelTypes)); // Start accounts update accountsService .RefreshAsync() .SafeFireAndForget(ex => { Logger.Error(ex, "Failed to refresh accounts"); notificationService.ShowPersistent( "Failed to update account status", ex.ToString(), NotificationType.Error ); }); } [SupportedOSPlatform("windows")] private void UpdateRegistrySettings() { try { using var fsKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\FileSystem") ?? throw new InvalidOperationException( "Could not open registry key 'SYSTEM\\CurrentControlSet\\Control\\FileSystem'" ); IsWindowsLongPathsEnabled = Convert.ToBoolean(fsKey.GetValue("LongPathsEnabled", null)); } catch (Exception e) { Logger.Error(e, "Could not read registry settings"); notificationService.Show("Could not read registry settings", e.Message, NotificationType.Error); } } private void OnHardwareInfoUpdateTimerTick(object? sender, EventArgs e) { if (HardwareHelper.IsMemoryInfoAvailable && HardwareHelper.TryGetMemoryInfo(out var newMemoryInfo)) { MemoryInfo = newMemoryInfo; } // Stop timer if live memory info is not available if (!HardwareHelper.IsLiveMemoryUsageInfoAvailable) { (sender as DispatcherTimer)?.Stop(); } } partial void OnSelectedThemeChanged(string? value) { // In case design / tests if (Application.Current is null) return; // Change theme Application.Current.RequestedThemeVariant = value switch { "Dark" => ThemeVariant.Dark, "Light" => ThemeVariant.Light, _ => ThemeVariant.Default, }; } partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue) { if (oldValue is null || newValue.Name == Cultures.Current?.Name) return; // Set locale if (AvailableLanguages.Contains(newValue)) { Logger.Info("Changing language from {Old} to {New}", oldValue, newValue); Cultures.TrySetSupportedCulture(newValue, settingsManager.Settings.NumberFormatMode); settingsManager.Transaction(s => s.Language = newValue.Name); var dialog = new BetterContentDialog { Title = Resources.Label_RelaunchRequired, Content = Resources.Text_RelaunchRequiredToApplyLanguage, DefaultButton = ContentDialogButton.Primary, PrimaryButtonText = Resources.Action_Relaunch, CloseButtonText = Resources.Action_RelaunchLater, }; Dispatcher.UIThread.InvokeAsync(async () => { if (await dialog.ShowAsync() == ContentDialogResult.Primary) { // Start the new app while passing our own PID to wait for exit Process.Start(Compat.AppCurrentPath, $"--wait-for-exit-pid {Environment.ProcessId}"); // Shutdown the current app App.Shutdown(); } }); } else { Logger.Info("Requested invalid language change from {Old} to {New}", oldValue, newValue); } } partial void OnRemoveSymlinksOnShutdownChanged(bool value) { settingsManager.Transaction(s => s.RemoveFolderLinksOnShutdown = value); } partial void OnMaxConcurrentDownloadsChanged(int value) { trackedDownloadService.UpdateMaxConcurrentDownloads(value); } public async Task ResetCheckpointCache() { await notificationService.TryAsync(modelIndexService.RefreshIndex()); notificationService.Show( "Checkpoint cache reset", "The checkpoint cache has been reset.", NotificationType.Success ); } [RelayCommand] private void NavigateToSubPage(Type viewModelType) { Dispatcher.UIThread.Post( () => settingsNavigationService.NavigateTo( viewModelType, BetterSlideNavigationTransition.PageSlideFromRight ), DispatcherPriority.Send ); } #region Package Environment [RelayCommand] private async Task OpenEnvVarsDialog() { var viewModel = dialogFactory.Get(); // Load current settings var current = settingsManager.Settings.UserEnvironmentVariables ?? new Dictionary(); viewModel.EnvVars = new ObservableCollection( current.Select(kvp => new EnvVarKeyPair(kvp.Key, kvp.Value)) ); var dialog = viewModel.GetDialog(); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { // Save settings var newEnvVars = viewModel .EnvVars.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key)) .GroupBy(kvp => kvp.Key, StringComparer.Ordinal) .ToDictionary(g => g.Key, g => g.First().Value, StringComparer.Ordinal); settingsManager.Transaction(s => s.UserEnvironmentVariables = newEnvVars); } } [RelayCommand] private async Task CheckPythonVersion() { var isInstalled = prerequisiteHelper.IsPythonInstalled; Logger.Debug($"Check python installed: {isInstalled}"); // Ensure python installed if (!prerequisiteHelper.IsPythonInstalled) { // Need 7z as well for site packages repack Logger.Debug("Python not installed, unpacking resources..."); await prerequisiteHelper.UnpackResourcesIfNecessary(); Logger.Debug("Unpacked resources, installing python..."); await prerequisiteHelper.InstallPythonIfNecessary(); } // Get python version await pyRunner.Initialize(); var result = await pyRunner.GetVersionInfo(); // Show dialog box var dialog = new ContentDialog { Title = Resources.Label_PythonVersionInfo, Content = result, PrimaryButtonText = Resources.Action_OK, IsPrimaryButtonEnabled = true, }; await dialog.ShowAsync(); } [RelayCommand] private async Task RunPythonProcess() { await prerequisiteHelper.UnpackResourcesIfNecessary(); await prerequisiteHelper.InstallPythonIfNecessary(); var processPath = new FilePath(PyRunner.PythonExePath); if ( await DialogHelper.GetTextEntryDialogResultAsync( new TextBoxField { Label = "Arguments", InnerLeftText = processPath.Name }, title: "Run Python" ) is not { IsPrimary: true } dialogResult ) { return; } var step = new ProcessStep { FileName = processPath, Args = dialogResult.Value.Text, WorkingDirectory = Compat.AppCurrentDir, EnvironmentVariables = settingsManager.Settings.EnvironmentVariables.ToImmutableDictionary(), }; ConsoleProcessRunner.RunProcessStepAsync(step).SafeFireAndForget(); } [RelayCommand] private async Task ClearPipCache() { await prerequisiteHelper.UnpackResourcesIfNecessary(); await prerequisiteHelper.InstallPythonIfNecessary(); var processPath = new FilePath(PyRunner.PythonExePath); var step = new ProcessStep { FileName = processPath, Args = ["-m", "pip", "cache", "purge"], WorkingDirectory = Compat.AppCurrentDir, EnvironmentVariables = settingsManager.Settings.EnvironmentVariables.ToImmutableDictionary(), }; ConsoleProcessRunner.RunProcessStepAsync(step).SafeFireAndForget(); } [RelayCommand] private async Task ClearUvCache() { await prerequisiteHelper.InstallUvIfNecessary(); var processPath = new FilePath(prerequisiteHelper.UvExePath); var step = new ProcessStep { FileName = processPath, Args = ["cache", "clean"], WorkingDirectory = Compat.AppCurrentDir, EnvironmentVariables = settingsManager.Settings.EnvironmentVariables.ToImmutableDictionary(), }; ConsoleProcessRunner.RunProcessStepAsync(step).SafeFireAndForget(); } [RelayCommand] private async Task RunGitProcess() { await prerequisiteHelper.InstallGitIfNecessary(); FilePath processPath; if (Compat.IsWindows) { processPath = new FilePath(prerequisiteHelper.GitBinPath, "git.exe"); } else { var whichGitResult = await ProcessRunner.RunBashCommand(["which", "git"]).EnsureSuccessExitCode(); processPath = new FilePath(whichGitResult.StandardOutput?.Trim() ?? "git"); } if ( await DialogHelper.GetTextEntryDialogResultAsync( new TextBoxField { Label = "Arguments", InnerLeftText = "git" }, title: "Run Git" ) is not { IsPrimary: true } dialogResult ) { return; } var step = new ProcessStep { FileName = processPath, Args = dialogResult.Value.Text, WorkingDirectory = Compat.AppCurrentDir, EnvironmentVariables = settingsManager.Settings.EnvironmentVariables.ToImmutableDictionary(), }; ConsoleProcessRunner.RunProcessStepAsync(step).SafeFireAndForget(); } [RelayCommand] private async Task FixGitLongPaths() { var result = await prerequisiteHelper.FixGitLongPaths(); if (result) { notificationService.Show( "Long Paths Enabled", "Git long paths have been enabled.", NotificationType.Success ); } else { notificationService.Show( "Long Paths Not Enabled", "Could not enable Git long paths.", NotificationType.Error ); } } partial void OnShowAllAvailablePythonVersionsChanged(bool value) { if (!value) return; Dispatcher.UIThread.InvokeAsync(async () => { var dialog = DialogHelper.CreateMarkdownDialog( Resources.Label_UnsupportedPythonVersionWarningDescription, Resources.Label_PythonVersionWarningTitle ); dialog.IsPrimaryButtonEnabled = true; dialog.IsSecondaryButtonEnabled = true; dialog.PrimaryButtonText = Resources.Action_Yes; dialog.CloseButtonText = Resources.Label_No; dialog.DefaultButton = ContentDialogButton.Primary; var result = await dialog.ShowAsync(); if (result is not ContentDialogResult.Primary) { ShowAllAvailablePythonVersions = false; } }); } #endregion #region Directory Shortcuts public CommandItem[] DirectoryShortcutCommands => [ new CommandItem( new AsyncRelayCommand(() => ProcessRunner.OpenFolderBrowser(Compat.AppDataHome)), Resources.Label_AppData ), new CommandItem( new AsyncRelayCommand(() => ProcessRunner.OpenFolderBrowser(Compat.AppDataHome.JoinDir("Logs")) ), Resources.Label_Logs ), new CommandItem( new AsyncRelayCommand(() => ProcessRunner.OpenFolderBrowser(settingsManager.LibraryDir)), Resources.Label_DataDirectory ), new CommandItem( new AsyncRelayCommand(() => ProcessRunner.OpenFolderBrowser(settingsManager.ModelsDirectory)), Resources.Label_Checkpoints ), new CommandItem( new AsyncRelayCommand(() => ProcessRunner.OpenFolderBrowser(settingsManager.LibraryDir.JoinDir("Packages")) ), Resources.Label_Packages ), ]; #endregion #region System /// /// Adds Stability Matrix to Start Menu for the current user. /// [RelayCommand] private async Task AddToStartMenu() { if (!Compat.IsWindows) { notificationService.Show("Not supported", "This feature is only supported on Windows."); return; } await using var _ = new MinimumDelay(200, 300); var shortcutDir = new DirectoryPath( Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs" ); var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk"); var appPath = Compat.AppCurrentPath; var iconPath = shortcutDir.JoinFile("Stability Matrix.ico"); await Assets.AppIcon.ExtractTo(iconPath); WindowsShortcuts.CreateShortcut(shortcutLink, appPath, iconPath, "Stability Matrix"); notificationService.Show( "Added to Start Menu", "Stability Matrix has been added to the Start Menu.", NotificationType.Success ); } /// /// Add Stability Matrix to Start Menu for all users. /// Requires Admin elevation. /// [RelayCommand] private async Task AddToGlobalStartMenu() { if (!Compat.IsWindows) { notificationService.Show("Not supported", "This feature is only supported on Windows."); return; } // Confirmation dialog var dialog = new BetterContentDialog { Title = "This will create a shortcut for Stability Matrix in the Start Menu for all users", Content = "You will be prompted for administrator privileges. Continue?", PrimaryButtonText = Resources.Action_Yes, CloseButtonText = Resources.Action_Cancel, DefaultButton = ContentDialogButton.Primary, }; if (await dialog.ShowAsync() != ContentDialogResult.Primary) { return; } await using var _ = new MinimumDelay(200, 300); var shortcutDir = new DirectoryPath( Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu), "Programs" ); var shortcutLink = shortcutDir.JoinFile("Stability Matrix.lnk"); var appPath = Compat.AppCurrentPath; var iconPath = shortcutDir.JoinFile("Stability Matrix.ico"); // We can't directly write to the targets, so extract to temporary directory first using var tempDir = new TempDirectoryPath(); await Assets.AppIcon.ExtractTo(tempDir.JoinFile("Stability Matrix.ico")); WindowsShortcuts.CreateShortcut( tempDir.JoinFile("Stability Matrix.lnk"), appPath, iconPath, "Stability Matrix" ); // Move to target try { var moveLinkResult = await WindowsElevated.MoveFiles( (tempDir.JoinFile("Stability Matrix.lnk"), shortcutLink), (tempDir.JoinFile("Stability Matrix.ico"), iconPath) ); if (moveLinkResult != 0) { notificationService.ShowPersistent( "Failed to create shortcut", $"Could not copy shortcut", NotificationType.Error ); } } catch (Win32Exception e) { // We'll get this exception if user cancels UAC Logger.Warn(e, "Could not create shortcut"); notificationService.Show("Could not create shortcut", "", NotificationType.Warning); return; } notificationService.Show( "Added to Start Menu", "Stability Matrix has been added to the Start Menu for all users.", NotificationType.Success ); } public async Task PickNewDataDirectory() { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog { IsPrimaryButtonEnabled = false, IsSecondaryButtonEnabled = false, IsFooterVisible = false, Content = new SelectDataDirectoryDialog { DataContext = viewModel }, }; var result = await dialog.ShowAsync(); if (result == ContentDialogResult.Primary) { // 1. For portable mode, call settings.SetPortableMode() if (viewModel.IsPortableMode) { settingsManager.SetPortableMode(); } // 2. For custom path, call settings.SetLibraryPath(path) else { settingsManager.SetLibraryPath(viewModel.DataDirectory); } // Restart var restartDialog = new BetterContentDialog { Title = "Restart required", Content = "Stability Matrix must be restarted for the changes to take effect.", PrimaryButtonText = Resources.Action_Restart, DefaultButton = ContentDialogButton.Primary, IsSecondaryButtonEnabled = false, }; await restartDialog.ShowAsync(); Process.Start(Compat.AppCurrentPath); App.Shutdown(); } } public async Task PickNewModelsFolder() { var provider = App.StorageProvider; var result = await provider.OpenFolderPickerAsync(new FolderPickerOpenOptions()); if (result.Count == 0) return; var newPath = (result[0].Path.LocalPath); settingsManager.Transaction(s => s.ModelDirectoryOverride = newPath); SharedFolders.SetupSharedModelFolders(newPath); // Restart var restartDialog = new BetterContentDialog { Title = "Restart required", Content = "Stability Matrix must be restarted for the changes to take effect.", PrimaryButtonText = Resources.Action_Restart, DefaultButton = ContentDialogButton.Primary, IsSecondaryButtonEnabled = false, }; await restartDialog.ShowAsync(); Process.Start(Compat.AppCurrentPath); App.Shutdown(); } #endregion #region Debug Section public void LoadDebugInfo() { var assembly = Assembly.GetExecutingAssembly(); var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); DebugPaths = $""" Current Working Directory [Environment.CurrentDirectory] "{Environment.CurrentDirectory}" App Directory [Assembly.GetExecutingAssembly().Location] "{assembly.Location}" App Directory [AppContext.BaseDirectory] "{AppContext.BaseDirectory}" AppData Directory [SpecialFolder.ApplicationData] "{appData}" """; // 1. Check portable mode var appDir = Compat.AppCurrentDir; var expectedPortableFile = Path.Combine(appDir, "Data", ".sm-portable"); var isPortableMode = File.Exists(expectedPortableFile); DebugCompatInfo = $""" Platform: {Compat.Platform} AppData: {Compat.AppData} AppDataHome: {Compat.AppDataHome} AppCurrentDir: {Compat.AppCurrentDir} ExecutableName: {Compat.GetExecutableName()} AppName: {Compat.GetAppName()} -- Settings -- Expected Portable Marker file: {expectedPortableFile} Portable Marker file exists: {isPortableMode} IsLibraryDirSet = {settingsManager.IsLibraryDirSet} IsPortableMode = {settingsManager.IsPortableMode} """; // Get Gpu info var gpuInfo = ""; foreach (var (i, gpu) in HardwareHelper.IterGpuInfo().Enumerate()) { gpuInfo += $"[{i + 1}] {gpu}\n"; } DebugGpuInfo = gpuInfo; } // Debug buttons [RelayCommand] private void DebugNotification() { notificationService.Show( new Notification( title: "Test Notification", message: "Here is some message", type: NotificationType.Information ) ); } [RelayCommand] private async Task DebugContentDialog() { var dialog = new ContentDialog { DefaultButton = ContentDialogButton.Primary, Title = "Test title", PrimaryButtonText = Resources.Action_OK, CloseButtonText = Resources.Action_Close, }; var result = await dialog.ShowAsync(); notificationService.Show(new Notification("Content dialog closed", $"Result: {result}")); } [RelayCommand] private void DebugThrowException() { throw new OperationCanceledException("Example Message"); } [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task DebugThrowAsyncException() { await Task.Yield(); throw new ApplicationException("Example Message"); } [RelayCommand] private void DebugThrowDispatcherException() { Dispatcher.UIThread.Post(() => throw new OperationCanceledException("Example Message")); } [RelayCommand] private async Task DebugMakeImageGrid() { var provider = App.StorageProvider; var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions() { AllowMultiple = true }); if (files.Count == 0) return; var images = await files.SelectAsync(async f => SKImage.FromEncodedData(await f.OpenReadAsync())); var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray()); // Show preview using var peekPixels = grid.PeekPixels(); using var data = peekPixels.Encode(SKEncodedImageFormat.Jpeg, 100); await using var stream = data.AsStream(); var bitmap = WriteableBitmap.Decode(stream); var galleryImages = new List { new(bitmap) }; galleryImages.AddRange(files.Select(f => new ImageSource(f.Path.ToString()))); var imageBox = new ImageGalleryCard { Width = 1000, Height = 900, DataContext = dialogFactory.Get(vm => { vm.ImageSources.AddRange(galleryImages); }), }; var dialog = new BetterContentDialog { MaxDialogWidth = 1000, MaxDialogHeight = 1000, FullSizeDesired = true, Content = imageBox, CloseButtonText = "Close", ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, }; await dialog.ShowAsync(); } [RelayCommand] private async Task DebugLoadCompletionCsv() { var provider = App.StorageProvider; var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()); if (files.Count == 0) return; await completionProvider.LoadFromFile(files[0].TryGetLocalPath()!, true); notificationService.Show("Loaded completion file", ""); } [RelayCommand] private async Task DebugImageMetadata() { var provider = App.StorageProvider; var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()); if (files.Count == 0) return; var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!); var textualTags = metadata.GetTextualData()?.ToArray(); if (textualTags is null) { notificationService.Show("No textual data found", ""); return; } if (metadata.GetGenerationParameters() is { } parameters) { var parametersJson = JsonSerializer.Serialize(parameters); var dialog = DialogHelper.CreateJsonDialog(parametersJson, "Generation Parameters"); await dialog.ShowAsync(); } } [RelayCommand] private async Task DebugRefreshModelsIndex() { await modelIndexService.RefreshIndex(); } [RelayCommand] private async Task DebugTrackedDownload() { var textFields = new TextBoxField[] { new() { Label = "Url" }, new() { Label = "File path" }, }; var dialog = DialogHelper.CreateTextEntryDialog("Add download", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var url = textFields[0].Text; var filePath = textFields[1].Text; var download = trackedDownloadService.NewDownload(new Uri(url), new FilePath(filePath)); await trackedDownloadService.TryStartDownload(download); } } [RelayCommand] private async Task DebugWhich() { var textFields = new TextBoxField[] { new() { Label = "Thing" } }; var dialog = DialogHelper.CreateTextEntryDialog("Which", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var result = await Utilities.WhichAsync(textFields[0].Text); await DialogHelper.CreateMarkdownDialog(result).ShowAsync(); } } [RelayCommand] private async Task DebugRobocopy() { var textFields = new TextBoxField[] { new() { Label = "Source" }, new() { Label = "Destination" }, }; var dialog = DialogHelper.CreateTextEntryDialog("Robocopy", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var result = await WindowsElevated.Robocopy(textFields[0].Text, textFields[1].Text); await DialogHelper.CreateMarkdownDialog(result.ToString()).ShowAsync(); } } [RelayCommand] private async Task DebugInstallUv() { await prerequisiteHelper.InstallUvIfNecessary(); notificationService.Show("Installed Uv", "Uv has been installed.", NotificationType.Success); } [RelayCommand] private async Task DebugRunUv() { var textFields = new TextBoxField[] { new() { Label = "uv", Watermark = "uv" }, }; var dialog = DialogHelper.CreateTextEntryDialog("UV Run", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var uv = new UvManager(settingsManager); var result = await uv.ListAvailablePythonsAsync(onConsoleOutput: output => { Logger.Info(output.Text); }); var sb = new StringBuilder(); foreach (var info in result) { sb.AppendLine($"{info}\r\n\r\n"); } await DialogHelper.CreateMarkdownDialog(sb.ToString()).ShowAsync(); } } #endregion #region Debug Commands public CommandItem[] DebugCommands => [ new CommandItem(DebugRefreshModelIndexCommand), new CommandItem(DebugFindLocalModelFromIndexCommand), new CommandItem(DebugExtractDmgCommand), new CommandItem(DebugShowNativeNotificationCommand), new CommandItem(DebugClearImageCacheCommand), new CommandItem(DebugGCCollectCommand), new CommandItem(DebugExtractImagePromptsToTxtCommand), new CommandItem(DebugShowImageMaskEditorCommand), new CommandItem(DebugExtractImagePromptsToTxtCommand), new CommandItem(DebugShowConfirmDeleteDialogCommand), new CommandItem(DebugShowModelMetadataEditorDialogCommand), new CommandItem(DebugNvidiaSmiCommand), new CommandItem(DebugShowGitVersionSelectorDialogCommand), new CommandItem(DebugShowMockGitVersionSelectorDialogCommand), new CommandItem(DebugWhichCommand), new CommandItem(DebugRobocopyCommand), new CommandItem(DebugInstallUvCommand), new CommandItem(DebugRunUvCommand), ]; [RelayCommand] private async Task DebugShowGitVersionSelectorDialog() { var vm = new GitVersionSelectorViewModel { GitVersionProvider = new CachedCommandGitVersionProvider( "https://github.com/ltdrdata/ComfyUI-Manager", prerequisiteHelper ), }; var dialog = vm.GetDialog(); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { notificationService.ShowPersistent("Selected version", $"{vm.SelectedGitVersion}"); } } [RelayCommand] private async Task DebugShowMockGitVersionSelectorDialog() { var vm = new GitVersionSelectorViewModel { GitVersionProvider = new MockGitVersionProvider() }; var dialog = vm.GetDialog(); await dialog.ShowAsync(); } [RelayCommand] private async Task DebugShowModelMetadataEditorDialog() { var vm = dialogFactory.Get(); vm.ThumbnailFilePath = Assets.NoImage.ToString(); vm.Tags = "tag1, tag2, tag3"; vm.ModelDescription = "This is a description"; vm.ModelName = "Model Name"; vm.VersionName = "1.0.0"; vm.TrainedWords = "word1, word2, word3"; vm.ModelType = CivitModelType.Checkpoint; vm.BaseModelType = "Pony"; var dialog = vm.GetDialog(); dialog.MinDialogHeight = 800; dialog.IsPrimaryButtonEnabled = true; dialog.IsFooterVisible = true; dialog.PrimaryButtonText = "Save"; dialog.DefaultButton = ContentDialogButton.Primary; dialog.CloseButtonText = "Cancel"; await dialog.ShowAsync(); } [RelayCommand] private async Task DebugShowConfirmDeleteDialog() { var vm = dialogFactory.Get(); vm.IsRecycleBinAvailable = false; vm.PathsToDelete = Enumerable .Range(1, 64) .Select(i => $"C:/Users/ExampleUser/Data/ExampleFile{i}.txt") .ToArray(); await vm.GetDialog().ShowAsync(); } [RelayCommand] private async Task DebugRefreshModelIndex() { await modelIndexService.RefreshIndex(); } [RelayCommand] private async Task DebugFindLocalModelFromIndex() { var textFields = new TextBoxField[] { new() { Label = "Blake3 Hash" }, new() { Label = "SharedFolderType" }, }; var dialog = DialogHelper.CreateTextEntryDialog("Find Local Model", "", textFields); if (await dialog.ShowAsync() == ContentDialogResult.Primary) { var timer = new Stopwatch(); List results; if (textFields.ElementAtOrDefault(0)?.Text is { } hash && !string.IsNullOrWhiteSpace(hash)) { timer.Restart(); results = (await modelIndexService.FindByHashAsync(hash)).ToList(); timer.Stop(); } else if (textFields.ElementAtOrDefault(1)?.Text is { } type && !string.IsNullOrWhiteSpace(type)) { var folderTypes = Enum.Parse(type, true); timer.Restart(); results = (await modelIndexService.FindByModelTypeAsync(folderTypes)).ToList(); timer.Stop(); } else { return; } if (results.Count != 0) { await DialogHelper .CreateMarkdownDialog( string.Join( "\n\n", results.Select( (model, i) => $"[{i + 1}] {model.RelativePath.ToRepr()} " + $"({model.DisplayModelName}, {model.DisplayModelVersion})" ) ), $"Found Models ({CodeTimer.FormatTime(timer.Elapsed)})" ) .ShowAsync(); } else { await DialogHelper .CreateMarkdownDialog(":(", $"No models found ({CodeTimer.FormatTime(timer.Elapsed)})") .ShowAsync(); } } } [RelayCommand(CanExecute = nameof(IsMacOS))] private async Task DebugExtractDmg() { if (!Compat.IsMacOS) return; // Select File var files = await App.StorageProvider.OpenFilePickerAsync( new FilePickerOpenOptions { Title = "Select .dmg file" } ); if (files.FirstOrDefault()?.TryGetLocalPath() is not { } dmgFile) return; // Select output directory var folders = await App.StorageProvider.OpenFolderPickerAsync( new FolderPickerOpenOptions { Title = "Select output directory" } ); if (folders.FirstOrDefault()?.TryGetLocalPath() is not { } outputDir) return; // Extract notificationService.Show("Extracting...", dmgFile); await ArchiveHelper.ExtractDmg(dmgFile, outputDir); notificationService.Show("Extraction Complete", dmgFile); } [RelayCommand] private async Task DebugShowNativeNotification() { var nativeManager = await notificationService.GetNativeNotificationManagerAsync(); if (nativeManager is null) { notificationService.Show( "Not supported", "Native notifications are not supported on this platform.", NotificationType.Warning ); return; } await nativeManager.ShowNotification( new DesktopNotifications.Notification { Title = "Test Notification", Body = "Here is some message", Buttons = { ("Action", "__Debug_Action"), ("Close", "__Debug_Close") }, } ); } [RelayCommand] private void DebugClearImageCache() { if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) { loader.ClearCache(); } } [RelayCommand] private void DebugGCCollect() { GC.Collect(); } [RelayCommand] private async Task DebugExtractImagePromptsToTxt() { // Choose images var provider = App.StorageProvider; var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = true }); if (files.Count == 0) return; var images = await Task.Run(() => files.Select(f => LocalImageFile.FromPath(f.TryGetLocalPath()!)).ToList() ); var successfulFiles = new List(); foreach (var localImage in images) { var imageFile = new FilePath(localImage.AbsolutePath); // Write a txt with the same name as the image var txtFile = imageFile.WithName(imageFile.NameWithoutExtension + ".txt"); // Read metadata if (localImage.GenerationParameters?.PositivePrompt is { } positivePrompt) { await File.WriteAllTextAsync(txtFile, positivePrompt); successfulFiles.Add(localImage); } } notificationService.Show( "Extracted prompts", $"Extracted prompts from {successfulFiles.Count}/{images.Count} images.", NotificationType.Success ); } [RelayCommand] private async Task DebugShowImageMaskEditor() { // Choose background image var provider = App.StorageProvider; var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()); if (files.Count == 0) return; var bitmap = await Task.Run(() => SKBitmap.Decode(files[0].TryGetLocalPath()!)); var vm = dialogFactory.Get(); vm.PaintCanvasViewModel.BackgroundImage = bitmap; await vm.GetDialog().ShowAsync(); } [RelayCommand] private void DebugNvidiaSmi() { HardwareHelper.IterGpuInfoNvidiaSmi(); } #endregion #region Systems Setting Section [RelayCommand] private async Task OnWindowsLongPathsToggleClick() { if (!Compat.IsWindows) return; // Command is called after value is set, so if false we need to disable var requestedValue = IsWindowsLongPathsEnabled; try { var result = await WindowsElevated.SetRegistryValue( @"HKLM\SYSTEM\CurrentControlSet\Control\FileSystem", @"LongPathsEnabled", requestedValue ? 1 : 0 ); if (result != 0) { notificationService.Show( "Failed to toggle long paths", $"Error code: {result}", NotificationType.Error ); return; } await new BetterContentDialog { Title = Resources.Label_ChangesApplied, Content = Resources.Text_RestartMayBeRequiredForSystemChanges, CloseButtonText = Resources.Action_Close, }.ShowAsync(); } catch (Win32Exception e) { if ( e.Message.EndsWith( @"The operation was canceled by the user.", StringComparison.OrdinalIgnoreCase ) ) return; notificationService.Show("Failed to toggle long paths", e.Message, NotificationType.Error); } finally { UpdateRegistrySettings(); } } #endregion #region Info Section public void OnVersionClick() { // Ignore if already enabled if (SharedState.IsDebugMode) return; VersionTapCount++; switch (VersionTapCount) { // Reached required threshold case >= VersionTapCountThreshold: { IsVersionTapTeachingTipOpen = false; // Enable debug options SharedState.IsDebugMode = true; notificationService.Show( "Debug options enabled", "Warning: Improper use may corrupt application state or cause loss of data." ); VersionTapCount = 0; break; } // Open teaching tip above 3rd click case >= 3: IsVersionTapTeachingTipOpen = true; break; } } [RelayCommand] private async Task ShowLicensesDialog() { try { var markdown = GetLicensesMarkdown(); var dialog = DialogHelper.CreateMarkdownDialog(markdown, "Licenses"); dialog.MaxDialogHeight = 600; await dialog.ShowAsync(); } catch (Exception e) { notificationService.Show("Failed to read licenses information", $"{e}", NotificationType.Error); } } private static string GetLicensesMarkdown() { // Read licenses.json using var reader = new StreamReader(Assets.LicensesJson.Open()); var licenses = JsonSerializer.Deserialize>(reader.ReadToEnd()) ?? throw new InvalidOperationException("Failed to read licenses.json"); // Generate markdown var builder = new StringBuilder(); foreach (var license in licenses) { builder.AppendLine( $"## [{license.PackageName}]({license.PackageUrl}) by {string.Join(", ", license.Authors)}" ); builder.AppendLine(); builder.AppendLine(license.Description); builder.AppendLine(); builder.AppendLine($"[{license.LicenseUrl}]({license.LicenseUrl})"); builder.AppendLine(); } return builder.ToString(); } #endregion } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/NotificationSettingsItem.cs ================================================ using System; using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Settings; public partial class NotificationSettingsItem(ISettingsManager settingsManager) : ObservableObject { public NotificationKey? Key { get; set; } [ObservableProperty] private NotificationOption? option; public static IEnumerable AvailableOptions => Enum.GetValues(); partial void OnOptionChanged(NotificationOption? oldValue, NotificationOption? newValue) { if (Key is null || oldValue is null || newValue is null) return; settingsManager.Transaction(settings => { settings.NotificationOptions[Key] = newValue.Value; }); } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/NotificationSettingsViewModel.cs ================================================ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(NotificationSettingsPage))] [RegisterSingleton] [ManagedService] public partial class NotificationSettingsViewModel(ISettingsManager settingsManager) : PageViewModelBase { public override string Title => "Notifications"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Alert }; [ObservableProperty] private IReadOnlyList items = []; public override void OnLoaded() { base.OnLoaded(); Items = GetItems().OrderBy(item => item.Key?.Value).ToImmutableArray(); } private IEnumerable GetItems() { var settingsOptions = settingsManager.Settings.NotificationOptions; foreach (var notificationKey in NotificationKey.All.Values) { // If in settings, include settings value, otherwise default if (!settingsOptions.TryGetValue(notificationKey, out var option)) { option = notificationKey.DefaultOption; } yield return new NotificationSettingsItem(settingsManager) { Key = notificationKey, Option = option }; } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/Settings/UpdateSettingsViewModel.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Exceptionless.DateTimeExtensions; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using FluentIcons.Common; using Injectio.Attributes; using Semver; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Settings; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels.Settings; [View(typeof(UpdateSettingsPage))] [ManagedService] [RegisterSingleton] public partial class UpdateSettingsViewModel : PageViewModelBase { private readonly IUpdateHelper updateHelper; private readonly IAccountsService accountsService; private readonly INavigationService settingsNavService; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsUpdateAvailable))] [NotifyPropertyChangedFor(nameof(HeaderText))] [NotifyPropertyChangedFor(nameof(SubtitleText))] private UpdateStatusChangedEventArgs? updateStatus; public bool IsUpdateAvailable => UpdateStatus?.LatestUpdate != null; public string HeaderText => IsUpdateAvailable ? Resources.Label_UpdateAvailable : Resources.Label_YouAreUpToDate; public string? SubtitleText => UpdateStatus is null ? null : string.Format( Resources.TextTemplate_LastChecked, UpdateStatus.CheckedAt.ToApproximateAgeString() ); [ObservableProperty] private bool isAutoCheckUpdatesEnabled = true; [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedUpdateChannelCard))] private UpdateChannel preferredUpdateChannel = UpdateChannel.Stable; public UpdateChannelCard? SelectedUpdateChannelCard { get => AvailableUpdateChannelCards.First(c => c.UpdateChannel == PreferredUpdateChannel); set => PreferredUpdateChannel = value?.UpdateChannel ?? UpdateChannel.Stable; } public IReadOnlyList AvailableUpdateChannelCards { get; } = new UpdateChannelCard[] { new() { UpdateChannel = UpdateChannel.Development, Description = Resources.Label_UpdatesDevChannelDescription }, new() { UpdateChannel = UpdateChannel.Preview, Description = Resources.Label_UpdatesPreviewChannelDescription }, new() { UpdateChannel = UpdateChannel.Stable } }; public UpdateSettingsViewModel( ISettingsManager settingsManager, IUpdateHelper updateHelper, IAccountsService accountsService, INavigationService settingsNavService ) { this.updateHelper = updateHelper; this.accountsService = accountsService; this.settingsNavService = settingsNavService; settingsManager.RelayPropertyFor( this, vm => vm.PreferredUpdateChannel, settings => settings.PreferredUpdateChannel, true ); settingsManager.RelayPropertyFor( this, vm => vm.IsAutoCheckUpdatesEnabled, settings => settings.CheckForUpdates, true ); accountsService.LykosAccountStatusUpdate += (_, args) => { var isBetaChannelsEnabled = args.User?.Permissions.Contains("read:stabilitymatrix.builds.preview") == true; foreach ( var card in AvailableUpdateChannelCards.Where(c => c.UpdateChannel > UpdateChannel.Stable) ) { card.IsSelectable = isBetaChannelsEnabled; } }; // On update status changed updateHelper.UpdateStatusChanged += (_, args) => { UpdateStatus = args; }; } /// public override async Task OnLoadedAsync() { if (UpdateStatus is null) { await CheckForUpdates(); } OnPropertyChanged(nameof(SubtitleText)); } [RelayCommand] private async Task CheckForUpdates() { if (Design.IsDesignMode) { return; } await updateHelper.CheckForUpdate(); } /// /// Verify a new channel selection is valid, else returns false. /// /// /// public bool VerifyChannelSelection(UpdateChannelCard card) { if (card.UpdateChannel == UpdateChannel.Stable) { return true; } if ( accountsService.LykosStatus?.User is { } user && user.Permissions.Any(p => p.StartsWith("read:stabilitymatrix.builds.")) ) { return true; } return false; } public void ShowLoginRequiredDialog() { Dispatcher .UIThread.InvokeAsync(async () => { var dialog = DialogHelper.CreateTaskDialog( "Become a Supporter", "" + "Support the Stability Matrix Team and get access to early development builds and be the first to test new features. " ); dialog.Buttons = new[] { new(Resources.Label_Accounts, TaskDialogStandardResult.OK), TaskDialogButton.CloseButton }; dialog.Commands = new[] { new TaskDialogCommand { Text = "Patreon", Description = "https://patreon.com/StabilityMatrix", Command = new RelayCommand(() => { ProcessRunner.OpenUrl("https://patreon.com/StabilityMatrix"); }) } }; if (await dialog.ShowAsync(true) is TaskDialogStandardResult.OK) { settingsNavService.NavigateTo( new SuppressNavigationTransitionInfo() ); } }) .SafeFireAndForget(); } partial void OnUpdateStatusChanged(UpdateStatusChangedEventArgs? value) { // Update the update channel cards // Use maximum version from platforms equal or lower than current foreach (var card in AvailableUpdateChannelCards) { card.LatestVersion = value ?.UpdateChannels .Where(kv => kv.Key <= card.UpdateChannel) .Select(kv => kv.Value) .MaxBy(info => info.Version, SemVersion.PrecedenceComparer) ?.Version; } } partial void OnPreferredUpdateChannelChanged(UpdateChannel value) { CheckForUpdatesCommand.ExecuteAsync(null).SafeFireAndForget(); } /// public override string Title => "Updates"; /// public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IconVariant = IconVariant.Filled }; } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs ================================================ using System.Collections.Generic; using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using FluentAvalonia.UI.Controls; using FluentIcons.Common; using Injectio.Attributes; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(SettingsPage))] [RegisterSingleton] public partial class SettingsViewModel : PageViewModelBase { public override string Title => "Settings"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Settings, IconVariant = IconVariant.Filled }; public IReadOnlyList SubPages { get; } [ObservableProperty] private ObservableCollection currentPagePath = []; [ObservableProperty] private PageViewModelBase? currentPage; public SettingsViewModel(IServiceManager vmFactory) { SubPages = new PageViewModelBase[] { vmFactory.Get(), vmFactory.Get(), vmFactory.Get(), vmFactory.Get(), vmFactory.Get(), vmFactory.Get() }; CurrentPagePath.AddRange(SubPages); CurrentPage = SubPages[0]; } partial void OnCurrentPageChanged(PageViewModelBase? value) { if (value is null) { return; } if (value is MainSettingsViewModel) { CurrentPagePath.Clear(); CurrentPagePath.Add(value); } else { CurrentPagePath.Clear(); CurrentPagePath.AddRange(new[] { SubPages[0], value }); } } } ================================================ FILE: StabilityMatrix.Avalonia/ViewModels/WorkflowsPageViewModel.cs ================================================ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels; [View(typeof(WorkflowsPage))] [RegisterSingleton] public partial class WorkflowsPageViewModel : PageViewModelBase { public override string Title => Resources.Label_Workflows; public override IconSource IconSource => new FASymbolIconSource { Symbol = "fa-solid fa-circle-nodes" }; public IReadOnlyList Pages { get; } [ObservableProperty] private TabItem? selectedPage; /// public WorkflowsPageViewModel( OpenArtBrowserViewModel openArtBrowserViewModel, InstalledWorkflowsViewModel installedWorkflowsViewModel ) { Pages = new List( new List([openArtBrowserViewModel, installedWorkflowsViewModel]).Select( vm => new TabItem { Header = vm.Header, Content = vm } ) ); SelectedPage = Pages.FirstOrDefault(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class CheckpointBrowserPage : UserControlBase { public CheckpointBrowserPage() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml ================================================  False True ================================================ FILE: StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs ================================================ using System; using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class CivitAiBrowserPage : UserControlBase { public CivitAiBrowserPage() { InitializeComponent(); AcceleratorButtonTeachingTip.Target = AcceleratorButton; AcceleratorButton.Click += (s, e) => { AcceleratorButtonTeachingTip.IsOpen ^= true; }; } private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) { if (sender is not ScrollViewer scrollViewer) return; if (scrollViewer.Offset.Y == 0) return; var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; if (isAtEnd && DataContext is IInfinitelyScroll scroll) { scroll.LoadNextPageAsync().SafeFireAndForget(); } } private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape && DataContext is CivitAiBrowserViewModel viewModel) { viewModel.ClearSearchQuery(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Views/CivitDetailsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/CivitDetailsPage.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views; [RegisterTransient] public partial class CivitDetailsPage : UserControlBase { public CivitDetailsPage() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void InputElement_OnPointerWheelChanged(object? sender, PointerWheelEventArgs e) { if (sender is not ScrollViewer sv) return; var scrollAmount = e.Delta.Y * 75; sv.Offset = new Vector(sv.Offset.X - scrollAmount, sv.Offset.Y); e.Handled = true; } } ================================================ FILE: StabilityMatrix.Avalonia/Views/ConsoleOutputPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml.cs ================================================ using Avalonia.Input; using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.Views.Dialogs; [RegisterTransient] public partial class DownloadResourceDialog : UserControlBase { public DownloadResourceDialog() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void LicenseButton_OnTapped(object? sender, TappedEventArgs e) { var url = ((DownloadResourceViewModel)DataContext!).Resource.LicenseUrl; ProcessRunner.OpenUrl(url!.ToString()); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Dialogs; [RegisterTransient] public partial class EnvVarsDialog : UserControlBase { public EnvVarsDialog() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/ModelMetadataEditorDialog.axaml.cs ================================================ using Avalonia.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Dialogs; [RegisterTransient] public partial class ModelMetadataEditorDialog : DropTargetUserControlBase { public ModelMetadataEditorDialog() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/NewOneClickInstallDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/PythonPackagesDialog.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Dialogs; [RegisterTransient] public partial class PythonPackagesDialog : UserControlBase { public PythonPackagesDialog() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/RecommendedModelsDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/RecommendedModelsDialog.axaml.cs ================================================ using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Dialogs; public partial class RecommendedModelsDialog : UserControlBase { public RecommendedModelsDialog() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/SafetensorMetadataDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/SafetensorMetadataDialog.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Dialogs; [RegisterTransient] public partial class SafetensorMetadataDialog : UserControlBase { public SafetensorMetadataDialog() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs ================================================ using System; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Interactivity; using FluentAvalonia.UI.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class InferencePage : UserControlBase { private Button? _addButton; private Button AddButton => _addButton ??= this.FindControl("TabView")! .GetTemplateChildren() .OfType ================================================ FILE: StabilityMatrix.Avalonia/Views/InstalledWorkflowsPage.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class InstalledWorkflowsPage : UserControlBase { public InstalledWorkflowsPage() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/LaunchPageView.axaml ================================================  True False ================================================ FILE: StabilityMatrix.Avalonia/Views/OpenArtBrowserPage.axaml.cs ================================================ using System; using AsyncAwaitBestPractices; using Avalonia.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class OpenArtBrowserPage : UserControlBase { private readonly ISettingsManager settingsManager; public OpenArtBrowserPage(ISettingsManager settingsManager) { this.settingsManager = settingsManager; InitializeComponent(); } private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) { if (sender is not ScrollViewer scrollViewer) return; if (scrollViewer.Offset.Y == 0) return; var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; if ( isAtEnd && settingsManager.Settings.IsWorkflowInfiniteScrollEnabled && DataContext is IInfinitelyScroll scroll ) { scroll.LoadNextPageAsync().SafeFireAndForget(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Views/OpenModelDbBrowserPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/OpenModelDbBrowserPage.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class OpenModelDbBrowserPage : UserControlBase { public OpenModelDbBrowserPage() { InitializeComponent(); } /*private readonly ISettingsManager settingsManager; public OpenModelDbBrowserPage(ISettingsManager settingsManager) { this.settingsManager = settingsManager; InitializeComponent(); } private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) { if (sender is not ScrollViewer scrollViewer) return; if (scrollViewer.Offset.Y == 0) return; var isAtEnd = Math.Abs(scrollViewer.Offset.Y - scrollViewer.ScrollBarMaximum.Y) < 1f; if ( isAtEnd && settingsManager.Settings.IsWorkflowInfiniteScrollEnabled && DataContext is IInfinitelyScroll scroll ) { scroll.LoadNextPageAsync().SafeFireAndForget(); } }*/ } ================================================ FILE: StabilityMatrix.Avalonia/Views/OutputsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs ================================================ using Avalonia.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class OutputsPage : UserControlBase { public OutputsPage() { InitializeComponent(); } private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape && DataContext is OutputsPageViewModel viewModel) { viewModel.ClearSearchQuery(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml.cs ================================================ using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Navigation; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Helper; using MainPackageManagerViewModel = StabilityMatrix.Avalonia.ViewModels.PackageManager.MainPackageManagerViewModel; namespace StabilityMatrix.Avalonia.Views.PackageManager; [RegisterSingleton] public partial class MainPackageManagerView : UserControlBase { public MainPackageManagerView() { InitializeComponent(); AddHandler(Frame.NavigatedToEvent, OnNavigatedTo, RoutingStrategies.Direct); EventManager.Instance.OneClickInstallFinished += OnOneClickInstallFinished; } private void OnOneClickInstallFinished(object? sender, bool skipped) { if (skipped) return; Dispatcher.UIThread.Invoke(() => { var target = this.FindDescendantOfType() ?.GetVisualChildren() .OfType ================================================ FILE: StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallBrowserView.axaml.cs ================================================ using Avalonia.Input; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels.PackageManager; namespace StabilityMatrix.Avalonia.Views.PackageManager; [RegisterSingleton] public partial class PackageInstallBrowserView : UserControlBase { public PackageInstallBrowserView() { InitializeComponent(); } private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape && DataContext is PackageInstallBrowserViewModel vm) { vm.ClearSearchQuery(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/ProgressManagerPage.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class ProgressManagerPage : UserControlBase { public ProgressManagerPage() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/AccountSettingsPage.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Settings; [RegisterSingleton] public partial class AccountSettingsPage : UserControlBase { public AccountSettingsPage() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/AnalyticsSettingsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/AnalyticsSettingsPage.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views.Settings; [RegisterTransient] public partial class AnalyticsSettingsPage : UserControlBase { public AnalyticsSettingsPage() { InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/InferenceSettingsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/Settings/UpdateSettingsPage.axaml.cs ================================================ using System.Linq; using Avalonia.Controls; using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Avalonia.Views.Settings; [RegisterSingleton] public partial class UpdateSettingsPage : UserControlBase { public UpdateSettingsPage() { InitializeComponent(); } private void ChannelListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { var listBox = (ListBox)sender!; if (e.AddedItems.Count == 0 || e.AddedItems[0] is not UpdateChannelCard item) { return; } var vm = (UpdateSettingsViewModel)DataContext!; if (!vm.VerifyChannelSelection(item)) { listBox.Selection.Clear(); listBox.Selection.SelectedItem = vm.AvailableUpdateChannelCards.First( c => c.UpdateChannel == UpdateChannel.Stable ); vm.ShowLoginRequiredDialog(); } } } ================================================ FILE: StabilityMatrix.Avalonia/Views/SettingsPage.axaml ================================================ 24 17 6,3 Medium ================================================ FILE: StabilityMatrix.Avalonia/Views/SettingsPage.axaml.cs ================================================ using System; using System.ComponentModel; using System.Linq; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Media.Animation; using FluentAvalonia.UI.Navigation; using Injectio.Attributes; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class SettingsPage : UserControlBase, IHandleNavigation { private readonly INavigationService settingsNavigationService; private bool hasLoaded; private SettingsViewModel ViewModel => (SettingsViewModel)DataContext!; [DesignOnly(true)] [Obsolete("For XAML use only", true)] public SettingsPage() : this(App.Services.GetRequiredService>()) { } public SettingsPage(INavigationService settingsNavigationService) { this.settingsNavigationService = settingsNavigationService; InitializeComponent(); settingsNavigationService.SetFrame(FrameView); settingsNavigationService.TypedNavigation += NavigationService_OnTypedNavigation; FrameView.Navigated += FrameView_Navigated; BreadcrumbBar.ItemClicked += BreadcrumbBar_ItemClicked; } /// protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); if (!hasLoaded) { // Initial load, navigate to first page Dispatcher.UIThread.Post( () => settingsNavigationService.NavigateTo( ViewModel.SubPages[0], new SuppressNavigationTransitionInfo() ) ); hasLoaded = true; } } private void NavigationService_OnTypedNavigation(object? sender, TypedNavigationEventArgs e) { ViewModel.CurrentPage = ViewModel.SubPages.FirstOrDefault(x => x.GetType() == e.ViewModelType); } private async void FrameView_Navigated(object? sender, NavigationEventArgs args) { if (args.Content is not PageViewModelBase vm) { return; } ViewModel.CurrentPage = vm; } private async void BreadcrumbBar_ItemClicked(BreadcrumbBar sender, BreadcrumbBarItemClickedEventArgs args) { // Skip if already on same page if (args.Item is not PageViewModelBase viewModel || viewModel == ViewModel.CurrentPage) { return; } settingsNavigationService.NavigateTo(viewModel, BetterSlideNavigationTransition.PageSlideFromLeft); } public bool GoBack() { return settingsNavigationService.GoBack(); } } ================================================ FILE: StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia/Views/WorkflowsPage.axaml.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Views; [RegisterSingleton] public partial class WorkflowsPage : UserControlBase { public WorkflowsPage() { InitializeComponent(); } } ================================================ FILE: StabilityMatrix.Avalonia/app.manifest ================================================  ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml ================================================ Black Transparent ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs ================================================ using System.Collections.Specialized; using Avalonia.Controls; using Avalonia.LogicalTree; using Avalonia.Threading; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls; public partial class LogViewerControl : UserControl { public LogViewerControl() => InitializeComponent(); private ILogDataStoreImpl? vm; private LogModel? item; protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (DataContext is null) return; vm = (ILogDataStoreImpl)DataContext; vm.DataStore.Entries.CollectionChanged += OnCollectionChanged; } private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { Dispatcher.UIThread.Post(() => { item = MyDataGrid.ItemsSource.Cast().LastOrDefault(); }); } protected void OnLayoutUpdated(object? sender, EventArgs e) { if (CanAutoScroll.IsChecked != true || item is null) return; MyDataGrid.ScrollIntoView(item, null); item = null; } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); if (vm is null) return; vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged; } } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs ================================================ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; using SysDrawColor = System.Drawing.Color; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters; public class ChangeColorTypeConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is null) return new SolidColorBrush((Color)(parameter ?? Colors.Black)); var sysDrawColor = (SysDrawColor)value!; return new SolidColorBrush( Color.FromArgb(sysDrawColor.A, sysDrawColor.R, sysDrawColor.G, sysDrawColor.B) ); } public object ConvertBack( object? value, Type targetType, object? parameter, CultureInfo culture ) => throw new NotImplementedException(); } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs ================================================ using System.Globalization; using Avalonia.Data.Converters; using Microsoft.Extensions.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters; public class EventIdConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is null) return "0"; var eventId = (EventId)value; return eventId.ToString(); } // If not implemented, an error is thrown public object ConvertBack( object? value, Type targetType, object? parameter, CultureInfo culture ) => new EventId(0, value?.ToString() ?? string.Empty); } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs ================================================ using Microsoft.Extensions.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Extensions; public static class LoggerExtensions { public static void Emit( this ILogger logger, EventId eventId, LogLevel logLevel, string message, Exception? exception = null, params object?[] args ) { if (logger is null) return; //if (!logger.IsEnabled(logLevel)) // return; switch (logLevel) { case LogLevel.Trace: logger.LogTrace(eventId, message, args); break; case LogLevel.Debug: logger.LogDebug(eventId, message, args); break; case LogLevel.Information: logger.LogInformation(eventId, message, args); break; case LogLevel.Warning: logger.LogWarning(eventId, exception, message, args); break; case LogLevel.Error: logger.LogError(eventId, exception, message, args); break; case LogLevel.Critical: logger.LogCritical(eventId, exception, message, args); break; } } public static void TestPattern(this ILogger logger, EventId eventId) { var exception = new Exception("Test Error Message"); logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern"); logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern"); logger.Emit(eventId, LogLevel.Information, "Information Test Pattern"); logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern"); logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception); logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception); } } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs ================================================ using System.Drawing; using Microsoft.Extensions.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public class DataStoreLoggerConfiguration { #region Properties public EventId EventId { get; set; } public Dictionary Colors { get; } = new() { [LogLevel.Trace] = new LogEntryColor { Foreground = Color.DarkGray }, [LogLevel.Debug] = new LogEntryColor { Foreground = Color.Gray }, [LogLevel.Information] = new LogEntryColor { Foreground = Color.WhiteSmoke, }, [LogLevel.Warning] = new LogEntryColor { Foreground = Color.Orange }, [LogLevel.Error] = new LogEntryColor { Foreground = Color.White, Background = Color.OrangeRed }, [LogLevel.Critical] = new LogEntryColor { Foreground = Color.White, Background = Color.Red }, [LogLevel.None] = new LogEntryColor { Foreground = Color.Magenta } }; #endregion } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs ================================================ using System.Collections.ObjectModel; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public interface ILogDataStore { ObservableCollection Entries { get; } void AddEntry(LogModel logModel); } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs ================================================ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public interface ILogDataStoreImpl { public ILogDataStore DataStore { get; } } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs ================================================ using System.Collections.ObjectModel; using Avalonia.Threading; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public class LogDataStore : ILogDataStore { public static LogDataStore Instance { get; } = new(); #region Fields private static readonly SemaphoreSlim _semaphore = new(initialCount: 1); #endregion #region Properties public ObservableCollection Entries { get; } = new(); #endregion #region Methods public virtual void AddEntry(LogModel logModel) { // ensure only one operation at time from multiple threads _semaphore.Wait(); Dispatcher.UIThread.Post(() => { Entries.Add(logModel); }); _semaphore.Release(); } #endregion } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs ================================================ using System.Drawing; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public class LogEntryColor { public LogEntryColor() { } public LogEntryColor(Color foreground, Color background) { Foreground = foreground; Background = background; } public Color Foreground { get; set; } = Color.Black; public Color Background { get; set; } = Color.Transparent; } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs ================================================ using Microsoft.Extensions.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; public class LogModel { #region Properties public DateTime Timestamp { get; set; } public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } public object? State { get; set; } public string? LoggerName { get; set; } public string? CallerClassName { get; set; } public string? CallerMemberName { get; set; } public string? Exception { get; set; } public LogEntryColor? Color { get; set; } #endregion public string LoggerDisplayName => LoggerName?.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? ""; } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs ================================================ using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl { #region Constructor public LogViewerControlViewModel(ILogDataStore dataStore) { DataStore = dataStore; } #endregion #region Properties public ILogDataStore DataStore { get; set; } #endregion } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs ================================================ using System.ComponentModel; using System.Runtime.CompilerServices; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; public class ObservableObject : INotifyPropertyChanged { protected bool Set( ref TValue field, TValue newValue, [CallerMemberName] string? propertyName = null ) { if (EqualityComparer.Default.Equals(field, newValue)) return false; field = newValue; OnPropertyChanged(propertyName); return true; } public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs ================================================ namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; public class ViewModel : ObservableObject { /* skip */ } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs ================================================ using System.Diagnostics; using Microsoft.Extensions.Logging; using NLog; using NLog.Targets; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer; [Target("DataStoreLogger")] public class DataStoreLoggerTarget : TargetWithLayout { #region Fields private ILogDataStore? _dataStore; private DataStoreLoggerConfiguration? _config; #endregion #region methods protected override void InitializeTarget() { // we need to inject dependencies // var serviceProvider = ResolveService(); // reference the shared instance _dataStore = LogDataStore.Instance; // _dataStore = serviceProvider.GetRequiredService(); // load the config options /*var options = serviceProvider.GetService>();*/ // _config = options?.CurrentValue ?? new DataStoreLoggerConfiguration(); _config = new DataStoreLoggerConfiguration(); base.InitializeTarget(); } protected override void Write(LogEventInfo logEvent) { // cast NLog Loglevel to Microsoft LogLevel type var logLevel = (MsLogLevel)Enum.ToObject(typeof(MsLogLevel), logEvent.Level.Ordinal); // format the message var message = RenderLogEvent(Layout, logEvent); // retrieve the EventId logEvent.Properties.TryGetValue("EventId", out var result); if (result is not EventId eventId) { eventId = _config!.EventId; } // add log entry _dataStore?.AddEntry( new LogModel { Timestamp = DateTime.UtcNow, LogLevel = logLevel, // do we override the default EventId if it exists? EventId = eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId, State = message, LoggerName = logEvent.LoggerName, CallerClassName = logEvent.CallerClassName, CallerMemberName = logEvent.CallerMemberName, Exception = logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""), Color = _config!.Colors[logLevel], } ); } #endregion } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs ================================================ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; using LogDataStore = StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging.LogDataStore; using MsLogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class ServicesExtension { public static IServiceCollection AddLogViewer(this IServiceCollection services) { services.AddSingleton(Core.Logging.LogDataStore.Instance); services.AddSingleton(); return services; } public static IServiceCollection AddLogViewer( this IServiceCollection services, Action configure ) { services.AddSingleton(Core.Logging.LogDataStore.Instance); services.AddSingleton(); services.Configure(configure); return services; } public static ILoggingBuilder AddNLogTargets( this ILoggingBuilder builder, IConfiguration config ) { LogManager .Setup() // Register custom Target .SetupExtensions( extensionBuilder => extensionBuilder.RegisterTarget("DataStoreLogger") ); /*builder .ClearProviders() .SetMinimumLevel(MsLogLevel.Trace) // Load NLog settings from appsettings*.json .AddNLog(config, // custom options for capturing the EventId information new NLogProviderOptions { // https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html#nlogextensionslogging-changes-capture-of-eventid IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy });*/ return builder; } public static ILoggingBuilder AddNLogTargets( this ILoggingBuilder builder, IConfiguration config, Action configure ) { builder.AddNLogTargets(config); builder.Services.Configure(configure); return builder; } } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE ================================================ MIT License Copyright (c) 2022 Graeme Grant Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs ================================================ using Avalonia.Threading; namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging; public class LogDataStore : Core.Logging.LogDataStore { #region Methods public override async void AddEntry(Core.Logging.LogModel logModel) => await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel)); #endregion } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md ================================================ ## LogViewer Source code in the `StabilityMatrix.Avalonia.Diagnostics.LogViewer `namespace is included from [CodeProject](https://www.codeproject.com/Articles/5357417/LogViewer-Control-for-WinForms-WPF-and-Avalonia-in) under the [MIT License](LICENSE). ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj ================================================ true true LogWindow.axaml ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs ================================================ using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels; namespace StabilityMatrix.Avalonia.Diagnostics.ViewModels; public class LogWindowViewModel { public LogViewerControlViewModel LogViewer { get; } public LogWindowViewModel(LogViewerControlViewModel logViewer) { LogViewer = logViewer; } public static LogWindowViewModel FromServiceProvider(IServiceProvider services) { return new LogWindowViewModel(services.GetRequiredService()); } } ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml ================================================  ================================================ FILE: StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs ================================================ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using StabilityMatrix.Avalonia.Diagnostics.ViewModels; namespace StabilityMatrix.Avalonia.Diagnostics.Views; public partial class LogWindow : Window { public LogWindow() { InitializeComponent(); } public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider) { return Attach(root, serviceProvider, new KeyGesture(Key.F11)); } public static IDisposable Attach( TopLevel root, IServiceProvider serviceProvider, KeyGesture gesture ) { return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler( KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel ); void PreviewKeyDown(object? sender, KeyEventArgs e) { if (gesture.Matches(e)) { var window = new LogWindow() { DataContext = LogWindowViewModel.FromServiceProvider(serviceProvider) }; window.Show(); } } } } ================================================ FILE: StabilityMatrix.Avalonia.pupnet.conf ================================================ ################################################################################ # PUPNET DEPLOY: 1.4.0 ################################################################################ ######################################## # APP PREAMBLE ######################################## # Mandatory application base name. This MUST BE the base name of the main executable file. It should NOT # include any directory part or extension, i.e. do not append '.exe' or '.dll'. It should not contain # spaces or invalid filename characters. AppBaseName = StabilityMatrix.Avalonia # Mandatory application friendly name. AppFriendlyName = Stability Matrix # Mandatory application ID in reverse DNS form. This should stay constant for lifetime of the software. AppId = zone.lykos.stabilitymatrix # Mandatory application version and package release of form: 'VERSION[RELEASE]'. Use optional square # brackets to denote package release, i.e. '1.2.3[1]'. Release refers to a change to the deployment # package, rather the application. If release part is absent (i.e. '1.2.3'), the release value defaults # to '1'. Note that the version-release value given here may be overridden from the command line. AppVersionRelease = 2.0.0[1] # Mandatory single line application short summary description. AppShortSummary = Package and checkpoint manager for Stable Diffusion. # Optional multi-line (surround with triple """ quotes) application description which may provide # longer text than AppShortSummary. Text separated by an empty line will be treated as paragraphs # (complex formatting should be avoided). The content is used by package builders where supported, # including RPM and DEB, and may optionally be used to populate the '' element in the # AppStream metadata through the use of a macro variable. AppDescription = # Mandatory application license ID. This should be one of the recognised SPDX license # identifiers, such as: 'MIT', 'GPL-3.0-or-later' or 'Apache-2.0'. For a proprietary or # custom license, use 'LicenseRef-Proprietary' or 'LicenseRef-LICENSE'. AppLicenseId = LicenseRef-Proprietary # Optional path to application copyright/license text file. If provided, it will be packaged with the # application and used with package builders where supported. AppLicenseFile = LICENSE # Optional path to application changelog file. IMPORTANT. If given, this file should contain version # information in a predefined format. Namely, it should contain one or more version headings of form: # '+ VERSION;DATE', under which are to be listed change items of form: '- Change description'. Formatted # information will be parsed and used to populate AppStream metadata. Additionally, it will be packaged # with the application and used with package builders where supported. NOTE. Superfluous text in the file # is ignored, so the file may also contain README information. # For information: https://github.com/kuiperzone/PupNet-Deploy. AppChangeFile = ######################################## # PUBLISHER ######################################## # Mandatory publisher, group or creator. PublisherName = Lykos # Optional copyright statement. PublisherCopyright = Copyright (C) Lykos 2023 # Optional publisher or application web-link name. Note that Windows Setup packages # require both PublisherLinkName and PublisherLinkUrl in order to include the link as # an item in program menu entries. Do not modify name, as may leave old entries in updated installations. PublisherLinkName = Home Page # Optional publisher or application web-link URL. PublisherLinkUrl = https://lykos.ai # Publisher or maintainer email contact. Although optional, some package builders (i.e. DEB) require it # and may warn or fail unless provided. PublisherEmail = stability-matrix@lykos.ai ######################################## # DESKTOP INTEGRATION ######################################## # Boolean (true or false) which indicates whether the application is hidden on the desktop. It is used to # populate the 'NoDisplay' field of the .desktop file. The default is false. Setting to true will also # cause the main application start menu entry to be omitted for Windows Setup. DesktopNoDisplay = false # Boolean (true or false) which indicates whether the application runs in the terminal, rather than # providing a GUI. It is used to populate the 'Terminal' field of the .desktop file. DesktopTerminal = false # Optional path to a Linux desktop file. If empty (default), one will be generated automatically from # the information in this file. Supplying a custom file, however, allows for mime-types and # internationalisation. If supplied, the file MUST contain the line: 'Exec=${INSTALL_EXEC}' # in order to use the correct install location. Other macros may be used to help automate the content. # Note. PupNet Deploy can generate you a desktop file. Use --help and 'pupnet --help macro' for reference. # See: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html DesktopFile = # Optional command name to start the application from the terminal. If, for example, AppBaseName is # 'Zone.Kuiper.HelloWorld', the value here may be set to a simpler and/or lower-case variant such as # 'helloworld'. It must not contain spaces or invalid filename characters. Do not add any extension such # as '.exe'. If empty, the application will not be in the path and cannot be started from the command line. # For Windows Setup packages, see also SetupCommandPrompt. StartCommand is not # supported for all packages kinds (i.e. Flatpak). Default is empty (none). StartCommand = stabilitymatrix # Optional category for the application. The value should be one of the recognised Freedesktop top-level # categories, such as: Audio, Development, Game, Office, Utility etc. Only a single value should be # provided here which will be used, where supported, to populate metadata. The default is empty. # See: https://specifications.freedesktop.org/menu-spec/latest/apa.html PrimeCategory = Utility # Path to AppStream metadata file. It is optional, but recommended as it is used by software centers. # Note. The contents of the files may use macro variables. Use 'pupnet --help macro' for reference. # See: https://docs.appimage.org/packaging-guide/optional/appstream.html MetaFile = # Optional icon file paths. The value may include multiple filenames separated with semicolon or given # in multi-line form. Valid types are SVG, PNG and ICO (ICO ignored on Linux). Note that the inclusion # of a scalable SVG is preferable on Linux, whereas PNGs must be one of the standard sizes and MUST # include the size in the filename in the form: name.32x32.png' or 'name.32.png'. IconFiles = """ StabilityMatrix.Avalonia/Assets/Icon.512x512.png StabilityMatrix.Avalonia/Assets/Icon.ico """ ######################################## # DOTNET PUBLISH ######################################## # Optional path relative to this file in which to find the dotnet project (.csproj) or solution (.sln) # file, or the directory containing it. If empty (default), a single project or solution file is # expected under the same directory as this file. IMPORTANT. If set to 'NONE', dotnet publish # is disabled (not called). Instead, only DotnetPostPublish is called. DotnetProjectPath = StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj # Optional arguments supplied to 'dotnet publish'. Do NOT include '-r' (runtime), or '-c' (configuration) # here as they will be added according to command line arguments. Typically you want as a minimum: # '-p:Version=${APP_VERSION} --self-contained true'. Additional useful arguments include: # '-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishReadyToRun=true # -p:PublishTrimmed=true -p:TrimMode=link'. Note. This value may use macro variables. Use 'pupnet --help macro' # for reference. See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish DotnetPublishArgs = -p:Version=${APP_VERSION} -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false --self-contained # Post-publish (or standalone build) command on Linux (ignored on Windows). It is called after dotnet # publish, but before the final output is built. This could, for example, be a script which copies # additional files into the build directory given by ${BUILD_APP_BIN}. The working directory will be # the location of this file. This value is optional, but becomes mandatory if DotnetProjectPath equals # 'NONE'. Note. This value may use macro variables. Additionally, scripts may use these as environment # variables. Use 'pupnet --help macro' for reference. DotnetPostPublish = # Post-publish (or standalone build) command on Windows (ignored on Linux). This should perform # the equivalent operation, as required, as DotnetPostPublish, but using DOS commands and batch # scripts. Multiple commands may be specified, separated by semicolon or given in multi-line form. # Note. This value may use macro variables. Additionally, scripts may use these as environment # variables. Use 'pupnet --help macro' for reference. DotnetPostPublishOnWindows = ######################################## # PACKAGE OUTPUT ######################################## # Optional package name (excludes version etc.). If empty, defaults to AppBaseName. However, it is # used not only to specify the base output filename, but to identify the application in DEB and RPM # packages. You may wish, therefore, to ensure that the value represents a unique name. Naming # requirements are strict and must contain only alpha-numeric and '-', '+' and '.' characters. PackageName = StabilityMatrix # Output directory, or subdirectory relative to this file. It will be created if it does not exist and # will contain the final deploy output files. If empty, it defaults to the location of this file. OutputDirectory = Release/linux-x64 ######################################## # APPIMAGE OPTIONS ######################################## # Additional arguments for use with appimagetool. Useful for signing. Default is empty. AppImageArgs = # Boolean (true or false) which sets whether to include the application version in the AppImage filename, # i.e. 'HelloWorld-1.2.3-x86_64.AppImage'. Default is false. It is ignored if the output filename is # specified at command line. AppImageVersionOutput = false ######################################## # FLATPAK OPTIONS ######################################## # The runtime platform. Invariably for .NET (inc. Avalonia), this should be 'org.freedesktop.Platform'. # Refer: https://docs.flatpak.org/en/latest/available-runtimes.html FlatpakPlatformRuntime = org.freedesktop.Platform # The platform SDK. Invariably for .NET (inc. Avalonia applications) this should be 'org.freedesktop.Sdk'. # The SDK must be installed on the build system. FlatpakPlatformSdk = org.freedesktop.Sdk # The platform runtime version. The latest available version may change periodically. # Refer to Flatpak documentation. FlatpakPlatformVersion = 22.08 # Flatpak manifest 'finish-args' sandbox permissions. Optional, but if empty, the application will have # extremely limited access to the host environment. This option may be used to grant required # application permissions. Values here should be prefixed with '--' and separated by semicolon or given # in multi-line form. Refer: https://docs.flatpak.org/en/latest/sandbox-permissions.html FlatpakFinishArgs = """ --socket=wayland --socket=x11 --filesystem=host --share=network """ # Additional arguments for use with flatpak-builder. Useful for signing. Default is empty. # See flatpak-builder --help. FlatpakBuilderArgs = ######################################## # RPM OPTIONS ######################################## # Boolean (true or false) which specifies whether to build the RPM package with 'AutoReq' equal to yes or no. # For dotnet application, the value should typically be false, but see RpmRequires below. # Refer: https://rpm-software-management.github.io/rpm/manual/spec.html RpmAutoReq = false # Boolean (true or false) which specifies whether to build the RPM package with 'AutoProv' equal to yes or no. # Refer: https://rpm-software-management.github.io/rpm/manual/spec.html RpmAutoProv = true # Optional list of RPM dependencies. The list may include multiple values separated with semicolon or given # in multi-line form. If empty, a self-contained dotnet package will successfully run on many (but not all) # Linux distros. In some cases, it will be necessary to explicitly specify additional dependencies. # Default values are recommended for use with dotnet and RPM packages at the time of writing. # For updated information, see: https://learn.microsoft.com/en-us/dotnet/core/install/linux-rhel#dependencies RpmRequires = """ krb5-libs libicu openssl-libs zlib """ ######################################## # DEBIAN OPTIONS ######################################## # Optional list of Debian dependencies. The list may include multiple values separated with semicolon or given # in multi-line form. If empty, a self-contained dotnet package will successfully run on many (but not all) # Linux distros. In some cases, it will be necessary to explicitly specify additional dependencies. # Default values are recommended for use with dotnet and Debian packages at the time of writing. # For updated information, see: https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu#dependencies DebianRecommends = """ libc6 libgcc1 libgcc-s1 libgssapi-krb5-2 libicu libssl libstdc++6 libunwind zlib1g """ ######################################## # WINDOWS SETUP OPTIONS ######################################## # Boolean (true or false) which specifies whether the application is to be installed in administrative # mode, or per-user. Default is false. See: https://jrsoftware.org/ishelp/topic_admininstallmode.htm SetupAdminInstall = false # Optional command prompt title. The Windows installer will NOT add your application to the path. However, # if your package contains a command-line utility, setting this value will ensure that a 'Command Prompt' # program menu entry is added (with this title) which, when launched, will open a dedicated command # window with your application directory in its path. Default is empty. See also StartCommand. SetupCommandPrompt = Command Prompt # Mandatory value which specifies minimum version of Windows that your software runs on. Windows 8 = 6.2, # Windows 10/11 = 10. Default: 10. See: https://jrsoftware.org/ishelp/topic_setup_minversion.htm SetupMinWindowsVersion = 10 # Optional name and parameters of the Sign Tool to be used to digitally sign: the installer, # uninstaller, and contained exe and dll files. If empty, files will not be signed. # See: https://jrsoftware.org/ishelp/topic_setup_signtool.htm SetupSignTool = # Optional suffix for the installer output filename. The default is empty, but you may wish set it to: # 'Setup' or similar. This, for example, will output a file of name: HelloWorldSetup-x86_64.exe # Ignored if the output filename is specified at command line. SetupSuffixOutput = # Boolean (true or false) which sets whether to include the application version in the setup filename, # i.e. 'HelloWorld-1.2.3-x86_64.exe'. Default is false. Ignored if the output filename is specified # at command line. SetupVersionOutput = false ================================================ FILE: StabilityMatrix.Core/Animation/GifConverter.cs ================================================ using KGySoft.Drawing.Imaging; using KGySoft.Drawing.SkiaSharp; using SkiaSharp; namespace StabilityMatrix.Core.Animation; public class GifConverter { public static IEnumerable EnumerateAnimatedWebp(Stream webpSource) { using var webp = new SKManagedStream(webpSource); using var codec = SKCodec.Create(webp); var info = new SKImageInfo(codec.Info.Width, codec.Info.Height); for (var i = 0; i < codec.FrameCount; i++) { using var tempSurface = new SKBitmap(info); codec.GetFrameInfo(i, out var frameInfo); var decodeInfo = info.WithAlphaType(frameInfo.AlphaType); tempSurface.TryAllocPixels(decodeInfo); var result = codec.GetPixels(decodeInfo, tempSurface.GetPixels(), new SKCodecOptions(i)); if (result != SKCodecResult.Success) throw new InvalidDataException($"Could not decode frame {i} of {codec.FrameCount}."); using var peekPixels = tempSurface.PeekPixels(); yield return peekPixels.GetReadableBitmapData(WorkingColorSpace.Default); } } public static Task ConvertAnimatedWebpToGifAsync(Stream webpSource, Stream gifOutput) { var gifBitmaps = EnumerateAnimatedWebp(webpSource); return GifEncoder.EncodeAnimationAsync( new AnimatedGifConfiguration(gifBitmaps, TimeSpan.FromMilliseconds(150)) { Quantizer = OptimizedPaletteQuantizer.Wu(alphaThreshold: 0), AllowDeltaFrames = true }, gifOutput ); } } ================================================ FILE: StabilityMatrix.Core/Api/A3WebApiManager.cs ================================================ using Refit; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Api; public class A3WebApiManager : IA3WebApiManager { private IA3WebApi? client; public IA3WebApi Client { get { // Return the existing client if it exists if (client != null) { return client; } // Create a new client and store it otherwise client = CreateClient(); return client; } } private readonly ISettingsManager settingsManager; private readonly IHttpClientFactory httpClientFactory; public RefitSettings? RefitSettings { get; init; } public string? BaseUrl { get; set; } public A3WebApiManager(ISettingsManager settingsManager, IHttpClientFactory httpClientFactory) { this.settingsManager = settingsManager; this.httpClientFactory = httpClientFactory; } public void ResetClient() { client = null; } private IA3WebApi CreateClient() { var settings = settingsManager.Settings; // First check override if (settings.WebApiHost != null) { BaseUrl = settings.WebApiHost; if (settings.WebApiPort != null) { BaseUrl += $":{settings.WebApiPort}"; } } else { // Otherwise use default BaseUrl = "http://localhost:7860"; } var httpClient = httpClientFactory.CreateClient("A3Client"); httpClient.BaseAddress = new Uri(BaseUrl); var api = RestService.For(httpClient, RefitSettings); return api; } } ================================================ FILE: StabilityMatrix.Core/Api/ApiFactory.cs ================================================ using Refit; namespace StabilityMatrix.Core.Api; public class ApiFactory : IApiFactory { private readonly IHttpClientFactory httpClientFactory; public RefitSettings? RefitSettings { get; init; } public ApiFactory(IHttpClientFactory httpClientFactory) { this.httpClientFactory = httpClientFactory; } public T CreateRefitClient(Uri baseAddress) { var httpClient = httpClientFactory.CreateClient(nameof(T)); httpClient.BaseAddress = baseAddress; return RestService.For(httpClient, RefitSettings); } public T CreateRefitClient(Uri baseAddress, RefitSettings refitSettings) { var httpClient = httpClientFactory.CreateClient(nameof(T)); httpClient.BaseAddress = baseAddress; return RestService.For(httpClient, refitSettings); } } ================================================ FILE: StabilityMatrix.Core/Api/CivitCompatApiManager.cs ================================================ using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Api; /// /// Provides a compatibility layer for interacting with Civita APIs and Discovery APIs. /// This class decides dynamically whether to use the Civita API or an alternative Discovery API /// based on internal conditions. /// [RegisterSingleton] public class CivitCompatApiManager( ILogger logger, ICivitApi civitApi, ILykosModelDiscoveryApi discoveryApi, ISettingsManager settingsManager ) : ICivitApi { private bool ShouldUseDiscoveryApi => settingsManager.Settings.CivitUseDiscoveryApi; public Task GetModels(CivitModelsRequest request) { if (ShouldUseDiscoveryApi) { logger.LogDebug($"Using Discovery API for {nameof(GetModels)}"); return discoveryApi.GetModels(request, transcodeAnimToImage: true, transcodeVideoToImage: true); } return civitApi.GetModels(request); } public Task GetModelById(int id) { /*if (ShouldUseDiscoveryApi) { logger.LogDebug($"Using Discovery API for {nameof(GetModelById)}"); return discoveryApi.GetModelById(id); }*/ return civitApi.GetModelById(id); } public Task GetModelVersionByHash(string hash) { return civitApi.GetModelVersionByHash(hash); } public Task GetModelVersionById(int id) { return civitApi.GetModelVersionById(id); } public Task GetBaseModelList() { return civitApi.GetBaseModelList(); } } ================================================ FILE: StabilityMatrix.Core/Api/IA3WebApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix")] public interface IA3WebApi { [Get("/internal/ping")] Task GetPing(CancellationToken cancellationToken = default); [Post("/sdapi/v1/txt2img")] Task TextToImage([Body] TextToImageRequest request, CancellationToken cancellationToken = default); [Get("/sdapi/v1/progress")] Task GetProgress([Body] ProgressRequest request, CancellationToken cancellationToken = default); [Get("/sdapi/v1/options")] Task GetOptions(CancellationToken cancellationToken = default); [Post("/sdapi/v1/options")] Task SetOptions([Body] A3Options request, CancellationToken cancellationToken = default); } ================================================ FILE: StabilityMatrix.Core/Api/IA3WebApiManager.cs ================================================ using Refit; namespace StabilityMatrix.Core.Api; public interface IA3WebApiManager { IA3WebApi Client { get; } RefitSettings? RefitSettings { get; init; } string? BaseUrl { get; set; } void ResetClient(); } ================================================ FILE: StabilityMatrix.Core/Api/IApiFactory.cs ================================================ using Refit; namespace StabilityMatrix.Core.Api; public interface IApiFactory { public T CreateRefitClient(Uri baseAddress); public T CreateRefitClient(Uri baseAddress, RefitSettings refitSettings); } ================================================ FILE: StabilityMatrix.Core/Api/ICivitApi.cs ================================================ using System.Text.Json.Nodes; using Refit; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix/1.0")] public interface ICivitApi { [Get("/api/v1/models")] Task GetModels(CivitModelsRequest request); [Get("/api/v1/models/{id}")] Task GetModelById([AliasAs("id")] int id); [Get("/api/v1/model-versions/by-hash/{hash}")] Task GetModelVersionByHash([Query] string hash); [Get("/api/v1/model-versions/{id}")] Task GetModelVersionById(int id); [Get("/api/v1/models?baseModels=gimmethelist")] Task GetBaseModelList(); } ================================================ FILE: StabilityMatrix.Core/Api/ICivitTRPCApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.CivitTRPC; namespace StabilityMatrix.Core.Api; [Headers( "Content-Type: application/x-www-form-urlencoded", "Referer: https://civitai.com", "Origin: https://civitai.com" )] public interface ICivitTRPCApi { [QueryUriFormat(UriFormat.UriEscaped)] [Get("/api/trpc/userProfile.get")] Task GetUserProfile( [Query] CivitUserProfileRequest input, [Authorize] string bearerToken, CancellationToken cancellationToken = default ); [QueryUriFormat(UriFormat.UriEscaped)] [Get("/api/trpc/buzz.getUserAccount")] Task> GetUserAccount( [Query] string input, [Authorize] string bearerToken, CancellationToken cancellationToken = default ); [QueryUriFormat(UriFormat.UriEscaped)] [Get("/api/trpc/buzz.getUserAccount")] Task> GetUserAccount( [Authorize] string bearerToken, CancellationToken cancellationToken = default ); /// /// Calls with default JSON input. /// Not required and returns 401 since Oct 2025 since civit changes. /// Mainly just use instead. /// Task> GetUserAccountDefault( string bearerToken, CancellationToken cancellationToken = default ) { return GetUserAccount( "{\"json\":null,\"meta\":{\"values\":[\"undefined\"]}}", bearerToken, cancellationToken ); } [QueryUriFormat(UriFormat.UriEscaped)] [Get("/api/trpc/user.getById")] Task> GetUserById( [Query] CivitGetUserByIdRequest input, [Authorize] string bearerToken, CancellationToken cancellationToken = default ); [Post("/api/trpc/user.toggleFavoriteModel")] Task ToggleFavoriteModel( [Body] CivitUserToggleFavoriteModelRequest request, [Authorize] string bearerToken, CancellationToken cancellationToken = default ); [QueryUriFormat(UriFormat.UriEscaped)] [Get("/api/trpc/image.getGenerationData")] Task> GetImageGenerationData( [Query] string input, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Api/IComfyApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix")] public interface IComfyApi { [Post("/prompt")] Task PostPrompt( [Body] ComfyPromptRequest prompt, CancellationToken cancellationToken = default ); [Post("/interrupt")] Task PostInterrupt(CancellationToken cancellationToken = default); /// /// Upload an image to Comfy /// /// Image as StreamPart /// Whether to overwrite at destination /// One of "input", "temp", "output" /// Subfolder /// Cancellation Token [Multipart] [Post("/upload/image")] Task PostUploadImage( StreamPart image, string? overwrite = null, string? type = null, string? subfolder = null, CancellationToken cancellationToken = default ); [Get("/history/{promptId}")] Task> GetHistory( string promptId, CancellationToken cancellationToken = default ); [Get("/object_info/{nodeType}")] Task> GetObjectInfo( string nodeType, CancellationToken cancellationToken = default ); [Get("/view")] Task GetImage( string filename, string subfolder, string type, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Api/IHuggingFaceApi.cs ================================================ using System.Threading.Tasks; using Refit; using StabilityMatrix.Core.Models.Api.HuggingFace; namespace StabilityMatrix.Core.Api; public interface IHuggingFaceApi { [Get("/api/whoami-v2")] Task> GetCurrentUserAsync([Header("Authorization")] string authorization); } ================================================ FILE: StabilityMatrix.Core/Api/IInvokeAiApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.Invoke; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix")] public interface IInvokeAiApi { [Get("/api/v2/models/scan_folder")] Task> ScanFolder( [Query, AliasAs("scan_path")] string scanPath, CancellationToken cancellationToken = default ); [Post("/api/v2/models/install")] Task InstallModel( [Body] InstallModelRequest request, [Query] string source, [Query] bool inplace = true, CancellationToken cancellationToken = default ); [Get("/api/v2/models/install")] Task> GetModelInstallStatus(CancellationToken cancellationToken = default); } ================================================ FILE: StabilityMatrix.Core/Api/ILykosAnalyticsApi.cs ================================================ using System.ComponentModel; using Refit; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Models.Api.Lykos.Analytics; namespace StabilityMatrix.Core.Api; [Localizable(false)] [Headers("User-Agent: StabilityMatrix")] public interface ILykosAnalyticsApi { [Post("/api/analytics")] Task PostInstallData([Body] AnalyticsRequest data, CancellationToken cancellationToken = default); } ================================================ FILE: StabilityMatrix.Core/Api/ILykosAuthApiV1.cs ================================================ using System.ComponentModel; using System.Net; using Refit; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.Lykos; namespace StabilityMatrix.Core.Api; [Localizable(false)] [Headers("User-Agent: StabilityMatrix")] [Obsolete("Use ILykosAuthApiV2")] public interface ILykosAuthApiV1 { [Headers("Authorization: Bearer")] [Get("/api/Users/{email}")] Task GetUser(string email, CancellationToken cancellationToken = default); [Headers("Authorization: Bearer")] [Get("/api/Users/me")] Task GetUserSelf(CancellationToken cancellationToken = default); [Post("/api/Accounts")] Task PostAccount( [Body] PostAccountRequest request, CancellationToken cancellationToken = default ); [Post("/api/Login")] Task PostLogin( [Body] PostLoginRequest request, CancellationToken cancellationToken = default ); [Headers("Authorization: Bearer")] [Post("/api/Login/Refresh")] Task PostLoginRefresh( [Body] PostLoginRefreshRequest request, CancellationToken cancellationToken = default ); [Get("/api/oauth/google/callback")] Task GetOAuthGoogleCallback( [Query] string code, [Query] string state, [Query] string codeVerifier, CancellationToken cancellationToken = default ); [Get("/api/oauth/google/links/login-or-signup")] Task GetOAuthGoogleLoginOrSignupLink( string redirectUri, string codeChallenge, string codeChallengeMethod, CancellationToken cancellationToken = default ); [Headers("Authorization: Bearer")] [Get("/api/oauth/patreon/redirect")] Task GetPatreonOAuthRedirect( string redirectUrl, CancellationToken cancellationToken = default ); public async Task GetPatreonOAuthUrl( string redirectUrl, CancellationToken cancellationToken = default ) { var result = await GetPatreonOAuthRedirect(redirectUrl, cancellationToken).ConfigureAwait(false); if (result.StatusCode != HttpStatusCode.Redirect) { result.EnsureSuccessStatusCode(); throw new InvalidOperationException($"Expected a redirect 302 response, got {result.StatusCode}"); } return result.Headers.Location?.ToString() ?? throw new InvalidOperationException("Expected a redirect URL, but got none"); } [Headers("Authorization: Bearer")] [Delete("/api/oauth/patreon")] Task DeletePatreonOAuth(CancellationToken cancellationToken = default); [Headers("Authorization: Bearer")] [Get("/api/files/download")] Task GetFilesDownload( string path, CancellationToken cancellationToken = default ); [Get("/api/Models/recommended")] Task GetRecommendedModels(); } ================================================ FILE: StabilityMatrix.Core/Api/ILykosModelDiscoveryApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Api; public interface ILykosModelDiscoveryApi { [Get("/civit/models")] Task GetModels( [Query] CivitModelsRequest request, [Header("X-Transcode-Video-To-Image")] bool? transcodeVideoToImage = null, [Header("X-Transcode-Anim-To-Image")] bool? transcodeAnimToImage = null ); [Get("/civit/models/{id}")] Task GetModelById([AliasAs("id")] int id); } ================================================ FILE: StabilityMatrix.Core/Api/IOpenArtApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.OpenArt; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix")] public interface IOpenArtApi { [Get("/feed")] Task GetFeedAsync([Query] OpenArtFeedRequest request); [Get("/list")] Task SearchAsync([Query] OpenArtSearchRequest request); [Post("/download")] Task DownloadWorkflowAsync([Body] OpenArtDownloadRequest request); } ================================================ FILE: StabilityMatrix.Core/Api/IOpenModelDbApi.cs ================================================ using System.Text.Json.Serialization; using Apizr.Caching; using Apizr.Caching.Attributes; using Apizr.Configuring; using Refit; using StabilityMatrix.Core.Models.Api.OpenModelsDb; namespace StabilityMatrix.Core.Api; [BaseAddress("https://openmodeldb.info")] public interface IOpenModelDbApi { [Get("/api/v1/models.json"), Cache(CacheMode.GetOrFetch, "0.00:02:00")] Task GetModels(); [Get("/api/v1/tags.json"), Cache(CacheMode.GetOrFetch, "0.00:10:00")] Task GetTags(); [Get("/api/v1/architectures.json"), Cache(CacheMode.GetOrFetch, "0.00:10:00")] Task GetArchitectures(); } [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(OpenModelDbModelsResponse))] [JsonSerializable(typeof(OpenModelDbTagsResponse))] [JsonSerializable(typeof(OpenModelDbArchitecturesResponse))] public partial class OpenModelDbApiJsonContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Api/IPyPiApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.Pypi; namespace StabilityMatrix.Core.Api; [Headers("User-Agent: StabilityMatrix/2.x")] public interface IPyPiApi { [Get("/pypi/{packageName}/json")] Task GetPackageInfo(string packageName); } ================================================ FILE: StabilityMatrix.Core/Api/ITokenProvider.cs ================================================ namespace StabilityMatrix.Core.Api; public interface ITokenProvider { Task GetAccessTokenAsync(); Task<(string AccessToken, string RefreshToken)> RefreshTokensAsync(); } ================================================ FILE: StabilityMatrix.Core/Api/LykosAuthApi/.refitter ================================================ { "openApiPath": "https://auth.lykos.ai/swagger/v2/swagger.json", "outputFolder": "./StabilityMatrix.Core/Api/LykosAuthApi/Generated", "outputFilename": "Refitter.g.cs", "namespace": "StabilityMatrix.Core.Api.LykosAuthApi", "naming": { "useOpenApiTitle": false, "interfaceName": "LykosAuthApiV2" }, "includePathMatches": [ "^/api/v2/Accounts/me$", "^/api/v2/oauth/patreon", "^/api/v2/files/download" ], "trimUnusedSchema": true, "operationNameGenerator": "SingleClientFromPathSegments" } ================================================ FILE: StabilityMatrix.Core/Api/LykosAuthApi/Generated/Refitter.g.cs ================================================ // // This code was generated by Refitter. // using Refit; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Threading.Tasks; #nullable enable annotations namespace StabilityMatrix.Core.Api.LykosAuthApi { [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.4.1.0")] public partial interface ILykosAuthApiV2 { /// OK /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 401 /// Unauthorized /// /// /// 404 /// Not Found /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/v2/Accounts/me")] Task ApiV2AccountsMe(); /// OK /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 400 /// Bad Request /// /// /// 401 /// Unauthorized /// /// /// 403 /// Forbidden /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/v2/files/download")] Task ApiV2FilesDownload([Query] string path); /// A that completes when the request is finished. /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 302 /// Found /// /// /// 401 /// Unauthorized /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/v2/oauth/patreon/redirect")] Task ApiV2OauthPatreonRedirect([Query] System.Uri redirectUrl); /// OK /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 401 /// Unauthorized /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/v2/oauth/patreon/link")] Task ApiV2OauthPatreonLink([Query] System.Uri redirectUrl); /// A that completes when the request is finished. /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 401 /// Unauthorized /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Delete("/api/v2/oauth/patreon")] Task ApiV2OauthPatreon(); /// A that completes when the request is finished. /// /// Thrown when the request returns a non-success status code: /// /// /// Status /// Description /// /// /// 302 /// Found /// /// /// 401 /// Unauthorized /// /// /// 404 /// Not Found /// /// /// 400 /// Bad Request /// /// /// [Headers("Accept: text/plain, application/json, text/json")] [Get("/api/v2/oauth/patreon/callback")] Task ApiV2OauthPatreonCallback([Query] string code, [Query] string state); } } //---------------------- // // Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // //---------------------- #pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." #pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." #pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' #pragma warning disable 612 // Disable "CS0612 '...' is obsolete" #pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" #pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... #pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." #pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" #pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" #pragma warning disable 8603 // Disable "CS8603 Possible null reference return" #pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" #pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" #pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." namespace StabilityMatrix.Core.Api.LykosAuthApi { using System = global::System; [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class AccountResponse { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("roles")] public ICollection Roles { get; set; } [JsonPropertyName("permissions")] public ICollection Permissions { get; set; } [JsonPropertyName("patreonId")] public string PatreonId { get; set; } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class FilesDownloadResponse { [JsonPropertyName("downloadUrl")] public System.Uri DownloadUrl { get; set; } [JsonPropertyName("expiresAt")] public System.DateTimeOffset? ExpiresAt { get; set; } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ProblemDetails { [JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("title")] public string Title { get; set; } [JsonPropertyName("status")] public int? Status { get; set; } [JsonPropertyName("detail")] public string Detail { get; set; } [JsonPropertyName("instance")] public string Instance { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } } #pragma warning restore 108 #pragma warning restore 114 #pragma warning restore 472 #pragma warning restore 612 #pragma warning restore 1573 #pragma warning restore 1591 #pragma warning restore 8073 #pragma warning restore 3016 #pragma warning restore 8603 #pragma warning restore 8604 #pragma warning restore 8625 ================================================ FILE: StabilityMatrix.Core/Api/LykosAuthApi/IRecommendedModelsApi.cs ================================================ using Refit; using StabilityMatrix.Core.Models.Api.Lykos; namespace StabilityMatrix.Core.Api.LykosAuthApi; public interface IRecommendedModelsApi { [Get("/api/v2/Models/recommended")] Task GetRecommendedModels(); } ================================================ FILE: StabilityMatrix.Core/Api/LykosAuthTokenProvider.cs ================================================ using Injectio.Attributes; using OpenIddict.Client; using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Api; [RegisterSingleton] public class LykosAuthTokenProvider( Lazy lazyLykosAuthApi, ISecretsManager secretsManager, OpenIddictClientService openIdClient ) : ITokenProvider { private readonly Lazy lazyLykosAuthApi = lazyLykosAuthApi; // Lazy as instantiating requires the current class to be instantiated. /// public async Task GetAccessTokenAsync() { var secrets = await secretsManager.SafeLoadAsync().ConfigureAwait(false); return secrets.LykosAccountV2?.AccessToken ?? ""; } /// public async Task<(string AccessToken, string RefreshToken)> RefreshTokensAsync() { var secrets = await secretsManager.SafeLoadAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(secrets.LykosAccountV2?.RefreshToken)) { throw new InvalidOperationException("No refresh token found"); } var result = await openIdClient .AuthenticateWithRefreshTokenAsync( new OpenIddictClientModels.RefreshTokenAuthenticationRequest { ProviderName = OpenIdClientConstants.LykosAccount.ProviderName, RefreshToken = secrets.LykosAccountV2.RefreshToken } ) .ConfigureAwait(false); if (string.IsNullOrEmpty(result.RefreshToken)) { throw new InvalidOperationException("No refresh token returned"); } secrets = secrets with { LykosAccountV2 = new LykosAccountV2Tokens( result.AccessToken, result.RefreshToken, result.IdentityToken ) }; await secretsManager.SaveAsync(secrets).ConfigureAwait(false); return (result.AccessToken, result.RefreshToken); } } ================================================ FILE: StabilityMatrix.Core/Api/OpenIdClientConstants.cs ================================================ namespace StabilityMatrix.Core.Api; /// /// Contains constant values related to OpenID Clients /// public static class OpenIdClientConstants { public static class LykosAccount { public const string ProviderName = "Lykos Account"; } } ================================================ FILE: StabilityMatrix.Core/Api/PromptGen/.refitter ================================================ { "openApiPath": "http://localhost:5174/api/swagger.json", "outputFolder": "./StabilityMatrix.Core/Api/PromptGen/Generated", "outputFilename": "Refitter.g.cs", "namespace": "StabilityMatrix.Core.Api.PromptGenApi", "naming": { "useOpenApiTitle": false, "interfaceName": "PromptGenApi" }, "includePathMatches": [ "^/account/me/tokens$", "^/expand-prompt$" ], "trimUnusedSchema": true, "operationNameGenerator": "SingleClientFromPathSegments" } ================================================ FILE: StabilityMatrix.Core/Api/PromptGen/Generated/Refitter.g.cs ================================================ // // This code was generated by Refitter. // using Refit; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Threading.Tasks; #nullable enable annotations namespace StabilityMatrix.Core.Api.PromptGenApi { [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.4.1.0")] public partial interface IPromptGenApi { /// Get current user's token balance /// Retrieves the token balance for the currently authenticated user. /// Payload of TokenBalance /// Thrown when the request returns a non-success status code. [Headers("Accept: application/json")] [Get("/account/me/tokens")] Task AccountMeTokens(); /// Expand a prompt /// Expand a prompt using the OpenAI Chat API. /// The prompt to expand. /// Payload of PromptExpansionResponse /// Thrown when the request returns a non-success status code. [Headers("Accept: application/json")] [Post("/expand-prompt")] Task ExpandPrompt([Body] PromptExpansionRequest body); } } //---------------------- // // Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // //---------------------- #pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." #pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." #pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' #pragma warning disable 612 // Disable "CS0612 '...' is obsolete" #pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" #pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... #pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." #pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" #pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" #pragma warning disable 8603 // Disable "CS8603 Possible null reference return" #pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" #pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" #pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." namespace StabilityMatrix.Core.Api.PromptGenApi { using System = global::System; [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class PromptEnhanceResponse { [JsonPropertyName("positivePrompt")] public string PositivePrompt { get; set; } [JsonPropertyName("negativePrompt")] public string NegativePrompt { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class PromptExpansionRequest { [JsonPropertyName("prompt")] public PromptToEnhance Prompt { get; set; } [JsonPropertyName("model")] public string Model { get; set; } [JsonPropertyName("seed")] public long? Seed { get; set; } [JsonPropertyName("mode")] [JsonConverter(typeof(JsonStringEnumConverter))] public PromptExpansionRequestMode Mode { get; set; } = StabilityMatrix.Core.Api.PromptGenApi.PromptExpansionRequestMode.Focused; [JsonPropertyName("clientOverride")] public string ClientOverride { get; set; } [JsonPropertyName("modelTags")] // TODO(system.text.json): Add string enum item converter public ICollection ModelTags { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class PromptExpansionResponse { [JsonPropertyName("availableTokens")] public int AvailableTokens { get; set; } [JsonPropertyName("operationId")] public string OperationId { get; set; } [JsonPropertyName("response")] public PromptEnhanceResponse Response { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class PromptToEnhance { [JsonPropertyName("positivePrompt")] public string PositivePrompt { get; set; } [JsonPropertyName("negativePrompt")] public string NegativePrompt { get; set; } [JsonPropertyName("model")] public string Model { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TokenBalance { [JsonPropertyName("available")] public int Available { get; set; } [JsonPropertyName("used")] public int Used { get; set; } [JsonPropertyName("total")] public int Total { get; set; } private IDictionary _additionalProperties; [JsonExtensionData] public IDictionary AdditionalProperties { get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } set { _additionalProperties = value; } } } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public enum PromptExpansionRequestMode { [System.Runtime.Serialization.EnumMember(Value = @"Focused")] Focused = 0, [System.Runtime.Serialization.EnumMember(Value = @"Balanced")] Balanced = 1, [System.Runtime.Serialization.EnumMember(Value = @"Imaginative")] Imaginative = 2, } [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")] public enum ModelTags { [System.Runtime.Serialization.EnumMember(Value = @"Flux")] Flux = 0, [System.Runtime.Serialization.EnumMember(Value = @"Pony")] Pony = 1, [System.Runtime.Serialization.EnumMember(Value = @"Sdxl")] Sdxl = 2, [System.Runtime.Serialization.EnumMember(Value = @"Illustrious")] Illustrious = 3, } } #pragma warning restore 108 #pragma warning restore 114 #pragma warning restore 472 #pragma warning restore 612 #pragma warning restore 1573 #pragma warning restore 1591 #pragma warning restore 8073 #pragma warning restore 3016 #pragma warning restore 8603 #pragma warning restore 8604 #pragma warning restore 8625 ================================================ FILE: StabilityMatrix.Core/Api/TokenAuthHeaderHandler.cs ================================================ using System.Net; using System.Net.Http.Headers; using NLog; using Polly; using Polly.Retry; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Api; public class TokenAuthHeaderHandler : DelegatingHandler { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly AsyncRetryPolicy policy; private readonly ITokenProvider tokenProvider; public Func RequestFilter { get; set; } = request => request.Headers.Authorization is { Scheme: "Bearer" }; public Func ResponseFilter { get; set; } = response => response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden && response.RequestMessage?.Headers.Authorization is { Scheme: "Bearer", Parameter: { } param } && !string.IsNullOrWhiteSpace(param); public TokenAuthHeaderHandler(ITokenProvider tokenProvider) { this.tokenProvider = tokenProvider; policy = Policy .HandleResult(ResponseFilter) .RetryAsync( async (result, _) => { var oldToken = ObjectHash.GetStringSignature( await tokenProvider.GetAccessTokenAsync().ConfigureAwait(false) ); Logger.Info( "Refreshing access token for status ({StatusCode})", result.Result.StatusCode ); var (newToken, _) = await tokenProvider.RefreshTokensAsync().ConfigureAwait(false); Logger.Info( "Access token refreshed: {OldToken} -> {NewToken}", ObjectHash.GetStringSignature(oldToken), ObjectHash.GetStringSignature(newToken) ); } ); } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) { return policy.ExecuteAsync(async () => { // Only add if Authorization is already set to Bearer and access token is not empty // this allows some routes to not use the access token if (RequestFilter(request)) { var accessToken = await tokenProvider.GetAccessTokenAsync().ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(accessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); }); } } ================================================ FILE: StabilityMatrix.Core/Attributes/BoolStringMemberAttribute.cs ================================================ namespace StabilityMatrix.Core.Attributes; [AttributeUsage(AttributeTargets.Property)] public class BoolStringMemberAttribute(string trueString, string falseString) : Attribute { public string TrueString { get; } = trueString; public string FalseString { get; } = falseString; } ================================================ FILE: StabilityMatrix.Core/Attributes/ManagedServiceAttribute.cs ================================================ using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; namespace StabilityMatrix.Core.Attributes; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] [MeansImplicitUse(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Itself)] [AttributeUsage(AttributeTargets.Class)] public class ManagedServiceAttribute : Attribute; ================================================ FILE: StabilityMatrix.Core/Attributes/PreloadAttribute.cs ================================================ namespace StabilityMatrix.Core.Attributes; /// /// Marks that a ViewModel should have its OnLoaded and OnLoadedAsync methods called in the background /// during MainWindow initialization, after LibraryDirectory is set. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class PreloadAttribute : Attribute { } ================================================ FILE: StabilityMatrix.Core/Attributes/SingletonAttribute.cs ================================================ using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; namespace StabilityMatrix.Core.Attributes; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] [MeansImplicitUse(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Itself)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public class SingletonAttribute : Attribute { [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type? InterfaceType { get; init; } [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type? ImplType { get; init; } public SingletonAttribute() { } public SingletonAttribute(Type interfaceType) { InterfaceType = interfaceType; } public SingletonAttribute(Type interfaceType, Type implType) { InterfaceType = implType; ImplType = implType; } } ================================================ FILE: StabilityMatrix.Core/Attributes/TransientAttribute.cs ================================================ using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; namespace StabilityMatrix.Core.Attributes; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] [MeansImplicitUse(ImplicitUseKindFlags.Access, ImplicitUseTargetFlags.Itself)] [AttributeUsage(AttributeTargets.Class)] public class TransientAttribute : Attribute { [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type? InterfaceType { get; } public TransientAttribute() { } public TransientAttribute(Type interfaceType) { InterfaceType = interfaceType; } } ================================================ FILE: StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Packages.Extensions; namespace StabilityMatrix.Core.Attributes; /// /// Options for /// [AttributeUsage(AttributeTargets.Class)] public class TypedNodeOptionsAttribute : Attribute { public string? Name { get; init; } public string[]? RequiredExtensions { get; init; } public IEnumerable GetRequiredExtensionSpecifiers() { return RequiredExtensions?.Select(ExtensionSpecifier.Parse) ?? Enumerable.Empty(); } } ================================================ FILE: StabilityMatrix.Core/Attributes/ViewAttribute.cs ================================================ namespace StabilityMatrix.Core.Attributes; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class ViewAttribute : Attribute { public Type ViewType { get; init; } public bool IsPersistent { get; init; } public ViewAttribute(Type type) { ViewType = type; } public ViewAttribute(Type type, bool persistent) { ViewType = type; IsPersistent = persistent; } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/AnalyticsRequestConverter.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Lykos.Analytics; namespace StabilityMatrix.Core.Converters.Json; public class AnalyticsRequestConverter : JsonConverter { public override AnalyticsRequest? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { using var jsonDocument = JsonDocument.ParseValue(ref reader); var root = jsonDocument.RootElement; if (!root.TryGetProperty("Type", out var typeProperty)) throw new JsonException("Missing Type property"); var type = typeProperty.GetString(); return type switch { "package-install" => JsonSerializer.Deserialize(root.GetRawText(), options), "first-time-install" => JsonSerializer.Deserialize(root.GetRawText(), options), "launch" => JsonSerializer.Deserialize(root.GetRawText(), options), _ => throw new JsonException($"Unknown Type: {type}") }; } public override void Write(Utf8JsonWriter writer, AnalyticsRequest value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, value.GetType(), options); } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/DefaultUnknownEnumConverter.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Converters.Json; public class DefaultUnknownEnumConverter< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T > : JsonConverter where T : Enum { /// /// Lazy initialization for . /// private readonly Lazy> _enumMemberValuesLazy = new( () => typeof(T) .GetFields() .Where(field => field.IsStatic) .Select( field => new { FieldName = field.Name, FieldValue = (T)field.GetValue(null)!, EnumMemberValue = field .GetCustomAttributes(false) .FirstOrDefault() ?.Value?.ToString() } ) .ToDictionary(x => x.EnumMemberValue ?? x.FieldName, x => x.FieldValue) ); /// /// Gets a dictionary of enum member values, keyed by the EnumMember attribute value, or the field name if no EnumMember attribute is present. /// private Dictionary EnumMemberValues => _enumMemberValuesLazy.Value; /// /// Lazy initialization for . /// private readonly Lazy> _enumMemberNamesLazy; /// /// Gets a dictionary of enum member names, keyed by the enum member value. /// private Dictionary EnumMemberNames => _enumMemberNamesLazy.Value; /// /// Gets the value of the "Unknown" enum member, or the 0 value if no "Unknown" member is present. /// private T UnknownValue => EnumMemberValues.TryGetValue("Unknown", out var res) ? res : (T)Enum.ToObject(typeof(T), 0); /// public override bool HandleNull => true; public DefaultUnknownEnumConverter() { _enumMemberNamesLazy = new Lazy>( () => EnumMemberValues.ToDictionary(x => x.Value, x => x.Key) ); } /// public override T Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName)) { throw new JsonException("Expected String or PropertyName token"); } if (reader.GetString() is { } readerString) { // First try get exact match if (EnumMemberValues.TryGetValue(readerString, out var enumMemberValue)) { return enumMemberValue; } // Otherwise try get case-insensitive match if ( EnumMemberValues.Keys.FirstOrDefault( key => key.Equals(readerString, StringComparison.OrdinalIgnoreCase) ) is { } enumMemberName ) { return EnumMemberValues[enumMemberName]; } Debug.WriteLine($"Unknown enum member value for {typeToConvert}: {readerString}"); } return UnknownValue; } /// public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { if (value == null) { writer.WriteNullValue(); return; } writer.WriteStringValue(EnumMemberNames[value]); } /// public override T ReadAsPropertyName( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) => Read(ref reader, typeToConvert, options); /// public override void WriteAsPropertyName( Utf8JsonWriter writer, T? value, JsonSerializerOptions options ) => Write(writer, value, options); } ================================================ FILE: StabilityMatrix.Core/Converters/Json/LaunchOptionValueJsonConverter.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Converters.Json; public class LaunchOptionValueJsonConverter : JsonConverter { public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { try { var boolValue = reader.GetBoolean(); return boolValue; } catch (InvalidOperationException) { // ignored } try { var intValue = reader.GetInt32(); return intValue; } catch (InvalidOperationException) { // ignored } try { var strValue = reader.GetString(); return strValue; } catch (InvalidOperationException) { return null; } } public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { switch (value) { case bool boolValue: writer.WriteBooleanValue(boolValue); break; case int intValue: writer.WriteNumberValue(intValue); break; case string strValue: writer.WriteStringValue(strValue); break; default: throw new ArgumentOutOfRangeException(nameof(value)); } } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/NodeConnectionBaseJsonConverter.cs ================================================ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Core.Converters.Json; public class NodeConnectionBaseJsonConverter : JsonConverter { /// public override NodeConnectionBase Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { // Read as Data array reader.Read(); var data = new object[2]; reader.Read(); data[0] = reader.GetString() ?? throw new JsonException("Expected string for node name"); reader.Read(); data[1] = reader.GetInt32(); reader.Read(); if (Activator.CreateInstance(typeToConvert) is not NodeConnectionBase instance) { throw new JsonException($"Failed to create instance of {typeToConvert}"); } var propertyInfo = typeToConvert.GetProperty("Data", BindingFlags.Public | BindingFlags.Instance) ?? throw new JsonException($"Failed to get Data property of {typeToConvert}"); propertyInfo.SetValue(instance, data); return instance; } /// public override void Write(Utf8JsonWriter writer, NodeConnectionBase value, JsonSerializerOptions options) { // Write as Data array writer.WriteStartArray(); writer.WriteStringValue(value.Data?[0] as string); writer.WriteNumberValue((int)value.Data?[1]!); writer.WriteEndArray(); } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/OneOfJsonConverter.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using OneOf; namespace StabilityMatrix.Core.Converters.Json; public class OneOfJsonConverter : JsonConverter> { /// public override OneOf Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { // Not sure how else to do this without polymorphic type markers but that would not serialize into T1/T2 // So just try to deserialize T1, if it fails, try T2 Exception? t1Exception = null; Exception? t2Exception = null; try { if (JsonSerializer.Deserialize(ref reader, options) is { } t1) { return t1; } } catch (JsonException e) { t1Exception = e; } try { if (JsonSerializer.Deserialize(ref reader, options) is { } t2) { return t2; } } catch (JsonException e) { t2Exception = e; } throw new JsonException( $"Failed to deserialize OneOf<{typeof(T1)}, {typeof(T2)}> as either {typeof(T1)} or {typeof(T2)}", new AggregateException([t1Exception, t2Exception]) ); } /// public override void Write(Utf8JsonWriter writer, OneOf value, JsonSerializerOptions options) { if (value.IsT0) { JsonSerializer.Serialize(writer, value.AsT0, options); } else { JsonSerializer.Serialize(writer, value.AsT1, options); } } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/ParsableStringValueJsonConverter.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Converters.Json; public class ParsableStringValueJsonConverter< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T > : JsonConverter where T : StringValue, IParsable { /// public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) { throw new JsonException(); } var value = reader.GetString(); if (value is null) { return default; } // Use TryParse result if available if (T.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } // Otherwise use Activator return (T?)Activator.CreateInstance(typeToConvert, value); } /// public override T ReadAsPropertyName( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException("Unexpected token type"); } var value = reader.GetString(); if (value is null) { throw new JsonException("Property name cannot be null"); } // Use TryParse result if available if (T.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } // Otherwise use Activator return (T?)Activator.CreateInstance(typeToConvert, value) ?? throw new JsonException("Property name cannot be null"); } /// public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { if (value is IFormattable formattable) { writer.WriteStringValue(formattable.ToString(null, CultureInfo.InvariantCulture)); } else { writer.WriteStringValue(value.ToString()); } } /// public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { if (value is null) { throw new JsonException("Property name cannot be null"); } if (value is IFormattable formattable) { writer.WritePropertyName(formattable.ToString(null, CultureInfo.InvariantCulture)); } else { writer.WritePropertyName(value.ToString()); } } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/SKColorJsonConverter.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using SkiaSharp; namespace StabilityMatrix.Core.Converters.Json; public class SKColorJsonConverter : JsonConverter { public override SKColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (!reader.TryGetUInt32(out var value)) { return SKColor.Empty; } return new SKColor(value); } public override void Write(Utf8JsonWriter writer, SKColor value, JsonSerializerOptions options) { // Convert to uint in the format ARGB var argbColor = (uint)((value.Alpha << 24) | (value.Red << 16) | (value.Green << 8) | value.Blue); writer.WriteNumberValue(argbColor); } } ================================================ FILE: StabilityMatrix.Core/Converters/Json/SemVersionJsonConverter.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Semver; namespace StabilityMatrix.Core.Converters.Json; public class SemVersionJsonConverter : JsonConverter { public override SemVersion Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => SemVersion.Parse(reader.GetString()!, SemVersionStyles.Strict); public override void Write( Utf8JsonWriter writer, SemVersion value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); } ================================================ FILE: StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using JetBrains.Annotations; namespace StabilityMatrix.Core.Converters.Json; /// /// Json converter for types that serialize to string by `ToString()` and /// can be created by `Activator.CreateInstance(Type, string)` /// Types implementing will be formatted with /// [PublicAPI] public class StringJsonConverter< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T > : JsonConverter { /// public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) { throw new JsonException(); } var value = reader.GetString(); if (value is null) { return default; } return (T?)Activator.CreateInstance(typeToConvert, value); } /// public override T ReadAsPropertyName( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { if (reader.TokenType != JsonTokenType.String) { throw new JsonException(); } var value = reader.GetString(); if (value is null) { throw new JsonException("Property name cannot be null"); } return (T?)Activator.CreateInstance(typeToConvert, value) ?? throw new JsonException("Property name cannot be null"); } /// public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { if (value is null) { writer.WriteNullValue(); return; } if (value is IFormattable formattable) { writer.WriteStringValue(formattable.ToString(null, CultureInfo.InvariantCulture)); } else { writer.WriteStringValue(value.ToString()); } } /// public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { if (value is null) { throw new JsonException("Property name cannot be null"); } if (value is IFormattable formattable) { writer.WriteStringValue(formattable.ToString(null, CultureInfo.InvariantCulture)); } else { writer.WriteStringValue(value.ToString()); } } } ================================================ FILE: StabilityMatrix.Core/Database/CivitModelQueryCacheEntry.cs ================================================ using LiteDB; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Database; /// /// Cache entry for the result of a Civit model query response /// public class CivitModelQueryCacheEntry { // This is set as the hash of the request object (ObjectHash.GetMd5Guid) public Guid Id { get; set; } public DateTimeOffset? InsertedAt { get; set; } public CivitModelsRequest? Request { get; set; } [BsonRef("CivitModels")] public List? Items { get; set; } public CivitMetadata? Metadata { get; set; } } ================================================ FILE: StabilityMatrix.Core/Database/ILiteDbContext.cs ================================================ using LiteDB.Async; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Database; public interface ILiteDbContext : IDisposable { LiteDatabaseAsync Database { get; } ILiteCollectionAsync CivitModels { get; } ILiteCollectionAsync CivitModelVersions { get; } ILiteCollectionAsync CivitModelQueryCache { get; } ILiteCollectionAsync LocalModelFiles { get; } ILiteCollectionAsync InferenceProjects { get; } ILiteCollectionAsync LocalImageFiles { get; } ILiteCollectionAsync CivitBaseModelTypeCache { get; } Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3); Task UpsertCivitModelAsync(CivitModel civitModel); Task UpsertCivitModelAsync(IEnumerable civitModels); Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry); Task GetGithubCacheEntry(string cacheKey); Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry); /// /// Clear all Collections that store re-fetchable cache type data. /// Task ClearAllCacheCollectionsAsync(); /// /// Executes a query with exception logging and collection clearing. /// This will handle unique exceptions once keyed by string representation for each collection, /// and throws if repeated. /// /// The type of collection to query. /// The type of result to return. /// The collection to query. /// The task representing the query to execute. /// The result of the query, or default value on handled exception. Task TryQueryWithClearOnExceptionAsync( ILiteCollectionAsync collection, Task task ); Task GetPyPiCacheEntry(string? cacheKey); Task UpsertPyPiCacheEntry(PyPiCacheEntry cacheEntry); Task GetCivitBaseModelTypeCacheEntry(string id); Task UpsertCivitBaseModelTypeCacheEntry(CivitBaseModelTypeCacheEntry entry); } ================================================ FILE: StabilityMatrix.Core/Database/LiteDbContext.cs ================================================ using System.Collections.Immutable; using System.Globalization; using AsyncAwaitBestPractices; using LiteDB; using LiteDB.Async; using LiteDB.Engine; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Database; public class LiteDbContext : ILiteDbContext { private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly DebugOptions debugOptions; // Tracks handled exceptions private readonly HashSet handledExceptions = []; private readonly Lazy lazyDatabase; public LiteDatabaseAsync Database => lazyDatabase.Value; // Notification events public event EventHandler? CivitModelsChanged; // Collections (Tables) public ILiteCollectionAsync CivitModels => Database.GetCollection("CivitModels"); public ILiteCollectionAsync CivitModelVersions => Database.GetCollection("CivitModelVersions"); public ILiteCollectionAsync CivitModelQueryCache => Database.GetCollection("CivitModelQueryCache"); public ILiteCollectionAsync GithubCache => Database.GetCollection("GithubCache"); public ILiteCollectionAsync LocalModelFiles => Database.GetCollection("LocalModelFiles"); public ILiteCollectionAsync InferenceProjects => Database.GetCollection("InferenceProjects"); public ILiteCollectionAsync LocalImageFiles => Database.GetCollection("LocalImageFiles"); public ILiteCollectionAsync PyPiCache => Database.GetCollection("PyPiCache"); public ILiteCollectionAsync CivitBaseModelTypeCache => Database.GetCollection("CivitBaseModelTypeCache"); public LiteDbContext( ILogger logger, ISettingsManager settingsManager, IOptions debugOptions ) { this.logger = logger; this.settingsManager = settingsManager; this.debugOptions = debugOptions.Value; lazyDatabase = new Lazy(CreateDatabase); } private LiteDatabaseAsync CreateDatabase() { // Try at most twice: // - attempt 0: open/repair if needed // - on "Detected loop in FindAll": dispose, delete file, try once more const int maxAttempts = 2; var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db"); for (var attempt = 0; attempt < maxAttempts; attempt++) { LiteDatabaseAsync? db = null; try { if (debugOptions.TempDatabase) { db = new LiteDatabaseAsync(":temp:"); RegisterRefs(); return db; } db = new LiteDatabaseAsync( new ConnectionString { Filename = dbPath, Connection = ConnectionType.Shared } ); var sortOption = db.Collation.SortOptions; if (sortOption is not CompareOptions.Ordinal) { logger.LogDebug( "Database collation is not Ordinal ({SortOption}), rebuilding...", sortOption ); var options = new RebuildOptions { Collation = new Collation(CultureInfo.InvariantCulture.LCID, CompareOptions.Ordinal), }; db.RebuildAsync(options).GetAwaiter().GetResult(); } RegisterRefs(); return db; } catch (LiteAsyncException ex) when (ex.InnerException is LiteException e && e.Message.Contains("Detected loop in FindAll", StringComparison.OrdinalIgnoreCase) ) { logger.LogWarning("Database corruption detected ({Message}), rebuilding...", e.Message); try { db?.Dispose(); } catch { // ignored } try { // Backup then delete, in case we want to inspect later. var corruptPath = dbPath + ".old-" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"); if (File.Exists(dbPath)) { File.Copy(dbPath, corruptPath, overwrite: false); File.Delete(dbPath); } } catch (Exception delEx) { logger.LogWarning("Failed to delete corrupt DB: {Message}", delEx.Message); // If we can't delete, no point retrying; break to fallback. break; } } catch (IOException ioEx) { logger.LogWarning( "Database in use or not accessible ({Message}), using temporary database", ioEx.Message ); break; // fall through to temp } } // Fallback to temporary database var tempDb = new LiteDatabaseAsync(":temp:"); RegisterRefs(); return tempDb; void RegisterRefs() { LiteDBExtensions.Register( m => m.ModelVersions, "CivitModelVersions" ); LiteDBExtensions.Register(e => e.Items, "CivitModels"); LiteDBExtensions.Register(e => e.LatestModelInfo, "CivitModels"); } } public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) { var version = await CivitModelVersions .Query() .Where(mv => mv.Files!.Select(f => f.Hashes) .Select(hashes => hashes.BLAKE3) .Any(hash => hash == hashBlake3) ) .FirstOrDefaultAsync() .ConfigureAwait(false); if (version is null) return (null, null); var model = await CivitModels .Query() .Include(m => m.ModelVersions) .Where(m => m.ModelVersions!.Select(v => v.Id).Any(id => id == version.Id)) .FirstOrDefaultAsync() .ConfigureAwait(false); return (model, version); } public async Task UpsertCivitModelAsync(CivitModel civitModel) { // Insert model versions first then model var versionsUpdated = await CivitModelVersions .UpsertAsync(civitModel.ModelVersions) .ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false); // Notify listeners on any change var anyUpdated = versionsUpdated > 0 || updated; if (anyUpdated) { CivitModelsChanged?.Invoke(this, EventArgs.Empty); } return anyUpdated; } public async Task UpsertCivitModelAsync(IEnumerable civitModels) { var civitModelsArray = civitModels.ToArray(); // Get all model versions then insert models var versions = civitModelsArray.SelectMany(model => model.ModelVersions ?? new()); var versionsUpdated = await CivitModelVersions.UpsertAsync(versions).ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModelsArray).ConfigureAwait(false); // Notify listeners on any change var anyUpdated = versionsUpdated > 0 || updated > 0; if (updated > 0 || versionsUpdated > 0) { CivitModelsChanged?.Invoke(this, EventArgs.Empty); } return anyUpdated; } // Add to cache public async Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry) { var changed = await CivitModelQueryCache.UpsertAsync(entry).ConfigureAwait(false); if (changed) { CivitModelsChanged?.Invoke(this, EventArgs.Empty); } return changed; } public async Task GetGithubCacheEntry(string? cacheKey) { if (string.IsNullOrEmpty(cacheKey)) return null; return await TryQueryWithClearOnExceptionAsync(GithubCache, GithubCache.FindByIdAsync(cacheKey)) .ConfigureAwait(false); } public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => GithubCache.UpsertAsync(cacheEntry); public async Task GetPyPiCacheEntry(string? cacheKey) { if (string.IsNullOrEmpty(cacheKey)) return null; return await TryQueryWithClearOnExceptionAsync(PyPiCache, PyPiCache.FindByIdAsync(cacheKey)) .ConfigureAwait(false); } public Task UpsertPyPiCacheEntry(PyPiCacheEntry cacheEntry) => PyPiCache.UpsertAsync(cacheEntry); /// /// Clear all Collections that store re-fetchable cache type data. /// public async Task ClearAllCacheCollectionsAsync() { var collectionNames = new List { nameof(CivitModels), nameof(CivitModelVersions), nameof(CivitModelQueryCache), nameof(GithubCache), nameof(LocalModelFiles), nameof(LocalImageFiles), }; logger.LogInformation("Clearing all cache collections: [{@Names}]", collectionNames); foreach (var name in collectionNames) { var collection = Database.GetCollection(name); await collection.DeleteAllAsync().ConfigureAwait(false); } } /// /// Executes a query with exception logging and collection clearing. /// This will handle unique exceptions once keyed by string representation for each collection, /// and throws if repeated. /// /// The type of collection to query. /// The type of result to return. /// The collection to query. /// The task representing the query to execute. /// The result of the query, or default value on handled exception. public async Task TryQueryWithClearOnExceptionAsync( ILiteCollectionAsync collection, Task task ) { try { return await task.ConfigureAwait(false); } catch (Exception ex) { var exceptionInfo = new HandledExceptionInfo( collection.Name, ex.ToString(), ex.InnerException?.ToString() ); lock (handledExceptions) { var exceptionString = ex.InnerException is null ? $"{ex.GetType()}" : $"{ex.GetType()} ({ex.InnerException.GetType()})"; // Throw if exception was already handled previously this session // then it's probably not a migration issue if (handledExceptions.Contains(exceptionInfo)) { throw new AggregateException( $"Repeated LiteDb error '{exceptionString}' while fetching from '{exceptionInfo.CollectionName}', previously handled", ex ); } // Log warning for known exception types, otherwise log error if ( ex is LiteException or LiteAsyncException && ex.InnerException is InvalidCastException // GitHub cache int type changes or ArgumentException // Unknown enum values ) { logger.LogWarning( ex, "LiteDb error while fetching from {Name}, collection will be cleared: {Exception}", collection.Name, exceptionString ); } else { #if DEBUG throw; #else logger.LogError( ex, "LiteDb unknown error while fetching from {Name}, collection will be cleared: {Exception}", collection.Name, exceptionString ); #endif } // Add to handled exceptions handledExceptions.Add(exceptionInfo); } // Clear collection await collection.DeleteAllAsync().ConfigureAwait(false); // Get referenced collections var referencedCollections = FindReferencedCollections(collection).ToArray(); if (referencedCollections.Length > 0) { logger.LogWarning( "Clearing referenced collections: [{@Names}]", referencedCollections.Select(c => c.Name) ); foreach (var referencedCollection in referencedCollections) { await referencedCollection.DeleteAllAsync().ConfigureAwait(false); } } } return default; } public void Dispose() { if (lazyDatabase.IsValueCreated) { try { Database.Dispose(); } catch (ObjectDisposedException) { } catch (ApplicationException) { // Ignores a mutex error from library // https://stability-matrix.sentry.io/share/issue/5c62f37462444e7eab18cea314af231f/ } } GC.SuppressFinalize(this); } /// /// Recursively find all referenced collections in the entity mapper of a collection. /// private IEnumerable> FindReferencedCollections( ILiteCollectionAsync collection ) { var collectionNames = Database.UnderlyingDatabase.GetCollectionNames().ToArray(); foreach ( var referencedCollectionName in FindReferencedCollectionNamesRecursive( collection.EntityMapper, [collection.Name] ) ) { yield return Database.GetCollection(referencedCollectionName); } yield break; IEnumerable FindReferencedCollectionNamesRecursive( EntityMapper entityMapper, ImmutableHashSet seenCollectionNames ) { foreach (var member in entityMapper.Members) { // Only look for members that are DBRef if (!member.IsDbRef || member.UnderlyingType is not { } dbRefType) continue; // Skip if not a collection or already seen if (!collectionNames.Contains(dbRefType.Name) || seenCollectionNames.Contains(dbRefType.Name)) continue; var memberCollection = Database.GetCollection(dbRefType.Name); seenCollectionNames = seenCollectionNames.Add(memberCollection.Name); yield return memberCollection.Name; // Also recursively find references in the referenced collection foreach ( var subCollectionName in FindReferencedCollectionNamesRecursive( memberCollection.EntityMapper, seenCollectionNames ) ) { seenCollectionNames = seenCollectionNames.Add(subCollectionName); yield return subCollectionName; } } } } public async Task GetCivitBaseModelTypeCacheEntry(string id) { if (string.IsNullOrEmpty(id)) return null; return await CivitBaseModelTypeCache.FindByIdAsync(id).ConfigureAwait(false); } public Task UpsertCivitBaseModelTypeCacheEntry(CivitBaseModelTypeCacheEntry entry) => CivitBaseModelTypeCache.UpsertAsync(entry); private readonly record struct HandledExceptionInfo( string CollectionName, string Exception, string? InnerException ); } ================================================ FILE: StabilityMatrix.Core/Exceptions/AppException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; /// /// Generic runtime exception with custom handling by notification service /// public class AppException : ApplicationException { public override string Message { get; } public string? Details { get; init; } public AppException(string message) { Message = message; } public AppException(string message, string details) { Message = message; Details = details; } } ================================================ FILE: StabilityMatrix.Core/Exceptions/CivitDownloadDisabledException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class CivitDownloadDisabledException : UnauthorizedAccessException; ================================================ FILE: StabilityMatrix.Core/Exceptions/CivitLoginRequiredException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class CivitLoginRequiredException : UnauthorizedAccessException; ================================================ FILE: StabilityMatrix.Core/Exceptions/ComfyNodeException.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; namespace StabilityMatrix.Core.Exceptions; public class ComfyNodeException : Exception { public required ComfyWebSocketExecutionErrorData ErrorData { get; init; } public required string JsonData { get; init; } } ================================================ FILE: StabilityMatrix.Core/Exceptions/EarlyAccessException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class EarlyAccessException : UnauthorizedAccessException; ================================================ FILE: StabilityMatrix.Core/Exceptions/FileExistsException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class FileTransferExistsException : IOException { public string SourceFile { get; } public string DestinationFile { get; } public FileTransferExistsException(string source, string destination) { SourceFile = source; DestinationFile = destination; } } ================================================ FILE: StabilityMatrix.Core/Exceptions/HuggingFaceLoginRequiredException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class HuggingFaceLoginRequiredException : UnauthorizedAccessException; ================================================ FILE: StabilityMatrix.Core/Exceptions/MissingPrerequisiteException.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class MissingPrerequisiteException( string missingPrereqName, string message, string? downloadLink = null ) : Exception($"{message}{Environment.NewLine}{downloadLink}{Environment.NewLine}") { public string MissingPrereqName { get; set; } = missingPrereqName; public string? DownloadLink { get; set; } = downloadLink; } ================================================ FILE: StabilityMatrix.Core/Exceptions/ProcessException.cs ================================================ using System.Diagnostics; using System.Text; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Exceptions; /// /// Exception that is thrown when a process fails. /// public class ProcessException : Exception { public ProcessResult? ProcessResult { get; } public ProcessException(string message) : base(message) { } public ProcessException(ProcessResult processResult) : base( $"Process {processResult.ProcessName} exited with code {processResult.ExitCode}. " + $"{{StdOut = {processResult.StandardOutput}, StdErr = {processResult.StandardError}}}" ) { ProcessResult = processResult; } public static void ThrowIfNonZeroExitCode(ProcessResult processResult) { if (processResult.IsSuccessExitCode) return; throw new ProcessException(processResult); } public static void ThrowIfNonZeroExitCode(Process process, string output) { if (!process.HasExited || process.ExitCode == 0) return; throw new ProcessException( new ProcessResult { ProcessName = process.StartInfo.FileName, ExitCode = process.ExitCode, StandardOutput = output } ); } public static void ThrowIfNonZeroExitCode(Process process, StringBuilder outputBuilder) { if (!process.HasExited || process.ExitCode == 0) return; throw new ProcessException( new ProcessResult { ProcessName = process.StartInfo.FileName, ExitCode = process.ExitCode, StandardOutput = outputBuilder.ToString() } ); } public static void ThrowIfNonZeroExitCode(Process process, string stdOut, string stdErr) { if (!process.HasExited || process.ExitCode == 0) return; throw new ProcessException( new ProcessResult { ProcessName = process.StartInfo.FileName, ExitCode = process.ExitCode, StandardOutput = stdOut, StandardError = stdErr } ); } } ================================================ FILE: StabilityMatrix.Core/Exceptions/PromptError.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public abstract class PromptError : ApplicationException { public int TextOffset { get; } public int TextEndOffset { get; } protected PromptError(string message, int textOffset, int textEndOffset) : base(message) { TextOffset = textOffset; TextEndOffset = textEndOffset; } } ================================================ FILE: StabilityMatrix.Core/Exceptions/PromptSyntaxError.cs ================================================ namespace StabilityMatrix.Core.Exceptions; public class PromptSyntaxError : PromptError { public static PromptSyntaxError Network_ExpectedSeparator(int textOffset, int textEndOffset) => new("Expected separator", textOffset, textEndOffset); public static PromptSyntaxError Network_ExpectedType(int textOffset, int textEndOffset) => new("Expected network type", textOffset, textEndOffset); public static PromptSyntaxError Network_ExpectedName(int textOffset, int textEndOffset) => new("Expected network name", textOffset, textEndOffset); public static PromptSyntaxError Network_ExpectedWeight(int textOffset, int textEndOffset) => new("Expected network weight", textOffset, textEndOffset); public static PromptSyntaxError UnexpectedEndOfText(int textOffset, int textEndOffset) => new("Unexpected end of text", textOffset, textEndOffset); /// public PromptSyntaxError(string message, int textOffset, int textEndOffset) : base(message, textOffset, textEndOffset) { } } ================================================ FILE: StabilityMatrix.Core/Exceptions/PromptUnknownModelError.cs ================================================ using StabilityMatrix.Core.Models.Tokens; namespace StabilityMatrix.Core.Exceptions; public class PromptUnknownModelError : PromptValidationError { public string ModelName { get; } public PromptExtraNetworkType ModelType { get; } /// public PromptUnknownModelError( string message, int textOffset, int textEndOffset, string modelName, PromptExtraNetworkType modelType ) : base(message, textOffset, textEndOffset) { ModelName = modelName; ModelType = modelType; } } ================================================ FILE: StabilityMatrix.Core/Exceptions/PromptValidationError.cs ================================================ using StabilityMatrix.Core.Models.Tokens; namespace StabilityMatrix.Core.Exceptions; public class PromptValidationError : PromptError { /// public PromptValidationError(string message, int textOffset, int textEndOffset) : base(message, textOffset, textEndOffset) { } public static PromptValidationError Network_UnknownType(int textOffset, int textEndOffset) => new("Unknown network type", textOffset, textEndOffset); public static PromptUnknownModelError Network_UnknownModel( string modelName, PromptExtraNetworkType modelType, int textOffset, int textEndOffset ) => new( $"Model '{modelName}' was not found locally", textOffset, textEndOffset, modelName, modelType ); public static PromptSyntaxError Network_InvalidWeight(int textOffset, int textEndOffset) => new("Invalid network weight, could not be parsed as double", textOffset, textEndOffset); } ================================================ FILE: StabilityMatrix.Core/Extensions/DictionaryExtensions.cs ================================================ using System.Text; namespace StabilityMatrix.Core.Extensions; public static class DictionaryExtensions { /// /// Adds all items from another dictionary to this dictionary. /// public static void Update( this IDictionary source, IReadOnlyDictionary collection ) where TKey : notnull { foreach (var item in collection) { source[item.Key] = item.Value; } } /// /// Formats a dictionary as a string for debug/logging purposes. /// public static string ToRepr( this IDictionary source ) where TKey : notnull { var sb = new StringBuilder(); sb.Append('{'); foreach (var (key, value) in source) { // for string types, use ToRepr if (key is string keyString) { sb.Append($"{keyString.ToRepr()}="); } else { sb.Append($"{key}="); } if (value is string valueString) { sb.Append($"{valueString.ToRepr()}, "); } else { sb.Append($"{value}, "); } } sb.Append('}'); return sb.ToString(); } /// /// Get or add a value to a dictionary. /// public static TValue GetOrAdd(this IDictionary dict, TKey key) where TValue : new() { if (!dict.TryGetValue(key, out var val)) { val = new TValue(); dict.Add(key, val); } return val; } } ================================================ FILE: StabilityMatrix.Core/Extensions/DirectoryPathExtensions.cs ================================================ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Polly; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Extensions; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class DirectoryPathExtensions { /// /// Deletes a directory and all of its contents recursively. /// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff. /// public static Task DeleteVerboseAsync( this DirectoryPath directory, ILogger? logger = default, CancellationToken cancellationToken = default ) { var policy = Policy .Handle() .WaitAndRetryAsync( 3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)), onRetry: (exception, calculatedWaitDuration) => { logger?.LogWarning( exception, "Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}", directory, calculatedWaitDuration ); } ); return policy.ExecuteAsync(async () => { await Task.Run(() => DeleteVerbose(directory, logger, cancellationToken), cancellationToken) .ConfigureAwait(false); }); } /// /// Deletes a directory and all of its contents recursively. /// Removes link targets without deleting the source. /// public static void DeleteVerbose( this DirectoryPath directory, ILogger? logger = default, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); // Skip if directory does not exist if (!directory.Exists) { return; } // For junction points, delete with recursive false if (directory.IsSymbolicLink) { logger?.LogInformation("Removing junction point {TargetDirectory}", directory.FullPath); try { directory.Delete(false); return; } catch (IOException ex) { throw new IOException($"Failed to delete junction point {directory.FullPath}", ex); } } // Recursively delete all subdirectories foreach (var subDir in directory.EnumerateDirectories()) { DeleteVerbose(subDir, logger, cancellationToken); } // Delete all files in the directory foreach (var filePath in directory.EnumerateFiles()) { cancellationToken.ThrowIfCancellationRequested(); try { filePath.Info.Attributes = FileAttributes.Normal; filePath.Delete(); } catch (IOException ex) { throw new IOException($"Failed to delete file {filePath.FullPath}", ex); } } // Delete this directory try { directory.Delete(false); } catch (IOException ex) { throw new IOException($"Failed to delete directory {directory}", ex); } } } ================================================ FILE: StabilityMatrix.Core/Extensions/DynamicDataExtensions.cs ================================================ using DynamicData; namespace StabilityMatrix.Core.Extensions; public static class DynamicDataExtensions { /// /// Loads the cache with the specified items in an optimised manner i.e. calculates the differences between the old and new items /// in the list and amends only the differences. /// /// The type of the object. /// The type of the key. /// The source. /// The items to add, update or delete. /// source. public static void EditDiff( this ISourceCache source, IEnumerable allItems ) where TObject : IEquatable where TKey : notnull { if (source is null) { throw new ArgumentNullException(nameof(source)); } if (allItems is null) { throw new ArgumentNullException(nameof(allItems)); } source.EditDiff(allItems, (x, y) => x.Equals(y)); } } ================================================ FILE: StabilityMatrix.Core/Extensions/EnumAttributes.cs ================================================ namespace StabilityMatrix.Core.Extensions; public static class EnumAttributeExtensions { private static T? GetAttributeValue(Enum value) { var type = value.GetType(); var fieldInfo = type.GetField(value.ToString()); // Get the string value attributes var attribs = fieldInfo?.GetCustomAttributes(typeof(T), false) as T[]; // Return the first if there was a match. return attribs?.Length > 0 ? attribs[0] : default; } /// /// Gets the StringValue field attribute on a given enum value. /// If not found, returns the enum value itself as a string. /// /// /// public static string GetStringValue(this Enum value) { var attr = GetAttributeValue(value)?.StringValue; return attr ?? Enum.GetName(value.GetType(), value)!; } /// /// Gets the Description field attribute on a given enum value. /// /// /// public static string? GetDescription(this Enum value) { return GetAttributeValue(value)?.Description; } } [AttributeUsage(AttributeTargets.Field)] public sealed class StringValueAttribute : Attribute { public string StringValue { get; } public StringValueAttribute(string value) { StringValue = value; } } [AttributeUsage(AttributeTargets.Field)] public sealed class DescriptionAttribute : Attribute { public string Description { get; } public DescriptionAttribute(string value) { Description = value; } } ================================================ FILE: StabilityMatrix.Core/Extensions/EnumConversion.cs ================================================ namespace StabilityMatrix.Core.Extensions; public static class EnumConversionExtensions { public static T? ConvertTo(this Enum value) where T : Enum { var type = value.GetType(); var fieldInfo = type.GetField(value.ToString()); // Get the string value attributes var attribs = fieldInfo?.GetCustomAttributes(typeof(ConvertToAttribute), false) as ConvertToAttribute[]; // Return the first if there was a match. return attribs?.Length > 0 ? attribs[0].ConvertToEnum : default; } } [AttributeUsage(AttributeTargets.Field)] public sealed class ConvertToAttribute : Attribute where T : Enum { public T ConvertToEnum { get; } public ConvertToAttribute(T toEnum) { ConvertToEnum = toEnum; } } ================================================ FILE: StabilityMatrix.Core/Extensions/EnumerableExtensions.cs ================================================ namespace StabilityMatrix.Core.Extensions; public static class EnumerableExtensions { public static IEnumerable<(int, T)> Enumerate(this IEnumerable items, int start) { return items.Select((item, index) => (index + start, item)); } public static IEnumerable<(int, T)> Enumerate(this IEnumerable items) { return items.Select((item, index) => (index, item)); } /// /// Nested for loop helper /// public static IEnumerable<(T, T)> Product(this IEnumerable items, IEnumerable other) { return from item1 in items from item2 in other select (item1, item2); } public static async Task> SelectAsync( this IEnumerable source, Func> method, int concurrency = int.MaxValue ) { using var semaphore = new SemaphoreSlim(concurrency); return await Task.WhenAll( source.Select(async s => { try { // ReSharper disable once AccessToDisposedClosure await semaphore.WaitAsync().ConfigureAwait(false); return await method(s).ConfigureAwait(false); } finally { // ReSharper disable once AccessToDisposedClosure semaphore.Release(); } }) ) .ConfigureAwait(false); } /// /// Executes a specified action on each element in a collection. /// /// The type of elements in the collection. /// The collection to iterate over. /// The action to perform on each element in the collection. public static void ForEach(this IEnumerable items, Action action) { foreach (var item in items) { action(item); } } // Concat an element if not null public static IEnumerable AppendIfNotNull(this IEnumerable source, T? element) where T : class { if (element != null) { return source.Append(element); } return source; } // Concat an enumerable if not null public static IEnumerable ConcatIfNotNull(this IEnumerable source, IEnumerable? elements) where T : class { if (elements != null) { return source.Concat(elements); } return source; } } ================================================ FILE: StabilityMatrix.Core/Extensions/HashExtensions.cs ================================================ using Blake3; namespace StabilityMatrix.Core.Extensions; public static class HashExtensions { public static Guid ToGuid(this Hash hash) { return new Guid(hash.AsSpan()[..16]); } } ================================================ FILE: StabilityMatrix.Core/Extensions/JsonObjectExtensions.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; using JetBrains.Annotations; namespace StabilityMatrix.Core.Extensions; [PublicAPI] public static class JsonObjectExtensions { /// /// Returns the value of a property with the specified name, or the specified default value if not found. /// public static T? GetPropertyValueOrDefault( this JsonObject jsonObject, string propertyName, T? defaultValue = default ) { if (!jsonObject.TryGetPropertyValue(propertyName, out var node)) { return defaultValue; } return node.Deserialize(); } /// /// Get a keyed value from a JsonObject if it is not null, /// otherwise add and return a new instance of a JsonObject. /// public static JsonObject GetOrAddNonNullJsonObject(this JsonObject jsonObject, string key) { if (jsonObject.TryGetPropertyValue(key, out var value) && value is JsonObject jsonObjectValue) { return jsonObjectValue; } var newJsonObject = new JsonObject(); jsonObject[key] = newJsonObject; return newJsonObject; } /// /// Get a keyed value path from a JsonObject if it is not null, /// otherwise add and return a new instance of a JsonObject. /// public static JsonObject GetOrAddNonNullJsonObject( this JsonObject jsonObject, IEnumerable keyPath ) { return keyPath.Aggregate(jsonObject, (current, key) => current.GetOrAddNonNullJsonObject(key)); } } ================================================ FILE: StabilityMatrix.Core/Extensions/LiteDBExtensions.cs ================================================ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; using LiteDB; using LiteDB.Async; namespace StabilityMatrix.Core.Extensions; // ReSharper disable once InconsistentNaming public static class LiteDBExtensions { private static readonly ConcurrentDictionary Mapper = new(); public static void Register(Expression?>> exp, string? collection = null) { var member = (exp.Body is MemberExpression body ? body.Member : null) as PropertyInfo; if (member == null) throw new ArgumentException("Expecting Member Expression"); BsonMapper.Global.Entity().DbRef(exp, collection); Mapper.TryAdd(typeof(T), (typeof(TU), member.Name, true)); } public static void Register(Expression> exp, string? collection = null) { var member = (exp.Body is MemberExpression body ? body.Member : null) as PropertyInfo; if (member == null) throw new ArgumentException("Expecting Member Expression"); BsonMapper.Global.Entity().DbRef(exp, collection); Mapper.TryAdd(typeof(T), (typeof(TU), member.Name, false)); } public static ILiteCollection? IncludeAll(this ILiteCollection col) { if (!Mapper.ContainsKey(typeof(T))) return null; var stringList = new List(); var key = typeof(T); var values = new List(); var flag = true; while (Mapper.TryGetValue(key, out var tuple)) { var str = tuple.MemberName + (tuple.IsList ? "[*]" : ""); values.Add(flag ? "$." + str : str); stringList.Add(string.Join(".", values)); key = tuple.PropertyType; flag = false; } return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression)keySelector)); } public static ILiteCollectionAsync IncludeAll(this ILiteCollectionAsync col) { if (!Mapper.ContainsKey(typeof(T))) return col; var stringList = new List(); var key = typeof(T); var values = new List(); var flag = true; while (Mapper.TryGetValue(key, out var tuple)) { var str = tuple.MemberName + (tuple.IsList ? "[*]" : ""); values.Add(flag ? "$." + str : str); stringList.Add(string.Join(".", values)); key = tuple.PropertyType; flag = false; } return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression)keySelector)); } } ================================================ FILE: StabilityMatrix.Core/Extensions/NullableExtensions.cs ================================================ using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace StabilityMatrix.Core.Extensions; public static class NullableExtensions { /// /// Unwraps a nullable object, throwing an exception if it is null. /// /// /// Thrown if () is null. /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("obj:null => halt; obj:notnull => notnull")] [return: System.Diagnostics.CodeAnalysis.NotNull] public static T Unwrap( [System.Diagnostics.CodeAnalysis.NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null ) where T : class { if (obj is null) { throw new ArgumentNullException(paramName, $"Unwrap of a null value ({typeof(T)}) {paramName}."); } return obj; } /// /// Unwraps a nullable struct object, throwing an exception if it is null. /// /// /// Thrown if () is null. /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] [MethodImpl(MethodImplOptions.AggressiveInlining)] [ContractAnnotation("obj:null => halt")] public static T Unwrap( [System.Diagnostics.CodeAnalysis.NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null ) where T : struct { if (obj is null) { throw new ArgumentNullException(paramName, $"Unwrap of a null value ({typeof(T)}) {paramName}."); } return obj.Value; } } ================================================ FILE: StabilityMatrix.Core/Extensions/ObjectExtensions.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Reflection; using JetBrains.Annotations; using RockLib.Reflection.Optimized; namespace StabilityMatrix.Core.Extensions; [PublicAPI] public static class ObjectExtensions { /// /// Cache of Types to named field getters /// private static readonly Dictionary>> FieldGetterTypeCache = new(); /// /// Cache of Types to named field setters /// private static readonly Dictionary>> FieldSetterTypeCache = new(); /// /// Cache of Types to named property getters /// private static readonly Dictionary>> PropertyGetterTypeCache = new(); /// /// Get the value of a named private field from an object /// /// /// The field must be defined by the runtime type of or its first base type. /// For higher inheritance levels, use to specify the exact defining type. /// public static T? GetPrivateField(this object obj, string fieldName) { // Check cache var fieldGetterCache = FieldGetterTypeCache.GetOrAdd(obj.GetType()); if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a getter for the field fieldGetter = field.CreateGetter(); // Add to cache fieldGetterCache.Add(fieldName, fieldGetter); } return (T?)fieldGetter(obj); } /// /// Get the value of a protected property from an object /// /// /// The property must be defined by the runtime type of or its first base type. /// public static object? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) { // Check cache var fieldGetterCache = PropertyGetterTypeCache.GetOrAdd(obj.GetType()); if (!fieldGetterCache.TryGetValue(propertyName, out var propertyGetter)) { // Get the field var propertyInfo = obj.GetType() .GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); // Try get from parent propertyInfo ??= obj.GetType() .BaseType ?.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (propertyInfo is null) { throw new ArgumentException($"Property {propertyName} not found on type {obj.GetType().Name}"); } // Create a getter for the field propertyGetter = o => propertyInfo.GetValue(o)!; // Add to cache fieldGetterCache.Add(propertyName, propertyGetter); } return (object?)propertyGetter(obj); } /// /// Get the value of a protected property from an object /// /// /// The property must be defined by the runtime type of or its first base type. /// public static T? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) where T : class { return (T?)GetProtectedProperty(obj, propertyName); } /// /// Get the value of a named private field from an object /// /// Type of the object that defines the field, must be a base class of /// Type of the field public static T? GetPrivateField(this TObject obj, string fieldName) where TObject : class { // Check cache var fieldGetterCache = FieldGetterTypeCache.GetOrAdd(typeof(TObject)); if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field var field = typeof(TObject).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { throw new ArgumentException( $"Field {typeof(TObject).Name}.{fieldName} not found on type {obj.GetType().Name}" ); } // Create a getter for the field fieldGetter = field.CreateGetter(); // Add to cache fieldGetterCache.Add(fieldName, fieldGetter); } return (T?)fieldGetter(obj); } /// /// Set the value of a named private field on an object /// public static void SetPrivateField(this object obj, string fieldName, object value) { // Check cache var fieldSetterCache = FieldSetterTypeCache.GetOrAdd(obj.GetType()); if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field fieldSetter = field.CreateSetter(); // Add to cache fieldSetterCache.Add(fieldName, fieldSetter); } fieldSetter(obj, value); } /// /// Set the value of a named private field on an object /// public static void SetPrivateField(this object obj, string fieldName, T? value) { // Check cache var fieldSetterCache = FieldSetterTypeCache.GetOrAdd(obj.GetType()); if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field fieldSetter = field.CreateSetter(); // Add to cache fieldSetterCache.Add(fieldName, fieldSetter); } fieldSetter(obj, value!); } } ================================================ FILE: StabilityMatrix.Core/Extensions/ProgressExtensions.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Extensions; public static class ProgressExtensions { [return: NotNullIfNotNull(nameof(progress))] public static Action? AsProcessOutputHandler( this IProgress? progress, bool setMessageAsOutput = true ) { if (progress is null) { return null; } return output => { progress.Report( new ProgressReport { Progress = -1f, IsIndeterminate = true, Message = setMessageAsOutput ? output.Text : null, ProcessOutput = output, PrintToConsole = true } ); }; } } ================================================ FILE: StabilityMatrix.Core/Extensions/SemVersionExtensions.cs ================================================ using Semver; namespace StabilityMatrix.Core.Extensions; public static class SemVersionExtensions { public static string ToDisplayString(this SemVersion version) { var versionString = $"{version.Major}.{version.Minor}.{version.Patch}"; // Add the build metadata if we have pre-release information if (version.PrereleaseIdentifiers.Count > 0) { versionString += $"-{version.Prerelease}"; if (!string.IsNullOrWhiteSpace(version.Metadata)) { // First 7 characters of the commit hash versionString += $"+{version.Metadata[..7]}"; } } return versionString; } } ================================================ FILE: StabilityMatrix.Core/Extensions/ServiceProviderExtensions.cs ================================================ using System.Collections.Immutable; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace StabilityMatrix.Core.Extensions; public static class ServiceProviderExtensions { /// /// Gets all managed services from the . /// Accesses the private field `Root[ServiceProviderEngineScope]._disposables[List<object>?]`. /// /// /// public static List GetDisposables(this ServiceProvider serviceProvider) { // ServiceProvider: internal ServiceProviderEngineScope Root { get; } var root = serviceProvider.GetProtectedProperty("Root") ?? throw new InvalidOperationException("Could not get ServiceProviderEngineScope Root."); // ServiceProviderEngineScope: private List? _disposables var disposables = root.GetPrivateField?>("_disposables"); return disposables ?? []; } private static void SafeDispose( this ServiceProvider serviceProvider, TimeSpan timeoutTotal, TimeSpan timeoutPerDispose, ILogger logger ) { var timeoutTotalCts = new CancellationTokenSource(timeoutTotal); // Dispose services var toDispose = serviceProvider.GetDisposables().OfType().ToImmutableList(); logger.LogDebug("OnExit: Preparing to Dispose {Count} Services", toDispose.Count); // Dispose IDisposable services foreach (var disposable in toDispose) { logger.LogTrace("OnExit: Disposing {Name}", disposable.GetType().Name); using var instanceCts = CancellationTokenSource.CreateLinkedTokenSource( timeoutTotalCts.Token, new CancellationTokenSource(timeoutPerDispose).Token ); try { Task.Run(() => disposable.Dispose(), instanceCts.Token).Wait(instanceCts.Token); } catch (OperationCanceledException) { logger.LogWarning("OnExit: Timeout disposing {Name}", disposable.GetType().Name); } catch (Exception e) { logger.LogError(e, "OnExit: Failed to dispose {Name}", disposable.GetType().Name); } } } private static async ValueTask SafeDisposeAsync( this ServiceProvider serviceProvider, TimeSpan timeoutTotal, TimeSpan timeoutPerDispose, ILogger logger ) { var timeoutTotalCts = new CancellationTokenSource(timeoutTotal); // Dispose services var toDispose = serviceProvider.GetDisposables().OfType().ToImmutableList(); // Dispose IDisposable services foreach (var disposable in toDispose) { logger.LogTrace("Disposing {Name}", disposable.GetType().Name); using var instanceCts = CancellationTokenSource.CreateLinkedTokenSource( timeoutTotalCts.Token, new CancellationTokenSource(timeoutPerDispose).Token ); try { if (disposable is IAsyncDisposable asyncDisposable) { await asyncDisposable .DisposeAsync() .AsTask() .WaitAsync(instanceCts.Token) .ConfigureAwait(false); } else { await Task.Run(() => disposable.Dispose(), instanceCts.Token) .WaitAsync(instanceCts.Token) .ConfigureAwait(false); } } catch (OperationCanceledException) { logger.LogWarning("Timeout disposing {Name}", disposable.GetType().Name); } catch (Exception e) { logger.LogError(e, "Failed to dispose {Name}", disposable.GetType().Name); } } } } ================================================ FILE: StabilityMatrix.Core/Extensions/SizeExtensions.cs ================================================ using System.Drawing; namespace StabilityMatrix.Core.Extensions; public static class SizeExtensions { public static Size WithScale(this Size size, double scale) { return new Size((int)Math.Floor(size.Width * scale), (int)Math.Floor(size.Height * scale)); } } ================================================ FILE: StabilityMatrix.Core/Extensions/StringExtensions.cs ================================================ using System.Diagnostics.Contracts; using System.Text; using System.Text.RegularExpressions; namespace StabilityMatrix.Core.Extensions; public static class StringExtensions { private static string EncodeNonAsciiCharacters(string value) { var sb = new StringBuilder(); foreach (var c in value) { // If not ascii or not printable if (c > 127 || c < 32) { // This character is too big for ASCII var encodedValue = "\\u" + ((int)c).ToString("x4"); sb.Append(encodedValue); } else { sb.Append(c); } } return sb.ToString(); } /// /// Converts string to repr /// public static string ToRepr(this string? str) { if (str is null) { return ""; } using var writer = new StringWriter(); writer.Write("'"); foreach (var ch in str) { writer.Write( ch switch { '\0' => "\\0", '\n' => "\\n", '\r' => "\\r", '\t' => "\\t", // Non ascii _ when ch > 127 || ch < 32 => $"\\u{(int)ch:x4}", _ => ch.ToString() } ); } writer.Write("'"); return writer.ToString(); } /// /// Counts continuous sequence of a character /// from the start of the string /// public static int CountStart(this string str, char c) { var count = 0; foreach (var ch in str) { if (ch == c) { count++; } else { break; } } return count; } /// /// Strips the substring from the start of the string /// [Pure] public static string StripStart(this string str, string subString) { var index = str.IndexOf(subString, StringComparison.Ordinal); return index < 0 ? str : str.Remove(index, subString.Length); } /// /// Strips the substring from the end of the string /// [Pure] public static string StripEnd(this string str, string subString) { var index = str.LastIndexOf(subString, StringComparison.Ordinal); return index < 0 ? str : str.Remove(index, subString.Length); } /// /// Splits lines by \n and \r\n /// [Pure] // ReSharper disable once ReturnTypeCanBeEnumerable.Global public static string[] SplitLines(this string str, StringSplitOptions options = StringSplitOptions.None) { return str.Split(new[] { "\r\n", "\n" }, options); } /// /// Normalizes directory separator characters in a given path /// [Pure] public static string NormalizePathSeparators(this string path) { return path.Replace('\\', '/'); } } ================================================ FILE: StabilityMatrix.Core/Extensions/TypeExtensions.cs ================================================ using System.Reflection; namespace StabilityMatrix.Core.Extensions; public static class TypeExtensions { /// /// Get all properties marked with an attribute of type /// public static IEnumerable GetPropertiesWithAttribute(this Type type) where TAttribute : Attribute { return type.GetProperties().Where(p => Attribute.IsDefined(p, typeof(TAttribute))); } } ================================================ FILE: StabilityMatrix.Core/Extensions/UriExtensions.cs ================================================ using System.ComponentModel; using System.Web; namespace StabilityMatrix.Core.Extensions; [Localizable(false)] public static class UriExtensions { public static Uri WithQuery(this Uri uri, string key, string value) { var builder = new UriBuilder(uri); var query = HttpUtility.ParseQueryString(builder.Query); query[key] = value; builder.Query = query.ToString() ?? string.Empty; return builder.Uri; } public static Uri Append(this Uri uri, params string[] paths) { return new Uri( paths.Aggregate( uri.AbsoluteUri, (current, path) => $"{current.TrimEnd('/')}/{path.TrimStart('/')}" ) ); } /// /// Returns a new Uri with the query values redacted. /// Non-empty query values are replaced with a single asterisk. /// public static Uri RedactQueryValues(this Uri uri) { var builder = new UriBuilder(uri); var queryCollection = HttpUtility.ParseQueryString(builder.Query); foreach (var key in queryCollection.AllKeys) { if (!string.IsNullOrEmpty(queryCollection[key])) { queryCollection[key] = "*"; } } builder.Query = queryCollection.ToString() ?? string.Empty; return builder.Uri; } } ================================================ FILE: StabilityMatrix.Core/Git/CachedCommandGitVersionProvider.cs ================================================ using Microsoft.Extensions.Caching.Memory; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Git; public class CachedCommandGitVersionProvider(string repositoryUri, IPrerequisiteHelper prerequisiteHelper) : IGitVersionProvider { private readonly CommandGitVersionProvider commandGitVersionProvider = new(repositoryUri, prerequisiteHelper); private readonly IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); public async Task> FetchTagsAsync( int limit = 0, CancellationToken cancellationToken = default ) { return ( await memoryCache .GetOrCreateAsync( "tags", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await commandGitVersionProvider .FetchTagsAsync(limit, cancellationToken) .ConfigureAwait(false); } ) .ConfigureAwait(false) )!; } public async Task> FetchBranchesAsync( int limit = 0, CancellationToken cancellationToken = default ) { return ( await memoryCache .GetOrCreateAsync( "branches", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await commandGitVersionProvider .FetchBranchesAsync(limit, cancellationToken) .ConfigureAwait(false); } ) .ConfigureAwait(false) )!; } public async Task> FetchCommitsAsync( string? branch, int limit = 0, CancellationToken cancellationToken = default ) { return ( await memoryCache .GetOrCreateAsync( $"commits-{branch}", async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return await commandGitVersionProvider .FetchCommitsAsync(branch, limit, cancellationToken) .ConfigureAwait(false); } ) .ConfigureAwait(false) )!; } } ================================================ FILE: StabilityMatrix.Core/Git/CommandGitVersionProvider.cs ================================================ using System.ComponentModel; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Git; /// /// Fetch git versions via commands to a git process runner /// [Localizable(false)] public class CommandGitVersionProvider(string repositoryUri, IPrerequisiteHelper prerequisiteHelper) : IGitVersionProvider { public async Task> FetchTagsAsync( int limit = 0, CancellationToken cancellationToken = default ) { var tags = new List(); ProcessArgs args = [ "-c", "versionsort.suffix=-", "ls-remote", "--tags", "--sort=-v:refname", repositoryUri, ]; var result = await prerequisiteHelper .GetGitOutput(args) .EnsureSuccessExitCode() .ConfigureAwait(false); if (result is { IsSuccessExitCode: true, StandardOutput: not null }) { var tagNames = result .StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Split('\t').LastOrDefault()?.Replace("refs/tags/", "").Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => { const string peel = "^{}"; return s!.EndsWith(peel, StringComparison.Ordinal) ? s[..^peel.Length] : s; }) .Distinct() .Take(limit > 0 ? limit : int.MaxValue); tags.AddRange(tagNames.Select(tag => new GitVersion { Tag = tag })); } return tags; } public async Task> FetchBranchesAsync( int limit = 0, CancellationToken cancellationToken = default ) { var branches = new List(); ProcessArgs args = ["ls-remote", "--heads", repositoryUri]; var result = await prerequisiteHelper .GetGitOutput(args) .EnsureSuccessExitCode() .ConfigureAwait(false); if (result is { IsSuccessExitCode: true, StandardOutput: not null }) { var branchLines = result.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries); var branchNames = branchLines .Select(line => line.Split('\t').LastOrDefault()?.Replace("refs/heads/", "").Trim()) .Where(line => !string.IsNullOrWhiteSpace(line)) .Take(limit > 0 ? limit : int.MaxValue); branches.AddRange(branchNames.Select(branch => new GitVersion { Branch = branch })); } return branches; } public async Task> FetchCommitsAsync( string? branch = null, int limit = 0, CancellationToken cancellationToken = default ) { // Cannot use ls-remote, so clone to temp directory and fetch var commits = new List(); using var tempDirectory = new TempDirectoryPath(); ProcessArgs args = ["clone", "--bare", "--filter=tree:0", "--single-branch"]; if (!string.IsNullOrEmpty(branch)) { args = args.Concat(["--branch", branch]); } args = args.Concat([repositoryUri, tempDirectory.FullPath]); _ = await prerequisiteHelper.GetGitOutput(args).EnsureSuccessExitCode().ConfigureAwait(false); _ = await prerequisiteHelper .GetGitOutput(["fetch", "--all"], tempDirectory.FullPath) .EnsureSuccessExitCode() .ConfigureAwait(false); // If not branch not specified, get it now if (string.IsNullOrEmpty(branch)) { var branchResult = await prerequisiteHelper .GetGitOutput(["rev-parse", "--abbrev-ref", "HEAD"], tempDirectory.FullPath) .EnsureSuccessExitCode() .ConfigureAwait(false); if (string.IsNullOrEmpty(branchResult.StandardOutput?.Trim())) { // Could not get branch return []; } branch = branchResult.StandardOutput.Trim(); } ProcessArgs logArgs = ["log", "--pretty=format:%H", "--no-decorate"]; if (limit > 0) { logArgs = logArgs.Concat([$"--max-count={limit}"]); } var logResult = await prerequisiteHelper .GetGitOutput(logArgs.Concat([branch]), tempDirectory.FullPath) .EnsureSuccessExitCode() .ConfigureAwait(false); if (logResult is { StandardOutput: not null }) { var commitLines = logResult.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries); // Only accept lines of 40 characters and valid hexadecimal hash var commitHashes = commitLines .Select(line => line.Trim()) .Where(line => !string.IsNullOrWhiteSpace(line)) .Where(line => line.Length == 40 && line.All(char.IsLetterOrDigit)); var commitObjs = commitHashes .Take(limit > 0 ? limit : int.MaxValue) .Select(commitHash => new GitVersion { Branch = branch, CommitSha = commitHash }); commits.AddRange(commitObjs); } return commits; } } ================================================ FILE: StabilityMatrix.Core/Git/IGitVersionProvider.cs ================================================ using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Git; public interface IGitVersionProvider { /// /// Fetches all tags from the remote repository. /// Task> FetchTagsAsync( int limit = 0, CancellationToken cancellationToken = default ); /// /// Fetches all branches from the remote repository. /// Task> FetchBranchesAsync( int limit = 0, CancellationToken cancellationToken = default ); /// /// Fetch the latest commits for a branch. /// If null, the default branch is used. /// Task> FetchCommitsAsync( string? branch = null, int limit = 0, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Helper/Analytics/AnalyticsHelper.cs ================================================ using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Models.Api.Lykos.Analytics; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Helper.Analytics; [RegisterSingleton] public class AnalyticsHelper( ILogger logger, ILykosAnalyticsApi analyticsApi, ISettingsManager settingsManager ) : IAnalyticsHelper { public AnalyticsSettings Settings => settingsManager.Settings.Analytics; public async Task TrackPackageInstallAsync(string packageName, string packageVersion, bool isSuccess) { if (!Settings.IsUsageDataEnabled) { return; } var data = new PackageInstallAnalyticsRequest { PackageName = packageName, PackageVersion = packageVersion, IsSuccess = isSuccess, Timestamp = DateTimeOffset.UtcNow }; try { await analyticsApi.PostInstallData(data).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Error sending install data"); } } public async Task TrackFirstTimeInstallAsync( string? selectedPackageName, IEnumerable? selectedRecommendedModels, bool firstTimeSetupSkipped ) { if (!Settings.IsUsageDataEnabled) { return; } var data = new FirstTimeInstallAnalytics { SelectedPackageName = selectedPackageName, SelectedRecommendedModels = selectedRecommendedModels, FirstTimeSetupSkipped = firstTimeSetupSkipped, Timestamp = DateTimeOffset.UtcNow }; try { await analyticsApi.PostInstallData(data).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Error sending first time install data"); } } public async Task TrackAsync(AnalyticsRequest data) { if (!Settings.IsUsageDataEnabled) { return; } try { await analyticsApi.PostInstallData(data).ConfigureAwait(false); } catch (Exception ex) { logger.LogError(ex, "Error sending analytics data"); } } } ================================================ FILE: StabilityMatrix.Core/Helper/Analytics/IAnalyticsHelper.cs ================================================ using StabilityMatrix.Core.Models.Api.Lykos.Analytics; namespace StabilityMatrix.Core.Helper.Analytics; public interface IAnalyticsHelper { Task TrackPackageInstallAsync(string packageName, string packageVersion, bool isSuccess); Task TrackFirstTimeInstallAsync( string? selectedPackageName, IEnumerable? selectedRecommendedModels, bool firstTimeSetupSkipped ); Task TrackAsync(AnalyticsRequest data); } ================================================ FILE: StabilityMatrix.Core/Helper/ArchiveHelper.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using NLog; using SharpCompress.Common; using SharpCompress.Readers; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using Timer = System.Timers.Timer; namespace StabilityMatrix.Core.Helper; public record struct ArchiveInfo(ulong Size, ulong CompressedSize); [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static partial class ArchiveHelper { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Platform-specific 7z executable name. /// public static string SevenZipFileName { get { if (Compat.IsWindows) { return "7za.exe"; } if (Compat.IsLinux) { return "7zzs"; } if (Compat.IsMacOS) { return "7zz"; } throw new PlatformNotSupportedException("7z is not supported on this platform."); } } // HomeDir is set by ISettingsManager.TryFindLibrary() public static string HomeDir { get; set; } = string.Empty; public static string SevenZipPath => Path.Combine(HomeDir, "Assets", SevenZipFileName); [GeneratedRegex(@"(?<=Size:\s*)\d+|(?<=Compressed:\s*)\d+")] private static partial Regex Regex7ZOutput(); [GeneratedRegex(@"(?<=\s*)\d+(?=%)")] private static partial Regex Regex7ZProgressDigits(); [GeneratedRegex(@"(\d+)%.*- (.*)")] private static partial Regex Regex7ZProgressFull(); public static async Task TestArchive(string archivePath) { var process = ProcessRunner.StartAnsiProcess(SevenZipPath, new[] { "t", archivePath }); await process.WaitForExitAsync().ConfigureAwait(false); var output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false); var matches = Regex7ZOutput().Matches(output); var size = ulong.Parse(matches[0].Value); var compressed = ulong.Parse(matches[1].Value); return new ArchiveInfo(size, compressed); } public static async Task AddToArchive7Z(string archivePath, string sourceDirectory) { // Start 7z in the parent directory of the source directory var sourceParent = Directory.GetParent(sourceDirectory)?.FullName ?? ""; // We must pass in as `directory\` for archive path to be correct var sourceDirName = new DirectoryInfo(sourceDirectory).Name; var result = await ProcessRunner .GetProcessResultAsync( SevenZipPath, new[] { "a", archivePath, sourceDirName + @"\", "-y" }, workingDirectory: sourceParent ) .ConfigureAwait(false); result.EnsureSuccessExitCode(); } public static async Task Extract7Z(string archivePath, string extractDirectory) { var args = $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y"; var result = await ProcessRunner .GetProcessResultAsync(SevenZipPath, args) .EnsureSuccessExitCode() .ConfigureAwait(false); var output = result.StandardOutput ?? ""; try { var matches = Regex7ZOutput().Matches(output); var size = ulong.Parse(matches[0].Value); var compressed = ulong.Parse(matches[1].Value); return new ArchiveInfo(size, compressed); } catch (Exception e) { throw new Exception($"Could not parse 7z output [{e.Message}]: {output.ToRepr()}"); } } public static async Task Extract7Z( string archivePath, string extractDirectory, IProgress? progress ) { var outputStore = new StringBuilder(); var onOutput = new Action(s => { if (s == null) return; // Parse progress Logger.Trace($"7z: {s}"); outputStore.AppendLine(s); var match = Regex7ZProgressFull().Match(s); if (match.Success) { var percent = int.Parse(match.Groups[1].Value); var currentFile = match.Groups[2].Value; progress?.Report( new ProgressReport( percent / (float)100, "Extracting", currentFile, type: ProgressType.Extract ) ); } }); progress?.Report(new ProgressReport(-1, isIndeterminate: true, type: ProgressType.Extract)); // Need -bsp1 for progress reports var args = $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y -bsp1"; Logger.Debug($"Starting process '{SevenZipPath}' with arguments '{args}'"); using var process = ProcessRunner.StartProcess(SevenZipPath, args, outputDataReceived: onOutput); await process.WaitForExitAsync().ConfigureAwait(false); ProcessException.ThrowIfNonZeroExitCode(process, outputStore); progress?.Report(new ProgressReport(1f, "Finished extracting", type: ProgressType.Extract)); var output = outputStore.ToString(); try { var matches = Regex7ZOutput().Matches(output); var size = ulong.Parse(matches[0].Value); var compressed = ulong.Parse(matches[1].Value); return new ArchiveInfo(size, compressed); } catch (Exception e) { throw new Exception($"Could not parse 7z output [{e.Message}]: {output.ToRepr()}"); } } /// /// Extracts a zipped tar (i.e. '.tar.gz') archive. /// First extracts the zipped tar, then extracts the tar and removes the tar. /// /// /// /// public static async Task Extract7ZTar(string archivePath, string extractDirectory) { if (!archivePath.EndsWith(".tar.gz")) { throw new ArgumentException("Archive must be a zipped tar."); } // Extract the tar.gz to tar await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false); // Extract the tar var tarPath = Path.Combine(extractDirectory, Path.GetFileNameWithoutExtension(archivePath)); if (!File.Exists(tarPath)) { throw new FileNotFoundException("Tar file not found.", tarPath); } try { return await Extract7Z(tarPath, extractDirectory).ConfigureAwait(false); } finally { // Remove the tar if (File.Exists(tarPath)) { File.Delete(tarPath); } } } /// /// Extracts with auto handling of tar.gz files. /// public static async Task Extract7ZAuto(string archivePath, string extractDirectory) { if (archivePath.EndsWith(".tar.gz")) { return await Extract7ZTar(archivePath, extractDirectory).ConfigureAwait(false); } else { return await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false); } } /// /// Extract an archive to the output directory. /// /// /// /// Output directory, created if does not exist. public static async Task Extract( string archivePath, string outputDirectory, IProgress? progress = default ) { Directory.CreateDirectory(outputDirectory); progress?.Report(new ProgressReport(-1, isIndeterminate: true)); var count = 0ul; // Get true size var (total, _) = await TestArchive(archivePath).ConfigureAwait(false); // If not available, use the size of the archive file if (total == 0) { total = (ulong)new FileInfo(archivePath).Length; } // Create an DispatchTimer that monitors the progress of the extraction var progressMonitor = progress switch { null => null, _ => new Timer(TimeSpan.FromMilliseconds(36)) }; if (progressMonitor != null) { progressMonitor.Elapsed += (_, _) => { if (count == 0) return; progress!.Report(new ProgressReport(count, total, message: "Extracting")); }; } await Task.Factory.StartNew( () => { var extractOptions = new ExtractionOptions { Overwrite = true, ExtractFullPath = true, }; using var stream = File.OpenRead(archivePath); using var archive = ReaderFactory.Open(stream); // Start the progress reporting timer progressMonitor?.Start(); while (archive.MoveToNextEntry()) { var entry = archive.Entry; if (!entry.IsDirectory) { count += (ulong)entry.CompressedSize; } archive.WriteEntryToDirectory(outputDirectory, extractOptions); } }, TaskCreationOptions.LongRunning ) .ConfigureAwait(false); progress?.Report(new ProgressReport(progress: 1, message: "Done extracting")); progressMonitor?.Stop(); Logger.Info("Finished extracting archive {}", archivePath); } /// /// Extract an archive to the output directory, using SharpCompress managed code. /// does not require 7z to be installed, but no progress reporting. /// public static async Task ExtractManaged(string archivePath, string outputDirectory) { await using var stream = File.OpenRead(archivePath); await ExtractManaged(stream, outputDirectory).ConfigureAwait(false); } /// /// Extract an archive to the output directory, using SharpCompress managed code. /// does not require 7z to be installed, but no progress reporting. /// public static async Task ExtractManaged(Stream stream, string outputDirectory) { var fullOutputDir = Path.GetFullPath(outputDirectory); using var reader = ReaderFactory.Open(stream); while (reader.MoveToNextEntry()) { var entry = reader.Entry; var outputPath = Path.Combine(outputDirectory, entry.Key); if (entry.IsDirectory) { if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } } else { var folder = Path.GetDirectoryName(entry.Key)!; var destDir = Path.GetFullPath(Path.Combine(fullOutputDir, folder)); if (!Directory.Exists(destDir)) { if (!destDir.StartsWith(fullOutputDir, StringComparison.Ordinal)) { throw new ExtractionException( "Entry is trying to create a directory outside of the destination directory." ); } Directory.CreateDirectory(destDir); } // Check if symbolic link if (entry.LinkTarget != null) { // Not sure why but symlink entries have a key that ends with a space // and some broken path suffix, so we'll remove everything after the last space Logger.Debug( $"Checking if output path {outputPath} contains space char: {outputPath.Contains(' ')}" ); if (outputPath.Contains(' ')) { outputPath = outputPath[..outputPath.LastIndexOf(' ')]; } Logger.Debug( $"Extracting symbolic link [{entry.Key.ToRepr()}] " + $"({outputPath.ToRepr()} to {entry.LinkTarget.ToRepr()})" ); // Try to write link, if fail, continue copy file try { // Delete path if exists File.Delete(outputPath); File.CreateSymbolicLink(outputPath, entry.LinkTarget); continue; } catch (IOException e) { Logger.Warn($"Could not extract symbolic link, copying file instead: {e.Message}"); } } // Write file await using var entryStream = reader.OpenEntryStream(); await using var fileStream = File.Create(outputPath); await entryStream.CopyToAsync(fileStream).ConfigureAwait(false); } } } [SupportedOSPlatform("macos")] public static async Task ExtractDmg(string archivePath, DirectoryPath extractDir) { using var mountPoint = new TempDirectoryPath(); // Mount the dmg await ProcessRunner .GetProcessResultAsync("hdiutil", ["attach", archivePath, "-mountpoint", mountPoint.FullPath]) .EnsureSuccessExitCode() .ConfigureAwait(false); try { // Copy apps foreach (var sourceDir in mountPoint.EnumerateDirectories("*.app")) { var destDir = extractDir.JoinDir(sourceDir.RelativeTo(mountPoint)); await ProcessRunner .GetProcessResultAsync("cp", ["-R", sourceDir.FullPath, destDir.FullPath]) .EnsureSuccessExitCode() .ConfigureAwait(false); } } finally { // Unmount the dmg await ProcessRunner .GetProcessResultAsync("hdiutil", ["detach", mountPoint.FullPath]) .ConfigureAwait(false); } } } ================================================ FILE: StabilityMatrix.Core/Helper/Cache/GithubApiCache.cs ================================================ using Injectio.Attributes; using Microsoft.Extensions.Logging; using Octokit; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Helper.Cache; [RegisterSingleton] public class GithubApiCache( ILiteDbContext dbContext, IGitHubClient githubApi, ILogger logger ) : IGithubApiCache { private readonly TimeSpan cacheDuration = TimeSpan.FromMinutes(15); public async Task> GetAllReleases(string username, string repository) { var cacheKey = $"Releases-{username}-{repository}"; var cacheEntry = await dbContext.GetGithubCacheEntry(cacheKey).ConfigureAwait(false); if (cacheEntry != null && !IsCacheExpired(cacheEntry.LastUpdated)) { return cacheEntry.AllReleases.OrderByDescending(x => x.CreatedAt); } try { var allReleases = await githubApi .Repository.Release.GetAll(username, repository) .ConfigureAwait(false); if (allReleases == null) { return new List(); } var newCacheEntry = new GithubCacheEntry { CacheKey = cacheKey, AllReleases = allReleases.OrderByDescending(x => x.CreatedAt) }; await dbContext.UpsertGithubCacheEntry(newCacheEntry).ConfigureAwait(false); return newCacheEntry.AllReleases; } catch (Exception ex) { logger.LogWarning(ex, "Failed to get releases from Github API."); return cacheEntry?.AllReleases.OrderByDescending(x => x.CreatedAt) ?? Enumerable.Empty(); } } public async Task> GetAllBranches(string username, string repository) { var cacheKey = $"Branches-{username}-{repository}"; var cacheEntry = await dbContext.GetGithubCacheEntry(cacheKey).ConfigureAwait(false); if (cacheEntry != null && !IsCacheExpired(cacheEntry.LastUpdated)) { return cacheEntry.Branches; } try { var branches = await githubApi .Repository.Branch.GetAll(username, repository) .ConfigureAwait(false); if (branches == null) { return new List(); } var newCacheEntry = new GithubCacheEntry { CacheKey = cacheKey, Branches = branches }; await dbContext.UpsertGithubCacheEntry(newCacheEntry).ConfigureAwait(false); return newCacheEntry.Branches; } catch (Exception ex) { logger.LogWarning(ex, "Failed to get branches from Github API."); return cacheEntry?.Branches ?? []; } } public async Task?> GetAllCommits( string username, string repository, string branch, int page = 1, int perPage = 10 ) { var cacheKey = $"Commits-{username}-{repository}-{branch}-{page}-{perPage}"; var cacheEntry = await dbContext.GetGithubCacheEntry(cacheKey).ConfigureAwait(false); if (cacheEntry != null && !IsCacheExpired(cacheEntry.LastUpdated)) { return cacheEntry.Commits; } try { var commits = await githubApi .Repository.Commit.GetAll( username, repository, new CommitRequest { Sha = branch }, new ApiOptions { PageCount = page, PageSize = perPage, StartPage = page } ) .ConfigureAwait(false); if (commits == null) { return new List(); } var newCacheEntry = new GithubCacheEntry { CacheKey = cacheKey, Commits = commits.Select(x => new GitCommit { Sha = x.Sha }) }; await dbContext.UpsertGithubCacheEntry(newCacheEntry).ConfigureAwait(false); return newCacheEntry.Commits; } catch (Exception ex) { logger.LogWarning(ex, "Failed to get commits from Github API."); return cacheEntry?.Commits ?? []; } } private bool IsCacheExpired(DateTimeOffset expiration) => expiration.Add(cacheDuration) < DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Helper/Cache/IGithubApiCache.cs ================================================ using Octokit; using Refit; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Helper.Cache; public interface IGithubApiCache { Task> GetAllReleases(string username, string repository); Task> GetAllBranches(string username, string repository); Task?> GetAllCommits( string username, string repository, string branch, int page = 1, [AliasAs("per_page")] int perPage = 10 ); } ================================================ FILE: StabilityMatrix.Core/Helper/Cache/IPyPiCache.cs ================================================ using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Helper.Cache; public interface IPyPiCache { Task> GetPackageVersions(string packageName); } ================================================ FILE: StabilityMatrix.Core/Helper/Cache/LRUCache.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace StabilityMatrix.Core.Helper.Cache; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class LRUCache where TK : notnull { private readonly int capacity; private readonly Dictionary>> cacheMap = new(); private readonly LinkedList> lruList = new(); public LRUCache(int capacity) { this.capacity = capacity; } [MethodImpl(MethodImplOptions.Synchronized)] public TV? Get(TK key) { if (cacheMap.TryGetValue(key, out var node)) { var value = node.Value.Value; lruList.Remove(node); lruList.AddLast(node); return value; } return default; } public bool Get(TK key, out TV? value) { value = Get(key); return value != null; } [MethodImpl(MethodImplOptions.Synchronized)] public void Add(TK key, TV val) { if (cacheMap.TryGetValue(key, out var existingNode)) { lruList.Remove(existingNode); } else if (cacheMap.Count >= capacity) { RemoveFirst(); } var cacheItem = new LRUCacheItem(key, val); var node = new LinkedListNode>(cacheItem); lruList.AddLast(node); cacheMap[key] = node; } [MethodImpl(MethodImplOptions.Synchronized)] public void Remove(TK key) { if (!cacheMap.TryGetValue(key, out var node)) return; lruList.Remove(node); cacheMap.Remove(key); } private void RemoveFirst() { // Remove from LRUPriority var node = lruList.First; lruList.RemoveFirst(); if (node == null) return; // Remove from cache cacheMap.Remove(node.Value.Key); } } // ReSharper disable once InconsistentNaming internal class LRUCacheItem { public LRUCacheItem(TK k, TV v) { Key = k; Value = v; } public TK Key; public TV Value; } ================================================ FILE: StabilityMatrix.Core/Helper/Cache/PyPiCache.cs ================================================ using Injectio.Attributes; using Microsoft.Extensions.Logging; using NLog; using Octokit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Pypi; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Helper.Cache; [RegisterSingleton] public class PyPiCache(ILiteDbContext dbContext, IPyPiApi pyPiApi, ILogger logger) : IPyPiCache { private readonly TimeSpan cacheDuration = TimeSpan.FromMinutes(15); public async Task> GetPackageVersions(string packageName) { var cacheKey = $"Pypi-{packageName}"; var cacheEntry = await dbContext.GetPyPiCacheEntry(cacheKey).ConfigureAwait(false); if (cacheEntry != null && !IsCacheExpired(cacheEntry.LastUpdated)) { return cacheEntry.Versions.OrderByDescending(x => x); } try { var packageInfo = await pyPiApi.GetPackageInfo(packageName).ConfigureAwait(false); if (packageInfo?.Releases == null) { return new List(); } var newCacheEntry = new PyPiCacheEntry { CacheKey = cacheKey, Versions = packageInfo.Releases.Select(x => new CustomVersion(x.Key)).ToList() }; await dbContext.UpsertPyPiCacheEntry(newCacheEntry).ConfigureAwait(false); return newCacheEntry.Versions.OrderByDescending(x => x); } catch (ApiException ex) { logger.LogWarning(ex, "Failed to get package info from PyPi API."); return cacheEntry?.Versions.OrderByDescending(x => x) ?? Enumerable.Empty(); } } private bool IsCacheExpired(DateTimeOffset expiration) => expiration.Add(cacheDuration) < DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Helper/CodeTimer.cs ================================================ using System.ComponentModel; using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Text; namespace StabilityMatrix.Core.Helper; [Localizable(false)] public class CodeTimer(string postFix = "", [CallerMemberName] string callerName = "") : IDisposable { private static readonly Stack RunningTimers = new(); private readonly string name = $"{callerName}" + (string.IsNullOrEmpty(postFix) ? "" : $" ({postFix})"); private readonly Stopwatch stopwatch = new(); private bool isDisposed; private CodeTimer? ParentTimer { get; set; } private List SubTimers { get; } = new(); public void Start() { ObjectDisposedException.ThrowIf(isDisposed, this); if (stopwatch.IsRunning) { return; } stopwatch.Start(); // Set parent as the top of the stack if (RunningTimers.TryPeek(out var timer)) { ParentTimer = timer; timer.SubTimers.Add(this); } // Add ourselves to the stack RunningTimers.Push(this); } /// /// Start a new timer and return it. /// /// /// /// public static CodeTimer StartNew(string postFix = "", [CallerMemberName] string callerName = "") { var timer = new CodeTimer(postFix, callerName); timer.Start(); return timer; } /// /// Starts a new timer and returns it if DEBUG is defined, otherwise returns an empty IDisposable /// /// /// /// public static IDisposable StartDebug(string postFix = "", [CallerMemberName] string callerName = "") { #if DEBUG return StartNew(postFix, callerName); #else return Disposable.Empty; #endif } /// /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. /// public static string FormatTime(TimeSpan duration) { if (duration.TotalSeconds < 1) { return $"{duration.TotalMilliseconds:0.00}ms"; } if (duration.TotalMinutes < 1) { return $"{duration.TotalSeconds:0.00}s"; } if (duration.TotalHours < 1) { return $"{duration.TotalMinutes:0.00}m"; } return $"{duration.TotalHours:0.00}h"; } private static void OutputDebug(string message) { Debug.Write(message); } /// /// Get results for this timer and all sub timers recursively /// private string GetResult() { var builder = new StringBuilder(); builder.AppendLine($"{name}:\ttook {FormatTime(stopwatch.Elapsed)}"); foreach (var timer in SubTimers) { // For each sub timer layer, add a `|-` prefix builder.AppendLine($"|- {timer.GetResult()}"); } return builder.ToString(); } public void Stop() { // Output if we're a root timer Stop(printOutput: ParentTimer is null); } public void Stop(bool printOutput) { if (isDisposed || !stopwatch.IsRunning) { return; } stopwatch.Stop(); // Remove ourselves from the stack if (RunningTimers.TryPop(out var timer)) { if (timer != this) { throw new InvalidOperationException("Timer stack is corrupted"); } } else { throw new InvalidOperationException("Timer stack is empty"); } // If we're a root timer, output all results if (printOutput) { #if DEBUG OutputDebug(GetResult()); #else Console.WriteLine(GetResult()); #endif SubTimers.Clear(); } } public void Dispose() { if (isDisposed) return; Stop(); isDisposed = true; GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Helper/Compat.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Semver; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Helper; /// /// Compatibility layer for checks and file paths on different platforms. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class Compat { private const string AppName = "StabilityMatrix"; public static SemVersion AppVersion { get; set; } // OS Platform public static PlatformKind Platform { get; } [SupportedOSPlatformGuard("windows")] public static bool IsWindows => Platform.HasFlag(PlatformKind.Windows); [SupportedOSPlatformGuard("linux")] public static bool IsLinux => Platform.HasFlag(PlatformKind.Linux); [SupportedOSPlatformGuard("macos")] public static bool IsMacOS => Platform.HasFlag(PlatformKind.MacOS); [UnsupportedOSPlatformGuard("windows")] public static bool IsUnix => Platform.HasFlag(PlatformKind.Unix); public static bool IsArm => Platform.HasFlag(PlatformKind.Arm); public static bool IsX64 => Platform.HasFlag(PlatformKind.X64); // Paths /// /// AppData directory path. On Windows this is %AppData%, on Linux and MacOS this is ~/.config /// public static DirectoryPath AppData { get; } /// /// AppData + AppName (e.g. %AppData%\StabilityMatrix) /// public static DirectoryPath AppDataHome { get; private set; } /// /// Set AppDataHome to a custom path. Used for testing. /// public static void SetAppDataHome(string path) { AppDataHome = path; } /// /// Current directory the app is in. /// public static DirectoryPath AppCurrentDir { get; } /// /// Current path to the app binary. /// public static FilePath AppCurrentPath => AppCurrentDir.JoinFile(GetExecutableName()); /// /// Path to the .app bundle on macOS. /// [SupportedOSPlatform("macos")] public static DirectoryPath? AppBundleCurrentPath { get; } /// /// Either the File or directory on macOS. /// public static FileSystemPath AppOrBundleCurrentPath => IsMacOS ? AppBundleCurrentPath! : AppCurrentPath; // File extensions /// /// Platform-specific executable extension. /// ".exe" on Windows, Empty string on Linux and MacOS. /// public static string ExeExtension { get; } /// /// Platform-specific dynamic library extension. /// ".dll" on Windows, ".dylib" on MacOS, ".so" on Linux. /// public static string DllExtension { get; } /// /// Delimiter for $PATH environment variable. /// public static char PathDelimiter => IsWindows ? ';' : ':'; static Compat() { var infoVersion = Assembly .GetCallingAssembly() .GetCustomAttribute() ?.InformationalVersion; AppVersion = SemVersion.Parse(infoVersion ?? "0.0.0", SemVersionStyles.Strict); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Platform = PlatformKind.Windows; AppCurrentDir = AppContext.BaseDirectory; ExeExtension = ".exe"; DllExtension = ".dll"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Platform = PlatformKind.MacOS | PlatformKind.Unix; // This is ./.app/Contents/MacOS var macDir = new DirectoryPath(AppContext.BaseDirectory); // We need to go up two directories to get the .app directory AppBundleCurrentPath = macDir.Parent?.Parent; // Then CurrentDir is the next parent AppCurrentDir = AppBundleCurrentPath!.Parent!; ExeExtension = ""; DllExtension = ".dylib"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Platform = PlatformKind.Linux | PlatformKind.Unix; // For AppImage builds, the path is in `$APPIMAGE` var appPath = Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory; AppCurrentDir = Path.GetDirectoryName(appPath) ?? throw new Exception("Could not find application directory"); ExeExtension = ""; DllExtension = ".so"; } else { throw new PlatformNotSupportedException(); } if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { Platform |= PlatformKind.Arm; } else { Platform |= PlatformKind.X64; } AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); AppDataHome = AppData + AppName; } /// /// Generic function to return different objects based on platform flags. /// Parameters are checked in sequence with Compat.Platform.HasFlag, /// the first match is returned. /// /// Thrown when no targets match public static T Switch(params (PlatformKind platform, T target)[] targets) { foreach (var (platform, target) in targets) { if (Platform.HasFlag(platform)) { return target; } } throw new PlatformNotSupportedException( $"Platform {Platform.ToString()} is not in supported targets: " + $"{string.Join(", ", targets.Select(t => t.platform.ToString()))}" ); } /// /// Get the current application executable name. /// public static string GetExecutableName() { if (IsLinux) { // Use name component of APPIMAGE var appImage = Environment.GetEnvironmentVariable("APPIMAGE"); if (string.IsNullOrEmpty(appImage)) { #if DEBUG return "DEBUG_NOT_RUNNING_IN_APPIMAGE"; #else throw new Exception("Could not find APPIMAGE environment variable"); #endif } return Path.GetFileName(appImage); } using var process = Process.GetCurrentProcess(); var fullPath = process.MainModule?.ModuleName; if (string.IsNullOrEmpty(fullPath)) { throw new Exception("Could not find executable name"); } return Path.GetFileName(fullPath); } /// /// Get the current application executable or bundle name. /// public static string GetAppName() { // For other platforms, this is the same as the executable name if (!IsMacOS) { return GetExecutableName(); } // On macOS, get name of current bundle return Path.GetFileName(AppBundleCurrentPath.Unwrap()); } public static string GetEnvPathWithExtensions(params string[] paths) { var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process); var newPath = string.Join(PathDelimiter, paths); if (string.IsNullOrEmpty(currentPath)) { return string.Join(PathDelimiter, paths); } else { return newPath + PathDelimiter + currentPath; } } } ================================================ FILE: StabilityMatrix.Core/Helper/EnumerationOptionConstants.cs ================================================ namespace StabilityMatrix.Core.Helper; public static class EnumerationOptionConstants { public static readonly EnumerationOptions TopLevelOnly = new() { RecurseSubdirectories = false, IgnoreInaccessible = true }; public static readonly EnumerationOptions AllDirectories = new() { RecurseSubdirectories = true, IgnoreInaccessible = true }; } ================================================ FILE: StabilityMatrix.Core/Helper/EnvPathBuilder.cs ================================================ namespace StabilityMatrix.Core.Helper; public class EnvPathBuilder(params string[] initialPaths) { private readonly List paths = [..initialPaths]; public EnvPathBuilder AddPath(string path) { paths.Add(path); return this; } public EnvPathBuilder RemovePath(string path) { paths.Remove(path); return this; } public override string ToString() { return string.Join(Compat.PathDelimiter, paths); } } ================================================ FILE: StabilityMatrix.Core/Helper/EventManager.cs ================================================ using System.Globalization; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Inference; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Core.Helper; public record struct RunningPackageStatusChangedEventArgs(PackagePair? CurrentPackagePair); public class EventManager { public static EventManager Instance { get; } = new(); private EventManager() { } public event EventHandler? GlobalProgressChanged; public event EventHandler? InstalledPackagesChanged; public event EventHandler? OneClickInstallFinished; public event EventHandler? TeachingTooltipNeeded; public event EventHandler? DevModeSettingChanged; public event EventHandler? UpdateAvailable; public event EventHandler? PackageLaunchRequested; public delegate Task PackageRelaunchRequestedEventHandler( object? sender, InstalledPackage package, RunPackageOptions runPackageOptions ); public event PackageRelaunchRequestedEventHandler? PackageRelaunchRequested; public event EventHandler? ScrollToBottomRequested; public event EventHandler? ProgressChanged; public event EventHandler? RunningPackageStatusChanged; public event EventHandler? PackageInstallProgressAdded; public delegate Task AddPackageInstallEventHandler( object? sender, IPackageModificationRunner runner, IReadOnlyList steps, Action onCompleted ); public event EventHandler? ToggleProgressFlyout; public event EventHandler? CultureChanged; public event EventHandler? ModelIndexChanged; public event EventHandler? ImageFileAdded; public delegate Task InferenceProjectRequestedEventHandler( object? sender, LocalImageFile imageFile, InferenceProjectType type ); public event InferenceProjectRequestedEventHandler? InferenceProjectRequested; public event EventHandler? InferenceQueueCustomPrompt; public event EventHandler? NavigateAndFindCivitModelRequested; public event EventHandler? NavigateAndFindCivitAuthorRequested; public event EventHandler? DownloadsTeachingTipRequested; public event EventHandler? RecommendedModelsDialogClosed; public event EventHandler? WorkflowInstalled; public event EventHandler? DeleteModelRequested; public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty); public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped); public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty); public void OnDevModeSettingChanged(bool value) => DevModeSettingChanged?.Invoke(this, value); public void OnUpdateAvailable(UpdateInfo args) => UpdateAvailable?.Invoke(this, args); public void OnPackageLaunchRequested(Guid packageId) => PackageLaunchRequested?.Invoke(this, packageId); public void OnScrollToBottomRequested() => ScrollToBottomRequested?.Invoke(this, EventArgs.Empty); public void OnProgressChanged(ProgressItem progress) => ProgressChanged?.Invoke(this, progress); public void OnRunningPackageStatusChanged(PackagePair? currentPackagePair) => RunningPackageStatusChanged?.Invoke( this, new RunningPackageStatusChangedEventArgs(currentPackagePair) ); public void OnPackageInstallProgressAdded(IPackageModificationRunner runner) => PackageInstallProgressAdded?.Invoke(this, runner); public void OnToggleProgressFlyout() => ToggleProgressFlyout?.Invoke(this, EventArgs.Empty); public void OnCultureChanged(CultureInfo culture) => CultureChanged?.Invoke(this, culture); public void OnModelIndexChanged() => ModelIndexChanged?.Invoke(this, EventArgs.Empty); public void OnImageFileAdded(FilePath filePath) => ImageFileAdded?.Invoke(this, filePath); public void OnInferenceQueueCustomPrompt(InferenceQueueCustomPromptEventArgs e) => InferenceQueueCustomPrompt?.Invoke(this, e); public void OnNavigateAndFindCivitModelRequested(int modelId) => NavigateAndFindCivitModelRequested?.Invoke(this, modelId); public void OnDownloadsTeachingTipRequested() => DownloadsTeachingTipRequested?.Invoke(this, EventArgs.Empty); public void OnRecommendedModelsDialogClosed() => RecommendedModelsDialogClosed?.Invoke(this, EventArgs.Empty); public void OnPackageRelaunchRequested(InstalledPackage package, RunPackageOptions runPackageOptions) => PackageRelaunchRequested?.Invoke(this, package, runPackageOptions); public void OnWorkflowInstalled() => WorkflowInstalled?.Invoke(this, EventArgs.Empty); public void OnDeleteModelRequested(object? sender, string relativePath) => DeleteModelRequested?.Invoke(sender, relativePath); public void OnInferenceProjectRequested(LocalImageFile imageFile, InferenceProjectType type) => InferenceProjectRequested?.Invoke(this, imageFile, type); public void OnNavigateAndFindCivitAuthorRequested(string? author) => NavigateAndFindCivitAuthorRequested?.Invoke(this, author); } ================================================ FILE: StabilityMatrix.Core/Helper/Factory/IPackageFactory.cs ================================================ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Helper.Factory; public interface IPackageFactory { IEnumerable GetAllAvailablePackages(); BasePackage? FindPackageByName(string? packageName); BasePackage? this[string packageName] { get; } PackagePair? GetPackagePair(InstalledPackage? installedPackage); IEnumerable GetPackagesByType(PackageType packageType); BasePackage GetNewBasePackage(InstalledPackage installedPackage); } ================================================ FILE: StabilityMatrix.Core/Helper/Factory/PackageFactory.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Helper.Factory; [RegisterSingleton] public class PackageFactory : IPackageFactory { private readonly IGithubApiCache githubApiCache; private readonly ISettingsManager settingsManager; private readonly IDownloadService downloadService; private readonly IPrerequisiteHelper prerequisiteHelper; private readonly IPyRunner pyRunner; private readonly IUvManager uvManager; private readonly IPyInstallationManager pyInstallationManager; private readonly IPipWheelService pipWheelService; /// /// Mapping of package.Name to package /// private readonly Dictionary basePackages; public PackageFactory( IEnumerable basePackages, IGithubApiCache githubApiCache, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPyRunner pyRunner, IPipWheelService pipWheelService ) { this.githubApiCache = githubApiCache; this.settingsManager = settingsManager; this.downloadService = downloadService; this.prerequisiteHelper = prerequisiteHelper; this.pyRunner = pyRunner; this.pyInstallationManager = pyInstallationManager; this.pipWheelService = pipWheelService; this.basePackages = basePackages.ToDictionary(x => x.Name); } public BasePackage GetNewBasePackage(InstalledPackage installedPackage) { return installedPackage.PackageName switch { "ComfyUI" => new ComfyUI( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "Fooocus" => new Fooocus( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "stable-diffusion-webui" => new A3WebUI( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "Fooocus-ControlNet-SDXL" => new FocusControlNet( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "Fooocus-MRE" => new FooocusMre( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "InvokeAI" => new InvokeAI( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "kohya_ss" => new KohyaSs( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyRunner, pyInstallationManager, pipWheelService ), "OneTrainer" => new OneTrainer( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "RuinedFooocus" => new RuinedFooocus( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "stable-diffusion-webui-forge" => new SDWebForge( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "stable-diffusion-webui-directml" => new StableDiffusionDirectMl( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "stable-diffusion-webui-ux" => new StableDiffusionUx( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "StableSwarmUI" => new StableSwarm( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "automatic" => new VladAutomatic( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "voltaML-fast-stable-diffusion" => new VoltaML( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "sdfx" => new Sdfx( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "mashb1t-fooocus" => new Mashb1tFooocus( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "reforge" => new Reforge( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "FluxGym" => new FluxGym( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "SimpleSDXL" => new SimpleSDXL( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "Cogstudio" => new Cogstudio( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "ComfyUI-Zluda" => new ComfyZluda( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "stable-diffusion-webui-amdgpu-forge" => new ForgeAmdGpu( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "forge-classic" => new ForgeClassic( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "forge-neo" => new ForgeNeo( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "framepack" => new FramePack( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "framepack-studio" => new FramePackStudio( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "ai-toolkit" => new AiToolkit( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), "Wan2GP" => new Wan2GP( githubApiCache, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ), _ => throw new ArgumentOutOfRangeException(nameof(installedPackage)), }; } public IEnumerable GetAllAvailablePackages() { return basePackages .Values.Where(p => !p.HasVulnerabilities) .OrderBy(p => p.InstallerSortOrder) .ThenBy(p => p.DisplayName); } public BasePackage? FindPackageByName(string? packageName) { return packageName == null ? null : basePackages.GetValueOrDefault(packageName); } public BasePackage? this[string packageName] => basePackages[packageName]; /// public PackagePair? GetPackagePair(InstalledPackage? installedPackage) { if (installedPackage?.PackageName is not { } packageName) return null; return !basePackages.TryGetValue(packageName, out var basePackage) ? null : new PackagePair(installedPackage, basePackage); } public IEnumerable GetPackagesByType(PackageType packageType) => basePackages.Values.Where(p => p.PackageType == packageType); } ================================================ FILE: StabilityMatrix.Core/Helper/FileHash.cs ================================================ using System.Buffers; using System.Diagnostics; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; using System.Security.Cryptography; using Blake3; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Helper; public static class FileHash { public static async Task GetHashAsync( HashAlgorithm hashAlgorithm, Stream stream, byte[] buffer, Action? progress = default ) { ulong totalBytesRead = 0; using (hashAlgorithm) { int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false)) != 0) { totalBytesRead += (ulong)bytesRead; hashAlgorithm.TransformBlock(buffer, 0, bytesRead, null, 0); progress?.Invoke(totalBytesRead); } hashAlgorithm.TransformFinalBlock(buffer, 0, 0); var hash = hashAlgorithm.Hash; if (hash == null || hash.Length == 0) { throw new InvalidOperationException("Hash algorithm did not produce a hash."); } return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); } } public static async Task GetSha256Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Could not find file: {filePath}"); } var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length); var shared = ArrayPool.Shared; var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes)); try { await using var stream = File.OpenRead(filePath); var hash = await GetHashAsync( SHA256.Create(), stream, buffer, totalBytesRead => { progress?.Report(new ProgressReport(totalBytesRead, totalBytes, type: ProgressType.Hashing)); } ) .ConfigureAwait(false); return hash; } finally { shared.Return(buffer); } } public static async Task GetBlake3Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Could not find file: {filePath}"); } var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length); var readBytes = 0ul; var shared = ArrayPool.Shared; var buffer = shared.Rent(GetBufferSize(totalBytes)); try { await using var stream = File.OpenRead(filePath); using var hasher = Hasher.New(); while (true) { var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); if (bytesRead == 0) { break; } readBytes += (ulong)bytesRead; hasher.Update(buffer.AsSpan(0, bytesRead)); progress?.Report(new ProgressReport(readBytes, totalBytes)); } return hasher.Finalize().ToString(); } finally { shared.Return(buffer); } } public static async Task GetBlake3Async(Stream stream, IProgress? progress = default) { var totalBytes = Convert.ToUInt64(stream.Length); var readBytes = 0ul; var shared = ArrayPool.Shared; var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes)); try { using var hasher = Hasher.New(); while (true) { var bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); if (bytesRead == 0) { break; } readBytes += (ulong)bytesRead; hasher.Update(buffer.AsSpan(0, bytesRead)); progress?.Report(new ProgressReport(readBytes, totalBytes)); } return hasher.Finalize(); } finally { shared.Return(buffer); } } /// /// Get the Blake3 hash of a span of data with multi-threading. /// public static Hash GetBlake3Parallel(ReadOnlySpan data) { using var hasher = Hasher.New(); hasher.UpdateWithJoin(data); return hasher.Finalize(); } /// /// Task.Run wrapped /// public static Task GetBlake3ParallelAsync(ReadOnlyMemory data) { return Task.Run(() => GetBlake3Parallel(data.Span)); } /// /// Get the Blake3 hash of a file as memory-mapped with multi-threading. /// public static Hash GetBlake3MemoryMappedParallel(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } var totalBytes = Convert.ToInt64(new FileInfo(filePath).Length); using var hasher = Hasher.New(); // Memory map using var fileStream = File.OpenRead(filePath); using var memoryMappedFile = MemoryMappedFile.CreateFromFile( fileStream, null, totalBytes, MemoryMappedFileAccess.Read, HandleInheritability.None, false ); using var accessor = memoryMappedFile.CreateViewAccessor(0, totalBytes, MemoryMappedFileAccess.Read); Debug.Assert(accessor.Capacity == fileStream.Length); var buffer = new byte[accessor.Capacity]; accessor.ReadArray(0, buffer, 0, buffer.Length); hasher.UpdateWithJoin(buffer); return hasher.Finalize(); } /// /// Task.Run wrapped /// public static Task GetBlake3MemoryMappedParallelAsync(string filePath) { return Task.Run(() => GetBlake3MemoryMappedParallel(filePath)); } /// /// Determines suitable buffer size for hashing based on stream length. /// private static int GetBufferSize(ulong totalBytes) => totalBytes switch { < Size.MiB => 8 * (int)Size.KiB, < 500 * Size.MiB => 16 * (int)Size.KiB, < Size.GiB => 32 * (int)Size.KiB, _ => 64 * (int)Size.KiB }; } ================================================ FILE: StabilityMatrix.Core/Helper/FileTransfers.cs ================================================ using System.Buffers; using System.Diagnostics.CodeAnalysis; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Helper; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class FileTransfers { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Determines suitable buffer size based on stream length. /// /// /// public static ulong GetBufferSize(ulong totalBytes) => totalBytes switch { < Size.MiB => 8 * Size.KiB, < 100 * Size.MiB => 16 * Size.KiB, < 500 * Size.MiB => Size.MiB, < Size.GiB => 16 * Size.MiB, _ => 32 * Size.MiB, }; /// /// Copy all files and subfolders using a dictionary of source and destination file paths. /// Non-existing directories within the paths will be created. /// /// Dictionary of source and destination file paths /// /// Optional (per file) progress /// /// Current - Bytes read for file. /// Total - Size of file in bytes. /// Title - /// /// /// /// Optional (total) progress. /// public static async Task CopyFiles( Dictionary files, IProgress? fileProgress = default, IProgress? totalProgress = default ) { var totalFiles = files.Count; var completedFiles = 0; var totalSize = Convert.ToUInt64(files.Keys.Select(x => new FileInfo(x).Length).Sum()); var totalRead = 0ul; foreach (var (sourcePath, destPath) in files) { var totalReadForFile = 0ul; await using var outStream = new FileStream( destPath, FileMode.Create, FileAccess.Write, FileShare.Read ); await using var inStream = new FileStream( sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read ); var fileSize = (ulong)inStream.Length; var fileName = Path.GetFileName(sourcePath); completedFiles++; var currentCompletedFiles = completedFiles; await CopyStream( inStream, outStream, fileReadBytes => { var lastRead = totalReadForFile; totalReadForFile = Convert.ToUInt64(fileReadBytes); totalRead += totalReadForFile - lastRead; fileProgress?.Report( new ProgressReport( totalReadForFile, fileSize, fileName, $"{currentCompletedFiles}/{totalFiles}" ) ); totalProgress?.Report( new ProgressReport( totalRead, totalSize, fileName, $"{currentCompletedFiles}/{totalFiles}" ) ); } ) .ConfigureAwait(false); } } private static async Task CopyStream(Stream from, Stream to, Action progress) { var shared = ArrayPool.Shared; var bufferSize = (int)GetBufferSize((ulong)from.Length); var buffer = shared.Rent(bufferSize); var totalRead = 0L; try { while (totalRead < from.Length) { var read = await from.ReadAsync(buffer.AsMemory(0, bufferSize)).ConfigureAwait(false); await to.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false); totalRead += read; progress(totalRead); } } finally { shared.Return(buffer); } } /// /// Move all files and sub-directories within the source directory to the destination directory. /// If the destination contains a file with the same name, we'll check if the hashes match. /// On matching hashes we skip the file, otherwise we throw an exception. /// /// /// If moving files results in name collision with different hashes. /// public static async Task MoveAllFilesAndDirectories( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, bool overwriteIfHashMatches = false, bool deleteSymlinks = false ) { // Create the destination directory if it doesn't exist if (!destinationDir.Exists) { destinationDir.Create(); } // First move files await MoveAllFiles(sourceDir, destinationDir, overwrite, overwriteIfHashMatches) .ConfigureAwait(false); // Then move directories foreach (var subDir in sourceDir.Info.EnumerateDirectories()) { if (deleteSymlinks && new DirectoryPath(subDir).IsSymbolicLink) { subDir.Delete(false); continue; } var destinationSubDir = destinationDir.JoinDir(subDir.Name); // Recursively move sub directories await MoveAllFilesAndDirectories(subDir, destinationSubDir, overwrite, overwriteIfHashMatches) .ConfigureAwait(false); if (!subDir.EnumerateFileSystemInfos().Any()) { subDir.Delete(); } if (destinationSubDir.Exists) continue; destinationSubDir.Create(); } } /// /// Move all files within the source directory to the destination directory. /// If the destination contains a file with the same name, we'll check if the hashes match. /// On matching hashes we skip the file, otherwise we throw an exception. /// /// /// If moving files results in name collision with different hashes. /// public static async Task MoveAllFiles( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, bool overwriteIfHashMatches = false ) { foreach (var file in sourceDir.Info.EnumerateFiles()) { var sourceFile = sourceDir.JoinFile(file.Name); var destinationFile = destinationDir.JoinFile(file.Name); await MoveFileAsync(sourceFile, destinationFile, overwrite, overwriteIfHashMatches) .ConfigureAwait(false); } } public static async Task MoveFileAsync( FilePath sourceFile, FilePath destinationFile, bool overwrite = false, bool overwriteIfHashMatches = false ) { if (destinationFile.Exists) { if (overwriteIfHashMatches) { // Check if files hashes are the same var sourceHash = await FileHash.GetBlake3Async(sourceFile).ConfigureAwait(false); var destinationHash = await FileHash.GetBlake3Async(destinationFile).ConfigureAwait(false); // For same hash, just delete original file if (sourceHash == destinationHash) { Logger.Info( $"Deleted source file {sourceFile.Name} as it already exists in {Path.GetDirectoryName(destinationFile)}." + $" Matching Blake3 hash: {sourceHash}" ); sourceFile.Delete(); return; } // append a number to the file name until it doesn't exist for (var i = 0; i < 100; i++) { var destDir = destinationFile.Directory; var baseName = destinationFile.NameWithoutExtension; var ext = destinationFile.Extension; var candidate = destDir?.JoinFile($"{baseName} ({i}){ext}"); if (candidate?.Exists is false) { destinationFile = candidate; break; } } } else if (!overwrite) { throw new FileTransferExistsException(sourceFile, destinationFile); } } // Move the file await sourceFile.MoveToAsync(destinationFile).ConfigureAwait(false); } } ================================================ FILE: StabilityMatrix.Core/Helper/GenerationParametersConverter.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Helper; public static class GenerationParametersConverter { private static readonly ImmutableDictionary ParamsToSamplerSchedulers = new Dictionary { ["DPM++ 2M Karras"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Karras), ["DPM++ SDE Karras"] = (ComfySampler.DpmppSde, ComfyScheduler.Karras), ["DPM++ 2M SDE Exponential"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Exponential), ["DPM++ 2M SDE Karras"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Karras), ["Euler a"] = (ComfySampler.EulerAncestral, ComfyScheduler.Normal), ["Euler"] = (ComfySampler.Euler, ComfyScheduler.Normal), ["Euler Simple"] = (ComfySampler.Euler, ComfyScheduler.Simple), ["LMS"] = (ComfySampler.LMS, ComfyScheduler.Normal), ["Heun"] = (ComfySampler.Heun, ComfyScheduler.Normal), ["Heun Beta"] = (ComfySampler.Heun, ComfyScheduler.Beta), ["DPM2"] = (ComfySampler.Dpm2, ComfyScheduler.Normal), ["DPM2 Karras"] = (ComfySampler.Dpm2, ComfyScheduler.Karras), ["DPM2 a"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Normal), ["DPM2 a Karras"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Karras), ["DPM++ 2S a"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Normal), ["DPM++ 2S a Karras"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Karras), ["DPM++ 2M"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Normal), ["DPM++ SDE"] = (ComfySampler.DpmppSde, ComfyScheduler.Normal), ["DPM++ 2M SDE"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Normal), ["DPM++ 3M SDE"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Normal), ["DPM++ 3M SDE Karras"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Karras), ["DPM++ 3M SDE Exponential"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Exponential), ["DPM fast"] = (ComfySampler.DpmFast, ComfyScheduler.Normal), ["DPM adaptive"] = (ComfySampler.DpmAdaptive, ComfyScheduler.Normal), ["LMS Karras"] = (ComfySampler.LMS, ComfyScheduler.Karras), ["DDIM"] = (ComfySampler.DDIM, ComfyScheduler.Normal), ["DDIM Beta"] = (ComfySampler.DDIM, ComfyScheduler.Beta), ["UniPC"] = (ComfySampler.UniPC, ComfyScheduler.Normal), }.ToImmutableDictionary(); private static readonly ImmutableDictionary SamplerSchedulersToParams = ParamsToSamplerSchedulers.ToImmutableDictionary(x => x.Value, x => x.Key); /// /// Converts a parameters-type string to a . /// public static bool TryGetSamplerScheduler(string parameters, out ComfySamplerScheduler samplerScheduler) { return ParamsToSamplerSchedulers.TryGetValue(parameters, out samplerScheduler); } /// /// Converts a to a parameters-type string. /// public static bool TryGetParameters( ComfySamplerScheduler samplerScheduler, [NotNullWhen(true)] out string? parameters ) { return SamplerSchedulersToParams.TryGetValue(samplerScheduler, out parameters); } } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs ================================================ namespace StabilityMatrix.Core.Helper.HardwareInfo; public readonly record struct CpuInfo { public string ProcessorCaption { get; init; } public string ProcessorName { get; init; } } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs ================================================ namespace StabilityMatrix.Core.Helper.HardwareInfo; public record GpuInfo { public int Index { get; set; } public string? Name { get; init; } = string.Empty; public ulong MemoryBytes { get; init; } public string? ComputeCapability { get; init; } /// /// Gets the compute capability as a comparable decimal value (e.g. "8.6" becomes 8.6m) /// Returns null if compute capability is not available /// public decimal? ComputeCapabilityValue => ComputeCapability != null && decimal.TryParse(ComputeCapability, out var value) ? value : null; public MemoryLevel? MemoryLevel => MemoryBytes switch { <= 0 => HardwareInfo.MemoryLevel.Unknown, < 4 * Size.GiB => HardwareInfo.MemoryLevel.Low, < 8 * Size.GiB => HardwareInfo.MemoryLevel.Medium, _ => HardwareInfo.MemoryLevel.High, }; public bool IsNvidia { get { var name = Name?.ToLowerInvariant(); if (string.IsNullOrEmpty(name)) return false; return name.Contains("nvidia") || name.Contains("tesla"); } } public bool IsBlackwellGpu() { if (ComputeCapability is null) return false; return ComputeCapabilityValue >= 12.0m; } public bool IsAmpereOrNewerGpu() { if (ComputeCapability is null) return false; return ComputeCapabilityValue >= 8.6m; } public bool IsLegacyNvidiaGpu() { if (ComputeCapability is null) return false; return ComputeCapabilityValue < 7.5m; } public bool IsWindowsRocmSupportedGpu() { var gfx = GetAmdGfxArch(); if (gfx is null) return false; return gfx.StartsWith("gfx110") || gfx.StartsWith("gfx120") || gfx.Equals("gfx1151"); } public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false; public bool IsIntel => Name?.Contains("arc", StringComparison.OrdinalIgnoreCase) ?? false; public string? GetAmdGfxArch() { if (!IsAmd || string.IsNullOrWhiteSpace(Name)) return null; // Normalize for safer substring checks (handles RX7800 vs RX 7800, etc.) var name = Name; var nameNoSpaces = name.Replace(" ", "", StringComparison.Ordinal); return name switch { // RDNA4 _ when Has("R9700") || Has("9070") => "gfx1201", _ when Has("9060") => "gfx1200", // RDNA3.5 APUs _ when Has("860M") => "gfx1152", _ when Has("890M") => "gfx1150", _ when Has("8040S") || Has("8050S") || Has("8060S") || Has("880M") || Has("Z2 Extreme") => "gfx1151", // RDNA3 APUs (Phoenix) _ when Has("740M") || Has("760M") || Has("780M") || Has("Z1") || Has("Z2") => "gfx1103", // RDNA3 dGPU Navi33 _ when Has("7400") || Has("7500") || Has("7600") || Has("7650") || Has("7700S") => "gfx1102", // RDNA3 dGPU Navi32 _ when Has("7700") || Has("RX 7800") || HasNoSpace("RX7800") => "gfx1101", // RDNA3 dGPU Navi31 (incl. Pro) _ when Has("W7800") || Has("7900") || Has("7950") || Has("7990") => "gfx1100", // RDNA2 APUs (Rembrandt) _ when Has("660M") || Has("680M") => "gfx1035", // RDNA2 Navi24 low-end (incl. some mobiles) _ when Has("6300") || Has("6400") || Has("6450") || Has("6500") || Has("6550") || Has("6500M") => "gfx1034", // RDNA2 Navi23 _ when Has("6600") || Has("6650") || Has("6700S") || Has("6800S") || Has("6600M") => "gfx1032", // RDNA2 Navi22 (note: desktop 6800 is NOT here; that’s Navi21/gfx1030) _ when Has("6700") || Has("6750") || Has("6800M") || Has("6850M") => "gfx1031", // RDNA2 Navi21 (big die) _ when Has("6800") || Has("6900") || Has("6950") => "gfx1030", _ => null, }; bool HasNoSpace(string s) => nameNoSpaces.Contains( s.Replace(" ", "", StringComparison.Ordinal), StringComparison.OrdinalIgnoreCase ); bool Has(string s) => name.Contains(s, StringComparison.OrdinalIgnoreCase); } public virtual bool Equals(GpuInfo? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Name == other.Name && MemoryBytes == other.MemoryBytes; } public override int GetHashCode() { return HashCode.Combine(Index, Name, MemoryBytes); } } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs ================================================ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text.RegularExpressions; using Hardware.Info; using Microsoft.Win32; using NLog; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Helper.HardwareInfo; public static partial class HardwareHelper { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static IReadOnlyList? cachedGpuInfos; private static readonly object cachedGpuInfosLock = new(); private static readonly Lazy HardwareInfoLazy = new(() => new Hardware.Info.HardwareInfo() ); public static IHardwareInfo HardwareInfo => HardwareInfoLazy.Value; private static string RunBashCommand(string command) { var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"") { UseShellExecute = false, RedirectStandardOutput = true, }; var process = Process.Start(processInfo); process.WaitForExit(); var output = process.StandardOutput.ReadToEnd(); return output; } [SupportedOSPlatform("windows")] private static IEnumerable IterGpuInfoWindows() { const string gpuRegistryKeyPath = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath); if (baseKey == null) yield break; var gpuIndex = 0; foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0"))) { using var subKey = baseKey.OpenSubKey(subKeyName); if (subKey != null) { yield return new GpuInfo { Index = gpuIndex++, Name = subKey.GetValue("DriverDesc")?.ToString(), MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")), }; } } } [SupportedOSPlatform("linux")] private static IEnumerable IterGpuInfoLinux() { var output = RunBashCommand("lspci | grep -E '(VGA|3D)'"); var gpuLines = output.Split("\n"); var gpuIndex = 0; foreach (var line in gpuLines) { if (string.IsNullOrWhiteSpace(line)) continue; var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}"); ulong memoryBytes = 0; string? name = null; // Parse output with regex var match = Regex.Match(gpuOutput, @"(VGA compatible controller|3D controller): ([^\n]*)"); if (match.Success) { name = match.Groups[2].Value.Trim(); } match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]"); if (match.Success) { memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024; } yield return new GpuInfo { Index = gpuIndex++, Name = name, MemoryBytes = memoryBytes, }; } } [SupportedOSPlatform("macos")] private static IEnumerable IterGpuInfoMacos() { HardwareInfo.RefreshVideoControllerList(); foreach (var (i, videoController) in HardwareInfo.VideoControllerList.Enumerate()) { var gpuMemoryBytes = 0ul; // For arm macs, use the shared system memory if (Compat.IsArm) { gpuMemoryBytes = GetMemoryInfoImplGeneric().TotalPhysicalBytes; } yield return new GpuInfo { Index = i, Name = videoController.Name, MemoryBytes = gpuMemoryBytes, }; } } /// /// Yields GpuInfo for each GPU in the system. /// /// If true, refreshes cached GPU info. public static IEnumerable IterGpuInfo(bool forceRefresh = false) { // Use cached if available if (!forceRefresh && cachedGpuInfos is not null) { return cachedGpuInfos; } using var _ = CodeTimer.StartDebug(); lock (cachedGpuInfosLock) { if (!forceRefresh && cachedGpuInfos is not null) { return cachedGpuInfos; } if (Compat.IsMacOS) { return cachedGpuInfos = IterGpuInfoMacos().ToList(); } if (Compat.IsLinux || Compat.IsWindows) { try { var smi = IterGpuInfoNvidiaSmi()?.ToList(); var fallback = Compat.IsLinux ? IterGpuInfoLinux().ToList() : IterGpuInfoWindows().ToList(); if (smi is null) { return cachedGpuInfos = fallback; } var newList = smi.Concat(fallback.Where(gpu => !gpu.IsNvidia)) .Select( (gpu, index) => new GpuInfo { Name = gpu.Name, Index = index, MemoryBytes = gpu.MemoryBytes, } ); return cachedGpuInfos = newList.ToList(); } catch (Exception e) { Logger.Error(e, "Failed to get GPU info using nvidia-smi, falling back to registry"); var fallback = Compat.IsLinux ? IterGpuInfoLinux().ToList() : IterGpuInfoWindows().ToList(); return cachedGpuInfos = fallback; } } Logger.Error("Unknown OS, returning empty GPU info list"); return cachedGpuInfos = []; } } public static IEnumerable? IterGpuInfoNvidiaSmi() { using var _ = CodeTimer.StartDebug(); var psi = new ProcessStartInfo { FileName = "nvidia-smi", UseShellExecute = false, Arguments = "--query-gpu name,memory.total,compute_cap --format=csv", RedirectStandardOutput = true, CreateNoWindow = true, }; var process = Process.Start(psi); process?.WaitForExit(); var stdout = process?.StandardOutput.ReadToEnd(); var split = stdout?.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); var results = split?[1..]; if (results is null) return null; var gpuInfos = new List(); for (var index = 0; index < results?.Length; index++) { var gpu = results[index]; var datas = gpu.Split(',', StringSplitOptions.RemoveEmptyEntries); if (datas is not { Length: 3 }) continue; var memory = Regex.Replace(datas[1], @"([A-Z])\w+", "").Trim(); gpuInfos.Add( new GpuInfo { Name = datas[0], Index = index, MemoryBytes = Convert.ToUInt64(memory) * Size.MiB, ComputeCapability = datas[2].Trim(), } ); } return gpuInfos; } /// /// Gets the NVIDIA driver version using nvidia-smi. /// Returns null if nvidia-smi is not available or fails. /// public static Version? GetNvidiaDriverVersion() { try { var psi = new ProcessStartInfo { FileName = "nvidia-smi", UseShellExecute = false, Arguments = "--query-gpu=driver_version --format=csv,noheader", RedirectStandardOutput = true, CreateNoWindow = true, }; var process = Process.Start(psi); process?.WaitForExit(); var stdout = process?.StandardOutput.ReadToEnd()?.Trim(); if (string.IsNullOrEmpty(stdout)) return null; // Driver version is typically in the format "xxx.xx" (e.g., "591.59") // We'll parse it as a Version object return Version.TryParse(stdout, out var version) ? version : null; } catch (Exception e) { Logger.Warn(e, "Failed to get NVIDIA driver version from nvidia-smi"); return null; } } /// /// Return true if the system has at least one Nvidia GPU. /// public static bool HasNvidiaGpu() { return IterGpuInfo().Any(gpu => gpu.IsNvidia); } public static bool HasBlackwellGpu() { return IterGpuInfo() .Any(gpu => gpu is { IsNvidia: true, Name: not null, ComputeCapabilityValue: >= 12.0m }); } public static bool HasLegacyNvidiaGpu() { return IterGpuInfo() .Any(gpu => gpu is { IsNvidia: true, Name: not null, ComputeCapabilityValue: < 7.5m }); } public static bool HasAmpereOrNewerGpu() { return IterGpuInfo() .Any(gpu => gpu is { IsNvidia: true, Name: not null, ComputeCapabilityValue: >= 8.6m }); } /// /// Return true if the system has at least one AMD GPU. /// public static bool HasAmdGpu() { return IterGpuInfo().Any(gpu => gpu.IsAmd); } public static bool HasWindowsRocmSupportedGpu() => IterGpuInfo().Any(gpu => gpu is { IsAmd: true, Name: not null } && gpu.IsWindowsRocmSupportedGpu()); public static GpuInfo? GetWindowsRocmSupportedGpu() { return IterGpuInfo().FirstOrDefault(gpu => gpu.IsWindowsRocmSupportedGpu()); } public static bool HasIntelGpu() => IterGpuInfo().Any(gpu => gpu.IsIntel); // Set ROCm for default if AMD and Linux public static bool PreferRocm() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsLinux; // Set DirectML for default if AMD and Windows public static bool PreferDirectMLOrZluda() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows && !HasWindowsRocmSupportedGpu(); private static readonly Lazy IsMemoryInfoAvailableLazy = new(() => TryGetMemoryInfo(out _)); public static bool IsMemoryInfoAvailable => IsMemoryInfoAvailableLazy.Value; public static bool IsLiveMemoryUsageInfoAvailable => Compat.IsWindows && IsMemoryInfoAvailable; public static bool TryGetMemoryInfo(out MemoryInfo memoryInfo) { try { memoryInfo = GetMemoryInfo(); return true; } catch (Exception ex) { Logger.Warn(ex, "Failed to get memory info"); memoryInfo = default; return false; } } /// /// Gets the total and available physical memory in bytes. /// public static MemoryInfo GetMemoryInfo() => Compat.IsWindows ? GetMemoryInfoImplWindows() : GetMemoryInfoImplGeneric(); [SupportedOSPlatform("windows")] private static MemoryInfo GetMemoryInfoImplWindows() { var memoryStatus = new Win32MemoryStatusEx(); if (!GlobalMemoryStatusEx(ref memoryStatus)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } if (!GetPhysicallyInstalledSystemMemory(out var installedMemoryKb)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } return new MemoryInfo { TotalInstalledBytes = (ulong)installedMemoryKb * 1024, TotalPhysicalBytes = memoryStatus.UllTotalPhys, AvailablePhysicalBytes = memoryStatus.UllAvailPhys, }; } private static MemoryInfo GetMemoryInfoImplGeneric() { HardwareInfo.RefreshMemoryStatus(); // On macos only TotalPhysical is reported if (Compat.IsMacOS) { return new MemoryInfo { TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical, TotalInstalledBytes = HardwareInfo.MemoryStatus.TotalPhysical, }; } return new MemoryInfo { TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical, TotalInstalledBytes = HardwareInfo.MemoryStatus.TotalPhysical, AvailablePhysicalBytes = HardwareInfo.MemoryStatus.AvailablePhysical, }; } /// /// Gets cpu info /// public static Task GetCpuInfoAsync() => Compat.IsWindows ? Task.FromResult(GetCpuInfoImplWindows()) : GetCpuInfoImplGenericAsync(); [SupportedOSPlatform("windows")] private static CpuInfo GetCpuInfoImplWindows() { var info = new CpuInfo(); using var processorKey = Registry.LocalMachine.OpenSubKey( @"Hardware\Description\System\CentralProcessor\0", RegistryKeyPermissionCheck.ReadSubTree ); if (processorKey?.GetValue("ProcessorNameString") is string processorName) { info = info with { ProcessorCaption = processorName.Trim() }; } return info; } private static Task GetCpuInfoImplGenericAsync() { return Task.Run(() => { HardwareInfo.RefreshCPUList(); if (HardwareInfo.CpuList.FirstOrDefault() is not { } cpu) { return default; } var processorCaption = cpu.Caption.Trim(); // Try name if caption is empty (like on macos) if (string.IsNullOrWhiteSpace(processorCaption)) { processorCaption = cpu.Name.Trim(); } return new CpuInfo { ProcessorCaption = processorCaption }; }); } [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes); [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool GlobalMemoryStatusEx(ref Win32MemoryStatusEx lpBuffer); } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs ================================================ namespace StabilityMatrix.Core.Helper.HardwareInfo; public readonly record struct MemoryInfo { public ulong TotalInstalledBytes { get; init; } public ulong TotalPhysicalBytes { get; init; } public ulong AvailablePhysicalBytes { get; init; } } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs ================================================ namespace StabilityMatrix.Core.Helper.HardwareInfo; public enum MemoryLevel { Unknown, Low, Medium, High } ================================================ FILE: StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs ================================================ using System.Runtime.InteropServices; namespace StabilityMatrix.Core.Helper.HardwareInfo; [StructLayout(LayoutKind.Sequential)] public struct Win32MemoryStatusEx { public uint DwLength = (uint)Marshal.SizeOf(typeof(Win32MemoryStatusEx)); public uint DwMemoryLoad = 0; public ulong UllTotalPhys = 0; public ulong UllAvailPhys = 0; public ulong UllTotalPageFile = 0; public ulong UllAvailPageFile = 0; public ulong UllTotalVirtual = 0; public ulong UllAvailVirtual = 0; public ulong UllAvailExtendedVirtual = 0; public Win32MemoryStatusEx() { } } ================================================ FILE: StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs ================================================ using System.Diagnostics; using System.Runtime.Versioning; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Helper; public interface IPrerequisiteHelper { string GitBinPath { get; } bool IsPythonInstalled { get; } bool IsVcBuildToolsInstalled { get; } bool IsHipSdkInstalled { get; } Task InstallAllIfNecessary(IProgress? progress = null); Task InstallUvIfNecessary(IProgress? progress = null); string UvExePath { get; } bool IsUvInstalled { get; } DirectoryPath DotnetDir { get; } Task UnpackResourcesIfNecessary(IProgress? progress = null); Task InstallGitIfNecessary(IProgress? progress = null); Task InstallPythonIfNecessary(IProgress? progress = null); [SupportedOSPlatform("Windows")] Task InstallVcRedistIfNecessary(IProgress? progress = null); /// /// Run embedded git with the given arguments. /// Task RunGit( ProcessArgs args, Action? onProcessOutput = null, string? workingDirectory = null ); Task GetGitOutput(ProcessArgs args, string? workingDirectory = null); async Task CheckIsGitRepository(string repositoryPath) { var result = await GetGitOutput(["rev-parse", "--is-inside-work-tree"], repositoryPath) .ConfigureAwait(false); return result.ExitCode == 0 && result.StandardOutput?.Trim().ToLowerInvariant() == "true"; } async Task GetGitRepositoryVersion(string repositoryPath) { var version = new GitVersion(); // Get tag if ( await GetGitOutput(["describe", "--tags", "--abbrev=0"], repositoryPath).ConfigureAwait(false) is { IsSuccessExitCode: true } tagResult ) { version = version with { Tag = tagResult.StandardOutput?.Trim() }; } // Get branch if ( await GetGitOutput(["rev-parse", "--abbrev-ref", "HEAD"], repositoryPath).ConfigureAwait(false) is { IsSuccessExitCode: true } branchResult ) { version = version with { Branch = branchResult.StandardOutput?.Trim() }; } // Get commit sha if ( await GetGitOutput(["rev-parse", "HEAD"], repositoryPath).ConfigureAwait(false) is { IsSuccessExitCode: true } shaResult ) { version = version with { CommitSha = shaResult.StandardOutput?.Trim() }; } return version; } async Task CloneGitRepository( string rootDir, string repositoryUrl, GitVersion? version = null, Action? onProcessOutput = null ) { // Decide shallow clone only when not pinning to arbitrary commit post-clone var isShallowOk = version is null || version.Tag is not null; var cloneArgs = new ProcessArgsBuilder("clone"); if (isShallowOk) { cloneArgs = cloneArgs.AddArgs("--depth", "1", "--single-branch"); } if (!string.IsNullOrWhiteSpace(version?.Tag)) { cloneArgs = cloneArgs.AddArgs("--branch", version.Tag!); } else if (!string.IsNullOrWhiteSpace(version?.Branch)) { cloneArgs = cloneArgs.AddArgs("--branch", version.Branch!); } cloneArgs = cloneArgs.AddArgs(repositoryUrl); await RunGit(cloneArgs.ToProcessArgs(), onProcessOutput, rootDir).ConfigureAwait(false); // If pinning to a specific commit, we need a destination directory to continue if (!string.IsNullOrWhiteSpace(version?.CommitSha)) { await RunGit(["fetch", "--depth", "1", "origin", version.CommitSha!], onProcessOutput, rootDir) .ConfigureAwait(false); await RunGit(["checkout", "--force", version.CommitSha!], onProcessOutput, rootDir) .ConfigureAwait(false); await RunGit( ["submodule", "update", "--init", "--recursive", "--depth", "1"], onProcessOutput, rootDir ) .ConfigureAwait(false); } } async Task UpdateGitRepository( string repositoryDir, string repositoryUrl, GitVersion version, Action? onProcessOutput = null, bool usePrune = false, bool allowRebaseFallback = true, bool allowResetHardFallback = false ) { if (!Directory.Exists(Path.Combine(repositoryDir, ".git"))) { await RunGit(["init"], onProcessOutput, repositoryDir).ConfigureAwait(false); await RunGit(["remote", "add", "origin", repositoryUrl], onProcessOutput, repositoryDir) .ConfigureAwait(false); } // Ensure origin url matches the expected one await RunGit(["remote", "set-url", "origin", repositoryUrl], onProcessOutput, repositoryDir) .ConfigureAwait(false); // Specify Tag if (version.Tag is not null) { await RunGit(["fetch", "--tags", "--force"], onProcessOutput, repositoryDir) .ConfigureAwait(false); await RunGit(["checkout", version.Tag, "--force"], onProcessOutput, repositoryDir) .ConfigureAwait(false); // Update submodules await RunGit(["submodule", "update", "--init", "--recursive"], onProcessOutput, repositoryDir) .ConfigureAwait(false); } // Specify Branch + CommitSha else if (version.Branch is not null && version.CommitSha is not null) { await RunGit(["fetch", "--force", "origin", version.CommitSha], onProcessOutput, repositoryDir) .ConfigureAwait(false); await RunGit(["checkout", "--force", version.CommitSha], onProcessOutput, repositoryDir) .ConfigureAwait(false); // Update submodules await RunGit( ["submodule", "update", "--init", "--recursive", "--depth", "1"], onProcessOutput, repositoryDir ) .ConfigureAwait(false); } // Specify Branch (Use latest commit) else if (version.Branch is not null) { // Fetch (optional prune) var fetchArgs = new ProcessArgsBuilder("fetch", "--force"); if (usePrune) fetchArgs = fetchArgs.AddArg("--prune"); fetchArgs = fetchArgs.AddArg("origin"); await RunGit(fetchArgs.ToProcessArgs(), onProcessOutput, repositoryDir).ConfigureAwait(false); // Checkout await RunGit(["checkout", "--force", version.Branch], onProcessOutput, repositoryDir) .ConfigureAwait(false); // Try ff-only first var ffOnlyResult = await GetGitOutput( ["pull", "--ff-only", "--autostash", "origin", version.Branch], repositoryDir ) .ConfigureAwait(false); if (ffOnlyResult.ExitCode != 0) { if (allowRebaseFallback) { var rebaseResult = await GetGitOutput( ["pull", "--rebase", "--autostash", "origin", version.Branch], repositoryDir ) .ConfigureAwait(false); rebaseResult.EnsureSuccessExitCode(); } else if (allowResetHardFallback) { await RunGit( ["fetch", "--force", "origin", version.Branch], onProcessOutput, repositoryDir ) .ConfigureAwait(false); await RunGit( ["reset", "--hard", $"origin/{version.Branch}"], onProcessOutput, repositoryDir ) .ConfigureAwait(false); } else { ffOnlyResult.EnsureSuccessExitCode(); } } // Update submodules await RunGit( ["submodule", "update", "--init", "--recursive", "--depth", "1"], onProcessOutput, repositoryDir ) .ConfigureAwait(false); } // Not specified else { throw new ArgumentException( "Version must have a tag, branch + commit sha, or branch only.", nameof(version) ); } } Task GetGitRepositoryRemoteOriginUrl(string repositoryPath) { return GetGitOutput(["config", "--get", "remote.origin.url"], repositoryPath); } Task InstallTkinterIfNecessary(IProgress? progress = null); Task RunNpm( ProcessArgs args, string? workingDirectory = null, Action? onProcessOutput = null, IReadOnlyDictionary? envVars = null ); AnsiProcess RunNpmDetached( ProcessArgs args, string? workingDirectory = null, Action? onProcessOutput = null, IReadOnlyDictionary? envVars = null ); Task InstallNodeIfNecessary(IProgress? progress = null); Task InstallPackageRequirements( BasePackage package, PyVersion? pyVersion = null, IProgress? progress = null ); Task InstallPackageRequirements( List prerequisites, PyVersion? pyVersion = null, IProgress? progress = null ); Task InstallDotnetIfNecessary(IProgress? progress = null); Task RunDotnet( ProcessArgs args, string? workingDirectory = null, Action? onProcessOutput = null, IReadOnlyDictionary? envVars = null, bool waitForExit = true ); Task FixGitLongPaths(); Task AddMissingLibsToVenv( DirectoryPath installedPackagePath, PyBaseInstall baseInstall, IProgress? progress = null ); Task InstallPythonIfNecessary(PyVersion version, IProgress? progress = null); Task InstallVirtualenvIfNecessary(PyVersion version, IProgress? progress = null); Task InstallTkinterIfNecessary(PyVersion version, IProgress? progress = null); string? GetGfxArchFromAmdGpuName(GpuInfo? gpu = null); } ================================================ FILE: StabilityMatrix.Core/Helper/ISharedFolders.cs ================================================ using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Helper; public interface ISharedFolders { void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory); void RemoveLinksForAllPackages(); } ================================================ FILE: StabilityMatrix.Core/Helper/ImageMetadata.cs ================================================ using System.Diagnostics; using System.Text; using System.Text.Json; using ExifLibrary; using KGySoft.CoreLibraries; using MetadataExtractor; using MetadataExtractor.Formats.Exif; using MetadataExtractor.Formats.Png; using MetadataExtractor.Formats.WebP; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using Directory = MetadataExtractor.Directory; namespace StabilityMatrix.Core.Helper; public class ImageMetadata { private IReadOnlyList? Directories { get; set; } private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; private static readonly byte[] Idat = "IDAT"u8.ToArray(); private static readonly byte[] Text = "tEXt"u8.ToArray(); private static readonly byte[] Riff = "RIFF"u8.ToArray(); private static readonly byte[] Webp = "WEBP"u8.ToArray(); public static ImageMetadata ParseFile(FilePath path) { return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) }; } public static ImageMetadata ParseFile(Stream stream) { return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(stream) }; } public System.Drawing.Size? GetImageSize() { if (Directories?.OfType().FirstOrDefault() is { } header) { header.TryGetInt32(PngDirectory.TagImageWidth, out var width); header.TryGetInt32(PngDirectory.TagImageHeight, out var height); return new System.Drawing.Size(width, height); } return null; } public static System.Drawing.Size GetImageSize(byte[] inputImage) { var imageWidthBytes = inputImage[0x10..0x14]; var imageHeightBytes = inputImage[0x14..0x18]; var imageWidth = BitConverter.ToInt32(imageWidthBytes.AsEnumerable().Reverse().ToArray()); var imageHeight = BitConverter.ToInt32(imageHeightBytes.AsEnumerable().Reverse().ToArray()); return new System.Drawing.Size(imageWidth, imageHeight); } public static System.Drawing.Size GetImageSize(BinaryReader reader) { var oldPosition = reader.BaseStream.Position; reader.BaseStream.Position = 0x10; var imageWidthBytes = reader.ReadBytes(4); var imageHeightBytes = reader.ReadBytes(4); var imageWidth = BitConverter.ToInt32(imageWidthBytes.AsEnumerable().Reverse().ToArray()); var imageHeight = BitConverter.ToInt32(imageHeightBytes.AsEnumerable().Reverse().ToArray()); reader.BaseStream.Position = oldPosition; return new System.Drawing.Size(imageWidth, imageHeight); } public static ( string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes, string? CivitParameters ) GetAllFileMetadata(FilePath filePath) { if (filePath.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { var paramsJson = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagImageDescription); var smProj = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagSoftware); return (null, paramsJson, smProj, null, null); } if ( filePath.Extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || filePath.Extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) ) { var file = ImageFile.FromFile(filePath.Info.FullName); var userComment = file.Properties.Get(ExifTag.UserComment); var bytes = userComment.Interoperability.Data.Skip(8).ToArray(); var userCommentString = Encoding.BigEndianUnicode.GetString(bytes); return (null, null, null, null, userCommentString); } using var stream = filePath.Info.OpenRead(); using var reader = new BinaryReader(stream); var parameters = ReadTextChunk(reader, "parameters"); var parametersJson = ReadTextChunk(reader, "parameters-json"); var smProject = ReadTextChunk(reader, "smproj"); var comfyNodes = ReadTextChunk(reader, "prompt"); var civitParameters = ReadTextChunk(reader, "user_comment"); return ( string.IsNullOrEmpty(parameters) ? null : parameters, string.IsNullOrEmpty(parametersJson) ? null : parametersJson, string.IsNullOrEmpty(smProject) ? null : smProject, string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes, string.IsNullOrEmpty(civitParameters) ? null : civitParameters ); } public IEnumerable? GetTextualData() { // Get the PNG-tEXt directory return Directories ?.Where(d => d.Name == "PNG-tEXt") .SelectMany(d => d.Tags) .Where(t => t.Name == "Textual Data"); } public GenerationParameters? GetGenerationParameters() { var textualData = GetTextualData()?.ToArray(); if (textualData is null) { return null; } // Use "parameters-json" tag if exists if ( textualData.FirstOrDefault(tag => tag.Description is { } desc && desc.StartsWith("parameters-json: ") ) is { Description: { } description } ) { description = description.StripStart("parameters-json: "); return JsonSerializer.Deserialize(description); } // Otherwise parse "parameters" tag if ( textualData.FirstOrDefault(tag => tag.Description is { } desc && desc.StartsWith("parameters: ") ) is { Description: { } parameters } ) { parameters = parameters.StripStart("parameters: "); if (GenerationParameters.TryParse(parameters, out var generationParameters)) { return generationParameters; } } return null; } public static string ReadTextChunk(BinaryReader byteStream, string key) { byteStream.BaseStream.Position = 0; // Read first 8 bytes and make sure they match the png header if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader)) { return string.Empty; } while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).AsEnumerable().Reverse().ToArray()); var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); if (chunkType == Encoding.UTF8.GetString(Idat)) { return string.Empty; } if (chunkType == Encoding.UTF8.GetString(Text)) { var textBytes = byteStream.ReadBytes(chunkSize); var text = Encoding.UTF8.GetString(textBytes); if (text.StartsWith($"{key}\0")) { return text[(key.Length + 1)..]; } } else { // skip chunk data byteStream.BaseStream.Position += chunkSize; } // skip crc byteStream.BaseStream.Position += 4; } return string.Empty; } public static MemoryStream? BuildImageWithoutMetadata(FilePath imagePath) { using var byteStream = new BinaryReader(File.OpenRead(imagePath)); byteStream.BaseStream.Position = 0; if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader)) { return null; } var memoryStream = new MemoryStream(); memoryStream.Write(PngHeader); // add the IHDR chunk var ihdrStuff = byteStream.ReadBytes(25); memoryStream.Write(ihdrStuff); // find IDATs while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkSizeBytes = byteStream.ReadBytes(4); var chunkSize = BitConverter.ToInt32(chunkSizeBytes.AsEnumerable().Reverse().ToArray()); var chunkTypeBytes = byteStream.ReadBytes(4); var chunkType = Encoding.UTF8.GetString(chunkTypeBytes); if (chunkType != Encoding.UTF8.GetString(Idat)) { // skip chunk data byteStream.BaseStream.Position += chunkSize; // skip crc byteStream.BaseStream.Position += 4; continue; } memoryStream.Write(chunkSizeBytes); memoryStream.Write(chunkTypeBytes); var idatBytes = byteStream.ReadBytes(chunkSize); memoryStream.Write(idatBytes); var crcBytes = byteStream.ReadBytes(4); memoryStream.Write(crcBytes); } // Add IEND chunk memoryStream.Write([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]); memoryStream.Position = 0; return memoryStream; } /// /// Reads an EXIF tag from a webp file and returns the value as string /// /// The webp file to read EXIF data from /// Use constants for the tag you'd like to search for /// public static string ReadTextChunkFromWebp(FilePath filePath, int exifTag) { var exifDirs = WebPMetadataReader.ReadMetadata(filePath).OfType().FirstOrDefault(); return exifDirs is null ? string.Empty : exifDirs.GetString(exifTag) ?? string.Empty; } public static IEnumerable AddMetadataToWebp( byte[] inputImage, Dictionary exifTagData ) { using var byteStream = new BinaryReader(new MemoryStream(inputImage)); byteStream.BaseStream.Position = 0; // Read first 8 bytes and make sure they match the RIFF header if (!byteStream.ReadBytes(4).SequenceEqual(Riff)) { return Array.Empty(); } // skip 4 bytes then read next 4 for webp header byteStream.BaseStream.Position += 4; if (!byteStream.ReadBytes(4).SequenceEqual(Webp)) { return Array.Empty(); } while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray()); if (chunkType != "EXIF") { // skip chunk data byteStream.BaseStream.Position += chunkSize; continue; } var exifStart = byteStream.BaseStream.Position - 8; var exifBytes = byteStream.ReadBytes(chunkSize); Debug.WriteLine($"Found exif chunk of size {chunkSize}"); using var stream = new MemoryStream(exifBytes[6..]); var img = new MyTiffFile(stream, Encoding.UTF8); foreach (var (key, value) in exifTagData) { img.Properties.Set(key, value); } using var newStream = new MemoryStream(); img.Save(newStream); newStream.Seek(0, SeekOrigin.Begin); var newExifBytes = exifBytes[..6].Concat(newStream.ToArray()); var newExifSize = newExifBytes.Count(); var newChunkSize = BitConverter.GetBytes(newExifSize); var newChunk = "EXIF"u8.ToArray().Concat(newChunkSize).Concat(newExifBytes).ToArray(); var inputEndIndex = (int)exifStart; var newImage = inputImage[..inputEndIndex].Concat(newChunk).ToArray(); // webp or tiff or something requires even number of bytes if (newImage.Length % 2 != 0) { newImage = newImage.Concat(new byte[] { 0x00 }).ToArray(); } // no clue why the minus 8 is needed but it is var newImageSize = BitConverter.GetBytes(newImage.Length - 8); newImage[4] = newImageSize[0]; newImage[5] = newImageSize[1]; newImage[6] = newImageSize[2]; newImage[7] = newImageSize[3]; return newImage; } return Array.Empty(); } private static byte[] GetExifChunks(FilePath imagePath) { using var byteStream = new BinaryReader(File.OpenRead(imagePath)); byteStream.BaseStream.Position = 0; // Read first 8 bytes and make sure they match the RIFF header if (!byteStream.ReadBytes(4).SequenceEqual(Riff)) { return Array.Empty(); } // skip 4 bytes then read next 4 for webp header byteStream.BaseStream.Position += 4; if (!byteStream.ReadBytes(4).SequenceEqual(Webp)) { return Array.Empty(); } while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray()); if (chunkType != "EXIF") { // skip chunk data byteStream.BaseStream.Position += chunkSize; continue; } var exifStart = byteStream.BaseStream.Position; var exifBytes = byteStream.ReadBytes(chunkSize); var exif = Encoding.UTF8.GetString(exifBytes); Debug.WriteLine($"Found exif chunk of size {chunkSize}"); return exifBytes; } return Array.Empty(); } } ================================================ FILE: StabilityMatrix.Core/Helper/LazyInstance.cs ================================================ using Microsoft.Extensions.DependencyInjection; namespace StabilityMatrix.Core.Helper; /// /// Lazy instance of a DI service. /// public class LazyInstance : Lazy where T : notnull { public LazyInstance(IServiceProvider serviceProvider) : base(serviceProvider.GetRequiredService) { } } public static class LazyInstanceServiceExtensions { /// /// Register to be used when resolving instances. /// public static IServiceCollection AddLazyInstance(this IServiceCollection services) { services.AddTransient(typeof(Lazy<>), typeof(LazyInstance<>)); return services; } } ================================================ FILE: StabilityMatrix.Core/Helper/MinimumDelay.cs ================================================ using System.Diagnostics; namespace StabilityMatrix.Core.Helper; /// /// Enforces a minimum delay if the function returns too quickly. /// Waits during async Dispose. /// public class MinimumDelay : IAsyncDisposable { private readonly Stopwatch stopwatch = new(); private readonly TimeSpan delay; /// /// Minimum random delay in milliseconds. /// public MinimumDelay(int randMin, int randMax) { stopwatch.Start(); Random rand = new(); delay = TimeSpan.FromMilliseconds(rand.Next(randMin, randMax)); } /// /// Minimum fixed delay in milliseconds. /// public MinimumDelay(int delayMilliseconds) { stopwatch.Start(); delay = TimeSpan.FromMilliseconds(delayMilliseconds); } public async ValueTask DisposeAsync() { stopwatch.Stop(); var elapsed = stopwatch.Elapsed; if (elapsed < delay) { await Task.Delay(delay - elapsed); } GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Helper/ModelCompatChecker.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Helper; public class ModelCompatChecker { private readonly Dictionary baseModelNamesToTypes = Enum.GetValues().ToDictionary(x => x.GetStringValue()); public bool? IsLoraCompatibleWithBaseModel(HybridModelFile? lora, HybridModelFile? baseModel) { // Require connected info for both if ( lora?.Local?.ConnectedModelInfo is not { } loraInfo || baseModel?.Local?.ConnectedModelInfo is not { } baseModelInfo ) return null; if ( loraInfo.BaseModel is null || !baseModelNamesToTypes.TryGetValue(loraInfo.BaseModel, out var loraBaseModelType) ) return null; if ( baseModelInfo.BaseModel is null || !baseModelNamesToTypes.TryGetValue(baseModelInfo.BaseModel, out var baseModelType) ) return null; // Normalize both var normalizedLoraBaseModelType = NormalizeBaseModelType(loraBaseModelType); var normalizedBaseModelType = NormalizeBaseModelType(baseModelType); // Ignore if either is "Other" if ( normalizedLoraBaseModelType == CivitBaseModelType.Other || normalizedBaseModelType == CivitBaseModelType.Other ) return null; return normalizedLoraBaseModelType == normalizedBaseModelType; } // Normalize base model type private static CivitBaseModelType NormalizeBaseModelType(CivitBaseModelType baseModel) { return baseModel switch { CivitBaseModelType.Sdxl09 => CivitBaseModelType.Sdxl10, CivitBaseModelType.Sdxl10Lcm => CivitBaseModelType.Sdxl10, CivitBaseModelType.SdxlDistilled => CivitBaseModelType.Sdxl10, CivitBaseModelType.SdxlHyper => CivitBaseModelType.Sdxl10, CivitBaseModelType.SdxlLightning => CivitBaseModelType.Sdxl10, CivitBaseModelType.SdxlTurbo => CivitBaseModelType.Sdxl10, CivitBaseModelType.Pony => CivitBaseModelType.Sdxl10, CivitBaseModelType.NoobAi => CivitBaseModelType.Sdxl10, CivitBaseModelType.Illustrious => CivitBaseModelType.Sdxl10, _ => baseModel, }; } } ================================================ FILE: StabilityMatrix.Core/Helper/ModelFinder.cs ================================================ using System.Net; using Injectio.Attributes; using NLog; using Refit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Helper; // return Model, ModelVersion, ModelFile public record struct ModelSearchResult(CivitModel Model, CivitModelVersion ModelVersion, CivitFile ModelFile); [RegisterSingleton] public class ModelFinder { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly ILiteDbContext liteDbContext; private readonly ICivitApi civitApi; public ModelFinder(ILiteDbContext liteDbContext, ICivitApi civitApi) { this.liteDbContext = liteDbContext; this.civitApi = civitApi; } // Finds a model from the local database using file hash public async Task LocalFindModel(string hashBlake3) { var (model, version) = await liteDbContext.FindCivitModelFromFileHashAsync(hashBlake3); if (model == null || version == null) { return null; } var file = version.Files!.First(file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3); return new ModelSearchResult(model, version, file); } // Finds a model using Civit API using file hash public async Task RemoteFindModel(string hashBlake3) { Logger.Info("Searching Civit API for model version using hash {Hash}", hashBlake3); try { var versionResponse = await civitApi.GetModelVersionByHash(hashBlake3); Logger.Info( "Found version {VersionId} with model id {ModelId}", versionResponse.Id, versionResponse.ModelId ); var model = await civitApi.GetModelById(versionResponse.ModelId); // 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.FirstOrDefault( file => hashBlake3.Equals(file.Hashes.BLAKE3, StringComparison.OrdinalIgnoreCase) ); // Archived models do not have files if (file == null) return null; 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) { 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 ); return null; } } public async Task> FindRemoteModelsById(IEnumerable ids) { var results = new List(); // split ids into batches of 100 var batches = ids.Chunk(100); foreach (var batch in batches) { try { var response = await civitApi .GetModels( new CivitModelsRequest { CommaSeparatedModelIds = string.Join(",", batch), Nsfw = "true", Query = string.Empty } ) .ConfigureAwait(false); if (response.Items == null || response.Items.Count == 0) continue; results.AddRange(response.Items); } catch (Exception e) { Logger.Error("Error while finding remote models by id: {Error}", e.Message); } } return results; } } ================================================ FILE: StabilityMatrix.Core/Helper/MyTiffFile.cs ================================================ using System.Text; using ExifLibrary; namespace StabilityMatrix.Core.Helper; public class MyTiffFile(MemoryStream stream, Encoding encoding) : TIFFFile(stream, encoding); ================================================ FILE: StabilityMatrix.Core/Helper/ObjectHash.cs ================================================ using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace StabilityMatrix.Core.Helper; public static class ObjectHash { /// /// Return a GUID based on the MD5 hash of the JSON representation of the object. /// public static Guid GetMd5Guid(T obj) { var json = JsonSerializer.Serialize(obj); var bytes = Encoding.UTF8.GetBytes(json); using var md5 = System.Security.Cryptography.MD5.Create(); var hash = md5.ComputeHash(bytes); return new Guid(hash); } /// /// Return a short Sha256 signature of a string /// public static string GetStringSignature(string? str) { if (str is null) { return "null"; } if (string.IsNullOrEmpty(str)) { return ""; } var bytes = Encoding.UTF8.GetBytes(str); var hash = Convert.ToBase64String(SHA256.HashData(bytes)); return $"[..{str.Length}, {hash[..7]}]"; } } ================================================ FILE: StabilityMatrix.Core/Helper/PlatformKind.cs ================================================ namespace StabilityMatrix.Core.Helper; [Flags] public enum PlatformKind { Unknown = 0, Windows = 1 << 0, Unix = 1 << 1, Linux = Unix | 1 << 2, MacOS = Unix | 1 << 3, Arm = 1 << 20, X64 = 1 << 21, } ================================================ FILE: StabilityMatrix.Core/Helper/ProcessTracker.cs ================================================ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; using NLog; namespace StabilityMatrix.Core.Helper; /// /// Allows processes to be automatically killed if this parent process unexpectedly quits. /// This feature requires Windows 8 or greater. On Windows 7, nothing is done. /// References: /// https://stackoverflow.com/a/4657392/386091 /// https://stackoverflow.com/a/9164742/386091 [SupportedOSPlatform("windows")] [SuppressMessage("ReSharper", "InconsistentNaming")] public static partial class ProcessTracker { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Lazy ProcessTrackerJobLazy = new(() => { if (!JobObject.IsAvailableOnCurrentPlatform) { return null; } // The job name is optional (and can be null) but it helps with diagnostics. // If it's not null, it has to be unique. Use SysInternals' Handle command-line // utility: handle -a ChildProcessTracker var jobName = $"SM_ProcessTracker_{Environment.ProcessId}"; Logger.Debug("Creating Job Object {Job}", jobName); try { return new JobObject(jobName); } catch (Exception e) { Logger.Error(e, "Failed to create Job Object, ProcessTracker will be unavailable"); return null; } }); private static JobObject? ProcessTrackerJob => ProcessTrackerJobLazy.Value; /// /// Add the process to be tracked. If our current process is killed, the child processes /// that we are tracking will be automatically killed, too. If the child process terminates /// first, that's fine, too. /// /// Ignored if the Process has already exited. /// public static void AddProcess(Process process) { // Skip if no job object if (ProcessTrackerJob is not { } job) { return; } // Skip if process already exited, this sometimes throws try { if (process.HasExited) { return; } } catch (InvalidOperationException) { return; } try { Logger.Debug( "Adding Process {Process} [{Id}] to Job Object {Job}", process.ProcessName, process.Id, job.Name ); job.AssignProcess(process); } catch (Exception) { // Check again if the process has exited, if it hasn't, rethrow try { if (process.HasExited) { return; } } catch (InvalidOperationException) { return; } throw; } } /// /// Add the process to be tracked in a new job. If our current process is killed, the child processes /// that we are tracking will be automatically killed, too. If the child process terminates /// first, that's fine, too. /// /// Ignored if the Process has already exited. /// public static void AttachExitHandlerJobToProcess(Process process) { // Skip if job object is not available if (!JobObject.IsAvailableOnCurrentPlatform) { return; } // Skip if process already exited, this sometimes throws try { if (process.HasExited) { return; } } catch (InvalidOperationException) { return; } // Create a new job object for this process var jobName = $"SM_ProcessTracker_{Environment.ProcessId}_Instance_{process.Id}"; Logger.Debug("Creating Instance Job Object {Job}", jobName); var instanceJob = new JobObject(jobName); try { Logger.Debug( "Adding Process {Process} [{Id}] to Job Object {Job}", process.ProcessName, process.Id, jobName ); instanceJob.AssignProcess(process); // Dispose the instance job when the process exits process.Exited += (_, _) => { Logger.Debug( "Process {Process} [{Id}] exited ({Code}), terminating instance Job Object {Job}", process.ProcessName, process.Id, process.ExitCode, jobName ); // ReSharper disable twice AccessToDisposedClosure if (!instanceJob.IsClosed) { // Convert from negative to two's complement if needed var exitCode = process.ExitCode < 0 ? (uint)(4294967296 + process.ExitCode) : (uint)process.ExitCode; instanceJob.Terminate(exitCode); instanceJob.Dispose(); } }; } catch (Exception) { instanceJob.Dispose(); throw; } } private class JobObject : SafeHandleZeroOrMinusOneIsInvalid { // This feature requires Windows 8 or later public static bool IsAvailableOnCurrentPlatform => Compat.IsWindows && Environment.OSVersion.Version >= new Version(6, 2); public string Name { get; } public JobObject(string name) : base(true) { if (!IsAvailableOnCurrentPlatform) { throw new PlatformNotSupportedException("This feature requires Windows 8 or later."); } Name = name; handle = CreateJobObject(IntPtr.Zero, name); var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION { // This is the key flag. When our process is killed, Windows will automatically // close the job handle, and when that happens, we want the child processes to // be killed, too. LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, }; var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION { BasicLimitInformation = info }; var extendedInfoPtr = Marshal.AllocHGlobal(length); try { Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); if ( !SetInformationJobObject( handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length ) ) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } finally { Marshal.FreeHGlobal(extendedInfoPtr); } } public void AssignProcess(Process process) { ObjectDisposedException.ThrowIf(handle == IntPtr.Zero, typeof(JobObject)); if (!AssignProcessToJobObject(handle, process.Handle)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public void AssignProcess(IntPtr processHandle) { ObjectDisposedException.ThrowIf(handle == IntPtr.Zero, typeof(JobObject)); if (!AssignProcessToJobObject(handle, processHandle)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public void Terminate(uint exitCode) { ObjectDisposedException.ThrowIf(handle == IntPtr.Zero, typeof(JobObject)); if (!TerminateJobObject(handle, exitCode)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } protected override bool ReleaseHandle() { return CloseHandle(handle); } } [LibraryImport( "kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16 )] private static partial IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SetInformationJobObject( IntPtr job, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength ); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool AssignProcessToJobObject(IntPtr job, IntPtr process); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool CloseHandle(IntPtr hObject); [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool TerminateJobObject(IntPtr job, uint exitCode); internal enum JobObjectInfoType { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, BasicUIRestrictions = 4, EndOfJobTimeInformation = 6, ExtendedLimitInformation = 9, SecurityLimitInformation = 5, GroupInformation = 11, } [StructLayout(LayoutKind.Sequential)] // ReSharper disable once IdentifierTypo internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public Int64 PerProcessUserTimeLimit; public Int64 PerJobUserTimeLimit; public JOBOBJECTLIMIT LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public UInt32 ActiveProcessLimit; public Int64 Affinity; public UInt32 PriorityClass; public UInt32 SchedulingClass; } [Flags] // ReSharper disable once IdentifierTypo internal enum JOBOBJECTLIMIT : uint { JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000, } [StructLayout(LayoutKind.Sequential)] internal struct IO_COUNTERS { public UInt64 ReadOperationCount; public UInt64 WriteOperationCount; public UInt64 OtherOperationCount; public UInt64 ReadTransferCount; public UInt64 WriteTransferCount; public UInt64 OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] // ReSharper disable once IdentifierTypo internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } } ================================================ FILE: StabilityMatrix.Core/Helper/PropertyComparer.cs ================================================ namespace StabilityMatrix.Core.Helper; public class PropertyComparer : IEqualityComparer where T : class { private Func Expr { get; set; } public PropertyComparer(Func expr) { Expr = expr; } public bool Equals(T? x, T? y) { if (x == null || y == null) return false; var first = Expr.Invoke(x); var second = Expr.Invoke(y); return first.Equals(second); } public int GetHashCode(T obj) { return obj.GetHashCode(); } } ================================================ FILE: StabilityMatrix.Core/Helper/ReaderWriterLockAdvanced.cs ================================================ namespace StabilityMatrix.Core.Helper; /// /// Extended with support for disposal pattern. /// public class ReaderWriterLockAdvanced : ReaderWriterLockSlim { public ReaderWriterLockAdvanced() { } public ReaderWriterLockAdvanced(LockRecursionPolicy recursionPolicy) : base(recursionPolicy) { } public DisposableLock EnterReadContext(TimeSpan timeout = default) { if (!TryEnterReadLock(timeout)) { throw new TimeoutException("Timeout waiting for read lock"); } return new DisposableLock(this, LockType.Read); } public DisposableLock EnterWriteContext(TimeSpan timeout = default) { if (!TryEnterWriteLock(timeout)) { throw new TimeoutException("Timeout waiting for write lock"); } return new DisposableLock(this, LockType.Write); } public DisposableLock EnterUpgradeableReadContext(TimeSpan timeout = default) { if (!TryEnterUpgradeableReadLock(timeout)) { throw new TimeoutException("Timeout waiting for upgradeable read lock"); } return new DisposableLock(this, LockType.UpgradeableRead); } } /// /// Wrapper for disposable lock /// public class DisposableLock : IDisposable { private readonly ReaderWriterLockAdvanced readerWriterLock; private readonly LockType lockType; public DisposableLock(ReaderWriterLockAdvanced @lock, LockType lockType) { readerWriterLock = @lock; this.lockType = lockType; } public DisposableLock UpgradeToWrite(TimeSpan timeout = default) { if (lockType != LockType.UpgradeableRead) { throw new InvalidOperationException("Can only upgrade from upgradeable read lock"); } return readerWriterLock.EnterWriteContext(timeout); } public void Dispose() { switch (lockType) { case LockType.Read: readerWriterLock.ExitReadLock(); break; case LockType.Write: readerWriterLock.ExitWriteLock(); break; case LockType.UpgradeableRead: readerWriterLock.ExitUpgradeableReadLock(); break; default: throw new ArgumentOutOfRangeException(); } GC.SuppressFinalize(this); } } public enum LockType { Read, Write, UpgradeableRead } ================================================ FILE: StabilityMatrix.Core/Helper/RemoteModels.cs ================================================ using System.Collections.Immutable; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Helper; /// /// Record of remote model resources /// public static class RemoteModels { /// /// Root of huggingface upscalers /// private static Uri UpscalersRoot { get; } = new("https://huggingface.co/LykosAI/Upscalers/"); /// /// Root of huggingface upscalers at the main branch /// private static Uri UpscalersRootMain { get; } = UpscalersRoot.Append("blob/main/"); public static IReadOnlyList Upscalers { get; } = new RemoteResource[] { new() { Url = UpscalersRoot.Append( "resolve/28a279ec60b5c47c9f39381dcaa997b9402ac09d/RealESRGAN/RealESRGAN_x2plus.pth" ), HashSha256 = "49fafd45f8fd7aa8d31ab2a22d14d91b536c34494a5cfe31eb5d89c2fa266abb", InfoUrl = new Uri("https://github.com/xinntao/ESRGAN"), Author = "xinntao", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "RealESRGAN/LICENSE.txt"), ContextType = SharedFolderType.RealESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/28a279ec60b5c47c9f39381dcaa997b9402ac09d/RealESRGAN/RealESRGAN_x2plus.pth" ), HashSha256 = "49fafd45f8fd7aa8d31ab2a22d14d91b536c34494a5cfe31eb5d89c2fa266abb", InfoUrl = new Uri("https://github.com/xinntao/ESRGAN"), Author = "xinntao", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "RealESRGAN/LICENSE.txt"), ContextType = SharedFolderType.RealESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/28a279ec60b5c47c9f39381dcaa997b9402ac09d/RealESRGAN/RealESRGAN_x4plus.pth" ), HashSha256 = "4fa0d38905f75ac06eb49a7951b426670021be3018265fd191d2125df9d682f1", InfoUrl = new Uri("https://github.com/xinntao/ESRGAN"), Author = "xinntao", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "RealESRGAN/LICENSE.txt"), ContextType = SharedFolderType.RealESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/28a279ec60b5c47c9f39381dcaa997b9402ac09d/RealESRGAN/RealESRGAN_x4plus_anime_6B.pth" ), HashSha256 = "f872d837d3c90ed2e05227bed711af5671a6fd1c9f7d7e91c911a61f155e99da", InfoUrl = new Uri("https://github.com/xinntao/ESRGAN"), Author = "xinntao", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "RealESRGAN/LICENSE.txt"), ContextType = SharedFolderType.RealESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/28a279ec60b5c47c9f39381dcaa997b9402ac09d/RealESRGAN/RealESRGAN_x4plus_anime_6B.pth" ), HashSha256 = "f872d837d3c90ed2e05227bed711af5671a6fd1c9f7d7e91c911a61f155e99da", InfoUrl = new Uri("https://github.com/xinntao/ESRGAN"), Author = "xinntao", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "RealESRGAN/LICENSE.txt"), ContextType = SharedFolderType.RealESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/4f813cc283de5cd66fe253bc05e66ca76bf68c51/SwinIR/SwinIR_4x.pth" ), HashSha256 = "99adfa91350a84c99e946c1eb3d8fce34bc28f57d807b09dc8fe40a316328c0a", InfoUrl = new Uri("https://github.com/JingyunLiang/SwinIR"), Author = "JingyunLiang", LicenseType = "Apache 2.0", LicenseUrl = new Uri(UpscalersRootMain, "SwinIR/LICENSE.txt"), ContextType = SharedFolderType.SwinIR }, new() { Url = UpscalersRoot.Append( "resolve/6a1d697dd18f8c4ff031f26e6dc523ada419517d/UltraMix/4x-UltraMix_Smooth.pth" ), HashSha256 = "7deeeac95ce7c28d616933b789f51642d169b200a6638edfb1c57ccecd903cd0", InfoUrl = new Uri("https://github.com/Kim2091"), Author = "Kim2091", LicenseType = "CC BY-NC-SA 4.0", LicenseUrl = new Uri(UpscalersRootMain, "UltraMix/LICENSE.txt"), ContextType = SharedFolderType.ESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/6a1d697dd18f8c4ff031f26e6dc523ada419517d/UltraMix/4x-UltraMix_Restore.pth" ), HashSha256 = "e8982b435557baeea5a08066aede41a9b3c8a6512c8688dab6d326e91ba82fa3", InfoUrl = new Uri("https://github.com/Kim2091"), Author = "Kim2091", LicenseType = "CC BY-NC-SA 4.0", LicenseUrl = new Uri(UpscalersRootMain, "UltraMix/LICENSE.txt"), ContextType = SharedFolderType.ESRGAN }, new() { Url = UpscalersRoot.Append( "resolve/6a1d697dd18f8c4ff031f26e6dc523ada419517d/UltraMix/4x-UltraMix_Balanced.pth" ), HashSha256 = "e23ca000107aae95ec9b8d7d1bf150f7884f1361cd9d669bdf824d72529f0e26", InfoUrl = new Uri("https://github.com/Kim2091"), Author = "Kim2091", LicenseType = "CC BY-NC-SA 4.0", LicenseUrl = new Uri(UpscalersRootMain, "UltraMix/LICENSE.txt"), ContextType = SharedFolderType.ESRGAN } }; private static Uri ControlNetRoot { get; } = new("https://huggingface.co/lllyasviel/ControlNet/"); private static RemoteResource ControlNetCommon(string path, string sha256) { const string commit = "38a62cbf79862c1bac73405ec8dc46133aee3e36"; return new RemoteResource { Url = ControlNetRoot.Append($"resolve/{commit}/").Append(path), HashSha256 = sha256, InfoUrl = ControlNetRoot, Author = "lllyasviel", LicenseType = "OpenRAIL", LicenseUrl = ControlNetRoot, ContextType = SharedFolderType.ControlNet }; } public static IReadOnlyList ControlNets { get; } = new[] { ControlNetCommon( "models/control_sd15_canny.pth", "4de384b16bc2d7a1fb258ca0cbd941d7dd0a721ae996aff89f905299d6923f45" ), ControlNetCommon( "models/control_sd15_depth.pth", "726cd0b472c4b5c0341b01afcb7fdc4a7b4ab7c37fe797fd394c9805cbef60bf" ), ControlNetCommon( "models/control_sd15_openpose.pth", "d19ffffeeaff6d9feb2204b234c3e1b9aec039ab3e63fca07f4fe5646f2ef591" ) }; public static HybridModelFile ControlNetReferenceOnlyModel { get; } = HybridModelFile.FromRemote("@ReferenceOnly"); public static IReadOnlyList ControlNetModels { get; } = ControlNets .Select(HybridModelFile.FromDownloadable) .Concat([ControlNetReferenceOnlyModel]) .ToImmutableArray(); private static IEnumerable PromptExpansions => [ new RemoteResource { Url = new Uri("https://cdn.lykos.ai/models/GPT-Prompt-Expansion-Fooocus-v2.zip"), HashSha256 = "82e69311787c0bb6736389710d80c0a2b653ed9bbe6ea6e70c6b90820fe42d88", InfoUrl = new Uri("https://huggingface.co/LykosAI/GPT-Prompt-Expansion-Fooocus-v2"), Author = "lllyasviel, LykosAI", LicenseType = "GPLv3", LicenseUrl = new Uri("https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"), ContextType = SharedFolderType.PromptExpansion, AutoExtractArchive = true, ExtractRelativePath = "GPT-Prompt-Expansion-Fooocus-v2" } ]; public static IEnumerable PromptExpansionModels => PromptExpansions.Select(HybridModelFile.FromDownloadable); private static IEnumerable UltralyticsModels => [ new RemoteResource { Url = new Uri("https://huggingface.co/Bingsu/adetailer/resolve/main/face_yolov8m.pt"), HashSha256 = "f02b8a23e6f12bd2c1b1f6714f66f984c728fa41ed749d033e7d6dea511ef70c", InfoUrl = new Uri("https://huggingface.co/Bingsu/adetailer"), Author = "Bingsu", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md" ), ContextType = SharedFolderType.Ultralytics, RelativeDirectory = "bbox" }, new RemoteResource { Url = new Uri("https://huggingface.co/Bingsu/adetailer/resolve/main/hand_yolov8s.pt"), HashSha256 = "5c4faf8d17286ace2c3d3346c6d0d4a0c8d62404955263a7ae95c1dd7eb877af", InfoUrl = new Uri("https://huggingface.co/Bingsu/adetailer"), Author = "Bingsu", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md" ), ContextType = SharedFolderType.Ultralytics, RelativeDirectory = "bbox" }, new RemoteResource { Url = new Uri("https://huggingface.co/Bingsu/adetailer/resolve/main/person_yolov8m-seg.pt"), HashSha256 = "9d881ec50b831f546e37977081b18f4e3bf65664aec163f97a311b0955499795", InfoUrl = new Uri("https://huggingface.co/Bingsu/adetailer"), Author = "Bingsu", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md" ), ContextType = SharedFolderType.Ultralytics, RelativeDirectory = "segm" }, new RemoteResource { Url = new Uri("https://huggingface.co/Bingsu/adetailer/resolve/main/person_yolov8s-seg.pt"), HashSha256 = "b5684835e79fd8b805459e0f7a0f9daa437e421cb4a214fff45ec4ac61767ef9", InfoUrl = new Uri("https://huggingface.co/Bingsu/adetailer"), Author = "Bingsu", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md" ), ContextType = SharedFolderType.Ultralytics, RelativeDirectory = "segm" } ]; public static IEnumerable UltralyticsModelFiles => UltralyticsModels.Select(HybridModelFile.FromDownloadable); private static IEnumerable SamModels => [ new RemoteResource { Url = new Uri("https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth"), InfoUrl = new Uri("https://github.com/facebookresearch/segment-anything"), Author = "Facebook Research", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://github.com/facebookresearch/segment-anything/blob/main/LICENSE" ), ContextType = SharedFolderType.Sams }, new RemoteResource { Url = new Uri("https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth"), InfoUrl = new Uri("https://github.com/facebookresearch/segment-anything"), Author = "Facebook Research", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://github.com/facebookresearch/segment-anything/blob/main/LICENSE" ), ContextType = SharedFolderType.Sams }, new RemoteResource { Url = new Uri("https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth"), InfoUrl = new Uri("https://github.com/facebookresearch/segment-anything"), Author = "Facebook Research", LicenseType = "Apache 2.0", LicenseUrl = new Uri( "https://github.com/facebookresearch/segment-anything/blob/main/LICENSE" ), ContextType = SharedFolderType.Sams } ]; public static IEnumerable SamModelFiles => SamModels.Select(HybridModelFile.FromDownloadable); private static IEnumerable ClipModels => [ new() { Url = new Uri( "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors" ), InfoUrl = new Uri("https://huggingface.co/comfyanonymous/flux_text_encoders"), HashSha256 = "660c6f5b1abae9dc498ac2d21e1347d2abdb0cf6c0c0c8576cd796491d9a6cdd", Author = "OpenAI", LicenseType = "MIT", ContextType = SharedFolderType.TextEncoders, }, new() { Url = new Uri( "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors" ), InfoUrl = new Uri("https://huggingface.co/comfyanonymous/flux_text_encoders"), HashSha256 = "6e480b09fae049a72d2a8c5fbccb8d3e92febeb233bbe9dfe7256958a9167635", Author = "Google", LicenseType = "Apache 2.0", ContextType = SharedFolderType.TextEncoders, }, new() { Url = new Uri( "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors" ), InfoUrl = new Uri("https://huggingface.co/comfyanonymous/flux_text_encoders"), HashSha256 = "7d330da4816157540d6bb7838bf63a0f02f573fc48ca4d8de34bb0cbfd514f09", Author = "Google", LicenseType = "Apache 2.0", ContextType = SharedFolderType.TextEncoders, }, new() { Url = new Uri( "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged"), HashSha256 = "c3355d30191f1f066b26d93fba017ae9809dce6c627dda5f6a66eaa651204f68", Author = "Google", LicenseType = "Apache 2.0", ContextType = SharedFolderType.TextEncoders, }, new() { Url = new Uri( "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp16.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged"), HashSha256 = "7b8850f1961e1cf8a77cca4c964a358d303f490833c6c087d0cff4b2f99db2af", Author = "Google", LicenseType = "Apache 2.0", ContextType = SharedFolderType.TextEncoders, }, new() { Url = new Uri( "https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI/resolve/main/split_files/text_encoders/clip_l_hidream.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI"), HashSha256 = "706fdb88e22e18177b207837c02f4b86a652abca0302821f2bfa24ac6aea4f71", Author = "OpenAI", LicenseType = "MIT", ContextType = SharedFolderType.TextEncoders }, new() { Url = new Uri( "https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI/resolve/main/split_files/text_encoders/clip_g_hidream.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI"), HashSha256 = "3771e70e36450e5199f30bad61a53faae85a2e02606974bcda0a6a573c0519d5", Author = "OpenAI", LicenseType = "MIT", ContextType = SharedFolderType.TextEncoders }, new() { Url = new Uri( "https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI/resolve/main/split_files/text_encoders/llama_3.1_8b_instruct_fp8_scaled.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/HiDream-I1_ComfyUI"), HashSha256 = "9f86897bbeb933ef4fd06297740edb8dd962c94efcd92b373a11460c33765ea6", Author = "Meta", LicenseType = "llama3.1", ContextType = SharedFolderType.TextEncoders } ]; public static IEnumerable ClipModelFiles => ClipModels.Select(HybridModelFile.FromDownloadable); private static IEnumerable ClipVisionModels => [ new() { Url = new Uri( "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/clip_vision/clip_vision_h.safetensors" ), InfoUrl = new Uri("https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged"), HashSha256 = "64a7ef761bfccbadbaa3da77366aac4185a6c58fa5de5f589b42a65bcc21f161", Author = "OpenAI", LicenseType = "MIT", ContextType = SharedFolderType.ClipVision, } ]; public static IEnumerable ClipVisionModelFiles => ClipVisionModels.Select(HybridModelFile.FromDownloadable); } ================================================ FILE: StabilityMatrix.Core/Helper/SharedFolders.cs ================================================ using Injectio.Attributes; using NLog; using OneOf.Types; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.ReparsePoints; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Helper; [RegisterSingleton] [RegisterSingleton] public class SharedFolders(ISettingsManager settingsManager, IPackageFactory packageFactory) : ISharedFolders, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); // mapping is old:new private static readonly Dictionary LegacySharedFolderMapping = new() { { "CLIP", "TextEncoders" }, { "Unet", "DiffusionModels" }, { "InvokeClipVision", "ClipVision" }, { "InvokeIpAdapters15", "IpAdapters15" }, { "InvokeIpAdaptersXl", "IpAdaptersXl" }, { "TextualInversion", "Embeddings" }, }; public bool IsDisposed { get; private set; } /// /// Platform redirect for junctions / symlinks /// private static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite) { if (Compat.IsWindows) { Junction.Create(junctionDir, targetDir, overwrite); } else { // Create parent directory if it doesn't exist, since CreateSymbolicLink doesn't seem to new DirectoryPath(junctionDir).Parent?.Create(); Directory.CreateSymbolicLink(junctionDir, targetDir); } } /// /// Creates or updates junction link from the source to the destination. /// Moves destination files to source if they exist. /// /// Shared source (i.e. "Models/") /// Destination (i.e. "webui/models/lora") /// Whether to overwrite the destination if it exists /// Whether to recursively delete the directory after moving data out of it public static async Task CreateOrUpdateLink( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, bool recursiveDelete = false ) { // Create source folder if it doesn't exist if (!sourceDir.Exists) { Logger.Info($"Creating junction source {sourceDir}"); sourceDir.Create(); } var destAsFile = new FilePath(destinationDir.ToString()); if (destAsFile.Exists) { await destAsFile.DeleteAsync().ConfigureAwait(false); } if (destinationDir.Exists) { // Existing dest is a link if (destinationDir.IsSymbolicLink) { // If link is already the same, just skip if (destinationDir.Info.LinkTarget == sourceDir) { Logger.Info($"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})"); return; } // Otherwise delete the link Logger.Info($"Deleting existing junction at target {destinationDir}"); destinationDir.Info.Attributes = FileAttributes.Normal; await destinationDir.DeleteAsync(false).ConfigureAwait(false); } else { // Move all files if not empty if (destinationDir.Info.EnumerateFileSystemInfos().Any()) { Logger.Info($"Moving files from {destinationDir} to {sourceDir}"); await FileTransfers .MoveAllFilesAndDirectories( destinationDir, sourceDir, overwriteIfHashMatches: true, overwrite: overwrite, deleteSymlinks: true ) .ConfigureAwait(false); } Logger.Info($"Deleting existing empty folder at target {destinationDir}"); await destinationDir.DeleteAsync(recursiveDelete).ConfigureAwait(false); } } Logger.Info($"Updating junction link from {sourceDir} to {destinationDir}"); CreateLinkOrJunction(destinationDir, sourceDir, true); } [Obsolete("Use static methods instead")] public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory) { var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var sharedFolders = basePackage.SharedFolders; if (sharedFolders == null) return; UpdateLinksForPackage(sharedFolders, modelsDirectory, installDirectory).GetAwaiter().GetResult(); } /// /// Updates or creates shared links for a package. /// Will attempt to move files from the destination to the source if the destination is not empty. /// public static async Task UpdateLinksForPackage( Dictionary> sharedFolders, DirectoryPath modelsDirectory, DirectoryPath installDirectory, bool recursiveDelete = false ) where T : Enum { foreach (var (folderType, relativePaths) in sharedFolders) { foreach (var relativePath in relativePaths) { var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue()); var destinationDir = installDirectory.JoinDir(relativePath); // Check and remove destinationDir parent if it's a link if (destinationDir.Parent is { IsSymbolicLink: true } parentLink) { Logger.Info("Deleting parent junction at target {Path}", parentLink.ToString()); await parentLink.DeleteAsync(false).ConfigureAwait(false); // Recreate parentLink.Create(); } await CreateOrUpdateLink(sourceDir, destinationDir, recursiveDelete: recursiveDelete) .ConfigureAwait(false); } } } public static void RemoveLinksForPackage( Dictionary>? sharedFolders, DirectoryPath installPath ) where T : Enum { if (sharedFolders == null) { return; } foreach (var (_, relativePaths) in sharedFolders) { foreach (var relativePath in relativePaths) { var destination = Path.GetFullPath(Path.Combine(installPath, relativePath)); // Delete the destination folder if it exists if (!Directory.Exists(destination)) continue; Logger.Info($"Deleting junction target {destination}"); Directory.Delete(destination, false); } } } public void RemoveLinksForAllPackages() { var packages = settingsManager.Settings.InstalledPackages; foreach (var package in packages) { try { if ( packageFactory.FindPackageByName(package.PackageName) is not { } basePackage || package.FullPath is null ) { continue; } var sharedFolderMethod = package.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod; basePackage .RemoveModelFolderLinks(package.FullPath, sharedFolderMethod) .GetAwaiter() .GetResult(); // Remove output folder links if enabled if (package.UseSharedOutputFolder) { basePackage.RemoveOutputFolderLinks(package.FullPath).GetAwaiter().GetResult(); } } catch (Exception e) { Logger.Warn( "Failed to remove links for package {Package} " + "({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message ); } } } public static void SetupSharedModelFolders(DirectoryPath rootModelsDir) { if (string.IsNullOrWhiteSpace(rootModelsDir)) return; Directory.CreateDirectory(rootModelsDir); var allSharedFolderTypes = Enum.GetValues(); foreach (var sharedFolder in allSharedFolderTypes) { if (sharedFolder == SharedFolderType.Unknown) continue; var dir = new DirectoryPath(rootModelsDir, sharedFolder.GetStringValue()); dir.Create(); if (sharedFolder == SharedFolderType.Ultralytics) { var bboxDir = new DirectoryPath(dir, "bbox"); var segmDir = new DirectoryPath(dir, "segm"); bboxDir.Create(); segmDir.Create(); } } MigrateOldSharedFolderPaths(rootModelsDir); } private static void MigrateOldSharedFolderPaths(DirectoryPath rootModelsDir) { foreach (var (legacyFolderName, newFolderName) in LegacySharedFolderMapping) { var fullPath = rootModelsDir.JoinDir(legacyFolderName); if (!fullPath.Exists) continue; foreach (var file in fullPath.EnumerateFiles(searchOption: SearchOption.AllDirectories)) { var relativePath = file.RelativeTo(fullPath); var newPath = rootModelsDir.JoinFile(newFolderName, relativePath); newPath.Directory?.Create(); file.MoveTo(newPath); } } // delete the old directories *only if they're empty* foreach ( var fullPath in from legacyFolderName in LegacySharedFolderMapping.Keys select rootModelsDir.JoinDir(legacyFolderName) into fullPath where fullPath.Exists where !fullPath.EnumerateFiles(searchOption: SearchOption.AllDirectories).Any() select fullPath ) { fullPath.Delete(true); } } public async ValueTask DisposeAsync() { if (IsDisposed) { return; } // Skip if library dir is not set or remove folder links on shutdown is disabled if (!settingsManager.IsLibraryDirSet || !settingsManager.Settings.RemoveFolderLinksOnShutdown) { return; } // Remove all package junctions Logger.Debug("SharedFolders Dispose: Removing package junctions"); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { await Task.Run(RemoveLinksForAllPackages, cts.Token).WaitAsync(cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { Logger.Warn("SharedFolders Dispose: Timeout removing package junctions"); } catch (Exception e) { Logger.Error(e, "SharedFolders Dispose: Failed to remove package junctions"); } Logger.Debug("SharedFolders Dispose: Finished removing package junctions"); IsDisposed = true; GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Helper/SharedFoldersConfigHelper.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages.Config; namespace StabilityMatrix.Core.Helper; public static class SharedFoldersConfigHelper { // Cache strategies to avoid repeated instantiation private static readonly Dictionary Strategies = new() { { ConfigFileType.Json, new JsonConfigSharingStrategy() }, { ConfigFileType.Yaml, new YamlConfigSharingStrategy() }, { ConfigFileType.Fds, new FdsConfigSharingStrategy() } // Add more strategies here as needed }; /// /// Updates a config file with shared folder layout rules, using the SourceTypes, /// converted to absolute paths using the sharedModelsDirectory. /// public static async Task UpdateConfigFileForSharedAsync( SharedFolderLayout layout, string packageRootDirectory, string sharedModelsDirectory, CancellationToken cancellationToken = default ) { var configPath = Path.Combine( packageRootDirectory, layout.RelativeConfigPath ?? throw new InvalidOperationException("RelativeConfigPath is null") ); Directory.CreateDirectory(Path.GetDirectoryName(configPath)!); // Ensure directory exists // Using FileStream ensures we handle file locking and async correctly await using var stream = new FileStream( configPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None ); await UpdateConfigFileForSharedAsync( layout, packageRootDirectory, sharedModelsDirectory, stream, cancellationToken ) .ConfigureAwait(false); } /// /// Updates a config file with shared folder layout rules, using the SourceTypes, /// converted to absolute paths using the sharedModelsDirectory. /// public static async Task UpdateConfigFileForSharedAsync( SharedFolderLayout layout, string packageRootDirectory, string sharedModelsDirectory, Stream stream, CancellationToken cancellationToken = default ) { var fileType = layout.ConfigFileType ?? throw new InvalidOperationException("ConfigFileType is null"); var options = layout.ConfigSharingOptions; if (!Strategies.TryGetValue(fileType, out var strategy)) { throw new NotSupportedException($"Configuration file type '{fileType}' is not supported."); } await strategy .UpdateAndWriteAsync( stream, layout, rule => { // Handle Root, just use models directory (e.g., Swarm) if (rule.IsRoot) { return [sharedModelsDirectory]; } var paths = rule.SourceTypes.Select(type => type.GetStringValue()) // Get the enum string value (e.g., "StableDiffusion") .Where(folderName => !string.IsNullOrEmpty(folderName)) // Filter out potentially empty mappings .Select(folderName => Path.Combine(sharedModelsDirectory, folderName)); // Combine with base models dir // If sub-path provided, add to all paths if (!string.IsNullOrEmpty(rule.SourceSubPath)) { paths = paths.Select(path => Path.Combine(path, rule.SourceSubPath)); } return paths; }, [], options, cancellationToken ) .ConfigureAwait(false); } /// /// Updates a config file with shared folder layout rules, using the TargetRelativePaths, /// converted to absolute paths using the packageRootDirectory (restores default paths). /// public static async Task UpdateConfigFileForDefaultAsync( SharedFolderLayout layout, string packageRootDirectory, CancellationToken cancellationToken = default ) { var configPath = Path.Combine( packageRootDirectory, layout.RelativeConfigPath ?? throw new InvalidOperationException("RelativeConfigPath is null") ); Directory.CreateDirectory(Path.GetDirectoryName(configPath)!); // Ensure directory exists // Using FileStream ensures we handle file locking and async correctly await using var stream = new FileStream( configPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None ); await UpdateConfigFileForDefaultAsync(layout, packageRootDirectory, stream, cancellationToken) .ConfigureAwait(false); } /// /// Updates a config file with shared folder layout rules, using the TargetRelativePaths, /// converted to absolute paths using the packageRootDirectory (restores default paths). /// public static async Task UpdateConfigFileForDefaultAsync( SharedFolderLayout layout, string packageRootDirectory, Stream stream, CancellationToken cancellationToken = default ) { var fileType = layout.ConfigFileType ?? throw new InvalidOperationException("ConfigFileType is null"); var options = layout.ConfigSharingOptions; if (!Strategies.TryGetValue(fileType, out var strategy)) { throw new NotSupportedException($"Configuration file type '{fileType}' is not supported."); } var clearPaths = new List(); // If using clear root option, add the root key if (options.ConfigDefaultType is ConfigDefaultType.ClearRoot) { clearPaths.Add(options.RootKey ?? ""); } await strategy .UpdateAndWriteAsync( stream, layout, rule => rule.TargetRelativePaths.Select( path => Path.Combine(packageRootDirectory, NormalizePathSlashes(path)) ), // Combine relative with package root clearPaths, options, cancellationToken ) .ConfigureAwait(false); } // Keep path normalization logic accessible if needed elsewhere, or inline it if only used here. private static string NormalizePathSlashes(string path) { // Replace forward slashes with backslashes on Windows, otherwise use forward slashes. return path.Replace(Compat.IsWindows ? '/' : '\\', Path.DirectorySeparatorChar); } } ================================================ FILE: StabilityMatrix.Core/Helper/SharedFoldersConfigOptions.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Helper; public class SharedFoldersConfigOptions { public static SharedFoldersConfigOptions Default => new(); public bool AlwaysWriteArray { get; set; } = false; public JsonSerializerOptions JsonSerializerOptions { get; set; } = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; } ================================================ FILE: StabilityMatrix.Core/Helper/Size.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Helper; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class Size { public const ulong KiB = 1024; public const ulong MiB = KiB * 1024; public const ulong GiB = MiB * 1024; private static string TrimZero(string value) { return value.TrimEnd('0').TrimEnd('.'); } public static string FormatBytes(ulong bytes, bool trimZero = false) { return bytes switch { < KiB => $"{bytes:0} Bytes", < MiB => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}") + " KiB", < GiB => (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}") + " MiB", _ => (trimZero ? $"{bytes / (double)GiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.0}") + " GiB" }; } public static string FormatBase10Bytes(ulong bytes, bool trimZero = false) { return bytes switch { < KiB => $"{bytes:0} Bytes", < MiB => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}") + " KB", < GiB => (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}") + " MB", _ => (trimZero ? $"{bytes / (double)GiB:0.00}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.00}") + " GB" }; } public static string FormatBase10Bytes(long bytes) { return FormatBase10Bytes(Convert.ToUInt64(bytes)); } } ================================================ FILE: StabilityMatrix.Core/Helper/SystemInfo.cs ================================================ using System.Runtime.InteropServices; using NLog; namespace StabilityMatrix.Core.Helper; public static class SystemInfo { public const long Gibibyte = 1024 * 1024 * 1024; public const long Mebibyte = 1024 * 1024; [DllImport("UXTheme.dll", SetLastError = true, EntryPoint = "#138")] public static extern bool ShouldUseDarkMode(); public static long? GetDiskFreeSpaceBytes(string path) { try { var drive = new DriveInfo(path); return drive.AvailableFreeSpace; } catch (Exception e) { LogManager.GetCurrentClassLogger().Error(e); } return null; } } ================================================ FILE: StabilityMatrix.Core/Helper/Utilities.cs ================================================ using System.Diagnostics; using System.Reflection; using System.Text.RegularExpressions; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Helper; public static partial class Utilities { public static string GetAppVersion() { var assembly = Assembly.GetExecutingAssembly(); var version = assembly.GetName().Version; return version == null ? "(Unknown)" : $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; } public static void CopyDirectory( string sourceDir, string destinationDir, bool recursive, bool includeReparsePoints = false ) { // Get information about the source directory var dir = new DirectoryInfo(sourceDir); // Check if the source directory exists if (!dir.Exists) throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); // Cache directories before we start copying var dirs = includeReparsePoints ? dir.GetDirectories() : dir.GetDirectories().Where(d => !d.Attributes.HasFlag(FileAttributes.ReparsePoint)); // Create the destination directory Directory.CreateDirectory(destinationDir); // Get the files in the source directory and copy to the destination directory foreach (var file in dir.GetFiles()) { var targetFilePath = Path.Combine(destinationDir, file.Name); if (file.FullName == targetFilePath) continue; file.CopyTo(targetFilePath, true); } if (!recursive) return; // If recursive and copying subdirectories, recursively call this method foreach (var subDir in dirs) { var newDestinationDir = Path.Combine(destinationDir, subDir.Name); CopyDirectory(subDir.FullName, newDestinationDir, true); } } public static MemoryStream? GetMemoryStreamFromFile(string filePath) { var fileBytes = File.ReadAllBytes(filePath); var stream = new MemoryStream(fileBytes); stream.Position = 0; return stream; } public static async Task WhichAsync(string arg) { using var process = new Process(); process.StartInfo.FileName = Compat.IsWindows ? "where.exe" : "which"; process.StartInfo.Arguments = arg; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardOutput = true; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); return await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false); } public static int GetNumDaysTilBeginningOfNextMonth() { var now = DateTimeOffset.UtcNow; var firstDayOfNextMonth = new DateTime(now.Year, now.Month, 1).AddMonths(1); var daysUntilNextMonth = (firstDayOfNextMonth - now).Days; return daysUntilNextMonth; } public static string RemoveHtml(string? stringWithHtml) { var pruned = stringWithHtml ?.Replace("
", $"{Environment.NewLine}{Environment.NewLine}") .Replace("
", $"{Environment.NewLine}{Environment.NewLine}") .Replace("

", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") .Replace("", $"{Environment.NewLine}{Environment.NewLine}") ?? string.Empty; pruned = HtmlRegex().Replace(pruned, string.Empty); return pruned; } /// /// Try to find pyvenv.cfg in common locations and parse its version into PyVersion. /// public static bool TryGetPyVenvVersion(string packageLocation, out PyVersion version) { version = default; if (string.IsNullOrWhiteSpace(packageLocation)) return false; // Common placements var candidates = new[] { Path.Combine(packageLocation, "pyvenv.cfg"), Path.Combine(packageLocation, "venv", "pyvenv.cfg"), Path.Combine(packageLocation, ".venv", "pyvenv.cfg"), }; foreach (var candidate in candidates) { if (!File.Exists(candidate)) continue; if (!TryReadVersionFromCfg(candidate, out var pyv)) continue; version = pyv; return true; } return false; } private static bool TryReadVersionFromCfg(string cfgFile, out PyVersion version) { version = default; var kv = ReadKeyValues(cfgFile); // Prefer version_info (e.g., "3.10.11.final.0"), fall back to version (e.g., "3.12.0") if (!kv.TryGetValue("version_info", out var raw) || string.IsNullOrWhiteSpace(raw)) kv.TryGetValue("version", out raw); if (string.IsNullOrWhiteSpace(raw)) return false; // Grab the first 1–3 dot-separated integers and ignore anything after (e.g., ".final.0", ".rc1") // Examples matched: "3", "3.12", "3.10.11" (from "3.10.11.final.0") var m = Regex.Match(raw, @"(? ReadKeyValues(string path) { var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var line in File.ReadAllLines(path)) { var trimmed = line.Trim(); if (trimmed.Length == 0 || trimmed.StartsWith("#")) continue; var idx = trimmed.IndexOf('='); if (idx <= 0) continue; var key = trimmed[..idx].Trim(); var val = trimmed[(idx + 1)..].Trim(); dict[key] = val; } return dict; } /// /// Returns the simplified aspect ratio as a tuple: (widthRatio, heightRatio). /// e.g. GetAspectRatio(1920,1080) -> (16,9) /// public static (int widthRatio, int heightRatio) GetAspectRatio(int width, int height) { if (width <= 0 || height <= 0) throw new ArgumentException("Width and height must be positive."); var gcd = Gcd(width, height); return (width / gcd, height / gcd); } // Euclidean GCD private static int Gcd(int a, int b) { a = Math.Abs(a); b = Math.Abs(b); while (b != 0) { var rem = a % b; a = b; b = rem; } return a; } [GeneratedRegex("<[^>]+>")] private static partial Regex HtmlRegex(); } ================================================ FILE: StabilityMatrix.Core/Helper/Webp/WebpReader.cs ================================================ using System.Text; namespace StabilityMatrix.Core.Helper.Webp; public class WebpReader(Stream stream) : BinaryReader(stream, Encoding.ASCII, true) { private uint headerFileSize; public bool GetIsAnimatedFlag() { ReadHeader(); while (BaseStream.Position < headerFileSize) { if (ReadVoidChunk() is "ANMF" or "ANIM") { return true; } } return false; } private void ReadHeader() { // RIFF var riff = ReadBytes(4); if (!riff.SequenceEqual([.."RIFF"u8])) { throw new InvalidDataException("Invalid RIFF header"); } // Size: uint32 headerFileSize = ReadUInt32(); // WEBP var webp = ReadBytes(4); if (!webp.SequenceEqual([.."WEBP"u8])) { throw new InvalidDataException("Invalid WEBP header"); } } // Read a single chunk and discard its contents private string ReadVoidChunk() { // FourCC: 4 bytes in ASCII var result = ReadBytes(4); // Size: uint32 var size = ReadUInt32(); BaseStream.Seek(size, SeekOrigin.Current); return Encoding.ASCII.GetString(result); } } ================================================ FILE: StabilityMatrix.Core/Inference/ComfyClient.cs ================================================ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Net.WebSockets; using System.Text.Json; using NLog; using Polly.Contrib.WaitAndRetry; using Refit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; using StabilityMatrix.Core.Models.FileInterfaces; using Websocket.Client; using Websocket.Client.Exceptions; using Yoh.Text.Json.NamingPolicies; namespace StabilityMatrix.Core.Inference; public class ComfyClient : InferenceClientBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly WebsocketClient webSocketClient; private readonly IComfyApi comfyApi; private bool isDisposed; private readonly JsonSerializerOptions jsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower, Converters = { new NodeConnectionBaseJsonConverter(), new OneOfJsonConverter() } }; // ReSharper disable once MemberCanBePrivate.Global public string ClientId { get; } = Guid.NewGuid().ToString(); public Uri BaseAddress { get; } /// /// If available, the local path to the server root directory. /// public DirectoryPath? LocalServerPath { get; set; } /// /// If available, the local server package pair /// public PackagePair? LocalServerPackage { get; set; } /// /// Path to the "output" folder from LocalServerPath /// public DirectoryPath? OutputImagesDir => LocalServerPath?.JoinDir("output"); /// /// Path to the "input" folder from LocalServerPath /// public DirectoryPath? InputImagesDir => LocalServerPath?.JoinDir("input"); /// /// Dictionary of ongoing prompt execution tasks /// public ConcurrentDictionary PromptTasks { get; } = new(); /// /// Current running prompt task /// private ComfyTask? currentPromptTask; /// /// Event raised when a progress update is received from the server /// public event EventHandler? ProgressUpdateReceived; /// /// Event raised when a status update is received from the server /// public event EventHandler? StatusUpdateReceived; /// /// Event raised when a executing update is received from the server /// public event EventHandler? ExecutingUpdateReceived; /// /// Event raised when a preview image is received from the server /// public event EventHandler? PreviewImageReceived; public ComfyClient(IApiFactory apiFactory, Uri baseAddress) { comfyApi = apiFactory.CreateRefitClient( baseAddress, new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(jsonSerializerOptions), } ); BaseAddress = baseAddress; // Setup websocket client var wsUri = new UriBuilder(baseAddress) { Scheme = "ws", Path = "/ws", Query = $"clientId={ClientId}" }.Uri; webSocketClient = new WebsocketClient(wsUri) { Name = nameof(ComfyClient), ReconnectTimeout = TimeSpan.FromSeconds(30) }; webSocketClient.DisconnectionHappened.Subscribe( info => Logger.Info("Websocket Disconnected, ({Type})", info.Type) ); webSocketClient.ReconnectionHappened.Subscribe( info => Logger.Info("Websocket Reconnected, ({Type})", info.Type) ); webSocketClient.MessageReceived.Subscribe(OnMessageReceived); } private void OnMessageReceived(ResponseMessage message) { switch (message.MessageType) { case WebSocketMessageType.Text: HandleTextMessage(message.Text); break; case WebSocketMessageType.Binary: HandleBinaryMessage(message.Binary); break; case WebSocketMessageType.Close: Logger.Trace("Received ws close message: {Text}", message.Text); break; default: throw new ArgumentOutOfRangeException(nameof(message)); } } private void HandleTextMessage(string text) { ComfyWebSocketResponse? json; try { json = JsonSerializer.Deserialize(text, jsonSerializerOptions); } catch (JsonException e) { Logger.Warn($"Failed to parse json {text} ({e}), skipping"); return; } if (json is null) { Logger.Warn($"Could not parse json {text}, skipping"); return; } Logger.Trace("Received json message: (Type = {Type}, Data = {Data})", json.Type, json.Data); if (json.Type == ComfyWebSocketResponseType.Executing) { var executingData = json.GetDataAsType(jsonSerializerOptions); if (executingData?.PromptId is null) { Logger.Warn($"Could not parse executing data {json.Data}, skipping"); return; } // When Node property is null, it means the prompt has finished executing // remove the task from the dictionary and set the result if (executingData.Node is null) { if (PromptTasks.TryRemove(executingData.PromptId, out var task)) { task.RunningNode = null; task.SetResult(); currentPromptTask = null; } else { Logger.Warn($"Could not find task for prompt {executingData.PromptId}, skipping"); } } // Otherwise set the task's active node to the one received else { if (PromptTasks.TryGetValue(executingData.PromptId, out var task)) { task.RunningNode = executingData.Node; } } ExecutingUpdateReceived?.Invoke(this, executingData); } else if (json.Type == ComfyWebSocketResponseType.Status) { var statusData = json.GetDataAsType(jsonSerializerOptions); if (statusData is null) { Logger.Warn($"Could not parse status data {json.Data}, skipping"); return; } StatusUpdateReceived?.Invoke(this, statusData); } else if (json.Type == ComfyWebSocketResponseType.Progress) { var progressData = json.GetDataAsType(jsonSerializerOptions); if (progressData is null) { Logger.Warn($"Could not parse progress data {json.Data}, skipping"); return; } // Set for the current prompt task currentPromptTask?.OnProgressUpdate(progressData); ProgressUpdateReceived?.Invoke(this, progressData); } else if (json.Type == ComfyWebSocketResponseType.ExecutionError) { if ( json.GetDataAsType(jsonSerializerOptions) is not { } errorData ) { Logger.Warn($"Could not parse ExecutionError data {json.Data}, skipping"); return; } // Set error status if (PromptTasks.TryRemove(errorData.PromptId, out var task)) { task.RunningNode = null; task.SetException( new ComfyNodeException { ErrorData = errorData, JsonData = json.Data.ToString() } ); currentPromptTask = null; } else { Logger.Warn($"Could not find task for prompt {errorData.PromptId}, skipping"); } } else { Logger.Warn($"Unknown message type {json.Type} ({json.Data}), skipping"); } } /// /// Parses binary data (previews) into image streams /// https://github.com/comfyanonymous/ComfyUI/blob/master/server.py#L518 /// private void HandleBinaryMessage(byte[] data) { if (data is not { Length: > 4 }) { Logger.Warn("The input data is null or not long enough."); return; } // The first 4 bytes is int32 of the message type // Subsequent 4 bytes following is int32 of the image format // The rest is the image data // Read the image type from the first 4 bytes of the data. // Python's struct.pack(">I", type_num) will pack the data as a big-endian unsigned int /*var typeBytes = new byte[4]; stream.Read(typeBytes, 0, 4); var imageType = BitConverter.ToInt32(typeBytes, 0);*/ /*if (!BitConverter.IsLittleEndian) { Array.Reverse(typeBytes); }*/ PreviewImageReceived?.Invoke(this, new ComfyWebSocketImageData { ImageBytes = data[8..], }); } public override async Task ConnectAsync(CancellationToken cancellationToken = default) { var delays = Backoff .DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(500), retryCount: 5) .ToImmutableArray(); foreach (var (i, retryDelay) in delays.Enumerate()) { cancellationToken.ThrowIfCancellationRequested(); try { await webSocketClient.StartOrFail().ConfigureAwait(false); return; } catch (WebsocketException e) { Logger.Info( "Failed to connect to websocket, retrying in {RetryDelay} ({Message})", retryDelay, e.Message ); if (i == delays.Length - 1) { throw; } } } } public override async Task CloseAsync(CancellationToken cancellationToken = default) { await webSocketClient.Stop(WebSocketCloseStatus.NormalClosure, string.Empty).ConfigureAwait(false); } public async Task QueuePromptAsync( Dictionary nodes, CancellationToken cancellationToken = default ) { var request = new ComfyPromptRequest { ClientId = ClientId, Prompt = nodes }; var result = await comfyApi.PostPrompt(request, cancellationToken).ConfigureAwait(false); // Add task to dictionary and set it as the current task var task = new ComfyTask(result.PromptId); PromptTasks.TryAdd(result.PromptId, task); currentPromptTask = task; return task; } public async Task InterruptPromptAsync(CancellationToken cancellationToken = default) { await comfyApi.PostInterrupt(cancellationToken).ConfigureAwait(false); // Set the current task to null, and remove it from the dictionary if (currentPromptTask is { } task) { PromptTasks.TryRemove(task.Id, out _); task.TrySetCanceled(cancellationToken); task.Dispose(); currentPromptTask = null; } } // Upload images public Task UploadImageAsync( Stream image, string fileName, CancellationToken cancellationToken = default ) { var streamPart = new StreamPart(image, fileName); return comfyApi.PostUploadImage(streamPart, "true", "input", "Inference", cancellationToken); } /// /// Upload a file to the server at the given relative path from server's root /// public async Task UploadFileAsync( string sourcePath, string destinationRelativePath, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); // Currently there is no api, so we do a local file copy if (LocalServerPath is null) { throw new InvalidOperationException("LocalServerPath is not set"); } var sourceFile = new FilePath(sourcePath); var destFile = LocalServerPath.JoinFile(destinationRelativePath); Logger.Info("Copying file from {Source} to {Dest}", sourcePath, destFile); if (!sourceFile.Exists) { throw new FileNotFoundException("Source file does not exist", sourcePath); } destFile.Directory?.Create(); await sourceFile.CopyToAsync(destFile, true).ConfigureAwait(false); } public async Task?>> GetImagesForExecutedPromptAsync( string promptId, CancellationToken cancellationToken = default ) { // Get history for images var history = await comfyApi.GetHistory(promptId, cancellationToken).ConfigureAwait(false); // Get the current prompt history var current = history[promptId]; var dict = new Dictionary?>(); foreach (var (nodeKey, output) in current.Outputs) { dict[nodeKey] = output.Images; } return dict; } public async Task GetImageStreamAsync( ComfyImage comfyImage, CancellationToken cancellationToken = default ) { var response = await comfyApi .GetImage(comfyImage.FileName, comfyImage.SubFolder, comfyImage.Type, cancellationToken) .ConfigureAwait(false); return response; } /// /// Get a list of strings representing available model names /// public Task?> GetModelNamesAsync(CancellationToken cancellationToken = default) { return GetNodeOptionNamesAsync("CheckpointLoaderSimple", "ckpt_name", cancellationToken); } /// /// Get a list of strings representing available sampler names /// public Task?> GetSamplerNamesAsync(CancellationToken cancellationToken = default) { return GetNodeOptionNamesAsync("KSampler", "sampler_name", cancellationToken); } /// /// Get a list of strings representing available options of a given node /// public async Task?> GetNodeOptionNamesAsync( string nodeName, string optionName, CancellationToken cancellationToken = default ) { var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false); var info = response[nodeName]; return info.Input.GetRequiredValueAsNestedList(optionName); } /// /// Get a list of strings representing available optional options of a given node /// public async Task?> GetOptionalNodeOptionNamesAsync( string nodeName, string optionName, CancellationToken cancellationToken = default ) { var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false); var info = response.GetValueOrDefault(nodeName); return info?.Input.GetOptionalValueAsNestedList(optionName); } public async Task?> GetRequiredNodeOptionNamesFromOptionalNodeAsync( string nodeName, string optionName, CancellationToken cancellationToken = default ) { var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false); var info = response.GetValueOrDefault(nodeName); return info?.Input.GetRequiredValueAsNestedList(optionName); } protected override void Dispose(bool disposing) { if (isDisposed) return; webSocketClient.Dispose(); isDisposed = true; } } ================================================ FILE: StabilityMatrix.Core/Inference/ComfyProgressUpdateEventArgs.cs ================================================ namespace StabilityMatrix.Core.Inference; public readonly record struct ComfyProgressUpdateEventArgs( int Value, int Maximum, string? TaskId, string? RunningNode); ================================================ FILE: StabilityMatrix.Core/Inference/ComfyTask.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; namespace StabilityMatrix.Core.Inference; public class ComfyTask : TaskCompletionSource, IDisposable { public string Id { get; } private string? runningNode; public string? RunningNode { get => runningNode; set { runningNode = value; RunningNodeChanged?.Invoke(this, value); if (value != null) { RunningNodesHistory.Push(value); } } } public Stack RunningNodesHistory { get; } = new(); public ComfyProgressUpdateEventArgs? LastProgressUpdate { get; private set; } public bool HasProgressUpdateStarted => LastProgressUpdate != null; public EventHandler? ProgressUpdate; public event EventHandler? RunningNodeChanged; public ComfyTask(string id) { Id = id; } /// /// Handler for progress updates /// public void OnProgressUpdate(ComfyWebSocketProgressData update) { RunningNodesHistory.TryPeek(out var lastNode); var args = new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, lastNode); ProgressUpdate?.Invoke(this, args); LastProgressUpdate = args; } /// public void Dispose() { ProgressUpdate = null; GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Inference/InferenceClientBase.cs ================================================ namespace StabilityMatrix.Core.Inference; public abstract class InferenceClientBase : IDisposable { /// /// Start the connection /// public virtual Task ConnectAsync(CancellationToken cancellationToken = default) { return Task.CompletedTask; } /// /// Close the connection to remote resources /// public virtual Task CloseAsync(CancellationToken cancellationToken = default) { return Task.CompletedTask; } protected virtual void Dispose(bool disposing) { } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/A3Options.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [SuppressMessage("ReSharper", "IdentifierTypo")] [SuppressMessage("ReSharper", "InconsistentNaming")] public class A3Options { [JsonPropertyName("samples_save")] public bool? SamplesSave { get; set; } [JsonPropertyName("samples_format")] public string? SamplesFormat { get; set; } [JsonPropertyName("samples_filename_pattern")] public string? SamplesFilenamePattern { get; set; } [JsonPropertyName("save_images_add_number")] public bool? SaveImagesAddNumber { get; set; } [JsonPropertyName("grid_save")] public bool? GridSave { get; set; } [JsonPropertyName("grid_format")] public string? GridFormat { get; set; } [JsonPropertyName("grid_extended_filename")] public bool? GridExtendedFilename { get; set; } [JsonPropertyName("grid_only_if_multiple")] public bool? GridOnlyIfMultiple { get; set; } [JsonPropertyName("grid_prevent_empty_spots")] public bool? GridPreventEmptySpots { get; set; } [JsonPropertyName("grid_zip_filename_pattern")] public string? GridZipFilenamePattern { get; set; } [JsonPropertyName("n_rows")] public int? NRows { get; set; } [JsonPropertyName("enable_pnginfo")] public bool? EnablePnginfo { get; set; } [JsonPropertyName("save_txt")] public bool? SaveTxt { get; set; } [JsonPropertyName("save_images_before_face_restoration")] public bool? SaveImagesBeforeFaceRestoration { get; set; } [JsonPropertyName("save_images_before_highres_fix")] public bool? SaveImagesBeforeHighresFix { get; set; } [JsonPropertyName("save_images_before_color_correction")] public bool? SaveImagesBeforeColorCorrection { get; set; } [JsonPropertyName("save_mask")] public bool? SaveMask { get; set; } [JsonPropertyName("save_mask_composite")] public bool? SaveMaskComposite { get; set; } [JsonPropertyName("jpeg_quality")] public int? JpegQuality { get; set; } [JsonPropertyName("webp_lossless")] public bool? WebpLossless { get; set; } [JsonPropertyName("export_for_4chan")] public bool? ExportFor4chan { get; set; } [JsonPropertyName("img_downscale_threshold")] public int? ImgDownscaleThreshold { get; set; } [JsonPropertyName("target_side_length")] public int? TargetSideLength { get; set; } [JsonPropertyName("img_max_size_mp")] public int? ImgMaxSizeMp { get; set; } [JsonPropertyName("use_original_name_batch")] public bool? UseOriginalNameBatch { get; set; } [JsonPropertyName("use_upscaler_name_as_suffix")] public bool? UseUpscalerNameAsSuffix { get; set; } [JsonPropertyName("save_selected_only")] public bool? SaveSelectedOnly { get; set; } [JsonPropertyName("save_init_img")] public bool? SaveInitImg { get; set; } [JsonPropertyName("temp_dir")] public string? TempDir { get; set; } [JsonPropertyName("clean_temp_dir_at_start")] public bool? CleanTempDirAtStart { get; set; } [JsonPropertyName("outdir_samples")] public string? OutdirSamples { get; set; } [JsonPropertyName("outdir_txt2img_samples")] public string? OutdirTxt2ImgSamples { get; set; } [JsonPropertyName("outdir_img2img_samples")] public string? OutdirImg2ImgSamples { get; set; } [JsonPropertyName("outdir_extras_samples")] public string? OutdirExtrasSamples { get; set; } [JsonPropertyName("outdir_grids")] public string? OutdirGrids { get; set; } [JsonPropertyName("outdir_txt2img_grids")] public string? OutdirTxt2ImgGrids { get; set; } [JsonPropertyName("outdir_img2img_grids")] public string? OutdirImg2ImgGrids { get; set; } [JsonPropertyName("outdir_save")] public string? OutdirSave { get; set; } [JsonPropertyName("outdir_init_images")] public string? OutdirInitImages { get; set; } [JsonPropertyName("save_to_dirs")] public bool? SaveToDirs { get; set; } [JsonPropertyName("grid_save_to_dirs")] public bool? GridSaveToDirs { get; set; } [JsonPropertyName("use_save_to_dirs_for_ui")] public bool? UseSaveToDirsForUi { get; set; } [JsonPropertyName("directories_filename_pattern")] public string? DirectoriesFilenamePattern { get; set; } [JsonPropertyName("directories_max_prompt_words")] public int? DirectoriesMaxPromptWords { get; set; } [JsonPropertyName("ESRGAN_tile")] public int? ESRGANTile { get; set; } [JsonPropertyName("ESRGAN_tile_overlap")] public int? ESRGANTileOverlap { get; set; } [JsonPropertyName("realesrgan_enabled_models")] public List? RealEsrganEnabledModels { get; set; } [JsonPropertyName("upscaler_for_img2img")] public string? UpscalerForImg2img { get; set; } [JsonPropertyName("face_restoration_model")] public string? FaceRestorationModel { get; set; } [JsonPropertyName("code_former_weight")] public double? CodeFormerWeight { get; set; } [JsonPropertyName("face_restoration_unload")] public bool? FaceRestorationUnload { get; set; } [JsonPropertyName("show_warnings")] public bool? ShowWarnings { get; set; } [JsonPropertyName("memmon_poll_rate")] public int? MemmonPollRate { get; set; } [JsonPropertyName("samples_log_stdout")] public bool? SamplesLogStdout { get; set; } [JsonPropertyName("multiple_tqdm")] public bool? MultipleTqdm { get; set; } [JsonPropertyName("print_hypernet_extra")] public bool? PrintHypernetExtra { get; set; } [JsonPropertyName("list_hidden_files")] public bool? ListHiddenFiles { get; set; } [JsonPropertyName("unload_models_when_training")] public bool? UnloadModelsWhenTraining { get; set; } [JsonPropertyName("pin_memory")] public bool? PinMemory { get; set; } [JsonPropertyName("save_optimizer_state")] public bool? SaveOptimizerState { get; set; } [JsonPropertyName("save_training_settings_to_txt")] public bool? SaveTrainingSettingsToTxt { get; set; } [JsonPropertyName("dataset_filename_word_regex")] public string? DatasetFilenameWordRegex { get; set; } [JsonPropertyName("dataset_filename_join_string")] public string? DatasetFilenameJoinString { get; set; } [JsonPropertyName("training_image_repeats_per_epoch")] public int? TrainingImageRepeatsPerEpoch { get; set; } [JsonPropertyName("training_write_csv_every")] public int? TrainingWriteCsvEvery { get; set; } [JsonPropertyName("training_xattention_optimizations")] public bool? TrainingXattentionOptimizations { get; set; } [JsonPropertyName("training_enable_tensorboard")] public bool? TrainingEnableTensorboard { get; set; } [JsonPropertyName("training_tensorboard_save_images")] public bool? TrainingTensorboardSaveImages { get; set; } [JsonPropertyName("training_tensorboard_flush_every")] public int? TrainingTensorboardFlushEvery { get; set; } [JsonPropertyName("sd_model_checkpoint")] public string? SdModelCheckpoint { get; set; } [JsonPropertyName("sd_checkpoint_cache")] public int? SdCheckpointCache { get; set; } [JsonPropertyName("sd_vae_checkpoint_cache")] public int? SdVaeCheckpointCache { get; set; } [JsonPropertyName("sd_vae")] public string? SdVae { get; set; } [JsonPropertyName("sd_vae_as_default")] public bool? SdVaeAsDefault { get; set; } [JsonPropertyName("sd_unet")] public string? SdUnet { get; set; } [JsonPropertyName("inpainting_mask_weight")] public int? InpaintingMaskWeight { get; set; } [JsonPropertyName("initial_noise_multiplier")] public int? InitialNoiseMultiplier { get; set; } [JsonPropertyName("img2img_color_correction")] public bool? Img2imgColorCorrection { get; set; } [JsonPropertyName("img2img_fix_steps")] public bool? Img2imgFixSteps { get; set; } [JsonPropertyName("img2img_background_color")] public string? Img2ImgBackgroundColor { get; set; } [JsonPropertyName("enable_quantization")] public bool? EnableQuantization { get; set; } [JsonPropertyName("enable_emphasis")] public bool? EnableEmphasis { get; set; } [JsonPropertyName("enable_batch_seeds")] public bool? EnableBatchSeeds { get; set; } [JsonPropertyName("comma_padding_backtrack")] public int? CommaPaddingBacktrack { get; set; } [JsonPropertyName("CLIP_stop_at_last_layers")] public int? CLIPStopAtLastLayers { get; set; } [JsonPropertyName("upcast_attn")] public bool? UpcastAttn { get; set; } [JsonPropertyName("randn_source")] public string? RandNSource { get; set; } [JsonPropertyName("cross_attention_optimization")] public string? CrossAttentionOptimization { get; set; } [JsonPropertyName("s_min_uncond")] public int? SMinUncond { get; set; } [JsonPropertyName("token_merging_ratio")] public int? TokenMergingRatio { get; set; } [JsonPropertyName("token_merging_ratio_img2img")] public int? TokenMergingRatioImg2Img { get; set; } [JsonPropertyName("token_merging_ratio_hr")] public int? TokenMergingRatioHr { get; set; } [JsonPropertyName("pad_cond_uncond")] public bool? PadCondUncond { get; set; } [JsonPropertyName("experimental_persistent_cond_cache")] public bool? ExperimentalPersistentCondCache { get; set; } [JsonPropertyName("use_old_emphasis_implementation")] public bool? UseOldEmphasisImplementation { get; set; } [JsonPropertyName("use_old_karras_scheduler_sigmas")] public bool? UseOldKarrasSchedulerSigmas { get; set; } [JsonPropertyName("no_dpmpp_sde_batch_determinism")] public bool? NoDpmppSdeBatchDeterminism { get; set; } [JsonPropertyName("use_old_hires_fix_width_height")] public bool? UseOldHiresFixWidthHeight { get; set; } [JsonPropertyName("dont_fix_second_order_samplers_schedule")] public bool? DontFixSecondOrderSamplersSchedule { get; set; } [JsonPropertyName("hires_fix_use_firstpass_conds")] public bool? HiresFixUseFirstpassConds { get; set; } [JsonPropertyName("interrogate_keep_models_in_memory")] public bool? InterrogateKeepModelsInMemory { get; set; } [JsonPropertyName("interrogate_return_ranks")] public bool? InterrogateReturnRanks { get; set; } [JsonPropertyName("interrogate_clip_num_beams")] public int? InterrogateClipNumBeams { get; set; } [JsonPropertyName("interrogate_clip_min_length")] public int? InterrogateClipMinLength { get; set; } [JsonPropertyName("interrogate_clip_max_length")] public int? InterrogateClipMaxLength { get; set; } [JsonPropertyName("interrogate_clip_dict_limit")] public int? InterrogateClipDictLimit { get; set; } [JsonPropertyName("interrogate_clip_skip_categories")] public List? InterrogateClipSkipCategories { get; set; } [JsonPropertyName("interrogate_deepbooru_score_threshold")] public double? InterrogateDeepbooruScoreThreshold { get; set; } [JsonPropertyName("deepbooru_sort_alpha")] public bool? DeepbooruSortAlpha { get; set; } [JsonPropertyName("deepbooru_use_spaces")] public bool? DeepbooruUseSpaces { get; set; } [JsonPropertyName("deepbooru_escape")] public bool? DeepbooruEscape { get; set; } [JsonPropertyName("deepbooru_filter_tags")] public string? DeepbooruFilterTags { get; set; } [JsonPropertyName("extra_networks_show_hidden_directories")] public bool? ExtraNetworksShowHiddenDirectories { get; set; } [JsonPropertyName("extra_networks_hidden_models")] public string? ExtraNetworksHiddenModels { get; set; } [JsonPropertyName("extra_networks_default_view")] public string? ExtraNetworksDefaultView { get; set; } [JsonPropertyName("extra_networks_default_multiplier")] public int? ExtraNetworksDefaultMultiplier { get; set; } [JsonPropertyName("extra_networks_card_width")] public int? ExtraNetworksCardWidth { get; set; } [JsonPropertyName("extra_networks_card_height")] public int? ExtraNetworksCardHeight { get; set; } [JsonPropertyName("extra_networks_add_text_separator")] public string? ExtraNetworksAddTextSeparator { get; set; } [JsonPropertyName("ui_extra_networks_tab_reorder")] public string? UiExtraNetworksTabReorder { get; set; } [JsonPropertyName("sd_hypernetwork")] public string? SdHypernetwork { get; set; } [JsonPropertyName("localization")] public string? Localization { get; set; } [JsonPropertyName("gradio_theme")] public string? GradioTheme { get; set; } [JsonPropertyName("img2img_editor_height")] public int? Img2ImgEditorHeight { get; set; } [JsonPropertyName("return_grid")] public bool? ReturnGrid { get; set; } [JsonPropertyName("return_mask")] public bool? ReturnMask { get; set; } [JsonPropertyName("return_mask_composite")] public bool? ReturnMaskComposite { get; set; } [JsonPropertyName("do_not_show_images")] public bool? DoNotShowImages { get; set; } [JsonPropertyName("send_seed")] public bool? SendSeed { get; set; } [JsonPropertyName("send_size")] public bool? SendSize { get; set; } [JsonPropertyName("font")] public string? Font { get; set; } [JsonPropertyName("js_modal_lightbox")] public bool? JsModalLightbox { get; set; } [JsonPropertyName("js_modal_lightbox_initially_zoomed")] public bool? JsModalLightboxInitiallyZoomed { get; set; } [JsonPropertyName("js_modal_lightbox_gamepad")] public bool? JsModalLightboxGamepad { get; set; } [JsonPropertyName("js_modal_lightbox_gamepad_repeat")] public int? JsModalLightboxGamepadRepeat { get; set; } [JsonPropertyName("show_progress_in_title")] public bool? ShowProgressInTitle { get; set; } [JsonPropertyName("samplers_in_dropdown")] public bool? SamplersInDropdown { get; set; } [JsonPropertyName("dimensions_and_batch_together")] public bool? DimensionsAndBatchTogether { get; set; } [JsonPropertyName("keyedit_precision_attention")] public double? KeyEditPrecisionAttention { get; set; } [JsonPropertyName("keyedit_precision_extra")] public double? KeyEditPrecisionExtra { get; set; } [JsonPropertyName("keyedit_delimiters")] public string? KeyEditDelimiters { get; set; } [JsonPropertyName("hires_fix_show_sampler")] public bool? HiresFixShowSampler { get; set; } [JsonPropertyName("hires_fix_show_prompts")] public bool? HiresFixShowPrompts { get; set; } [JsonPropertyName("disable_token_counters")] public bool? DisableTokenCounters { get; set; } [JsonPropertyName("add_model_hash_to_info")] public bool? AddModelHashToInfo { get; set; } [JsonPropertyName("add_model_name_to_info")] public bool? AddModelNameToInfo { get; set; } [JsonPropertyName("add_version_to_infotext")] public bool? AddVersionToInfotext { get; set; } [JsonPropertyName("disable_weights_auto_swap")] public bool? DisableWeightsAutoSwap { get; set; } [JsonPropertyName("infotext_styles")] public string? InfotextStyles { get; set; } [JsonPropertyName("show_progressbar")] public bool? ShowProgressbar { get; set; } [JsonPropertyName("live_previews_enable")] public bool? LivePreviewsEnable { get; set; } [JsonPropertyName("live_previews_image_format")] public string? LivePreviewsImageFormat { get; set; } [JsonPropertyName("show_progress_grid")] public bool? ShowProgressGrid { get; set; } [JsonPropertyName("show_progress_every_n_steps")] public int? ShowProgressEveryNSteps { get; set; } [JsonPropertyName("show_progress_type")] public string? ShowProgressType { get; set; } [JsonPropertyName("live_preview_content")] public string? LivePreviewContent { get; set; } [JsonPropertyName("live_preview_refresh_period")] public int? LivePreviewRefreshPeriod { get; set; } // TODO: hide_samplers [JsonPropertyName("eta_ddim")] public int? EtaDdim { get; set; } [JsonPropertyName("eta_ancestral")] public int? EtaAncestral { get; set; } [JsonPropertyName("ddim_discretize")] public string? DDIMDiscretize { get; set; } [JsonPropertyName("s_churn")] public int? SChurn { get; set; } [JsonPropertyName("s_tmin")] public int? STmin { get; set; } [JsonPropertyName("s_noise")] public int? SNoise { get; set; } [JsonPropertyName("k_sched_type")] public string? KSchedType { get; set; } [JsonPropertyName("sigma_min")] public int? SigmaMin { get; set; } [JsonPropertyName("sigma_max")] public int? SigmaMax { get; set; } [JsonPropertyName("rho")] public int? Rho { get; set; } [JsonPropertyName("eta_noise_seed_delta")] public int? EtaNoiseSeedDelta { get; set; } [JsonPropertyName("always_discard_next_to_last_sigma")] public bool? AlwaysDiscardNextToLastSigma { get; set; } [JsonPropertyName("uni_pc_variant")] public string? UniPcVariant { get; set; } [JsonPropertyName("uni_pc_skip_type")] public string? UniPcSkipType { get; set; } [JsonPropertyName("uni_pc_order")] public int? UniPcOrder { get; set; } [JsonPropertyName("uni_pc_lower_order_final")] public bool? UniPcLowerOrderFinal { get; set; } // TODO: postprocessing_enable_in_main_ui // TODO: postprocessing_operation_order [JsonPropertyName("upscaling_max_images_in_cache")] public int? UpscalingMaxImagesInCache { get; set; } [JsonPropertyName("disabled_extensions")] public List? DisabledExtensions { get; set; } [JsonPropertyName("disable_all_extensions")] public string? DisableAllExtensions { get; set; } [JsonPropertyName("restore_config_state_file")] public string? RestoreConfigStateFile { get; set; } [JsonPropertyName("sd_checkpoint_hash")] public string? SdCheckpointHash { get; set; } [JsonPropertyName("sd_lora")] public string? SdLora { get; set; } [JsonPropertyName("lora_preferred_name")] public string? LoraPreferredName { get; set; } [JsonPropertyName("lora_add_hashes_to_infotext")] public bool? LoraAddHashesToInfotext { get; set; } [JsonPropertyName("lora_functional")] public bool? LoraFunctional { get; set; } [JsonPropertyName("canvas_hotkey_move")] public string? CanvasHotkeyMove { get; set; } [JsonPropertyName("canvas_hotkey_fullscreen")] public string? CanvasHotkeyFullscreen { get; set; } [JsonPropertyName("canvas_hotkey_reset")] public string? CanvasHotkeyReset { get; set; } [JsonPropertyName("canvas_hotkey_overlap")] public string? CanvasHotkeyOverlap { get; set; } [JsonPropertyName("canvas_show_tooltip")] public bool? CanvasShowTooltip { get; set; } [JsonPropertyName("canvas_swap_controls")] public bool? CanvasSwapControls { get; set; } // TODO: List ExtraOptions [JsonPropertyName("extra_options_accordion")] public bool? ExtraOptionsAccordion { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitAccountStatusUpdateEventArgs.cs ================================================ using StabilityMatrix.Core.Models.Api.CivitTRPC; namespace StabilityMatrix.Core.Models.Api; public class CivitAccountStatusUpdateEventArgs : EventArgs { public static CivitAccountStatusUpdateEventArgs Disconnected { get; } = new(); public bool IsConnected { get; init; } public CivitUserProfileResponse? UserProfile { get; init; } public string? UsernameWithParentheses => string.IsNullOrEmpty(UserProfile?.Username) ? null : $"({UserProfile.Username})"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitBaseModelType.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] public enum CivitBaseModelType { All, [StringValue("AuraFlow")] AuraFlow, [StringValue("CogVideoX")] CogVideoX, [StringValue("Flux.1 S")] Flux1S, [StringValue("Flux.1 D")] Flux1D, [StringValue("HiDream")] HiDream, [StringValue("Hunyuan 1")] Hunyuan1, [StringValue("Hunyuan Video")] HunyuanVideo, [StringValue("Illustrious")] Illustrious, [StringValue("Kolors")] Kolors, [StringValue("LTXV")] Ltxv, [StringValue("Lumina")] Lumina, [StringValue("Mochi")] Mochi, [StringValue("NoobAI")] NoobAi, [StringValue("ODOR")] Odor, [StringValue("PixArt a")] PixArtA, [StringValue("PixArt E")] PixArtE, [StringValue("Playground v2")] PlaygroundV2, [StringValue("Pony")] Pony, [StringValue("SD 1.4")] Sd14, [StringValue("SD 1.5")] Sd15, [StringValue("SD 1.5 Hyper")] Sd15Hyper, [StringValue("SD 1.5 LCM")] Sd15Lcm, [StringValue("SD 2.0")] Sd20, [StringValue("SD 2.0 768")] Sd20_768, [StringValue("SD 2.1")] Sd21, [StringValue("SD 2.1 768")] Sd21_768, [StringValue("SD 2.1 Unclip")] Sd21Unclip, [StringValue("SD 3")] Sd3, [StringValue("SD 3.5")] Sd35, [StringValue("SD 3.5 Large")] Sd35Large, [StringValue("SD 3.5 Large Turbo")] Sd35LargeTurbo, [StringValue("SD 3.5 Medium")] Sd35Medium, [StringValue("SDXL 0.9")] Sdxl09, [StringValue("SDXL 1.0")] Sdxl10, [StringValue("SDXL 1.0 LCM")] Sdxl10Lcm, [StringValue("SDXL Distilled")] SdxlDistilled, [StringValue("SDXL Hyper")] SdxlHyper, [StringValue("SDXL Lightning")] SdxlLightning, [StringValue("SDXL Turbo")] SdxlTurbo, [StringValue("SVD")] SVD, [StringValue("SVD XT")] SvdXt, [StringValue("Stable Cascade")] StableCascade, [StringValue("Wan Video")] WanVideo, Other, } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitCommercialUse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] public enum CivitCommercialUse { None, Image, Rent, Sell } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitCreator.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitCreator { [JsonPropertyName("username")] public string? Username { get; init; } [JsonPropertyName("image")] public string? Image { get; init; } [JsonIgnore] public string? ProfileUrl => Username is null ? null : $"https://civitai.com/user/{Username}"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitFile.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class CivitFile { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("sizeKB")] public double SizeKb { get; set; } [JsonPropertyName("pickleScanResult")] public string PickleScanResult { get; set; } [JsonPropertyName("virusScanResult")] public string VirusScanResult { get; set; } [JsonPropertyName("scannedAt")] public DateTime? ScannedAt { get; set; } [JsonPropertyName("metadata")] public CivitFileMetadata Metadata { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("downloadUrl")] public string DownloadUrl { get; set; } [JsonPropertyName("hashes")] public CivitFileHashes Hashes { get; set; } [JsonPropertyName("type")] public CivitFileType Type { get; set; } [JsonPropertyName("primary")] public bool IsPrimary { get; set; } private FileSizeType? fullFilesSize; public FileSizeType FullFilesSize { get { if (fullFilesSize != null) return fullFilesSize; fullFilesSize = new FileSizeType(SizeKb); return fullFilesSize; } } public string DisplayName => Path.GetFileNameWithoutExtension(Name); } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitFileHashes.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitFileHashes { public string? SHA256 { get; set; } public string? CRC32 { get; set; } public string? BLAKE3 { get; set; } [JsonIgnore] public string ShortSha256 => SHA256?[..8] ?? string.Empty; [JsonIgnore] public string ShortBlake3 => BLAKE3?[..8] ?? string.Empty; } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitFileMetadata.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitFileMetadata { [JsonPropertyName("fp")] public string? Fp { get; set; } [JsonPropertyName("size")] public string? Size { get; set; } [JsonPropertyName("format")] public CivitModelFormat? Format { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitFileType.cs ================================================ using System.Runtime.Serialization; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(DefaultUnknownEnumConverter))] public enum CivitFileType { Unknown, Model, VAE, Config, Archive, [EnumMember(Value = "Pruned Model")] PrunedModel, [EnumMember(Value = "Training Data")] TrainingData } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitImage.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class CivitImage { [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("nsfwLevel")] public int? NsfwLevel { get; set; } [JsonPropertyName("width")] public int Width { get; set; } [JsonPropertyName("height")] public int Height { get; set; } [JsonPropertyName("hash")] public string Hash { get; set; } [JsonPropertyName("type")] public string Type { get; set; } // TODO: "meta" ( object? ) } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitMetadata.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class CivitMetadata { [JsonPropertyName("totalItems")] public int TotalItems { get; set; } [JsonPropertyName("currentPage")] public int CurrentPage { get; set; } [JsonPropertyName("pageSize")] public int PageSize { get; set; } [JsonPropertyName("totalPages")] public int TotalPages { get; set; } [JsonPropertyName("nextPage")] public string? NextPage { get; set; } [JsonPropertyName("prevPage")] public string? PrevPage { get; set; } [JsonPropertyName("nextCursor")] public string? NextCursor { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitMode.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] public enum CivitMode { Archived, TakenDown } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModel.cs ================================================ using System.Text.Json.Serialization; using LiteDB; namespace StabilityMatrix.Core.Models.Api; public class CivitModel { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("description")] public string? Description { get; set; } [JsonPropertyName("type")] public CivitModelType Type { get; set; } [JsonPropertyName("nsfw")] public bool Nsfw { get; set; } [JsonPropertyName("tags")] public string[] Tags { get; set; } [JsonPropertyName("mode")] public CivitMode? Mode { get; set; } [JsonPropertyName("creator")] public CivitCreator? Creator { get; set; } [JsonPropertyName("stats")] public CivitModelStats Stats { get; set; } [BsonRef("ModelVersions")] [JsonPropertyName("modelVersions")] public List? ModelVersions { get; set; } private FileSizeType? fullFilesSize; public FileSizeType FullFilesSize { get { if (fullFilesSize != null) return fullFilesSize; var kbs = 0.0; var latestVersion = ModelVersions?.FirstOrDefault(); if (latestVersion?.Files != null && latestVersion.Files.Any()) { var latestModelFile = latestVersion.Files.FirstOrDefault(x => x.Type == CivitFileType.Model); kbs = latestModelFile?.SizeKb ?? 0; } fullFilesSize = new FileSizeType(kbs); return fullFilesSize; } } public string LatestModelVersionName => ModelVersions is { Count: > 0 } ? ModelVersions[0].Name : string.Empty; public string? BaseModelType => ModelVersions is { Count: > 0 } ? ModelVersions[0].BaseModel?.Replace("SD", "").Trim() : string.Empty; public CivitModelStats ModelVersionStats => ModelVersions is { Count: > 0 } ? ModelVersions[0].Stats : new CivitModelStats(); public string LatestVersionCreatedAt => ( ModelVersions is { Count: > 0 } ? ModelVersions[0].PublishedAt ?? DateTimeOffset.MinValue : DateTimeOffset.MinValue ).ToString("d", System.Globalization.CultureInfo.CurrentCulture); } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelFormat.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(DefaultUnknownEnumConverter))] public enum CivitModelFormat { Unknown, SafeTensor, PickleTensor, Diffusers, GGUF, Other } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelFpType.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] [SuppressMessage("ReSharper", "InconsistentNaming")] public enum CivitModelFpType { bf16, fp16, fp32, tf32, fp8, nf4, } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelSize.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] [SuppressMessage("ReSharper", "InconsistentNaming")] public enum CivitModelSize { full, pruned, } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelStats.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitModelStats : CivitStats { [JsonPropertyName("favoriteCount")] public int FavoriteCount { get; set; } [JsonPropertyName("commentCount")] public int CommentCount { get; set; } [JsonPropertyName("thumbsUpCount")] public int ThumbsUpCount { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelType.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(DefaultUnknownEnumConverter))] [SuppressMessage("ReSharper", "InconsistentNaming")] public enum CivitModelType { Unknown, [ConvertTo(SharedFolderType.StableDiffusion)] Checkpoint, [ConvertTo(SharedFolderType.Embeddings)] TextualInversion, [ConvertTo(SharedFolderType.Hypernetwork)] Hypernetwork, [ConvertTo(SharedFolderType.Lora)] DoRA, [ConvertTo(SharedFolderType.Lora)] LORA, [ConvertTo(SharedFolderType.ControlNet)] Controlnet, [ConvertTo(SharedFolderType.LyCORIS)] LoCon, [ConvertTo(SharedFolderType.VAE)] VAE, // Unused/obsolete/unknown/meta options AestheticGradient, Model, MotionModule, Poses, [ConvertTo(SharedFolderType.ESRGAN)] Upscaler, Wildcards, Workflows, Other, All } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelVersion.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class CivitModelVersion { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("description")] public string Description { get; set; } [JsonPropertyName("downloadUrl")] public string DownloadUrl { get; set; } [JsonPropertyName("trainedWords")] public string[] TrainedWords { get; set; } [JsonPropertyName("baseModel")] public string? BaseModel { get; set; } [JsonPropertyName("availability")] public string? Availability { get; set; } [JsonPropertyName("files")] public List? Files { get; set; } [JsonPropertyName("images")] public List? Images { get; set; } [JsonPropertyName("stats")] public CivitModelStats Stats { get; set; } [JsonPropertyName("publishedAt")] public DateTimeOffset? PublishedAt { get; set; } [JsonIgnore] public bool IsEarlyAccess => Availability?.Equals("EarlyAccess", StringComparison.OrdinalIgnoreCase) ?? false; } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelVersionResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitModelVersionResponse( [property: JsonPropertyName("id")] int Id, [property: JsonPropertyName("modelId")] int ModelId, [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("baseModel")] string BaseModel, [property: JsonPropertyName("files")] List Files, [property: JsonPropertyName("images")] List Images, [property: JsonPropertyName("downloadUrl")] string DownloadUrl ); ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs ================================================ using Refit; namespace StabilityMatrix.Core.Models.Api; public class CivitModelsRequest { /// /// The number of results to be returned per page. This can be a number between 1 and 200. By default, each page will return 100 results /// [AliasAs("limit")] public int? Limit { get; set; } /// /// The page from which to start fetching models /// [AliasAs("page")] public int? Page { get; set; } /// /// Search query to filter models by name /// [AliasAs("query")] public string? Query { get; set; } /// /// Search query to filter models by tag /// [AliasAs("tag")] public string? Tag { get; set; } /// /// Search query to filter models by user /// [AliasAs("username")] public string? Username { get; set; } /// /// The type of model you want to filter with. If none is specified, it will return all types /// [AliasAs("types")] public CivitModelType[]? Types { get; set; } /// /// The order in which you wish to sort the results /// [AliasAs("sort")] public CivitSortMode? Sort { get; set; } /// /// The time frame in which the models will be sorted /// [AliasAs("period")] public CivitPeriod? Period { get; set; } /// /// The rating you wish to filter the models with. If none is specified, it will return models with any rating /// [AliasAs("rating")] public int? Rating { get; set; } /// /// Filter to models that require or don't require crediting the creator /// Requires Authentication /// [AliasAs("favorites")] public bool? Favorites { get; set; } /// /// Filter to hidden models of the authenticated user /// Requires Authentication /// [AliasAs("hidden")] public bool? Hidden { get; set; } /// /// Only include the primary file for each model (This will use your preferred format options if you use an API token or session cookie) /// [AliasAs("primaryFileOnly")] public bool? PrimaryFileOnly { get; set; } /// /// Filter to models that allow or don't allow creating derivatives /// [AliasAs("allowDerivatives")] public bool? AllowDerivatives { get; set; } /// /// Filter to models that allow or don't allow derivatives to have a different license /// [AliasAs("allowDifferentLicenses")] public bool? AllowDifferentLicenses { get; set; } /// /// Filter to models based on their commercial permissions /// [AliasAs("allowCommercialUse")] public CivitCommercialUse? AllowCommercialUse { get; set; } /// /// If false, will return safer images and hide models that don't have safe images /// [AliasAs("nsfw")] public string? Nsfw { get; set; } /// /// options:
/// SD 1.4
/// SD 1.5
/// SD 2.0
/// SD 2.0 768
/// SD 2.1
/// SD 2.1 768
/// SD 2.1 Unclip
/// SDXL 0.9
/// SDXL 1.0 ///
[AliasAs("baseModels")] [Query(CollectionFormat.Multi)] public string[]? BaseModels { get; set; } [AliasAs("ids")] public string CommaSeparatedModelIds { get; set; } [AliasAs("cursor")] public string? Cursor { get; set; } public override string ToString() { return $"Page: {Page}, " + $"Query: {Query}, " + $"Tag: {Tag}, " + $"Username: {Username}, " + $"Types: {Types}, " + $"Sort: {Sort}, " + $"Period: {Period}, " + $"Rating: {Rating}, " + $"Nsfw: {Nsfw}, " + $"BaseModel: {string.Join(",", BaseModels ?? [])}, " + $"CommaSeparatedModelIds: {CommaSeparatedModelIds}"; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitModelsResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class CivitModelsResponse { [JsonPropertyName("items")] public List? Items { get; set; } [JsonPropertyName("metadata")] public CivitMetadata? Metadata { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitPeriod.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] public enum CivitPeriod { AllTime, Year, Month, Week, Day } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitSortMode.cs ================================================ using System.Runtime.Serialization; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; [JsonConverter(typeof(JsonStringEnumConverter))] public enum CivitSortMode { [EnumMember(Value = "Highest Rated")] HighestRated, [EnumMember(Value = "Most Downloaded")] MostDownloaded, [EnumMember(Value = "Newest")] Newest, [EnumMember(Value = "Installed")] Installed, [EnumMember(Value = "Favorites")] Favorites, } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitStats.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public record CivitStats { [JsonPropertyName("downloadCount")] public int DownloadCount { get; set; } [JsonPropertyName("ratingCount")] public int RatingCount { get; set; } [JsonPropertyName("rating")] public double Rating { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitApiTokens.cs ================================================ namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitApiTokens(string ApiToken, string Username); ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitGetUserByIdRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitGetUserByIdRequest : IFormattable { [JsonPropertyName("id")] public required int Id { get; set; } /// public string ToString(string? format, IFormatProvider? formatProvider) { return JsonSerializer.Serialize(new { json = this }); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitGetUserByIdResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitGetUserByIdResponse(int Id, string Username, string? Image); ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitImageGenerationDataResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public class CivitImageGenerationDataResponse { [JsonPropertyName("process")] public string? Process { get; set; } [JsonPropertyName("meta")] public CivitImageMetadata? Metadata { get; set; } [JsonPropertyName("resources")] public List? Resources { get; set; } [JsonIgnore] public IReadOnlyDictionary? OtherMetadata { get; set; } } public class CivitImageMetadata { [JsonPropertyName("prompt")] public string? Prompt { get; set; } [JsonPropertyName("negativePrompt")] public string? NegativePrompt { get; set; } [JsonPropertyName("cfgScale")] public double? CfgScale { get; set; } [JsonPropertyName("steps")] public int? Steps { get; set; } [JsonPropertyName("sampler")] public string? Sampler { get; set; } [JsonPropertyName("seed")] public long? Seed { get; set; } [JsonPropertyName("Eta")] public string? Eta { get; set; } [JsonPropertyName("RNG")] public string? Rng { get; set; } [JsonPropertyName("ENSD")] public string? Ensd { get; set; } [JsonPropertyName("Size")] public string? Size { get; set; } [JsonPropertyName("width")] public int? Width { get; set; } [JsonPropertyName("height")] public int? Height { get; set; } [JsonPropertyName("Model")] public string? Model { get; set; } [JsonPropertyName("Version")] public string? Version { get; set; } [JsonPropertyName("resources")] public List? Resources { get; set; } [JsonPropertyName("ModelHash")] public string? ModelHash { get; set; } [JsonPropertyName("Hires steps")] public string? HiresSteps { get; set; } [JsonPropertyName("Hires upscale")] public string? HiresUpscaleAmount { get; set; } [JsonPropertyName("Schedule type")] public string? ScheduleType { get; set; } [JsonPropertyName("Hires upscaler")] public string? HiresUpscaler { get; set; } [JsonPropertyName("Denoising strength")] public string? DenoisingStrength { get; set; } [JsonPropertyName("clipSkip")] public int? ClipSkip { get; set; } [JsonPropertyName("scheduler")] public string? Scheduler { get; set; } [JsonIgnore] public string Dimensions => string.IsNullOrWhiteSpace(Size) ? $"{Width}x{Height}" : Size; } public class CivitImageResource { public string? Hash { get; set; } public string? Name { get; set; } public string? Type { get; set; } public int ModelId { get; set; } public string? ModelType { get; set; } public string? ModelName { get; set; } public int VersionId { get; set; } public string? VersionName { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitUserAccountResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitUserAccountResponse(int Id, int Balance, int LifetimeBalance); public record CivitTrpcResponse { [JsonPropertyName("result")] public required CivitTrpcResponseData Result { get; set; } public record CivitTrpcResponseData { [JsonPropertyName("data")] public required CivitTrpcResponseDataJson Data { get; set; } } public record CivitTrpcResponseDataJson { [JsonPropertyName("Json")] public required TJson Json { get; set; } } } /// /// Like CivitTrpcResponse, but wrapped as the first item of an array. /// /// public record CivitTrpcArrayResponse { [JsonPropertyName("result")] public required CivitTrpcResponseData Result { get; set; } [JsonIgnore] public T? InnerJson => Result.Data.Json.FirstOrDefault(); public record CivitTrpcResponseData { [JsonPropertyName("data")] public required CivitTrpcResponseDataJson Data { get; set; } } public record CivitTrpcResponseDataJson { [JsonPropertyName("Json")] public required List Json { get; set; } } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitUserProfileRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using System.Web; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitUserProfileRequest : IFormattable { [JsonPropertyName("username")] public required string Username { get; set; } [JsonPropertyName("authed")] public bool Authed { get; set; } /// public string ToString(string? format, IFormatProvider? formatProvider) { return JsonSerializer.Serialize(new { json = this }); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitUserProfileResponse.cs ================================================ using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; /* * Example: * { "result": { "data": { "json": { "id": 1020931, "username": "owo", "deletedAt": null, "image": "https://lh3.googleusercontent.com/a/...", "leaderboardShowcase": null, "createdAt": "2023-02-01T21:05:31.125Z", "cosmetics": [], "links": [], "rank": null, "stats": null, "profile": { "bio": null, "coverImageId": null, "coverImage": null, "message": null, "messageAddedAt": null, "profileSectionsSettings": [ { "key": "showcase", "enabled": true }, { "key": "popularModels", "enabled": true }, { "key": "popularArticles", "enabled": true }, { "key": "modelsOverview", "enabled": true }, { "key": "imagesOverview", "enabled": true }, { "key": "recentReviews", "enabled": true } ], "privacySettings": { "showFollowerCount": true, "showReviewsRating": true, "showFollowingCount": true }, "showcaseItems": [], "location": null, "nsfw": false, "userId": 1020931 } }, "meta": { "values": { "createdAt": [ "Date" ] } } } } } * */ public record CivitUserProfileResponse { [JsonPropertyName("result")] public required JsonObject Result { get; init; } public int? UserId => Result["data"]?["json"]?["id"]?.GetValue(); public string? Username => Result["data"]?["json"]?["username"]?.GetValue(); public string? ImageUrl => Result["data"]?["json"]?["image"]?.GetValue(); public DateTimeOffset? CreatedAt => Result["data"]?["json"]?["createdAt"]?.GetValue(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/CivitTRPC/CivitUserToggleFavoriteModelRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.CivitTRPC; public record CivitUserToggleFavoriteModelRequest : IFormattable { [JsonPropertyName("modelId")] public required int ModelId { get; set; } [JsonPropertyName("authed")] public bool Authed { get; set; } = true; /// public string ToString(string? format, IFormatProvider? formatProvider) { return JsonSerializer.Serialize(new { json = this }); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyAuxPreprocessor.cs ================================================ using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; namespace StabilityMatrix.Core.Models.Api.Comfy; /// /// Collection of preprocessors included in /// /// [PublicAPI] [SuppressMessage("ReSharper", "InconsistentNaming")] public record ComfyAuxPreprocessor(string Value) : StringValue(Value) { public static ComfyAuxPreprocessor None { get; } = new("none"); public static ComfyAuxPreprocessor AnimeFaceSemSegPreprocessor { get; } = new("AnimeFace_SemSegPreprocessor"); public static ComfyAuxPreprocessor BinaryPreprocessor { get; } = new("BinaryPreprocessor"); public static ComfyAuxPreprocessor CannyEdgePreprocessor { get; } = new("CannyEdgePreprocessor"); public static ComfyAuxPreprocessor ColorPreprocessor { get; } = new("ColorPreprocessor"); public static ComfyAuxPreprocessor DensePosePreprocessor { get; } = new("DensePosePreprocessor"); public static ComfyAuxPreprocessor DepthAnythingPreprocessor { get; } = new("DepthAnythingPreprocessor"); public static ComfyAuxPreprocessor ZoeDepthAnythingPreprocessor { get; } = new("Zoe_DepthAnythingPreprocessor"); public static ComfyAuxPreprocessor DiffusionEdgePreprocessor { get; } = new("DiffusionEdge_Preprocessor"); public static ComfyAuxPreprocessor DWPreprocessor { get; } = new("DWPreprocessor"); public static ComfyAuxPreprocessor AnimalPosePreprocessor { get; } = new("AnimalPosePreprocessor"); public static ComfyAuxPreprocessor HEDPreprocessor { get; } = new("HEDPreprocessor"); public static ComfyAuxPreprocessor FakeScribblePreprocessor { get; } = new("FakeScribblePreprocessor"); public static ComfyAuxPreprocessor LeReSDepthMapPreprocessor { get; } = new("LeReS-DepthMapPreprocessor"); public static ComfyAuxPreprocessor LineArtPreprocessor { get; } = new("LineArtPreprocessor"); public static ComfyAuxPreprocessor AnimeLineArtPreprocessor { get; } = new("AnimeLineArtPreprocessor"); public static ComfyAuxPreprocessor LineartStandardPreprocessor { get; } = new("LineartStandardPreprocessor"); public static ComfyAuxPreprocessor Manga2AnimeLineArtPreprocessor { get; } = new("Manga2Anime_LineArt_Preprocessor"); public static ComfyAuxPreprocessor MediaPipeFaceMeshPreprocessor { get; } = new("MediaPipe-FaceMeshPreprocessor"); public static ComfyAuxPreprocessor MeshGraphormerDepthMapPreprocessor { get; } = new("MeshGraphormer-DepthMapPreprocessor"); public static ComfyAuxPreprocessor MiDaSNormalMapPreprocessor { get; } = new("MiDaS-NormalMapPreprocessor"); public static ComfyAuxPreprocessor MiDaSDepthMapPreprocessor { get; } = new("MiDaS-DepthMapPreprocessor"); public static ComfyAuxPreprocessor MLSDPreprocessor { get; } = new("M-LSDPreprocessor"); public static ComfyAuxPreprocessor BAENormalMapPreprocessor { get; } = new("BAE-NormalMapPreprocessor"); public static ComfyAuxPreprocessor OneFormerCOCOSemSegPreprocessor { get; } = new("OneFormer-COCO-SemSegPreprocessor"); public static ComfyAuxPreprocessor OneFormerADE20KSemSegPreprocessor { get; } = new("OneFormer-ADE20K-SemSegPreprocessor"); public static ComfyAuxPreprocessor OpenposePreprocessor { get; } = new("OpenposePreprocessor"); public static ComfyAuxPreprocessor PiDiNetPreprocessor { get; } = new("PiDiNetPreprocessor"); public static ComfyAuxPreprocessor SavePoseKpsAsJsonFile { get; } = new("SavePoseKpsAsJsonFile"); public static ComfyAuxPreprocessor FacialPartColoringFromPoseKps { get; } = new("FacialPartColoringFromPoseKps"); public static ComfyAuxPreprocessor ImageLuminanceDetector { get; } = new("ImageLuminanceDetector"); public static ComfyAuxPreprocessor ImageIntensityDetector { get; } = new("ImageIntensityDetector"); public static ComfyAuxPreprocessor ScribblePreprocessor { get; } = new("ScribblePreprocessor"); public static ComfyAuxPreprocessor ScribbleXDoGPreprocessor { get; } = new("Scribble_XDoG_Preprocessor"); public static ComfyAuxPreprocessor SAMPreprocessor { get; } = new("SAMPreprocessor"); public static ComfyAuxPreprocessor ShufflePreprocessor { get; } = new("ShufflePreprocessor"); public static ComfyAuxPreprocessor TEEDPreprocessor { get; } = new("TEEDPreprocessor"); public static ComfyAuxPreprocessor TilePreprocessor { get; } = new("TilePreprocessor"); public static ComfyAuxPreprocessor UniFormerSemSegPreprocessor { get; } = new("UniFormer-SemSegPreprocessor"); public static ComfyAuxPreprocessor SemSegPreprocessor { get; } = new("SemSegPreprocessor"); public static ComfyAuxPreprocessor UnimatchOptFlowPreprocessor { get; } = new("Unimatch_OptFlowPreprocessor"); public static ComfyAuxPreprocessor MaskOptFlow { get; } = new("MaskOptFlow"); public static ComfyAuxPreprocessor ZoeDepthMapPreprocessor { get; } = new("Zoe-DepthMapPreprocessor"); private static Dictionary DisplayNamesMapping { get; } = new() { [None] = "None", [AnimeFaceSemSegPreprocessor] = "Anime Face SemSeg Preprocessor", [BinaryPreprocessor] = "Binary Preprocessor", [CannyEdgePreprocessor] = "Canny Edge Preprocessor", [ColorPreprocessor] = "Color Preprocessor", [DensePosePreprocessor] = "DensePose Preprocessor", [DepthAnythingPreprocessor] = "Depth Anything Preprocessor", [ZoeDepthAnythingPreprocessor] = "Zoe Depth Anything Preprocessor", [DiffusionEdgePreprocessor] = "Diffusion Edge Preprocessor", [DWPreprocessor] = "DW Preprocessor", [AnimalPosePreprocessor] = "Animal Pose Preprocessor", [HEDPreprocessor] = "HED Preprocessor", [FakeScribblePreprocessor] = "Fake Scribble Preprocessor", [LeReSDepthMapPreprocessor] = "LeReS-DepthMap Preprocessor", [LineArtPreprocessor] = "LineArt Preprocessor", [AnimeLineArtPreprocessor] = "Anime LineArt Preprocessor", [LineartStandardPreprocessor] = "Lineart Standard Preprocessor", [Manga2AnimeLineArtPreprocessor] = "Manga2Anime LineArt Preprocessor", [MediaPipeFaceMeshPreprocessor] = "MediaPipe FaceMesh Preprocessor", [MeshGraphormerDepthMapPreprocessor] = "MeshGraphormer DepthMap Preprocessor", [MiDaSNormalMapPreprocessor] = "MiDaS NormalMap Preprocessor", [MiDaSDepthMapPreprocessor] = "MiDaS DepthMap Preprocessor", [MLSDPreprocessor] = "M-LSD Preprocessor", [BAENormalMapPreprocessor] = "BAE NormalMap Preprocessor", [OneFormerCOCOSemSegPreprocessor] = "OneFormer COCO SemSeg Preprocessor", [OneFormerADE20KSemSegPreprocessor] = "OneFormer ADE20K SemSeg Preprocessor", [OpenposePreprocessor] = "Openpose Preprocessor", [PiDiNetPreprocessor] = "PiDiNet Preprocessor", [SavePoseKpsAsJsonFile] = "Save Pose Kps As Json File", [FacialPartColoringFromPoseKps] = "Facial Part Coloring From Pose Kps", [ImageLuminanceDetector] = "Image Luminance Detector", [ImageIntensityDetector] = "Image Intensity Detector", [ScribblePreprocessor] = "Scribble Preprocessor", [ScribbleXDoGPreprocessor] = "Scribble XDoG Preprocessor", [SAMPreprocessor] = "SAM Preprocessor", [ShufflePreprocessor] = "Shuffle Preprocessor", [TEEDPreprocessor] = "TEED Preprocessor", [TilePreprocessor] = "Tile Preprocessor", [UniFormerSemSegPreprocessor] = "UniFormer SemSeg Preprocessor", [SemSegPreprocessor] = "SemSeg Preprocessor", [UnimatchOptFlowPreprocessor] = "Unimatch OptFlow Preprocessor", [MaskOptFlow] = "Mask OptFlow", [ZoeDepthMapPreprocessor] = "Zoe DepthMap Preprocessor" }; public static IEnumerable Defaults => DisplayNamesMapping.Keys; public string DisplayName => DisplayNamesMapping.GetValueOrDefault(this, Value); /// public override string ToString() => Value; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyHistoryOutput.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyHistoryOutput { [JsonPropertyName("images")] public List? Images { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyHistoryResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyHistoryResponse { [JsonPropertyName("outputs")] public required Dictionary Outputs { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyImage.cs ================================================ using System.Text.Json.Serialization; using System.Web; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyImage { [JsonPropertyName("filename")] public required string FileName { get; set; } [JsonPropertyName("subfolder")] public required string SubFolder { get; set; } [JsonPropertyName("type")] public required string Type { get; set; } public Uri ToUri(Uri baseAddress) { var query = HttpUtility.ParseQueryString(string.Empty); query["filename"] = FileName; query["subfolder"] = SubFolder; query["type"] = Type; return new UriBuilder(baseAddress) { Path = "/view", Query = query.ToString() }.Uri; } public FilePath ToFilePath(DirectoryPath outputDir) { return new FilePath(outputDir, SubFolder, FileName); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyInputInfo.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyInputInfo { [JsonPropertyName("required")] public Dictionary? Required { get; set; } [JsonPropertyName("optional")] public Dictionary? Optional { get; set; } public List? GetRequiredValueAsNestedList(string key) { var value = Required?[key]; // value usually is a [["a", "b"]] array // but can also be [["a", "b"], {"x": "y"}] array // or sometimes ["COMBO", {"options": ["a", "b"]}] array var outerArray = value?.Deserialize(); if ( outerArray?.Count > 1 && outerArray.FirstOrDefault() is JsonValue jsonValue && jsonValue.ToString().Equals("COMBO") ) { var options = outerArray[1]?["options"]; if (options is JsonArray optionsArray) { return optionsArray.Deserialize>(); } } if (outerArray?.FirstOrDefault() is not { } innerNode) { return null; } var innerList = innerNode.Deserialize>(); return innerList; } public List? GetOptionalValueAsNestedList(string key) { var value = Optional?[key]; // value usually is a [["a", "b"]] array // but can also be [["a", "b"], {"x": "y"}] array var outerArray = value?.Deserialize(); if (outerArray?.FirstOrDefault() is not { } innerNode) { return null; } var innerList = innerNode.Deserialize>(); return innerList; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyObjectInfo.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyObjectInfo { [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("display_name")] public string? DisplayName { get; set; } [JsonPropertyName("description")] public string? Description { get; set; } [JsonPropertyName("category")] public string? Category { get; set; } [JsonPropertyName("output_node")] public bool IsOutputNode { get; set; } /// /// Input info /// [JsonPropertyName("input")] public required ComfyInputInfo Input { get; set; } /// /// List of output point types /// i.e. ["MODEL", "CLIP", "VAE"] /// [JsonPropertyName("output")] public required List Output { get; set; } /// /// List of output point display names /// i.e. ["MODEL", "CLIP", "VAE"] /// [JsonPropertyName("output_name")] public required List OutputName { get; set; } /// /// List of whether the indexed output is a list /// i.e. [false, false, false] /// [JsonPropertyName("output_is_list")] public required List OutputIsList { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptRequest.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyPromptRequest { [JsonPropertyName("client_id")] public required string ClientId { get; set; } [JsonPropertyName("prompt")] public required Dictionary Prompt { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; // ReSharper disable once ClassNeverInstantiated.Global public class ComfyPromptResponse { [JsonPropertyName("prompt_id")] public required string PromptId { get; set; } [JsonPropertyName("number")] public required int Number { get; set; } [JsonPropertyName("node_errors")] public required Dictionary NodeErrors { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models.Api.Comfy; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "StringLiteralTypo")] [SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "IdentifierTypo")] public readonly record struct ComfySampler(string Name) { public static ComfySampler Euler { get; } = new("euler"); public static ComfySampler EulerAncestral { get; } = new("euler_ancestral"); public static ComfySampler Heun { get; } = new("heun"); public static ComfySampler HeunPp2 { get; } = new("heunpp2"); public static ComfySampler Dpm2 { get; } = new("dpm_2"); public static ComfySampler Dpm2Ancestral { get; } = new("dpm_2_ancestral"); public static ComfySampler LMS { get; } = new("lms"); public static ComfySampler DpmFast { get; } = new("dpm_fast"); public static ComfySampler DpmAdaptive { get; } = new("dpm_adaptive"); public static ComfySampler Dpmpp2SAncestral { get; } = new("dpmpp_2s_ancestral"); public static ComfySampler DpmppSde { get; } = new("dpmpp_sde"); public static ComfySampler DpmppSdeGpu { get; } = new("dpmpp_sde_gpu"); public static ComfySampler Dpmpp2M { get; } = new("dpmpp_2m"); public static ComfySampler Dpmpp2MSde { get; } = new("dpmpp_2m_sde"); public static ComfySampler Dpmpp2MSdeGpu { get; } = new("dpmpp_2m_sde_gpu"); public static ComfySampler Dpmpp3M { get; } = new("dpmpp_3m"); public static ComfySampler Dpmpp3MSde { get; } = new("dpmpp_3m_sde"); public static ComfySampler Dpmpp3MSdeGpu { get; } = new("dpmpp_3m_sde_gpu"); public static ComfySampler DDIM { get; } = new("ddim"); public static ComfySampler DDPM { get; } = new("ddpm"); public static ComfySampler UniPC { get; } = new("uni_pc"); public static ComfySampler UniPCBh2 { get; } = new("uni_pc_bh2"); public static ComfySampler LCM { get; } = new("lcm"); private static Dictionary ConvertDict { get; } = new() { [Euler] = "Euler", [EulerAncestral] = "Euler Ancestral", [Heun] = "Heun", [HeunPp2] = "Heun++ 2", [Dpm2] = "DPM 2", [Dpm2Ancestral] = "DPM 2 Ancestral", [LMS] = "LMS", [DpmFast] = "DPM Fast", [DpmAdaptive] = "DPM Adaptive", [Dpmpp2SAncestral] = "DPM++ 2S Ancestral", [DpmppSde] = "DPM++ SDE", [DpmppSdeGpu] = "DPM++ SDE GPU", [Dpmpp2M] = "DPM++ 2M", [Dpmpp2MSde] = "DPM++ 2M SDE", [Dpmpp2MSdeGpu] = "DPM++ 2M SDE GPU", [Dpmpp3M] = "DPM++ 3M", [Dpmpp3MSde] = "DPM++ 3M SDE", [Dpmpp3MSdeGpu] = "DPM++ 3M SDE GPU", [DDIM] = "DDIM", [DDPM] = "DDPM", [UniPC] = "UniPC", [UniPCBh2] = "UniPC BH2", [LCM] = "LCM" }; public static IReadOnlyList Defaults { get; } = ConvertDict.Keys.ToImmutableArray(); public string DisplayName => ConvertDict.GetValueOrDefault(this, Name); /// public bool Equals(ComfySampler other) { return Name == other.Name; } /// public override int GetHashCode() { return Name.GetHashCode(); } private sealed class NameEqualityComparer : IEqualityComparer { public bool Equals(ComfySampler x, ComfySampler y) { return x.Name == y.Name; } public int GetHashCode(ComfySampler obj) { return obj.Name.GetHashCode(); } } public static IEqualityComparer Comparer { get; } = new NameEqualityComparer(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy; /// /// Pair of and /// public readonly record struct ComfySamplerScheduler(ComfySampler Sampler, ComfyScheduler Scheduler) { /// public bool Equals(ComfySamplerScheduler other) { return Sampler.Equals(other.Sampler) && Scheduler.Equals(other.Scheduler); } /// public override int GetHashCode() { return HashCode.Combine(Sampler, Scheduler); } // Implicit conversion from (ComfySampler, ComfyScheduler) public static implicit operator ComfySamplerScheduler((ComfySampler, ComfyScheduler) tuple) { return new ComfySamplerScheduler(tuple.Item1, tuple.Item2); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs ================================================ using System.Collections.Immutable; namespace StabilityMatrix.Core.Models.Api.Comfy; public readonly record struct ComfyScheduler(string Name) { public static ComfyScheduler Normal { get; } = new("normal"); public static ComfyScheduler Karras { get; } = new("karras"); public static ComfyScheduler Exponential { get; } = new("exponential"); public static ComfyScheduler SDTurbo { get; } = new("sd_turbo"); public static ComfyScheduler Simple { get; } = new("simple"); public static ComfyScheduler Beta { get; } = new("beta"); public static ComfyScheduler AlignYourSteps { get; } = new("align_your_steps"); public static ComfyScheduler LinearQuadratic { get; } = new("linear_quadratic"); public static ComfyScheduler KLOptimal { get; } = new("kl_optimal"); public static ComfyScheduler FaceDetailerAlignYourStepsSD1 { get; } = new("AYS SD1"); public static ComfyScheduler FaceDetailerAlignYourStepsSDXL { get; } = new("AYS SDXL"); public static ComfyScheduler FaceDetailerGits { get; } = new("GITS[coeff=1.2]"); public static ComfyScheduler FaceDetailerLtxv { get; } = new("LTXV[default]"); private static Dictionary ConvertDict { get; } = new() { [Normal.Name] = "Normal", [Karras.Name] = "Karras", [Exponential.Name] = "Exponential", ["sgm_uniform"] = "SGM Uniform", [Simple.Name] = "Simple", ["ddim_uniform"] = "DDIM Uniform", [SDTurbo.Name] = "SD Turbo", [Beta.Name] = "Beta", [AlignYourSteps.Name] = "Align Your Steps", [LinearQuadratic.Name] = "Linear Quadratic", [KLOptimal.Name] = "KL Optimal" }; private static Dictionary FaceDetailerConvertDict { get; } = new() { [FaceDetailerAlignYourStepsSD1.Name] = "Align Your Steps SD1", [FaceDetailerAlignYourStepsSDXL.Name] = "Align Your Steps SDXL", [FaceDetailerGits.Name] = "GITS[coeff=1.2]", [FaceDetailerLtxv.Name] = "LTXV[default]" }; public static IReadOnlyList Defaults { get; } = ConvertDict.Keys.Select(k => new ComfyScheduler(k)).ToImmutableArray(); public static IReadOnlyList FaceDetailerDefaults { get; } = Defaults .Except([AlignYourSteps]) .Concat(FaceDetailerConvertDict.Keys.Select(k => new ComfyScheduler(k))) .ToImmutableArray(); public string DisplayName => ConvertDict.GetValueOrDefault(Name, Name); private sealed class NameEqualityComparer : IEqualityComparer { public bool Equals(ComfyScheduler x, ComfyScheduler y) { return x.Name == y.Name; } public int GetHashCode(ComfyScheduler obj) { return obj.Name.GetHashCode(); } } public static IEqualityComparer Comparer { get; } = new NameEqualityComparer(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyUploadImageResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy; public record ComfyUploadImageResponse { [JsonPropertyName("name")] public required string Name { get; set; } [JsonPropertyName("type")] public required string Type { get; set; } [JsonPropertyName("subfolder")] public required string SubFolder { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Models.Api.Comfy; public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) : IDownloadableResource { public static ComfyUpscaler NearestExact { get; } = new("nearest-exact", ComfyUpscalerType.Latent); private static Dictionary ConvertDict { get; } = new() { ["nearest-exact"] = "Nearest Exact", ["bilinear"] = "Bilinear", ["area"] = "Area", ["bicubic"] = "Bicubic", ["bislerp"] = "Bislerp", }; public static IReadOnlyList Defaults { get; } = ConvertDict.Keys.Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)).ToImmutableArray(); public static ComfyUpscaler FromDownloadable(RemoteResource resource) { return new ComfyUpscaler(resource.FileName, ComfyUpscalerType.DownloadableModel) { DownloadableResource = resource }; } /// /// Downloadable model information. /// If this is set, should be . /// public RemoteResource? DownloadableResource { get; init; } [MemberNotNullWhen(true, nameof(DownloadableResource))] public bool IsDownloadable => DownloadableResource != null; [JsonIgnore] public string DisplayType { get { return Type switch { ComfyUpscalerType.Latent => "Latent", ComfyUpscalerType.ESRGAN => "ESRGAN", ComfyUpscalerType.DownloadableModel => "Downloadable", ComfyUpscalerType.None => "None", _ => throw new ArgumentOutOfRangeException(nameof(Type), Type, null) }; } } [JsonIgnore] public string DisplayName { get { if (Type == ComfyUpscalerType.Latent) { return ConvertDict.TryGetValue(Name, out var displayName) ? displayName : Name; } if (Type is ComfyUpscalerType.ESRGAN or ComfyUpscalerType.DownloadableModel) { // Remove file extensions return Path.GetFileNameWithoutExtension(Name); } return Name; } } [JsonIgnore] public string ShortDisplayName { get { if (Type != ComfyUpscalerType.Latent) { // Remove file extensions return Path.GetFileNameWithoutExtension(Name); } return DisplayName; } } /// /// Default remote downloadable models /// public static IReadOnlyList DefaultDownloadableModels { get; } = RemoteModels.Upscalers.Select(FromDownloadable).ToImmutableArray(); private sealed class NameTypeEqualityComparer : IEqualityComparer { public bool Equals(ComfyUpscaler x, ComfyUpscaler y) { return x.Name == y.Name && x.Type == y.Type; } public int GetHashCode(ComfyUpscaler obj) { return HashCode.Combine(obj.Name, (int)obj.Type); } } public static IEqualityComparer Comparer { get; } = new NameTypeEqualityComparer(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscalerType.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; public enum ComfyUpscalerType { None, Latent, ESRGAN, DownloadableModel } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyWebSocketResponse.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; namespace StabilityMatrix.Core.Models.Api.Comfy; public class ComfyWebSocketResponse { [JsonPropertyName("type")] public required ComfyWebSocketResponseType Type { get; set; } /// /// Depending on the value of , /// this property will be one of these types /// /// Status - /// Progress - /// Executing - /// /// [JsonPropertyName("data")] public required JsonObject Data { get; set; } public T? GetDataAsType(JsonSerializerOptions? options = null) where T : class { return Data.Deserialize(options); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyWebSocketResponseType.cs ================================================ using System.Runtime.Serialization; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; [JsonConverter(typeof(DefaultUnknownEnumConverter))] public enum ComfyWebSocketResponseType { Unknown, [EnumMember(Value = "status")] Status, [EnumMember(Value = "execution_start")] ExecutionStart, [EnumMember(Value = "execution_cached")] ExecutionCached, [EnumMember(Value = "execution_error")] ExecutionError, [EnumMember(Value = "executing")] Executing, [EnumMember(Value = "progress")] Progress, [EnumMember(Value = "executed")] Executed, } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/ComfyWebSocketResponseUnion.cs ================================================ using System.Net.WebSockets; namespace StabilityMatrix.Core.Models.Api.Comfy; public record ComfyWebSocketResponseUnion { public WebSocketMessageType MessageType { get; set; } public ComfyWebSocketResponse? Json { get; set; } public byte[]? Bytes { get; set; } }; ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; /// /// Combination of the positive and negative conditioning connections. /// public record ConditioningConnections(ConditioningNodeConnection Positive, ConditioningNodeConnection Negative) { // Implicit from tuple public static implicit operator ConditioningConnections( (ConditioningNodeConnection Positive, ConditioningNodeConnection Negative) value ) => new(value.Positive, value.Negative); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; /// /// Connections from a loaded model /// public record ModelConnections(string Name) { public ModelConnections(ModelConnections other) { Name = other.Name; Model = other.Model; VAE = other.VAE; Clip = other.Clip; Conditioning = other.Conditioning; ClipVision = other.ClipVision; } public ModelNodeConnection? Model { get; set; } public VAENodeConnection? VAE { get; set; } public ClipNodeConnection? Clip { get; set; } public ConditioningConnections? Conditioning { get; set; } public ClipVisionNodeConnection? ClipVision { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; [JsonConverter(typeof(NodeConnectionBaseJsonConverter))] public abstract class NodeConnectionBase { /// /// Array data for the connection. /// [(string) Node Name, (int) Connection Index] /// public object[]? Data { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; public class LatentNodeConnection : NodeConnectionBase; public class VAENodeConnection : NodeConnectionBase; public class ImageNodeConnection : NodeConnectionBase; public class ImageMaskConnection : NodeConnectionBase; public class UpscaleModelNodeConnection : NodeConnectionBase; public class ModelNodeConnection : NodeConnectionBase; public class ConditioningNodeConnection : NodeConnectionBase; public class ClipNodeConnection : NodeConnectionBase; public class ControlNetNodeConnection : NodeConnectionBase; public class ClipVisionNodeConnection : NodeConnectionBase; public class ClipVisionOutputNodeConnection : NodeConnectionBase; public class SamplerNodeConnection : NodeConnectionBase; public class SigmasNodeConnection : NodeConnectionBase; public class StringNodeConnection : NodeConnectionBase; public class BboxDetectorNodeConnection : NodeConnectionBase; public class SegmDetectorNodeConnection : NodeConnectionBase; public class SamModelNodeConnection : NodeConnectionBase; public class GuiderNodeConnection : NodeConnectionBase; public class NoiseNodeConnection : NodeConnectionBase; ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/PrimaryNodeConnection.cs ================================================ using OneOf; namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; /// /// Union for the primary Image or Latent node connection /// [GenerateOneOf] public partial class PrimaryNodeConnection : OneOfBase { } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNode.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; [JsonSerializable(typeof(ComfyNode))] [SuppressMessage("ReSharper", "CollectionNeverQueried.Global")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public record ComfyNode { [JsonPropertyName("class_type")] public required string ClassType { get; init; } [JsonPropertyName("inputs")] public required Dictionary Inputs { get; init; } public NamedComfyNode ToNamedNode(string name) { return new NamedComfyNode(name) { ClassType = ClassType, Inputs = Inputs }; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs ================================================ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Text.Json.Serialization; using OneOf; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; /// /// Builder functions for comfy nodes /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [Localizable(false)] public class ComfyNodeBuilder { public NodeDictionary Nodes { get; } = new(); private static string GetRandomPrefix() => Guid.NewGuid().ToString()[..8]; private const int MaxResolution = 16384; private string GetUniqueName(string nameBase) { var name = $"{nameBase}_1"; for (var i = 0; Nodes.ContainsKey(name); i++) { if (i > 1_000_000) { throw new InvalidOperationException($"Could not find unique name for base {nameBase}"); } name = $"{nameBase}_{i + 1}"; } return name; } public record VAEEncode : ComfyTypedNodeBase { public required ImageNodeConnection Pixels { get; init; } public required VAENodeConnection Vae { get; init; } } public record VAEEncodeForInpaint : ComfyTypedNodeBase { public required ImageNodeConnection Pixels { get; init; } public required VAENodeConnection Vae { get; init; } public required ImageMaskConnection Mask { get; init; } [Range(0, 64)] public int GrowMaskBy { get; init; } = 6; } public record VAEDecode : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } public required VAENodeConnection Vae { get; init; } } [TypedNodeOptions(Name = "VAEDecodeTiled")] public record TiledVAEDecode : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } public required VAENodeConnection Vae { get; init; } [Range(64, 4096)] [JsonPropertyName("tile_size")] public int TileSize { get; init; } = 512; [Range(0, 4096)] [JsonPropertyName("overlap")] public int Overlap { get; init; } = 64; [Range(8, 4096)] [JsonPropertyName("temporal_size")] public int TemporalSize { get; init; } = 64; [Range(4, 4096)] [JsonPropertyName("temporal_overlap")] public int TemporalOverlap { get; init; } = 8; } public record KSampler : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required ulong Seed { get; init; } public required int Steps { get; init; } public required double Cfg { get; init; } public required string SamplerName { get; init; } public required string Scheduler { get; init; } public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required LatentNodeConnection LatentImage { get; init; } public required double Denoise { get; init; } } public record KSamplerAdvanced : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } [BoolStringMember("enable", "disable")] public required bool AddNoise { get; init; } public required ulong NoiseSeed { get; init; } public required int Steps { get; init; } public required double Cfg { get; init; } public required string SamplerName { get; init; } public required string Scheduler { get; init; } public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required LatentNodeConnection LatentImage { get; init; } public required int StartAtStep { get; init; } public required int EndAtStep { get; init; } [BoolStringMember("enable", "disable")] public bool ReturnWithLeftoverNoise { get; init; } } public record SamplerCustom : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required bool AddNoise { get; init; } public required ulong NoiseSeed { get; init; } [Range(0d, 100d)] public required double Cfg { get; init; } public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required SamplerNodeConnection Sampler { get; init; } public required SigmasNodeConnection Sigmas { get; init; } public required LatentNodeConnection LatentImage { get; init; } } public record KSamplerSelect : ComfyTypedNodeBase { public required string SamplerName { get; init; } } public record SDTurboScheduler : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } [Range(1, 10)] public required int Steps { get; init; } [Range(0, 1.0)] public required double Denoise { get; init; } } public record EmptyLatentImage : ComfyTypedNodeBase { public required int BatchSize { get; init; } public required int Height { get; init; } public required int Width { get; init; } } public record EmptyHunyuanLatentVideo : ComfyTypedNodeBase { public required int Width { get; init; } public required int Height { get; init; } public required int Length { get; init; } public required int BatchSize { get; init; } } public record CLIPSetLastLayer : ComfyTypedNodeBase { public required ClipNodeConnection Clip { get; init; } [Range(-24, -1)] public int StopAtClipLayer { get; init; } = -1; } public record LatentFromBatch : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } [Range(0, 63)] public int BatchIndex { get; init; } = 0; [Range(1, 64)] public int Length { get; init; } = 1; } public record RepeatLatentBatch : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } [Range(1, 64)] public int Amount { get; init; } = 1; } public record LatentBlend : ComfyTypedNodeBase { public required LatentNodeConnection Samples1 { get; init; } public required LatentNodeConnection Samples2 { get; init; } [Range(0d, 1d)] public double BlendFactor { get; init; } = 0.5; } public record ModelMergeSimple : ComfyTypedNodeBase { public required ModelNodeConnection Model1 { get; init; } public required ModelNodeConnection Model2 { get; init; } [Range(0d, 1d)] public double Ratio { get; init; } = 1; } public static NamedComfyNode ImageUpscaleWithModel( string name, UpscaleModelNodeConnection upscaleModel, ImageNodeConnection image ) { return new NamedComfyNode(name) { ClassType = "ImageUpscaleWithModel", Inputs = new Dictionary { ["upscale_model"] = upscaleModel.Data, ["image"] = image.Data, }, }; } public static NamedComfyNode UpscaleModelLoader(string name, string modelName) { return new NamedComfyNode(name) { ClassType = "UpscaleModelLoader", Inputs = new Dictionary { ["model_name"] = modelName }, }; } public static NamedComfyNode ImageScale( string name, ImageNodeConnection image, string method, int height, int width, bool crop ) { return new NamedComfyNode(name) { ClassType = "ImageScale", Inputs = new Dictionary { ["image"] = image.Data, ["upscale_method"] = method, ["height"] = height, ["width"] = width, ["crop"] = crop ? "center" : "disabled", }, }; } public record VAELoader : ComfyTypedNodeBase { public required string VaeName { get; init; } } public static NamedComfyNode LoraLoader( string name, ModelNodeConnection model, ClipNodeConnection clip, string loraName, double strengthModel, double strengthClip ) { return new NamedComfyNode(name) { ClassType = "LoraLoader", Inputs = new Dictionary { ["model"] = model.Data, ["clip"] = clip.Data, ["lora_name"] = loraName, ["strength_model"] = strengthModel, ["strength_clip"] = strengthClip, }, }; } public record CheckpointLoader : ComfyTypedNodeBase { public required string ConfigName { get; init; } public required string CkptName { get; init; } } public record CheckpointLoaderSimple : ComfyTypedNodeBase { public required string CkptName { get; init; } } public record ImageOnlyCheckpointLoader : ComfyTypedNodeBase { public required string CkptName { get; init; } } public record FreeU : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required double B1 { get; init; } public required double B2 { get; init; } public required double S1 { get; init; } public required double S2 { get; init; } } [SuppressMessage("ReSharper", "InconsistentNaming")] public record CLIPTextEncode : ComfyTypedNodeBase { public required ClipNodeConnection Clip { get; init; } public required OneOf Text { get; init; } } public record LoadImage : ComfyTypedNodeBase { /// /// Path relative to the Comfy input directory /// public required string Image { get; init; } } public record LoadImageMask : ComfyTypedNodeBase { /// /// Path relative to the Comfy input directory /// public required string Image { get; init; } /// /// Color channel to use as mask. /// ("alpha", "red", "green", "blue") /// public string Channel { get; init; } = "alpha"; } public record PreviewImage : ComfyTypedNodeBase { public required ImageNodeConnection Images { get; init; } } public record ImageSharpen : ComfyTypedNodeBase { public required ImageNodeConnection Image { get; init; } public required int SharpenRadius { get; init; } public required double Sigma { get; init; } public required double Alpha { get; init; } } public record ControlNetLoader : ComfyTypedNodeBase { public required string ControlNetName { get; init; } } public record ControlNetApplyAdvanced : ComfyTypedNodeBase { public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required ControlNetNodeConnection ControlNet { get; init; } public required ImageNodeConnection Image { get; init; } public required double Strength { get; init; } public required double StartPercent { get; init; } public required double EndPercent { get; init; } } public record SVD_img2vid_Conditioning : ComfyTypedNodeBase { public required ClipVisionNodeConnection ClipVision { get; init; } public required ImageNodeConnection InitImage { get; init; } public required VAENodeConnection Vae { get; init; } public required int Width { get; init; } public required int Height { get; init; } public required int VideoFrames { get; init; } public required int MotionBucketId { get; init; } public required int Fps { get; set; } public required double AugmentationLevel { get; init; } } public record VideoLinearCFGGuidance : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required double MinCfg { get; init; } } public record SaveAnimatedWEBP : ComfyTypedNodeBase { public required ImageNodeConnection Images { get; init; } public required string FilenamePrefix { get; init; } public required double Fps { get; init; } public required bool Lossless { get; init; } public required int Quality { get; init; } public required string Method { get; init; } } public record UNETLoader : ComfyTypedNodeBase { public required string UnetName { get; init; } /// /// possible values: "default", "fp8_e4m3fn", "fp8_e5m2" /// public required string WeightDtype { get; init; } } public record CLIPLoader : ComfyTypedNodeBase { public required string ClipName { get; init; } /// /// possible values: "stable_diffusion", "stable_cascade", "sd3", "stable_audio", "mochi" /// public required string Type { get; init; } } public record DualCLIPLoader : ComfyTypedNodeBase { public required string ClipName1 { get; init; } public required string ClipName2 { get; init; } /// /// possible values: "sdxl", "sd3", "flux" /// public required string Type { get; init; } } public record TripleCLIPLoader : ComfyTypedNodeBase { public required string ClipName1 { get; init; } public required string ClipName2 { get; init; } public required string ClipName3 { get; init; } // no type, always sd3 I guess? } public record QuadrupleCLIPLoader : ComfyTypedNodeBase { public required string ClipName1 { get; init; } public required string ClipName2 { get; init; } public required string ClipName3 { get; init; } public required string ClipName4 { get; init; } // no type, always HiDream I guess? } public record CLIPVisionLoader : ComfyTypedNodeBase { public required string ClipName { get; init; } } public record CLIPVisionEncode : ComfyTypedNodeBase { public required ClipVisionNodeConnection ClipVision { get; init; } public required ImageNodeConnection Image { get; init; } public required string Crop { get; set; } } public record FluxGuidance : ComfyTypedNodeBase { public required ConditioningNodeConnection Conditioning { get; init; } [Range(0.0d, 100.0d)] public required double Guidance { get; init; } } public record BasicGuider : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required ConditioningNodeConnection Conditioning { get; init; } } public record EmptySD3LatentImage : ComfyTypedNodeBase { [Range(16, MaxResolution)] public int Width { get; init; } = 1024; [Range(16, MaxResolution)] public int Height { get; init; } = 1024; [Range(1, 4096)] public int BatchSize { get; init; } = 1; } public record RandomNoise : ComfyTypedNodeBase { [Range(0, int.MaxValue)] public ulong NoiseSeed { get; init; } } public record BasicScheduler : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required string Scheduler { get; init; } [Range(1, 10000)] public int Steps { get; init; } = 20; [Range(0.0d, 1.0d)] public double Denoise { get; init; } = 1.0; } public record SamplerCustomAdvanced : ComfyTypedNodeBase { public required NoiseNodeConnection Noise { get; init; } public required GuiderNodeConnection Guider { get; init; } public required SamplerNodeConnection Sampler { get; init; } public required SigmasNodeConnection Sigmas { get; init; } public required LatentNodeConnection LatentImage { get; init; } } public record ModelSamplingDiscrete : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } /// /// Options: "eps", "v_prediction", "lcm", "x0" /// public required string Sampling { get; set; } public required bool Zsnr { get; init; } } public record ModelSamplingSD3 : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } [Range(0, 100)] public required double Shift { get; init; } } public record RescaleCFG : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required double Multiplier { get; init; } } public record SetLatentNoiseMask : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } public required ImageMaskConnection Mask { get; init; } } public record AlignYourStepsScheduler : ComfyTypedNodeBase { /// /// options: SD1, SDXL, SVD /// public required string ModelType { get; init; } [Range(1, 10000)] public required int Steps { get; init; } [Range(0.0d, 1.0d)] public required double Denoise { get; init; } } public record CFGGuider : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; set; } public required ConditioningNodeConnection Positive { get; set; } public required ConditioningNodeConnection Negative { get; set; } [Range(0.0d, 100.0d)] public required double Cfg { get; set; } } /// /// outputs: positive, negative, latent /// public record WanImageToVideo : ComfyTypedNodeBase { public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required VAENodeConnection Vae { get; init; } public required ClipVisionOutputNodeConnection ClipVisionOutput { get; init; } public required ImageNodeConnection StartImage { get; init; } public required int Width { get; init; } public required int Height { get; init; } public required int Length { get; init; } public required int BatchSize { get; init; } } [TypedNodeOptions( Name = "CheckpointLoaderNF4", RequiredExtensions = ["https://github.com/comfyanonymous/ComfyUI_bitsandbytes_NF4"] )] public record CheckpointLoaderNF4 : ComfyTypedNodeBase { public required string CkptName { get; init; } } [TypedNodeOptions( Name = "UnetLoaderGGUF", RequiredExtensions = ["https://github.com/city96/ComfyUI-GGUF"] )] public record UnetLoaderGGUF : ComfyTypedNodeBase { public required string UnetName { get; init; } } [TypedNodeOptions( Name = "Inference_Core_PromptExpansion", RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.2.0"] )] public record PromptExpansion : ComfyTypedNodeBase { public required string ModelName { get; init; } public required OneOf Text { get; init; } public required ulong Seed { get; init; } public bool LogPrompt { get; init; } } [TypedNodeOptions( Name = "Inference_Core_AIO_Preprocessor", RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.2.0"] )] public record AIOPreprocessor : ComfyTypedNodeBase { public required ImageNodeConnection Image { get; init; } public required string Preprocessor { get; init; } [Range(64, 16384)] public int Resolution { get; init; } = 512; } [TypedNodeOptions( Name = "Inference_Core_ReferenceOnlySimple", RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.3.0"] )] public record ReferenceOnlySimple : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } public required LatentNodeConnection Reference { get; init; } [Range(1, 64)] public int BatchSize { get; init; } = 1; } [TypedNodeOptions( Name = "Inference_Core_LayeredDiffusionApply", RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.4.0"] )] public record LayeredDiffusionApply : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } /// /// Available configs: /// SD15, Attention Injection, attn_sharing /// SDXL, Conv Injection /// SDXL, Attention Injection /// public required string Config { get; init; } [Range(-1d, 3d)] public double Weight { get; init; } = 1.0; } [TypedNodeOptions( Name = "Inference_Core_LayeredDiffusionDecodeRGBA", RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes >= 0.4.0"] )] public record LayeredDiffusionDecodeRgba : ComfyTypedNodeBase { public required LatentNodeConnection Samples { get; init; } public required ImageNodeConnection Images { get; init; } /// /// Either "SD15" or "SDXL" /// public required string SdVersion { get; init; } [Range(1, 4096)] public int SubBatchSize { get; init; } = 16; } [TypedNodeOptions( Name = "UltralyticsDetectorProvider", RequiredExtensions = [ "https://github.com/ltdrdata/ComfyUI-Impact-Pack", "https://github.com/ltdrdata/ComfyUI-Impact-Subpack", ] )] public record UltralyticsDetectorProvider : ComfyTypedNodeBase { public required string ModelName { get; init; } } [TypedNodeOptions( Name = "SAMLoader", RequiredExtensions = [ "https://github.com/ltdrdata/ComfyUI-Impact-Pack", "https://github.com/ltdrdata/ComfyUI-Impact-Subpack", ] )] public record SamLoader : ComfyTypedNodeBase { public required string ModelName { get; init; } /// /// options: AUTO, Prefer GPU, CPU /// public required string DeviceMode { get; init; } } [TypedNodeOptions( Name = "FaceDetailer", RequiredExtensions = [ "https://github.com/ltdrdata/ComfyUI-Impact-Pack", "https://github.com/ltdrdata/ComfyUI-Impact-Subpack", ] )] public record FaceDetailer : ComfyTypedNodeBase { public required ImageNodeConnection Image { get; init; } public required ModelNodeConnection Model { get; init; } public required ClipNodeConnection Clip { get; init; } public required VAENodeConnection Vae { get; init; } public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required BboxDetectorNodeConnection BboxDetector { get; init; } public required double GuideSize { get; init; } = 512.0; /// /// true: 'bbox' /// false: 'crop_region' /// public required bool GuideSizeFor { get; init; } = true; public required double MaxSize { get; init; } = 1024.0; public required ulong Seed { get; init; } public required int Steps { get; init; } = 20; public required double Cfg { get; init; } = 8.0d; public required string SamplerName { get; init; } public required string Scheduler { get; init; } public required double Denoise { get; init; } = 0.5d; public required int Feather { get; init; } = 5; public required bool NoiseMask { get; init; } = true; public required bool ForceInpaint { get; init; } = true; [Range(0.0, 1.0)] public required double BboxThreshold { get; init; } = 0.5d; [Range(-512, 512)] public required int BboxDilation { get; init; } = 10; [Range(1.0, 10.0)] public required double BboxCropFactor { get; init; } = 3.0d; /// /// options: ["center-1", "horizontal-2", "vertical-2", "rect-4", "diamond-4", "mask-area", "mask-points", "mask-point-bbox", "none"] /// public required string SamDetectionHint { get; init; } [Range(-512, 512)] public required int SamDilation { get; init; } [Range(0.0d, 1.0d)] public required double SamThreshold { get; init; } = 0.93d; [Range(0, 1000)] public required int SamBboxExpansion { get; init; } [Range(0.0d, 1.0d)] public required double SamMaskHintThreshold { get; init; } = 0.7d; /// /// options: ["False", "Small", "Outter"] /// public required string SamMaskHintUseNegative { get; init; } = "False"; public required string Wildcard { get; init; } [Range(1, 32768)] public required int DropSize { get; init; } = 10; [Range(1, 10)] public required int Cycle { get; init; } = 1; public SamModelNodeConnection? SamModelOpt { get; set; } public SegmDetectorNodeConnection? SegmDetectorOpt { get; set; } public bool TiledEncode { get; init; } public bool TiledDecode { get; init; } } /// /// Plasma Noise generation node (Lykos_JDC_Plasma) /// [TypedNodeOptions( Name = "Lykos_JDC_Plasma", RequiredExtensions = ["https://github.com/LykosAI/inference-comfy-plasma"] )] // Name corrected, Extensions added public record PlasmaNoise : ComfyTypedNodeBase { [Range(128, 8192)] public required int Width { get; init; } = 512; [Range(128, 8192)] public required int Height { get; init; } = 512; [Range(0.5d, 32.0d)] public required double Turbulence { get; init; } = 2.75; [Range(-1, 255)] public required int ValueMin { get; init; } = -1; [Range(-1, 255)] public required int ValueMax { get; init; } = -1; [Range(-1, 255)] public required int RedMin { get; init; } = -1; [Range(-1, 255)] public required int RedMax { get; init; } = -1; [Range(-1, 255)] public required int GreenMin { get; init; } = -1; [Range(-1, 255)] public required int GreenMax { get; init; } = -1; [Range(-1, 255)] public required int BlueMin { get; init; } = -1; [Range(-1, 255)] public required int BlueMax { get; init; } = -1; [Range(0UL, ulong.MaxValue)] // Match Python's max int size public required ulong Seed { get; init; } = 0; } /// /// Random Noise generation node (Lykos_JDC_RandNoise) /// [TypedNodeOptions( Name = "Lykos_JDC_RandNoise", RequiredExtensions = ["https://github.com/LykosAI/inference-comfy-plasma"] )] // Name corrected, Extensions added public record RandNoise : ComfyTypedNodeBase { [Range(128, 8192)] public required int Width { get; init; } = 512; [Range(128, 8192)] public required int Height { get; init; } = 512; [Range(-1, 255)] public required int ValueMin { get; init; } = -1; [Range(-1, 255)] public required int ValueMax { get; init; } = -1; [Range(-1, 255)] public required int RedMin { get; init; } = -1; [Range(-1, 255)] public required int RedMax { get; init; } = -1; [Range(-1, 255)] public required int GreenMin { get; init; } = -1; [Range(-1, 255)] public required int GreenMax { get; init; } = -1; [Range(-1, 255)] public required int BlueMin { get; init; } = -1; [Range(-1, 255)] public required int BlueMax { get; init; } = -1; [Range(0UL, ulong.MaxValue)] public required ulong Seed { get; init; } = 0; } /// /// Greyscale Noise generation node (Lykos_JDC_GreyNoise) /// [TypedNodeOptions( Name = "Lykos_JDC_GreyNoise", RequiredExtensions = ["https://github.com/LykosAI/inference-comfy-plasma"] )] // Name corrected, Extensions added public record GreyNoise : ComfyTypedNodeBase { [Range(128, 8192)] public required int Width { get; init; } = 512; [Range(128, 8192)] public required int Height { get; init; } = 512; [Range(-1, 255)] public required int ValueMin { get; init; } = -1; [Range(-1, 255)] public required int ValueMax { get; init; } = -1; [Range(-1, 255)] public required int RedMin { get; init; } = -1; [Range(-1, 255)] public required int RedMax { get; init; } = -1; [Range(-1, 255)] public required int GreenMin { get; init; } = -1; [Range(-1, 255)] public required int GreenMax { get; init; } = -1; [Range(-1, 255)] public required int BlueMin { get; init; } = -1; [Range(-1, 255)] public required int BlueMax { get; init; } = -1; [Range(0UL, ulong.MaxValue)] public required ulong Seed { get; init; } = 0; } /// /// Pink Noise generation node (Lykos_JDC_PinkNoise) /// [TypedNodeOptions( Name = "Lykos_JDC_PinkNoise", RequiredExtensions = ["https://github.com/LykosAI/inference-comfy-plasma"] )] // Name corrected, Extensions added public record PinkNoise : ComfyTypedNodeBase { [Range(128, 8192)] public required int Width { get; init; } = 512; [Range(128, 8192)] public required int Height { get; init; } = 512; [Range(-1, 255)] public required int ValueMin { get; init; } = -1; [Range(-1, 255)] public required int ValueMax { get; init; } = -1; [Range(-1, 255)] public required int RedMin { get; init; } = -1; [Range(-1, 255)] public required int RedMax { get; init; } = -1; [Range(-1, 255)] public required int GreenMin { get; init; } = -1; [Range(-1, 255)] public required int GreenMax { get; init; } = -1; [Range(-1, 255)] public required int BlueMin { get; init; } = -1; [Range(-1, 255)] public required int BlueMax { get; init; } = -1; [Range(0UL, ulong.MaxValue)] public required ulong Seed { get; init; } = 0; } /// /// Brown Noise generation node (Lykos_JDC_BrownNoise) /// [TypedNodeOptions( Name = "Lykos_JDC_BrownNoise", RequiredExtensions = new[] { "https://github.com/LykosAI/inference-comfy-plasma" } )] // Name corrected, Extensions added public record BrownNoise : ComfyTypedNodeBase { [Range(128, 8192)] public required int Width { get; init; } = 512; [Range(128, 8192)] public required int Height { get; init; } = 512; [Range(-1, 255)] public required int ValueMin { get; init; } = -1; [Range(-1, 255)] public required int ValueMax { get; init; } = -1; [Range(-1, 255)] public required int RedMin { get; init; } = -1; [Range(-1, 255)] public required int RedMax { get; init; } = -1; [Range(-1, 255)] public required int GreenMin { get; init; } = -1; [Range(-1, 255)] public required int GreenMax { get; init; } = -1; [Range(-1, 255)] public required int BlueMin { get; init; } = -1; [Range(-1, 255)] public required int BlueMax { get; init; } = -1; [Range(0UL, ulong.MaxValue)] public required ulong Seed { get; init; } = 0; } /// /// CUDNN Toggle node for controlling CUDA Deep Neural Network library settings (CUDNNToggleAutoPassthrough) /// [TypedNodeOptions(Name = "CUDNNToggleAutoPassthrough")] public record CUDNNToggleAutoPassthrough : ComfyTypedNodeBase { public ModelNodeConnection? Model { get; init; } public ConditioningNodeConnection? Conditioning { get; init; } public LatentNodeConnection? Latent { get; init; } public required bool EnableCudnn { get; init; } = false; public required bool CudnnBenchmark { get; init; } = false; } /// /// Custom KSampler node using alternative noise distribution (Lykos_JDC_PlasmaSampler) /// [TypedNodeOptions( Name = "Lykos_JDC_PlasmaSampler", RequiredExtensions = ["https://github.com/LykosAI/inference-comfy-plasma"] )] // Name corrected, Extensions added public record PlasmaSampler : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } [Range(0UL, ulong.MaxValue)] public required ulong NoiseSeed { get; init; } = 0; [Range(1, 10000)] public required int Steps { get; init; } = 20; [Range(0.0d, 100.0d)] public required double Cfg { get; init; } = 7.0; [Range(0.0d, 1.0d)] public required double Denoise { get; init; } = 0.9; // Default from Python code [Range(0.0d, 1.0d)] public required double LatentNoise { get; init; } = 0.05; // Default from Python code /// /// Noise distribution type. Expected values: "default", "rand". /// Validation should ensure one of these values is passed. /// public required string DistributionType { get; init; } = "rand"; /// /// Name of the KSampler sampler (e.g., "euler", "dpmpp_2m_sde"). /// Should correspond to available samplers in comfy.samplers.KSampler.SAMPLERS. /// public required string SamplerName { get; init; } // No default in Python, must be provided /// /// Name of the KSampler scheduler (e.g., "normal", "karras", "sgm_uniform"). /// Should correspond to available schedulers in comfy.samplers.KSampler.SCHEDULERS. /// public required string Scheduler { get; init; } // No default in Python, must be provided public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } public required LatentNodeConnection LatentImage { get; init; } } [TypedNodeOptions( Name = "NRS", RequiredExtensions = ["https://github.com/Reithan/negative_rejection_steering"] )] public record NRS : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } [Range(-30.0f, 30.0f)] public required double Skew { get; set; } [Range(-30.0f, 30.0f)] public required double Stretch { get; set; } [Range(0f, 1f)] public required double Squash { get; set; } } public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae) { var name = GetUniqueName("VAEDecode"); return Nodes .AddTypedNode( new VAEDecode { Name = name, Samples = latent, Vae = vae, } ) .Output; } public LatentNodeConnection Lambda_ImageToLatent(ImageNodeConnection pixels, VAENodeConnection vae) { var name = GetUniqueName("VAEEncode"); return Nodes .AddTypedNode( new VAEEncode { Name = name, Pixels = pixels, Vae = vae, } ) .Output; } /// /// Create a group node that upscales a given image with a given model /// public NamedComfyNode Group_UpscaleWithModel( string name, string modelName, ImageNodeConnection image ) { var modelLoader = Nodes.AddNamedNode(UpscaleModelLoader($"{name}_UpscaleModelLoader", modelName)); var upscaler = Nodes.AddNamedNode( ImageUpscaleWithModel($"{name}_ImageUpscaleWithModel", modelLoader.Output, image) ); return upscaler; } /// /// Create a group node that scales a given image to image output /// public PrimaryNodeConnection Group_Upscale( string name, PrimaryNodeConnection primary, VAENodeConnection vae, ComfyUpscaler upscaleInfo, int width, int height ) { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { return primary.Match( latent => Nodes .AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "LatentUpscale", Inputs = new Dictionary { ["upscale_method"] = upscaleInfo.Name, ["width"] = width, ["height"] = height, ["crop"] = "disabled", ["samples"] = latent.Data, }, } ) .Output, image => Nodes .AddNamedNode( ImageScale($"{name}_ImageUpscale", image, upscaleInfo.Name, height, width, false) ) .Output ); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space if needed var samplerImage = GetPrimaryAsImage(primary, vae); // Do group upscale var modelUpscaler = Group_UpscaleWithModel( $"{name}_ModelUpscale", upscaleInfo.Name, samplerImage ); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); return resizedScaled.Output; } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); } /// /// Create a group node that scales a given image to a given size /// public NamedComfyNode Group_UpscaleToLatent( string name, LatentNodeConnection latent, VAENodeConnection vae, ComfyUpscaler upscaleInfo, int width, int height ) { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { return Nodes.AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "LatentUpscale", Inputs = new Dictionary { ["upscale_method"] = upscaleInfo.Name, ["width"] = width, ["height"] = height, ["crop"] = "disabled", ["samples"] = latent.Data, }, } ); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space var samplerImage = Nodes.AddTypedNode( new VAEDecode { Name = $"{name}_VAEDecode", Samples = latent, Vae = vae, } ); // Do group upscale var modelUpscaler = Group_UpscaleWithModel( $"{name}_ModelUpscale", upscaleInfo.Name, samplerImage.Output ); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // Convert back to latent space return Nodes.AddTypedNode( new VAEEncode { Name = $"{name}_VAEEncode", Pixels = resizedScaled.Output, Vae = vae, } ); } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); } /// /// Create a group node that scales a given image to image output /// public NamedComfyNode Group_LatentUpscaleToImage( string name, LatentNodeConnection latent, VAENodeConnection vae, ComfyUpscaler upscaleInfo, int width, int height ) { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { var latentUpscale = Nodes.AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "LatentUpscale", Inputs = new Dictionary { ["upscale_method"] = upscaleInfo.Name, ["width"] = width, ["height"] = height, ["crop"] = "disabled", ["samples"] = latent.Data, }, } ); // Convert to image space return Nodes.AddTypedNode( new VAEDecode { Name = $"{name}_VAEDecode", Samples = latentUpscale.Output, Vae = vae, } ); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space var samplerImage = Nodes.AddTypedNode( new VAEDecode { Name = $"{name}_VAEDecode", Samples = latent, Vae = vae, } ); // Do group upscale var modelUpscaler = Group_UpscaleWithModel( $"{name}_ModelUpscale", upscaleInfo.Name, samplerImage.Output ); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // No need to convert back to latent space return resizedScaled; } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); } /// /// Create a group node that scales a given image to image output /// public NamedComfyNode Group_UpscaleToImage( string name, ImageNodeConnection image, ComfyUpscaler upscaleInfo, int width, int height ) { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { return Nodes.AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "ImageScale", Inputs = new Dictionary { ["image"] = image, ["upscale_method"] = upscaleInfo.Name, ["width"] = width, ["height"] = height, ["crop"] = "disabled", }, } ); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Do group upscale var modelUpscaler = Group_UpscaleWithModel($"{name}_ModelUpscale", upscaleInfo.Name, image); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // No need to convert back to latent space return resizedScaled; } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); } /// /// Create a group node that loads multiple Lora's in series /// public NamedComfyNode Group_LoraLoadMany( string name, ModelNodeConnection model, ClipNodeConnection clip, IEnumerable<(string FileName, double? ModelWeight, double? ClipWeight)> loras ) { NamedComfyNode? currentNode = null; foreach (var (i, loraNetwork) in loras.Enumerate()) { currentNode = Nodes.AddNamedNode( LoraLoader( $"{name}_LoraLoader_{i + 1}", model, clip, loraNetwork.FileName, loraNetwork.ModelWeight ?? 1, loraNetwork.ClipWeight ?? 1 ) ); // Connect to previous node model = currentNode.Output1; clip = currentNode.Output2; } return currentNode ?? throw new InvalidOperationException("No lora networks given"); } /// /// Create a group node that loads multiple Lora's in series /// public NamedComfyNode Group_LoraLoadMany( string name, ModelNodeConnection model, ClipNodeConnection clip, IEnumerable<(LocalModelFile ModelFile, double? ModelWeight, double? ClipWeight)> loras ) { NamedComfyNode? currentNode = null; foreach (var (i, loraNetwork) in loras.Enumerate()) { currentNode = Nodes.AddNamedNode( LoraLoader( $"{name}_LoraLoader_{i + 1}", model, clip, loraNetwork.ModelFile.RelativePathFromSharedFolder, loraNetwork.ModelWeight ?? 1, loraNetwork.ClipWeight ?? 1 ) ); // Connect to previous node model = currentNode.Output1; clip = currentNode.Output2; } return currentNode ?? throw new InvalidOperationException("No lora networks given"); } /// /// Get or convert latest primary connection to latent /// public LatentNodeConnection GetPrimaryAsLatent() { if (Connections.Primary?.IsT0 == true) { return Connections.Primary.AsT0; } return GetPrimaryAsLatent( Connections.Primary ?? throw new NullReferenceException("No primary connection"), Connections.GetDefaultVAE() ); } /// /// Get or convert latest primary connection to latent /// public LatentNodeConnection GetPrimaryAsLatent(PrimaryNodeConnection primary, VAENodeConnection vae) { return primary.Match(latent => latent, image => Lambda_ImageToLatent(image, vae)); } /// /// Get or convert latest primary connection to latent /// public LatentNodeConnection GetPrimaryAsLatent(VAENodeConnection vae) { if (Connections.Primary?.IsT0 == true) { return Connections.Primary.AsT0; } return GetPrimaryAsLatent( Connections.Primary ?? throw new NullReferenceException("No primary connection"), vae ); } /// /// Get or convert latest primary connection to image /// public ImageNodeConnection GetPrimaryAsImage() { if (Connections.Primary?.IsT1 == true) { return Connections.Primary.AsT1; } return GetPrimaryAsImage( Connections.Primary ?? throw new NullReferenceException("No primary connection"), Connections.GetDefaultVAE() ); } /// /// Get or convert latest primary connection to image /// public ImageNodeConnection GetPrimaryAsImage(PrimaryNodeConnection primary, VAENodeConnection vae) { return primary.Match(latent => Lambda_LatentToImage(latent, vae), image => image); } /// /// Get or convert latest primary connection to image /// public ImageNodeConnection GetPrimaryAsImage(VAENodeConnection vae) { if (Connections.Primary?.IsT1 == true) { return Connections.Primary.AsT1; } return GetPrimaryAsImage( Connections.Primary ?? throw new NullReferenceException("No primary connection"), vae ); } /// /// Convert to a NodeDictionary /// public NodeDictionary ToNodeDictionary() { Nodes.NormalizeConnectionTypes(); return Nodes; } public class NodeBuilderConnections { public ulong Seed { get; set; } public int BatchSize { get; set; } = 1; public int? BatchIndex { get; set; } public int? PrimarySteps { get; set; } public double? PrimaryCfg { get; set; } public string? PrimaryModelType { get; set; } public OneOf PositivePrompt { get; set; } public OneOf NegativePrompt { get; set; } public ClipNodeConnection? BaseClip { get; set; } public ClipVisionNodeConnection? BaseClipVision { get; set; } public Dictionary Models { get; } = new() { ["Base"] = new ModelConnections("Base"), ["Refiner"] = new ModelConnections("Refiner") }; /// /// ModelConnections from with set /// public IEnumerable LoadedModels => Models.Values.Where(m => m.Model is not null); public ModelConnections Base => Models["Base"]; public ModelConnections Refiner => Models["Refiner"]; public Dictionary SamplerTemporaryArgs { get; } = new(); public ModuleApplyStepTemporaryArgs? BaseSamplerTemporaryArgs { get => SamplerTemporaryArgs.GetValueOrDefault("Base"); set => SamplerTemporaryArgs["Base"] = value; } /// /// The last primary set latent value, updated when is set to a latent value. /// public LatentNodeConnection? LastPrimaryLatent { get; private set; } private PrimaryNodeConnection? primary; public PrimaryNodeConnection? Primary { get => primary; set { if (value?.IsT0 == true) { LastPrimaryLatent = value.AsT0; } primary = value; } } public VAENodeConnection? PrimaryVAE { get; set; } public Size PrimarySize { get; set; } public ComfySampler? PrimarySampler { get; set; } public ComfyScheduler? PrimaryScheduler { get; set; } public GuiderNodeConnection PrimaryGuider { get; set; } public NoiseNodeConnection PrimaryNoise { get; set; } public SigmasNodeConnection PrimarySigmas { get; set; } public SamplerNodeConnection PrimarySamplerNode { get; set; } public List OutputNodes { get; } = new(); public IEnumerable OutputNodeNames => OutputNodes.Select(n => n.Name); public ModelNodeConnection GetRefinerOrBaseModel() { return Refiner.Model ?? Base.Model ?? throw new NullReferenceException("No Refiner or Base Model"); } public ConditioningConnections GetRefinerOrBaseConditioning() { return Refiner.Conditioning ?? Base.Conditioning ?? throw new NullReferenceException("No Refiner or Base Conditioning"); } public VAENodeConnection GetDefaultVAE() { return PrimaryVAE ?? Refiner.VAE ?? Base.VAE ?? throw new NullReferenceException("No VAE"); } } public NodeBuilderConnections Connections { get; } = new(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyTypedNodeBase.cs ================================================ using System.ComponentModel; using System.Reflection; using System.Text.Json.Serialization; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using Yoh.Text.Json.NamingPolicies; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; public abstract record ComfyTypedNodeBase { [Localizable(false)] protected virtual string ClassType { get { var type = GetType(); // Use options name if available if (type.GetCustomAttribute() is { } options) { if (!string.IsNullOrEmpty(options.Name)) { return options.Name; } } // Otherwise use class name return type.Name; } } [Localizable(false)] [JsonIgnore] public required string Name { get; init; } protected NamedComfyNode ToNamedNode() { var inputs = new Dictionary(); // Loop through all properties, key is property name as snake_case, or JsonPropertyName var namingPolicy = JsonNamingPolicies.SnakeCaseLower; foreach (var property in GetType().GetProperties()) { if (property.Name == nameof(Name) || property.GetValue(this) is not { } value) continue; // Skip JsonIgnore if (property.GetCustomAttribute() is not null) continue; var key = property.GetCustomAttribute()?.Name ?? namingPolicy.ConvertName(property.Name); // If theres a BoolStringMember attribute, convert to one of the strings if (property.GetCustomAttribute() is { } converter) { if (value is bool boolValue) { inputs.Add(key, boolValue ? converter.TrueString : converter.FalseString); } else { throw new InvalidOperationException( $"Property {property.Name} is not a bool, but has a BoolStringMember attribute" ); } continue; } // For connection types, use data property if (value is NodeConnectionBase connection) { inputs.Add(key, connection.Data); } else { inputs.Add(key, value); } } return new NamedComfyNode(Name) { ClassType = ClassType, Inputs = inputs }; } // Implicit conversion to NamedComfyNode public static implicit operator NamedComfyNode(ComfyTypedNodeBase node) => node.ToNamedNode(); } public abstract record ComfyTypedNodeBase : ComfyTypedNodeBase where TOutput : NodeConnectionBase, new() { [JsonIgnore] public TOutput Output => new() { Data = new object[] { Name, 0 } }; public static implicit operator NamedComfyNode(ComfyTypedNodeBase node) => (NamedComfyNode)node.ToNamedNode(); } public abstract record ComfyTypedNodeBase : ComfyTypedNodeBase where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() { [JsonIgnore] public TOutput1 Output1 => new() { Data = new object[] { Name, 0 } }; [JsonIgnore] public TOutput2 Output2 => new() { Data = new object[] { Name, 1 } }; public static implicit operator NamedComfyNode( ComfyTypedNodeBase node ) => (NamedComfyNode)node.ToNamedNode(); } public abstract record ComfyTypedNodeBase : ComfyTypedNodeBase where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() { [JsonIgnore] public TOutput1 Output1 => new() { Data = new object[] { Name, 0 } }; [JsonIgnore] public TOutput2 Output2 => new() { Data = new object[] { Name, 1 } }; [JsonIgnore] public TOutput3 Output3 => new() { Data = new object[] { Name, 2 } }; public static implicit operator NamedComfyNode( ComfyTypedNodeBase node ) => (NamedComfyNode)node.ToNamedNode(); } public abstract record ComfyTypedNodeBase : ComfyTypedNodeBase where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() where TOutput4 : NodeConnectionBase, new() { [JsonIgnore] public TOutput1 Output1 => new() { Data = new object[] { Name, 0 } }; [JsonIgnore] public TOutput2 Output2 => new() { Data = new object[] { Name, 1 } }; [JsonIgnore] public TOutput3 Output3 => new() { Data = new object[] { Name, 2 } }; [JsonIgnore] public TOutput4 Output4 => new() { Data = new object[] { Name, 3 } }; public static implicit operator NamedComfyNode( ComfyTypedNodeBase node ) => (NamedComfyNode)node.ToNamedNode(); } public abstract record ComfyTypedNodeBase : ComfyTypedNodeBase where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() where TOutput4 : NodeConnectionBase, new() where TOutput5 : NodeConnectionBase, new() { [JsonIgnore] public TOutput1 Output1 => new() { Data = new object[] { Name, 0 } }; [JsonIgnore] public TOutput2 Output2 => new() { Data = new object[] { Name, 1 } }; [JsonIgnore] public TOutput3 Output3 => new() { Data = new object[] { Name, 2 } }; [JsonIgnore] public TOutput4 Output4 => new() { Data = new object[] { Name, 3 } }; [JsonIgnore] public TOutput5 Output5 => new() { Data = new object[] { Name, 4 } }; public static implicit operator NamedComfyNode( ComfyTypedNodeBase node ) => (NamedComfyNode)node.ToNamedNode(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/IOutputNode.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; public interface IOutputNode { /// /// Returns { Name, index } for use as a node connection /// public object[] GetOutput(int index); } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/NamedComfyNode.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; [JsonSerializable(typeof(NamedComfyNode))] public record NamedComfyNode([property: JsonIgnore] string Name) : ComfyNode, IOutputNode { /// /// Returns { Name, index } for use as a node connection /// public object[] GetOutput(int index) { return new object[] { Name, index }; } /// /// Returns typed { Name, index } for use as a node connection /// public TOutput GetOutput(int index) where TOutput : NodeConnectionBase, new() { return new TOutput { Data = GetOutput(index) }; } } [JsonSerializable(typeof(NamedComfyNode<>))] public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput : NodeConnectionBase, new() { public TOutput Output => new TOutput { Data = GetOutput(0) }; } [JsonSerializable(typeof(NamedComfyNode<>))] public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() { public TOutput1 Output1 => new() { Data = GetOutput(0) }; public TOutput2 Output2 => new() { Data = GetOutput(1) }; } [JsonSerializable(typeof(NamedComfyNode<>))] public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() { public TOutput1 Output1 => new() { Data = GetOutput(0) }; public TOutput2 Output2 => new() { Data = GetOutput(1) }; public TOutput3 Output3 => new() { Data = GetOutput(2) }; } [JsonSerializable(typeof(NamedComfyNode<>))] public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() where TOutput4 : NodeConnectionBase, new() { public TOutput1 Output1 => new() { Data = GetOutput(0) }; public TOutput2 Output2 => new() { Data = GetOutput(1) }; public TOutput3 Output3 => new() { Data = GetOutput(2) }; public TOutput4 Output4 => new() { Data = GetOutput(3) }; } [JsonSerializable(typeof(NamedComfyNode<>))] public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput1 : NodeConnectionBase, new() where TOutput2 : NodeConnectionBase, new() where TOutput3 : NodeConnectionBase, new() where TOutput4 : NodeConnectionBase, new() where TOutput5 : NodeConnectionBase, new() { public TOutput1 Output1 => new() { Data = GetOutput(0) }; public TOutput2 Output2 => new() { Data = GetOutput(1) }; public TOutput3 Output3 => new() { Data = GetOutput(2) }; public TOutput4 Output4 => new() { Data = GetOutput(3) }; public TOutput5 Output5 => new() { Data = GetOutput(4) }; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs ================================================ using System.ComponentModel; using System.Reflection; using System.Text.Json.Serialization; using KGySoft.CoreLibraries; using OneOf; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Packages.Extensions; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; public class NodeDictionary : Dictionary { /// /// Tracks base names and their highest index resulting from /// private readonly Dictionary _baseNameIndex = new(); /// /// When inserting TypedNodes, this holds a mapping of ClassType to required extensions /// [JsonIgnore] public Dictionary ClassTypeRequiredExtensions { get; } = new(); public IEnumerable RequiredExtensions => ClassTypeRequiredExtensions.Values.SelectMany(x => x); /// /// Finds a unique node name given a base name, by appending _2, _3, etc. /// public string GetUniqueName([Localizable(false)] string nameBase) { if (_baseNameIndex.TryGetValue(nameBase, out var index)) { var newIndex = checked(index + 1); _baseNameIndex[nameBase] = newIndex; return $"{nameBase}_{newIndex}"; } // Ensure new name does not exist if (ContainsKey(nameBase)) { throw new InvalidOperationException($"Initial unique name already exists for base {nameBase}"); } _baseNameIndex.Add(nameBase, 1); return nameBase; } public TNamedNode AddNamedNode(TNamedNode node) where TNamedNode : NamedComfyNode { Add(node.Name, node); return node; } public TTypedNode AddTypedNode(TTypedNode node) where TTypedNode : ComfyTypedNodeBase { var namedNode = (NamedComfyNode)node; Add(node.Name, namedNode); // Check statically annotated stuff for TypedNodeOptionsAttribute if (node.GetType().GetCustomAttribute() is { } options) { if (options.RequiredExtensions != null) { ClassTypeRequiredExtensions.AddOrUpdate( namedNode.ClassType, _ => options.GetRequiredExtensionSpecifiers().ToArray(), (_, specifiers) => options.GetRequiredExtensionSpecifiers().Concat(specifiers).ToArray() ); } } return node; } public void NormalizeConnectionTypes() { using var _ = CodeTimer.StartDebug(); // Convert all node inputs containing NodeConnectionBase objects to their Data property foreach (var node in Values) { lock (node.Inputs) { foreach (var (key, input) in node.Inputs) { if (input is NodeConnectionBase connection) { node.Inputs[key] = connection.Data; } else if (input is IOneOf { Value: NodeConnectionBase oneOfConnection }) { node.Inputs[key] = oneOfConnection.Data; } } } } } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/Nodes/RerouteNode.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; /// /// Skeleton node that relays the output of another node /// public record RerouteNode(object[] Connection) : IOutputNode { /// public object[] GetOutput(int index) { if (index != 0) { throw new ArgumentOutOfRangeException(nameof(index)); } return Connection; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyStatus.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public record ComfyStatus { [JsonPropertyName("exec_info")] public required ComfyStatusExecInfo ExecInfo { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyStatusExecInfo.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public record ComfyStatusExecInfo { [JsonPropertyName("queue_remaining")] public required int QueueRemaining { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutingData.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public class ComfyWebSocketExecutingData { [JsonPropertyName("prompt_id")] public string? PromptId { get; set; } /// /// When this is null it indicates completed /// [JsonPropertyName("node")] public string? Node { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutionErrorData.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public record ComfyWebSocketExecutionErrorData { public required string PromptId { get; set; } public string? NodeId { get; set; } public string? NodeType { get; set; } public string? ExceptionMessage { get; set; } public string? ExceptionType { get; set; } public string[]? Traceback { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketImageData.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public readonly record struct ComfyWebSocketImageData(byte[] ImageBytes, string? MimeType = null); ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketProgressData.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public record ComfyWebSocketProgressData { [JsonPropertyName("value")] public required int Value { get; set; } [JsonPropertyName("max")] public required int Max { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketStatusData.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public record ComfyWebSocketStatusData { [JsonPropertyName("status")] public required ComfyStatus Status { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/HuggingFace/HuggingFaceUser.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.HuggingFace; public record HuggingFaceUser { [JsonPropertyName("name")] public string? Name { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/HuggingFaceAccountStatusUpdateEventArgs.cs ================================================ namespace StabilityMatrix.Core.Models.Api; // Or StabilityMatrix.Core.Models.Api.HuggingFace public class HuggingFaceAccountStatusUpdateEventArgs : EventArgs { public bool IsConnected { get; init; } public string? Username { get; init; } // Optional: if we decide to fetch/display username public string? ErrorMessage { get; init; } public static HuggingFaceAccountStatusUpdateEventArgs Disconnected => new(false, null); // Constructor to allow initialization, matching the usage in AccountSettingsViewModel public HuggingFaceAccountStatusUpdateEventArgs() { } public HuggingFaceAccountStatusUpdateEventArgs(bool isConnected, string? username, string? errorMessage = null) { IsConnected = isConnected; Username = username; ErrorMessage = errorMessage; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/ImageResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class ImageResponse { [JsonPropertyName("images")] public string[] Images { get; set; } [JsonPropertyName("info")] public string? Info { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Invoke/InstallModelRequest.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Invoke; public class InstallModelRequest { public string Name { get; set; } public string Description { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Invoke/ModelInstallResult.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Invoke; public class ModelInstallResult { [JsonPropertyName("id")] public long Id { get; set; } [JsonPropertyName("status")] public string? Status { get; set; } [JsonPropertyName("error_reason")] public string? ErrorReason { get; set; } [JsonPropertyName("inplace")] public bool Inplace { get; set; } [JsonPropertyName("local_path")] public string? LocalPath { get; set; } [JsonPropertyName("bytes")] public long Bytes { get; set; } [JsonPropertyName("total_bytes")] public long TotalBytes { get; set; } [JsonPropertyName("error")] public string? Error { get; set; } [JsonPropertyName("error_traceback")] public string? ErrorTraceback { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Invoke/ScanFolderResult.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Invoke; public class ScanFolderResult { public string Path { get; set; } [JsonPropertyName("is_installed")] public bool IsInstalled { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/Analytics/AnalyticsRequest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Lykos.Analytics; public class AnalyticsRequest { [JsonPropertyName("type")] public virtual string Type { get; set; } = "unknown"; public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/Analytics/FirstTimeInstallAnalytics.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos.Analytics; public class FirstTimeInstallAnalytics : AnalyticsRequest { public string? SelectedPackageName { get; set; } public IEnumerable? SelectedRecommendedModels { get; set; } public bool FirstTimeSetupSkipped { get; set; } public override string Type { get; set; } = "first-time-install"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/Analytics/LaunchAnalyticsRequest.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos.Analytics; public class LaunchAnalyticsRequest : AnalyticsRequest { public string? Version { get; set; } public string? RuntimeIdentifier { get; set; } public string? OsDescription { get; set; } public override string Type { get; set; } = "launch"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/Analytics/PackageInstallAnalyticsRequest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.Lykos.Analytics; public class PackageInstallAnalyticsRequest : AnalyticsRequest { public required string PackageName { get; set; } public required string PackageVersion { get; set; } public bool IsSuccess { get; set; } [JsonPropertyName("type")] public override string Type { get; set; } = "package-install"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/GetDownloadResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record GetFilesDownloadResponse { public required Uri DownloadUrl { get; set; } public DateTimeOffset? ExpiresAt { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/GetRecommendedModelsResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public class GetRecommendedModelsResponse { public required ModelLists Sd15 { get; set; } public required ModelLists Sdxl { get; set; } public required ModelLists Decoders { get; set; } } public class ModelLists { public IEnumerable? CivitAi { get; set; } public IEnumerable? HuggingFace { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/GetUserResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record GetUserResponse { public required string Id { get; init; } public required LykosAccount Account { get; init; } public required HashSet UserRoles { get; init; } public string? PatreonId { get; init; } public bool IsEmailVerified { get; init; } public bool CanHasDevBuild { get; init; } public bool CanHasPreviewBuild { get; init; } public bool IsActiveSupporter => CanHasDevBuild || CanHasPreviewBuild; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/GoogleOAuthResponse.cs ================================================ using System.Web; namespace StabilityMatrix.Core.Models.Api.Lykos; public class GoogleOAuthResponse { public string? Code { get; init; } public string? State { get; init; } public string? Nonce { get; init; } public string? Error { get; init; } public static GoogleOAuthResponse ParseFromQueryString(string query) { var queryCollection = HttpUtility.ParseQueryString(query); return new GoogleOAuthResponse { Code = queryCollection.Get("code"), State = queryCollection.Get("state"), Nonce = queryCollection.Get("nonce"), Error = queryCollection.Get("error") }; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/LykosAccount.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record LykosAccount(string Id, string Name); ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/LykosAccountStatusUpdateEventArgs.cs ================================================ using System.Security.Claims; using OpenIddict.Abstractions; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api.LykosAuthApi; namespace StabilityMatrix.Core.Models.Api.Lykos; public class LykosAccountStatusUpdateEventArgs : EventArgs { public static LykosAccountStatusUpdateEventArgs Disconnected { get; } = new(); public bool IsConnected { get; init; } public ClaimsPrincipal? Principal { get; init; } public AccountResponse? User { get; init; } public string? Id => Principal?.GetClaim(OpenIddictConstants.Claims.Subject); public string? DisplayName => Principal?.GetClaim(OpenIddictConstants.Claims.PreferredUsername) ?? Principal?.Identity?.Name; public string? Email => Principal?.GetClaim(OpenIddictConstants.Claims.Email); public bool IsPatreonConnected => User?.PatreonId != null; public bool IsActiveSupporter => User?.Roles.Contains("ActivePatron") == true; } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV1Tokens.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; [Obsolete("Use LykosAccountV2Tokens instead")] public record LykosAccountV1Tokens(string AccessToken, string RefreshToken); ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV2Tokens.cs ================================================ using System.Security.Claims; using Microsoft.IdentityModel.JsonWebTokens; namespace StabilityMatrix.Core.Models.Api.Lykos; public record LykosAccountV2Tokens(string AccessToken, string? RefreshToken, string? IdentityToken) { public JsonWebToken? GetDecodedIdentityToken() { if (string.IsNullOrWhiteSpace(IdentityToken)) { return null; } var handler = new JsonWebTokenHandler(); return handler.ReadJsonWebToken(IdentityToken); } public ClaimsPrincipal? GetIdentityTokenPrincipal() { if (GetDecodedIdentityToken() is not { } token) { return null; } return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "IdentityToken")); } public DateTimeOffset? GetIdentityTokenExpiration() { if (GetDecodedIdentityToken() is not { } token) { return null; } return token.ValidTo; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/LykosRole.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public enum LykosRole { Unknown = -1, Basic = 0, Supporter = 1, PatreonSupporter = 2, Insider = 3, // 4 and 5 have been retired 🫡 [Obsolete("Roles restructured, use the new BetaTester role")] OLD_BetaTester = 4, [Obsolete("Roles restructured, use the new Developer role")] OLD_Developer = 5, Pioneer = 6, Visionary = 7, BetaTester = 100, Translator = 101, Developer = 900, } ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/PostAccountRequest.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record PostAccountRequest( string Email, string Password, string ConfirmPassword, string AccountName ); ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/PostLoginRefreshRequest.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record PostLoginRefreshRequest(string RefreshToken); ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/PostLoginRequest.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public record PostLoginRequest(string Email, string Password); ================================================ FILE: StabilityMatrix.Core/Models/Api/Lykos/RecommendedModelsV2Response.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Lykos; public class RecommendedModelsV2Response { public Dictionary> RecommendedModelsByCategory { get; set; } = new(); } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/NodesCount.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class NodesCount { [JsonPropertyName("total")] public long Total { get; set; } [JsonPropertyName("primitive")] public long Primitive { get; set; } [JsonPropertyName("custom")] public long Custom { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtCreator.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtCreator { [JsonPropertyName("uid")] public string Uid { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("bio")] public string Bio { get; set; } [JsonPropertyName("avatar")] public Uri Avatar { get; set; } [JsonPropertyName("username")] public string Username { get; set; } [JsonPropertyName("dev_profile_url")] public string DevProfileUrl { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDateTime.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtDateTime { [JsonPropertyName("_seconds")] public long Seconds { get; set; } public DateTimeOffset ToDateTimeOffset() { return DateTimeOffset.FromUnixTimeSeconds(Seconds); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadRequest.cs ================================================ using System.Text.Json.Serialization; using Refit; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtDownloadRequest { [AliasAs("workflow_id")] [JsonPropertyName("workflow_id")] public required string WorkflowId { get; set; } [AliasAs("version_tag")] [JsonPropertyName("version_tag")] public string VersionTag { get; set; } = "latest"; } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtDownloadResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtDownloadResponse { [JsonPropertyName("filename")] public string Filename { get; set; } [JsonPropertyName("payload")] public string Payload { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtFeedRequest.cs ================================================ using Refit; namespace StabilityMatrix.Core.Models.Api.OpenArt; /// /// Note that parameters Category, Custom Node and Sort should be used separately /// public class OpenArtFeedRequest { [AliasAs("category")] public string Category { get; set; } [AliasAs("sort")] public string Sort { get; set; } [AliasAs("custom_node")] public string CustomNode { get; set; } [AliasAs("cursor")] public string Cursor { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchRequest.cs ================================================ using Refit; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtSearchRequest { [AliasAs("keyword")] public required string Keyword { get; set; } [AliasAs("pageSize")] public int PageSize { get; set; } = 30; /// /// 0-based index of the page to retrieve /// [AliasAs("currentPage")] public int CurrentPage { get; set; } = 0; } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtSearchResponse { [JsonPropertyName("items")] public IEnumerable Items { get; set; } [JsonPropertyName("total")] public int Total { get; set; } [JsonPropertyName("nextCursor")] public string? NextCursor { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtSearchResult.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtSearchResult { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("creator")] public OpenArtCreator Creator { get; set; } [JsonPropertyName("stats")] public OpenArtStats Stats { get; set; } [JsonPropertyName("nodes_index")] public IEnumerable NodesIndex { get; set; } [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("description")] public string Description { get; set; } [JsonPropertyName("categories")] public IEnumerable Categories { get; set; } [JsonPropertyName("thumbnails")] public List Thumbnails { get; set; } [JsonPropertyName("nodes_count")] public NodesCount NodesCount { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtStats.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtStats { [JsonPropertyName("num_shares")] public int NumShares { get; set; } [JsonPropertyName("num_bookmarks")] public int NumBookmarks { get; set; } [JsonPropertyName("num_reviews")] public int NumReviews { get; set; } [JsonPropertyName("rating")] public double Rating { get; set; } [JsonPropertyName("num_comments")] public int NumComments { get; set; } [JsonPropertyName("num_likes")] public int NumLikes { get; set; } [JsonPropertyName("num_downloads")] public int NumDownloads { get; set; } [JsonPropertyName("num_runs")] public int NumRuns { get; set; } [JsonPropertyName("num_views")] public int NumViews { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenArt/OpenArtThumbnail.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenArt; public class OpenArtThumbnail { [JsonPropertyName("width")] public int Width { get; set; } [JsonPropertyName("url")] public Uri Url { get; set; } [JsonPropertyName("height")] public int Height { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbArchitecture.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbArchitecture { public string? Name { get; set; } public string? Input { get; set; } public string[]? CompatiblePlatforms { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbArchitecturesResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbArchitecturesResponse : Dictionary; ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbImage.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; /// /// Apparently all the urls can be either standalone or paired. /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type", IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(Paired), "paired")] [JsonDerivedType(typeof(Standalone), "standalone")] public class OpenModelDbImage { /*{ "type": "paired", "LR": "https://images2.imgbox.com/09/3f/ZcIq3bwn_o.jpeg", "SR": "https://images2.imgbox.com/c7/dd/lIHpU4PZ_o.png", "thumbnail": "/thumbs/small/6e36848722bccb84eca5232a.jpg" }*/ /*{ "type": "paired", "LR": "/thumbs/573c7a3162c9831716c3bb35.jpg", "SR": "/thumbs/9a7cb6631356006c30f177d1.jpg", "LRSize": { "width": 366, "height": 296 }, "SRSize": { "width": 366, "height": 296 } }*/ public class Paired : OpenModelDbImage { [JsonPropertyName("LR")] public Uri? Lr { get; set; } [JsonPropertyName("SR")] public Uri? Sr { get; set; } public Uri? Thumbnail { get; set; } [JsonIgnore] public Uri? SrAbsoluteUri => ToAbsoluteUri(Sr); [JsonIgnore] public Uri? LrAbsoluteUri => ToAbsoluteUri(Lr); public override IEnumerable GetImageAbsoluteUris() { var hasHq = false; if (ToAbsoluteUri(Sr) is { } srUri) { hasHq = true; yield return srUri; } if (ToAbsoluteUri(Lr) is { } lrUri) { hasHq = true; yield return lrUri; } if (!hasHq && ToAbsoluteUri(Thumbnail) is { } thumbnail) { yield return thumbnail; } } } /* { "type": "standalone", "url": "https://i.slow.pics/rE3PKKTD.webp", "thumbnail": "/thumbs/small/85e62ea0e6801e7a0bf5acb6.jpg" } */ public class Standalone : OpenModelDbImage { public Uri? Url { get; set; } public Uri? Thumbnail { get; set; } public override IEnumerable GetImageAbsoluteUris() { if (ToAbsoluteUri(Url) is { } url) { yield return url; } else if (ToAbsoluteUri(Thumbnail) is { } thumbnail) { yield return thumbnail; } } } private static Uri? ToAbsoluteUri(Uri? url) { if (url is null) { return null; } if (url.IsAbsoluteUri) { return url; } var baseUri = new Uri("https://openmodeldb.info/"); return new Uri(baseUri, url); } public virtual IEnumerable GetImageAbsoluteUris() { return []; } } public static class OpenModelDbImageEnumerableExtensions { public static IEnumerable SelectImageAbsoluteUris(this IEnumerable images) { return images.SelectMany(image => image.GetImageAbsoluteUris()); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbKeyedModel.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public record OpenModelDbKeyedModel : OpenModelDbModel { public required string Id { get; set; } public OpenModelDbKeyedModel() { } public OpenModelDbKeyedModel(OpenModelDbModel model) : base(model) { } public OpenModelDbKeyedModel(OpenModelDbKeyedModel model) : base(model) { Id = model.Id; } public SharedFolderType? GetSharedFolderType() { return Architecture?.ToLowerInvariant() switch { "swinir" => SharedFolderType.SwinIR, _ => SharedFolderType.ESRGAN }; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbModel.cs ================================================ using System.Text.Json.Serialization; using OneOf; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public record OpenModelDbModel { public string? Name { get; set; } [JsonConverter(typeof(OneOfJsonConverter))] public OneOf? Author { get; set; } public string? License { get; set; } public List? Tags { get; set; } public string? Description { get; set; } public DateOnly? Date { get; set; } public string? Architecture { get; set; } public List? Size { get; set; } public int? Scale { get; set; } public int? InputChannels { get; set; } public int? OutputChannels { get; set; } public List? Resources { get; set; } public List? Images { get; set; } public OpenModelDbImage? Thumbnail { get; set; } public OpenModelDbModel(OpenModelDbModel model) { Name = model.Name; Author = model.Author; License = model.License; Tags = model.Tags; Description = model.Description; Date = model.Date; Architecture = model.Architecture; Size = model.Size; Scale = model.Scale; InputChannels = model.InputChannels; OutputChannels = model.OutputChannels; Resources = model.Resources; Images = model.Images; Thumbnail = model.Thumbnail; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbModelsResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbModelsResponse : Dictionary { public ParallelQuery GetKeyedModels() { return this.AsParallel().Select(kv => new OpenModelDbKeyedModel(kv.Value) { Id = kv.Key }); } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbResource.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbResource { public string? Platform { get; set; } public string? Type { get; set; } public long Size { get; set; } public string? Sha256 { get; set; } public List? Urls { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbTag.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbTag { public string? Name { get; set; } public string? Description { get; set; } public string[]? Implies { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/OpenModelsDb/OpenModelDbTagsResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.OpenModelsDb; public class OpenModelDbTagsResponse : Dictionary; ================================================ FILE: StabilityMatrix.Core/Models/Api/ProgressRequest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class ProgressRequest { [JsonPropertyName("skip_current_image")] public bool? SkipCurrentImage { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/ProgressResponse.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class ProgressResponse { // Range from 0 to 1 [JsonPropertyName("progress")] public float Progress { get; set; } // ETA in seconds [JsonPropertyName("eta_relative")] public float EtaRelative { get; set; } // state: dict // The current image in base64 format. opts.show_progress_every_n_steps is required for this to work [JsonPropertyName("current_image")] public string? CurrentImage { get; set; } [JsonPropertyName("textinfo")] public string? TextInfo { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/Pypi/PyPiReleaseFile.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Pypi; public class PyPiReleaseFile { } ================================================ FILE: StabilityMatrix.Core/Models/Api/Pypi/PyPiResponse.cs ================================================ namespace StabilityMatrix.Core.Models.Api.Pypi; public class PyPiResponse { public Dictionary>? Releases { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Api/TextToImageRequest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Api; public class TextToImageRequest { [JsonPropertyName("enable_hr")] public bool? EnableHr { get; set; } [JsonPropertyName("denoising_strength")] public int? DenoisingStrength { get; set; } [JsonPropertyName("firstphase_width")] public int? FirstPhaseWidth { get; set; } [JsonPropertyName("firstphase_height")] public int? FirstPhaseHeight { get; set; } [JsonPropertyName("hr_scale")] public int? HrScale { get; set; } [JsonPropertyName("hr_upscaler")] public string? HrUpscaler { get; set; } [JsonPropertyName("hr_second_pass_steps")] public int? HrSecondPassSteps { get; set; } [JsonPropertyName("hr_resize_x")] public int? HrResizeX { get; set; } [JsonPropertyName("hr_resize_y")] public int? HrResizeY { get; set; } [JsonPropertyName("prompt")] public string Prompt { get; set; } [JsonPropertyName("styles")] public string?[] Styles { get; set; } [JsonPropertyName("seed")] public int? Seed { get; set; } [JsonPropertyName("subseed")] public int? Subseed { get; set; } [JsonPropertyName("subseed_strength")] public int? SubseedStrength { get; set; } [JsonPropertyName("seed_resize_from_h")] public int? SeedResizeFromH { get; set; } [JsonPropertyName("seed_resize_from_w")] public int? SeedResizeFromW { get; set; } [JsonPropertyName("sampler_name")] public string? SamplerName { get; set; } [JsonPropertyName("batch_size")] public int? BatchSize { get; set; } [JsonPropertyName("n_iter")] public int? NIter { get; set; } [JsonPropertyName("steps")] public int? Steps { get; set; } [JsonPropertyName("cfg_scale")] public int? CfgScale { get; set; } [JsonPropertyName("width")] public int? Width { get; set; } [JsonPropertyName("height")] public int? Height { get; set; } [JsonPropertyName("restore_faces")] public bool? RestoreFaces { get; set; } [JsonPropertyName("tiling")] public bool? Tiling { get; set; } [JsonPropertyName("do_not_save_samples")] public bool? DoNotSaveSamples { get; set; } [JsonPropertyName("do_not_save_grid")] public bool? DoNotSaveGrid { get; set; } [JsonPropertyName("negative_prompt")] public string? NegativePrompt { get; set; } [JsonPropertyName("eta")] public int? Eta { get; set; } [JsonPropertyName("s_min_uncond")] public int? SMinUncond { get; set; } [JsonPropertyName("s_churn")] public int? SChurn { get; set; } [JsonPropertyName("s_tmax")] public int? STmax { get; set; } [JsonPropertyName("s_tmin")] public int? STmin { get; set; } [JsonPropertyName("s_noise")] public int? SNoise { get; set; } [JsonPropertyName("override_settings")] public Dictionary? OverrideSettings { get; set; } [JsonPropertyName("override_settings_restore_afterwards")] public bool? OverrideSettingsRestoreAfterwards { get; set; } [JsonPropertyName("script_args")] public string[]? ScriptArgs { get; set; } [JsonPropertyName("sampler_index")] public string? SamplerIndex { get; set; } [JsonPropertyName("script_name")] public string? ScriptName { get; set; } [JsonPropertyName("send_images")] public bool? SendImages { get; set; } [JsonPropertyName("save_images")] public bool? SaveImages { get; set; } [JsonPropertyName("alwayson_scripts")] public Dictionary? AlwaysOnScripts { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Base/StringValue.cs ================================================ namespace StabilityMatrix.Core.Models.Base; /// /// Base class for a string value object /// /// String value public abstract record StringValue(string Value) { /// public override string ToString() { return Value; } public static implicit operator string(StringValue stringValue) => stringValue.ToString(); } ================================================ FILE: StabilityMatrix.Core/Models/CheckpointSortMode.cs ================================================ using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models; public enum CheckpointSortMode { [StringValue("Base Model")] BaseModel, [StringValue("Date Created")] Created, [StringValue("Date Last Modified")] LastModified, [StringValue("File Name")] FileName, [StringValue("File Size")] FileSize, [StringValue("Title")] Title, [StringValue("Type")] SharedFolderType, [StringValue("Update Available")] UpdateAvailable, } ================================================ FILE: StabilityMatrix.Core/Models/CheckpointSortOptions.cs ================================================ using System.ComponentModel; namespace StabilityMatrix.Core.Models; public class CheckpointSortOptions { public CheckpointSortMode SortMode { get; set; } = CheckpointSortMode.SharedFolderType; public ListSortDirection SortDirection { get; set; } = ListSortDirection.Descending; public bool SortConnectedModelsFirst { get; set; } = true; } ================================================ FILE: StabilityMatrix.Core/Models/CivitPostDownloadContextAction.cs ================================================ using System.Diagnostics; using System.Text.Json; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models; public class CivitPostDownloadContextAction : IContextAction { /// public object? Context { get; set; } public static CivitPostDownloadContextAction FromCivitFile(CivitFile file) { return new CivitPostDownloadContextAction { Context = file.Hashes.BLAKE3 }; } public void Invoke(ISettingsManager settingsManager, IModelIndexService modelIndexService) { var result = Context as string; if (Context is JsonElement jsonElement) { result = jsonElement.GetString(); } if (result is null) { Debug.WriteLine($"Context {Context} is not a string."); return; } Debug.WriteLine($"Adding {result} to installed models."); // Also request reindex modelIndexService.BackgroundRefreshIndex(); } } ================================================ FILE: StabilityMatrix.Core/Models/CivitaiResource.cs ================================================ namespace StabilityMatrix.Core.Models; public class CivitaiResource { public string Type { get; set; } public int ModelVersionId { get; set; } public string ModelName { get; set; } public string ModelVersionName { get; set; } public double? Weight { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/ComfyNodeMap.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; namespace StabilityMatrix.Core.Models; public class ComfyNodeMap { private static Dictionary lookup; public static Dictionary Lookup { get => lookup ??= Deserialize(); } public static Dictionary Deserialize() { var json = JsonSerializer.Deserialize>(Json); return json! .Select( pair => ( Name: (pair.Value[1] as JsonObject)?["title_aux"]?.ToString() ?? string.Empty, Url: pair.Key ) ) .DistinctBy(x => x.Name) .ToDictionary(pair => pair.Name, pair => pair.Url); } private const string Json = """ { "https://gist.github.com/alkemann/7361b8eb966f29c8238fd323409efb68/raw/f9605be0b38d38d3e3a2988f89248ff557010076/alkemann.py": [ [ "Int to Text", "Save A1 Image", "Seed With Text" ], { "title_aux": "alkemann nodes" } ], "https://git.mmaker.moe/mmaker/sd-webui-color-enhance": [ [ "MMakerColorBlend", "MMakerColorEnhance" ], { "title_aux": "Color Enhance" } ], "https://github.com/0xbitches/ComfyUI-LCM": [ [ "LCM_Sampler", "LCM_Sampler_Advanced", "LCM_img2img_Sampler", "LCM_img2img_Sampler_Advanced" ], { "title_aux": "Latent Consistency Model for ComfyUI" } ], "https://github.com/1shadow1/hayo_comfyui_nodes/raw/main/LZCNodes.py": [ [ "LoadPILImages", "MergeImages", "make_transparentmask", "tensor_trans_pil", "words_generatee" ], { "title_aux": "Hayo comfyui nodes" } ], "https://github.com/42lux/ComfyUI-safety-checker": [ [ "Safety Checker" ], { "title_aux": "ComfyUI-safety-checker" } ], "https://github.com/54rt1n/ComfyUI-DareMerge": [ [ "DM_AdvancedDareModelMerger", "DM_AdvancedModelMerger", "DM_AttentionGradient", "DM_BlockGradient", "DM_BlockModelMerger", "DM_DareClipMerger", "DM_DareModelMergerBlock", "DM_DareModelMergerElement", "DM_DareModelMergerMBW", "DM_GradientEdit", "DM_GradientOperations", "DM_GradientReporting", "DM_InjectNoise", "DM_LoRALoaderTags", "DM_LoRAReporting", "DM_MBWGradient", "DM_MagnitudeMasker", "DM_MaskEdit", "DM_MaskOperations", "DM_MaskReporting", "DM_ModelReporting", "DM_NormalizeModel", "DM_QuadMasker", "DM_ShellGradient", "DM_SimpleMasker" ], { "title_aux": "ComfyUI-DareMerge" } ], "https://github.com/80sVectorz/ComfyUI-Static-Primitives": [ [ "FloatStaticPrimitive", "IntStaticPrimitive", "StringMlStaticPrimitive", "StringStaticPrimitive" ], { "title_aux": "ComfyUI-Static-Primitives" } ], "https://github.com/AInseven/ComfyUI-fastblend": [ [ "FillDarkMask", "InterpolateKeyFrame", "MaskListcaptoBatch", "MyOpenPoseNode", "SmoothVideo", "reBatchImage" ], { "title_aux": "ComfyUI-fastblend" } ], "https://github.com/AIrjen/OneButtonPrompt": [ [ "AutoNegativePrompt", "CreatePromptVariant", "OneButtonPreset", "OneButtonPrompt", "SavePromptToFile" ], { "title_aux": "One Button Prompt" } ], "https://github.com/AbdullahAlfaraj/Comfy-Photoshop-SD": [ [ "APS_LatentBatch", "APS_Seed", "ContentMaskLatent", "ControlNetScript", "ControlnetUnit", "GaussianLatentImage", "GetConfig", "LoadImageBase64", "LoadImageWithMetaData", "LoadLorasFromPrompt", "MaskExpansion" ], { "title_aux": "Comfy-Photoshop-SD" } ], "https://github.com/AbyssYuan0/ComfyUI_BadgerTools": [ [ "ApplyMaskToImage-badger", "CropImageByMask-badger", "ExpandImageWithColor-badger", "FindThickLinesFromCanny-badger", "FloatToInt-badger", "FloatToString-badger", "FrameToVideo-badger", "GarbageCollect-badger", "GetColorFromBorder-badger", "GetDirName-badger", "GetUUID-badger", "IdentifyBorderColorToMask-badger", "IdentifyColorToMask-badger", "ImageNormalization-badger", "ImageOverlap-badger", "ImageScaleToSide-badger", "IntToString-badger", "SegmentToMaskByPoint-badger", "StringToFizz-badger", "TextListToString-badger", "TrimTransparentEdges-badger", "VideoCutFromDir-badger", "VideoToFrame-badger", "deleteDir-badger", "findCenterOfMask-badger", "getImageSide-badger", "getParentDir-badger", "mkdir-badger" ], { "title_aux": "ComfyUI_BadgerTools" } ], "https://github.com/Acly/comfyui-inpaint-nodes": [ [ "INPAINT_ApplyFooocusInpaint", "INPAINT_InpaintWithModel", "INPAINT_LoadFooocusInpaint", "INPAINT_LoadInpaintModel", "INPAINT_MaskedBlur", "INPAINT_MaskedFill", "INPAINT_VAEEncodeInpaintConditioning" ], { "title_aux": "ComfyUI Inpaint Nodes" } ], "https://github.com/Acly/comfyui-tooling-nodes": [ [ "ETN_ApplyMaskToImage", "ETN_CropImage", "ETN_LoadImageBase64", "ETN_LoadMaskBase64", "ETN_SendImageWebSocket" ], { "title_aux": "ComfyUI Nodes for External Tooling" } ], "https://github.com/Amorano/Jovimetrix": [ [], { "author": "amorano", "description": "Webcams, GLSL shader, Media Streaming, Tick animation, Image manipulation,", "nodename_pattern": " \\(jov\\)$", "title": "Jovimetrix", "title_aux": "Jovimetrix Composition Nodes" } ], "https://github.com/ArtBot2023/CharacterFaceSwap": [ [ "Color Blend", "Crop Face", "Exclude Facial Feature", "Generation Parameter Input", "Generation Parameter Output", "Image Full BBox", "Load BiseNet", "Load RetinaFace", "Mask Contour", "Segment Face", "Uncrop Face" ], { "title_aux": "Character Face Swap" } ], "https://github.com/ArtVentureX/comfyui-animatediff": [ [ "AnimateDiffCombine", "AnimateDiffLoraLoader", "AnimateDiffModuleLoader", "AnimateDiffSampler", "AnimateDiffSlidingWindowOptions", "ImageSizeAndBatchSize", "LoadVideo" ], { "title_aux": "AnimateDiff" } ], "https://github.com/AustinMroz/ComfyUI-SpliceTools": [ [ "LogSigmas", "RerangeSigmas", "SpliceDenoised", "SpliceLatents", "TemporalSplice" ], { "title_aux": "SpliceTools" } ], "https://github.com/BadCafeCode/masquerade-nodes-comfyui": [ [ "Blur", "Change Channel Count", "Combine Masks", "Constant Mask", "Convert Color Space", "Create QR Code", "Create Rect Mask", "Cut By Mask", "Get Image Size", "Image To Mask", "Make Image Batch", "Mask By Text", "Mask Morphology", "Mask To Region", "MasqueradeIncrementer", "Mix Color By Mask", "Mix Images By Mask", "Paste By Mask", "Prune By Mask", "Separate Mask Components", "Unary Image Op", "Unary Mask Op" ], { "title_aux": "Masquerade Nodes" } ], "https://github.com/Beinsezii/bsz-cui-extras": [ [ "BSZAbsoluteHires", "BSZAspectHires", "BSZColoredLatentImageXL", "BSZCombinedHires", "BSZHueChromaXL", "BSZInjectionKSampler", "BSZLatentDebug", "BSZLatentFill", "BSZLatentGradient", "BSZLatentHSVAImage", "BSZLatentOffsetXL", "BSZLatentRGBAImage", "BSZLatentbuster", "BSZPixelbuster", "BSZPixelbusterHelp", "BSZPrincipledConditioning", "BSZPrincipledSampler", "BSZPrincipledScale", "BSZStrangeResample" ], { "title_aux": "bsz-cui-extras" } ], "https://github.com/BennyKok/comfyui-deploy": [ [ "ComfyUIDeployExternalCheckpoint", "ComfyUIDeployExternalImage", "ComfyUIDeployExternalImageAlpha", "ComfyUIDeployExternalLora", "ComfyUIDeployExternalNumber", "ComfyUIDeployExternalNumberInt", "ComfyUIDeployExternalText" ], { "author": "BennyKok", "description": "", "nickname": "Comfy Deploy", "title": "comfyui-deploy", "title_aux": "ComfyUI Deploy" } ], "https://github.com/Bikecicle/ComfyUI-Waveform-Extensions/raw/main/EXT_AudioManipulation.py": [ [ "BatchJoinAudio", "CutAudio", "DuplicateAudio", "JoinAudio", "ResampleAudio", "ReverseAudio", "StretchAudio" ], { "title_aux": "Waveform Extensions" } ], "https://github.com/Billius-AI/ComfyUI-Path-Helper": [ [ "Add File Name Prefix", "Add File Name Prefix Advanced", "Add Folder", "Add Folder Advanced", "Create Project Root", "Join Variables", "Show Path", "Show String" ], { "title_aux": "ComfyUI-Path-Helper" } ], "https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb": [ [ "BNK_AddCLIPSDXLParams", "BNK_AddCLIPSDXLRParams", "BNK_CLIPTextEncodeAdvanced", "BNK_CLIPTextEncodeSDXLAdvanced" ], { "title_aux": "Advanced CLIP Text Encode" } ], "https://github.com/BlenderNeko/ComfyUI_Cutoff": [ [ "BNK_CutoffBasePrompt", "BNK_CutoffRegionsToConditioning", "BNK_CutoffRegionsToConditioning_ADV", "BNK_CutoffSetRegions" ], { "title_aux": "ComfyUI Cutoff" } ], "https://github.com/BlenderNeko/ComfyUI_Noise": [ [ "BNK_DuplicateBatchIndex", "BNK_GetSigma", "BNK_InjectNoise", "BNK_NoisyLatentImage", "BNK_SlerpLatent", "BNK_Unsampler" ], { "title_aux": "ComfyUI Noise" } ], "https://github.com/BlenderNeko/ComfyUI_SeeCoder": [ [ "ConcatConditioning", "SEECoderImageEncode" ], { "title_aux": "SeeCoder [WIP]" } ], "https://github.com/BlenderNeko/ComfyUI_TiledKSampler": [ [ "BNK_TiledKSampler", "BNK_TiledKSamplerAdvanced" ], { "title_aux": "Tiled sampling for ComfyUI" } ], "https://github.com/CYBERLOOM-INC/ComfyUI-nodes-hnmr": [ [ "CLIPIter", "Dict2Model", "GridImage", "ImageBlend2", "KSamplerOverrided", "KSamplerSetting", "KSamplerXYZ", "LatentToHist", "LatentToImage", "ModelIter", "RandomLatentImage", "SaveStateDict", "SaveText", "StateDictLoader", "StateDictMerger", "StateDictMergerBlockWeighted", "StateDictMergerBlockWeightedMulti", "VAEDecodeBatched", "VAEEncodeBatched", "VAEIter" ], { "title_aux": "ComfyUI-nodes-hnmr" } ], "https://github.com/CaptainGrock/ComfyUIInvisibleWatermark/raw/main/Invisible%20Watermark.py": [ [ "Apply Invisible Watermark", "Extract Watermark" ], { "title_aux": "ComfyUIInvisibleWatermark" } ], "https://github.com/Chan-0312/ComfyUI-IPAnimate": [ [ "IPAdapterAnimate" ], { "title_aux": "ComfyUI-IPAnimate" } ], "https://github.com/Chaoses-Ib/ComfyUI_Ib_CustomNodes": [ [ "ImageToPIL", "LoadImageFromPath", "PILToImage", "PILToMask" ], { "title_aux": "ComfyUI_Ib_CustomNodes" } ], "https://github.com/Clybius/ComfyUI-Extra-Samplers": [ [ "SamplerCLYB_4M_SDE_Momentumized", "SamplerCustomModelMixtureDuo", "SamplerCustomNoise", "SamplerCustomNoiseDuo", "SamplerDPMPP_DualSDE_Momentumized", "SamplerEulerAncestralDancing_Experimental", "SamplerLCMCustom", "SamplerRES_Momentumized", "SamplerTTM" ], { "title_aux": "ComfyUI Extra Samplers" } ], "https://github.com/Clybius/ComfyUI-Latent-Modifiers": [ [ "Latent Diffusion Mega Modifier" ], { "title_aux": "ComfyUI-Latent-Modifiers" } ], "https://github.com/CosmicLaca/ComfyUI_Primere_Nodes": [ [ "PrimereAnyDetailer", "PrimereAnyOutput", "PrimereCKPT", "PrimereCKPTLoader", "PrimereCLIPEncoder", "PrimereClearPrompt", "PrimereDynamicParser", "PrimereEmbedding", "PrimereEmbeddingHandler", "PrimereEmbeddingKeywordMerger", "PrimereHypernetwork", "PrimereImageSegments", "PrimereKSampler", "PrimereLCMSelector", "PrimereLORA", "PrimereLYCORIS", "PrimereLatentNoise", "PrimereLoraKeywordMerger", "PrimereLoraStackMerger", "PrimereLycorisKeywordMerger", "PrimereLycorisStackMerger", "PrimereMetaCollector", "PrimereMetaRead", "PrimereMetaSave", "PrimereMidjourneyStyles", "PrimereModelConceptSelector", "PrimereModelKeyword", "PrimereNetworkTagLoader", "PrimerePrompt", "PrimerePromptSwitch", "PrimereRefinerPrompt", "PrimereResolution", "PrimereResolutionMultiplier", "PrimereResolutionMultiplierMPX", "PrimereSamplers", "PrimereSamplersSteps", "PrimereSeed", "PrimereStepsCfg", "PrimereStyleLoader", "PrimereStylePile", "PrimereTextOutput", "PrimereVAE", "PrimereVAELoader", "PrimereVAESelector", "PrimereVisualCKPT", "PrimereVisualEmbedding", "PrimereVisualHypernetwork", "PrimereVisualLORA", "PrimereVisualLYCORIS", "PrimereVisualStyle" ], { "title_aux": "Primere nodes for ComfyUI" } ], "https://github.com/Danand/ComfyUI-ComfyCouple": [ [ "Attention couple", "Comfy Couple" ], { "author": "Rei D.", "description": "If you want to draw two different characters together without blending their features, so you could try to check out this custom node.", "nickname": "Danand", "title": "Comfy Couple", "title_aux": "ComfyUI-ComfyCouple" } ], "https://github.com/Davemane42/ComfyUI_Dave_CustomNode": [ [ "ABGRemover", "ConditioningStretch", "ConditioningUpscale", "MultiAreaConditioning", "MultiLatentComposite" ], { "title_aux": "Visual Area Conditioning / Latent composition" } ], "https://github.com/Derfuu/Derfuu_ComfyUI_ModdedNodes": [ [ "ABSNode_DF", "Absolute value", "Ceil", "CeilNode_DF", "Conditioning area scale by ratio", "ConditioningSetArea with tuples", "ConditioningSetAreaEXT_DF", "ConditioningSetArea_DF", "CosNode_DF", "Cosines", "Divide", "DivideNode_DF", "EmptyLatentImage_DF", "Float", "Float debug print", "Float2Tuple_DF", "FloatDebugPrint_DF", "FloatNode_DF", "Floor", "FloorNode_DF", "Get image size", "Get latent size", "GetImageSize_DF", "GetLatentSize_DF", "Image scale by ratio", "Image scale to side", "ImageScale_Ratio_DF", "ImageScale_Side_DF", "Int debug print", "Int to float", "Int to tuple", "Int2Float_DF", "IntDebugPrint_DF", "Integer", "IntegerNode_DF", "Latent Scale by ratio", "Latent Scale to side", "LatentComposite with tuples", "LatentScale_Ratio_DF", "LatentScale_Side_DF", "MultilineStringNode_DF", "Multiply", "MultiplyNode_DF", "PowNode_DF", "Power", "Random", "RandomFloat_DF", "SinNode_DF", "Sinus", "SqrtNode_DF", "Square root", "String debug print", "StringNode_DF", "Subtract", "SubtractNode_DF", "Sum", "SumNode_DF", "TanNode_DF", "Tangent", "Text", "Text box", "Tuple", "Tuple debug print", "Tuple multiply", "Tuple swap", "Tuple to floats", "Tuple to ints", "Tuple2Float_DF", "TupleDebugPrint_DF", "TupleNode_DF" ], { "title_aux": "Derfuu_ComfyUI_ModdedNodes" } ], "https://github.com/DonBaronFactory/ComfyUI-Cre8it-Nodes": [ [ "ApplySerialPrompter", "ImageSizer", "SerialPrompter" ], { "author": "CRE8IT GmbH", "description": "This extension offers various nodes.", "nickname": "cre8Nodes", "title": "cr8SerialPrompter", "title_aux": "ComfyUI-Cre8it-Nodes" } ], "https://github.com/Electrofried/ComfyUI-OpenAINode": [ [ "OpenAINode" ], { "title_aux": "OpenAINode" } ], "https://github.com/EllangoK/ComfyUI-post-processing-nodes": [ [ "ArithmeticBlend", "AsciiArt", "Blend", "Blur", "CannyEdgeMask", "ChromaticAberration", "ColorCorrect", "ColorTint", "Dissolve", "Dither", "DodgeAndBurn", "FilmGrain", "Glow", "HSVThresholdMask", "KMeansQuantize", "KuwaharaBlur", "Parabolize", "PencilSketch", "PixelSort", "Pixelize", "Quantize", "Sharpen", "SineWave", "Solarize", "Vignette" ], { "title_aux": "ComfyUI-post-processing-nodes" } ], "https://github.com/Extraltodeus/ComfyUI-AutomaticCFG": [ [ "Automatic CFG", "Automatic CFG channels multipliers" ], { "title_aux": "ComfyUI-AutomaticCFG" } ], "https://github.com/Extraltodeus/LoadLoraWithTags": [ [ "LoraLoaderTagsQuery" ], { "title_aux": "LoadLoraWithTags" } ], "https://github.com/Extraltodeus/noise_latent_perlinpinpin": [ [ "NoisyLatentPerlin" ], { "title_aux": "noise latent perlinpinpin" } ], "https://github.com/Extraltodeus/sigmas_tools_and_the_golden_scheduler": [ [ "Get sigmas as float", "Graph sigmas", "Manual scheduler", "Merge sigmas by average", "Merge sigmas gradually", "Multiply sigmas", "Split and concatenate sigmas", "The Golden Scheduler" ], { "title_aux": "sigmas_tools_and_the_golden_scheduler" } ], "https://github.com/Fannovel16/ComfyUI-Frame-Interpolation": [ [ "AMT VFI", "CAIN VFI", "EISAI VFI", "FILM VFI", "FLAVR VFI", "GMFSS Fortuna VFI", "IFRNet VFI", "IFUnet VFI", "KSampler Gradually Adding More Denoise (efficient)", "M2M VFI", "Make Interpolation State List", "RIFE VFI", "STMFNet VFI", "Sepconv VFI" ], { "title_aux": "ComfyUI Frame Interpolation" } ], "https://github.com/Fannovel16/ComfyUI-Loopchain": [ [ "EmptyLatentImageLoop", "FolderToImageStorage", "ImageStorageExportLoop", "ImageStorageImport", "ImageStorageReset", "LatentStorageExportLoop", "LatentStorageImport", "LatentStorageReset" ], { "title_aux": "ComfyUI Loopchain" } ], "https://github.com/Fannovel16/ComfyUI-MotionDiff": [ [ "EmptyMotionData", "ExportSMPLTo3DSoftware", "MotionCLIPTextEncode", "MotionDataVisualizer", "MotionDiffLoader", "MotionDiffSimpleSampler", "RenderSMPLMesh", "SMPLLoader", "SaveSMPL", "SmplifyMotionData" ], { "title_aux": "ComfyUI MotionDiff" } ], "https://github.com/Fannovel16/ComfyUI-Video-Matting": [ [ "BRIAAI Matting", "Robust Video Matting" ], { "title_aux": "ComfyUI-Video-Matting" } ], "https://github.com/Fannovel16/comfyui_controlnet_aux": [ [ "AIO_Preprocessor", "AnimalPosePreprocessor", "AnimeFace_SemSegPreprocessor", "AnimeLineArtPreprocessor", "BAE-NormalMapPreprocessor", "BinaryPreprocessor", "CannyEdgePreprocessor", "ColorPreprocessor", "DWPreprocessor", "DensePosePreprocessor", "DepthAnythingPreprocessor", "DiffusionEdge_Preprocessor", "FacialPartColoringFromPoseKps", "FakeScribblePreprocessor", "HEDPreprocessor", "HintImageEnchance", "ImageGenResolutionFromImage", "ImageGenResolutionFromLatent", "ImageIntensityDetector", "ImageLuminanceDetector", "InpaintPreprocessor", "LeReS-DepthMapPreprocessor", "LineArtPreprocessor", "LineartStandardPreprocessor", "M-LSDPreprocessor", "Manga2Anime_LineArt_Preprocessor", "MaskOptFlow", "MediaPipe-FaceMeshPreprocessor", "MeshGraphormer-DepthMapPreprocessor", "MiDaS-DepthMapPreprocessor", "MiDaS-NormalMapPreprocessor", "OneFormer-ADE20K-SemSegPreprocessor", "OneFormer-COCO-SemSegPreprocessor", "OpenposePreprocessor", "PiDiNetPreprocessor", "PixelPerfectResolution", "SAMPreprocessor", "SavePoseKpsAsJsonFile", "ScribblePreprocessor", "Scribble_XDoG_Preprocessor", "SemSegPreprocessor", "ShufflePreprocessor", "TEEDPreprocessor", "TilePreprocessor", "UniFormer-SemSegPreprocessor", "Unimatch_OptFlowPreprocessor", "Zoe-DepthMapPreprocessor", "Zoe_DepthAnythingPreprocessor" ], { "author": "tstandley", "title_aux": "ComfyUI's ControlNet Auxiliary Preprocessors" } ], "https://github.com/Feidorian/feidorian-ComfyNodes": [ [], { "nodename_pattern": "^Feidorian_", "title_aux": "feidorian-ComfyNodes" } ], "https://github.com/Fictiverse/ComfyUI_Fictiverse": [ [ "Add Noise to Image with Mask", "Color correction", "Displace Image with Depth", "Displace Images with Mask", "Zoom Image with Depth" ], { "title_aux": "ComfyUI Fictiverse Nodes" } ], "https://github.com/FizzleDorf/ComfyUI-AIT": [ [ "AIT_Unet_Loader", "AIT_VAE_Encode_Loader" ], { "title_aux": "ComfyUI-AIT" } ], "https://github.com/FizzleDorf/ComfyUI_FizzNodes": [ [ "AbsCosWave", "AbsSinWave", "BatchGLIGENSchedule", "BatchPromptSchedule", "BatchPromptScheduleEncodeSDXL", "BatchPromptScheduleLatentInput", "BatchPromptScheduleNodeFlowEnd", "BatchPromptScheduleSDXLLatentInput", "BatchStringSchedule", "BatchValueSchedule", "BatchValueScheduleLatentInput", "CalculateFrameOffset", "ConcatStringSingle", "CosWave", "FizzFrame", "FizzFrameConcatenate", "ImageBatchFromValueSchedule", "Init FizzFrame", "InvCosWave", "InvSinWave", "Lerp", "PromptSchedule", "PromptScheduleEncodeSDXL", "PromptScheduleNodeFlow", "PromptScheduleNodeFlowEnd", "SawtoothWave", "SinWave", "SquareWave", "StringConcatenate", "StringSchedule", "TriangleWave", "ValueSchedule", "convertKeyframeKeysToBatchKeys" ], { "title_aux": "FizzNodes" } ], "https://github.com/FlyingFireCo/tiled_ksampler": [ [ "Asymmetric Tiled KSampler", "Circular VAEDecode", "Tiled KSampler" ], { "title_aux": "tiled_ksampler" } ], "https://github.com/Franck-Demongin/NX_PromptStyler": [ [ "NX_PromptStyler" ], { "title_aux": "NX_PromptStyler" } ], "https://github.com/GMapeSplat/ComfyUI_ezXY": [ [ "ConcatenateString", "ItemFromDropdown", "IterationDriver", "JoinImages", "LineToConsole", "NumberFromList", "NumbersToList", "PlotImages", "StringFromList", "StringToLabel", "StringsToList", "ezMath", "ezXY_AssemblePlot", "ezXY_Driver" ], { "title_aux": "ezXY scripts and nodes" } ], "https://github.com/GTSuya-Studio/ComfyUI-Gtsuya-Nodes": [ [ "Danbooru (ID)", "Danbooru (Random)", "Random File From Path", "Replace Strings", "Simple Wildcards", "Simple Wildcards (Dir.)", "Wildcards Nodes" ], { "title_aux": "ComfyUI-GTSuya-Nodes" } ], "https://github.com/Gourieff/comfyui-reactor-node": [ [ "ReActorFaceSwap", "ReActorLoadFaceModel", "ReActorRestoreFace", "ReActorSaveFaceModel" ], { "title_aux": "ReActor Node for ComfyUI" } ], "https://github.com/HAL41/ComfyUI-aichemy-nodes": [ [ "aichemyYOLOv8Segmentation" ], { "title_aux": "ComfyUI aichemy nodes" } ], "https://github.com/Hangover3832/ComfyUI-Hangover-Moondream": [ [ "Moondream Interrogator (NO COMMERCIAL USE)" ], { "title_aux": "ComfyUI-Hangover-Moondream" } ], "https://github.com/Hangover3832/ComfyUI-Hangover-Nodes": [ [ "Image Scale Bounding Box", "MS kosmos-2 Interrogator", "Make Inpaint Model", "Save Image w/o Metadata" ], { "title_aux": "ComfyUI-Hangover-Nodes" } ], "https://github.com/Haoming02/comfyui-diffusion-cg": [ [ "Normalization", "NormalizationXL", "Recenter", "Recenter XL" ], { "title_aux": "ComfyUI Diffusion Color Grading" } ], "https://github.com/Haoming02/comfyui-floodgate": [ [ "FloodGate" ], { "title_aux": "ComfyUI Floodgate" } ], "https://github.com/HaydenReeve/ComfyUI-Better-Strings": [ [ "BetterString" ], { "title_aux": "ComfyUI Better Strings" } ], "https://github.com/HebelHuber/comfyui-enhanced-save-node": [ [ "EnhancedSaveNode" ], { "title_aux": "comfyui-enhanced-save-node" } ], "https://github.com/Hiero207/ComfyUI-Hiero-Nodes": [ [ "Post to Discord w/ Webhook" ], { "author": "Hiero", "description": "Just some nodes that I wanted/needed, so I made them.", "nickname": "HNodes", "title": "Hiero-Nodes", "title_aux": "ComfyUI-Hiero-Nodes" } ], "https://github.com/IDGallagher/ComfyUI-IG-Nodes": [ [ "IG Analyze SSIM", "IG Cross Fade Images", "IG Explorer", "IG Float", "IG Folder", "IG Int", "IG Load Image", "IG Load Images", "IG Multiply", "IG Path Join", "IG String", "IG ZFill" ], { "author": "IDGallagher", "description": "Custom nodes to aid in the exploration of Latent Space", "nickname": "IG Interpolation Nodes", "title": "IG Interpolation Nodes", "title_aux": "IG Interpolation Nodes" } ], "https://github.com/Inzaniak/comfyui-ranbooru": [ [ "PromptBackground", "PromptLimit", "PromptMix", "PromptRandomWeight", "PromptRemove", "Ranbooru", "RanbooruURL", "RandomPicturePath" ], { "title_aux": "Ranbooru for ComfyUI" } ], "https://github.com/JPS-GER/ComfyUI_JPS-Nodes": [ [ "Conditioning Switch (JPS)", "ControlNet Switch (JPS)", "Crop Image Pipe (JPS)", "Crop Image Settings (JPS)", "Crop Image Square (JPS)", "Crop Image TargetSize (JPS)", "CtrlNet CannyEdge Pipe (JPS)", "CtrlNet CannyEdge Settings (JPS)", "CtrlNet MiDaS Pipe (JPS)", "CtrlNet MiDaS Settings (JPS)", "CtrlNet OpenPose Pipe (JPS)", "CtrlNet OpenPose Settings (JPS)", "CtrlNet ZoeDepth Pipe (JPS)", "CtrlNet ZoeDepth Settings (JPS)", "Disable Enable Switch (JPS)", "Enable Disable Switch (JPS)", "Generation TXT IMG Settings (JPS)", "Get Date Time String (JPS)", "Get Image Size (JPS)", "IP Adapter Settings (JPS)", "IP Adapter Settings Pipe (JPS)", "IP Adapter Single Settings (JPS)", "IP Adapter Single Settings Pipe (JPS)", "IPA Switch (JPS)", "Image Switch (JPS)", "ImageToImage Pipe (JPS)", "ImageToImage Settings (JPS)", "Images Masks MultiPipe (JPS)", "Integer Switch (JPS)", "Largest Int (JPS)", "Latent Switch (JPS)", "Lora Loader (JPS)", "Mask Switch (JPS)", "Model Switch (JPS)", "Multiply Float Float (JPS)", "Multiply Int Float (JPS)", "Multiply Int Int (JPS)", "Resolution Multiply (JPS)", "Revision Settings (JPS)", "Revision Settings Pipe (JPS)", "SDXL Basic Settings (JPS)", "SDXL Basic Settings Pipe (JPS)", "SDXL Fundamentals MultiPipe (JPS)", "SDXL Prompt Handling (JPS)", "SDXL Prompt Handling Plus (JPS)", "SDXL Prompt Styler (JPS)", "SDXL Recommended Resolution Calc (JPS)", "SDXL Resolutions (JPS)", "Sampler Scheduler Settings (JPS)", "Save Images Plus (JPS)", "Substract Int Int (JPS)", "Text Concatenate (JPS)", "Text Prompt (JPS)", "VAE Switch (JPS)" ], { "author": "JPS", "description": "Various nodes to handle SDXL Resolutions, SDXL Basic Settings, IP Adapter Settings, Revision Settings, SDXL Prompt Styler, Crop Image to Square, Crop Image to Target Size, Get Date-Time String, Resolution Multiply, Largest Integer, 5-to-1 Switches for Integer, Images, Latents, Conditioning, Model, VAE, ControlNet", "nickname": "JPS Custom Nodes", "title": "JPS Custom Nodes for ComfyUI", "title_aux": "JPS Custom Nodes for ComfyUI" } ], "https://github.com/JaredTherriault/ComfyUI-JNodes": [ [ "JNodes_AddOrSetMetaDataKey", "JNodes_AnyToString", "JNodes_AppendReversedFrames", "JNodes_BooleanSelectorWithString", "JNodes_CheckpointSelectorWithString", "JNodes_GetOutputDirectory", "JNodes_GetParameterFromList", "JNodes_GetParameterGlobal", "JNodes_GetTempDirectory", "JNodes_ImageFormatSelector", "JNodes_ImageSizeSelector", "JNodes_LoadVideo", "JNodes_LoraExtractor", "JNodes_OutVideoInfo", "JNodes_ParseDynamicPrompts", "JNodes_ParseParametersToGlobalList", "JNodes_ParseWildcards", "JNodes_PromptBuilderSingleSubject", "JNodes_RemoveCommentedText", "JNodes_RemoveMetaDataKey", "JNodes_RemoveParseableDataForInference", "JNodes_SamplerSelectorWithString", "JNodes_SaveImageWithOutput", "JNodes_SaveVideo", "JNodes_SchedulerSelectorWithString", "JNodes_SearchAndReplace", "JNodes_SearchAndReplaceFromFile", "JNodes_SearchAndReplaceFromList", "JNodes_SetNegativePromptInMetaData", "JNodes_SetPositivePromptInMetaData", "JNodes_SplitAndJoin", "JNodes_StringLiteral", "JNodes_SyncedStringLiteral", "JNodes_TokenCounter", "JNodes_TrimAndStrip", "JNodes_UploadVideo", "JNodes_VaeSelectorWithString" ], { "title_aux": "ComfyUI-JNodes" } ], "https://github.com/JcandZero/ComfyUI_GLM4Node": [ [ "GLM3_turbo_CHAT", "GLM4_CHAT", "GLM4_Vsion_IMGURL" ], { "title_aux": "ComfyUI_GLM4Node" } ], "https://github.com/Jcd1230/rembg-comfyui-node": [ [ "Image Remove Background (rembg)" ], { "title_aux": "Rembg Background Removal Node for ComfyUI" } ], "https://github.com/JerryOrbachJr/ComfyUI-RandomSize": [ [ "JOJR_RandomSize" ], { "author": "JerryOrbachJr", "description": "A ComfyUI custom node that randomly selects a height and width pair from a list in a config file", "nickname": "Random Size", "title": "Random Size", "title_aux": "ComfyUI-RandomSize" } ], "https://github.com/Jordach/comfy-plasma": [ [ "JDC_AutoContrast", "JDC_BlendImages", "JDC_BrownNoise", "JDC_Contrast", "JDC_EqualizeGrey", "JDC_GaussianBlur", "JDC_GreyNoise", "JDC_Greyscale", "JDC_ImageLoader", "JDC_ImageLoaderMeta", "JDC_PinkNoise", "JDC_Plasma", "JDC_PlasmaSampler", "JDC_PowerImage", "JDC_RandNoise", "JDC_ResizeFactor" ], { "title_aux": "comfy-plasma" } ], "https://github.com/Kaharos94/ComfyUI-Saveaswebp": [ [ "Save_as_webp" ], { "title_aux": "ComfyUI-Saveaswebp" } ], "https://github.com/Kangkang625/ComfyUI-paint-by-example": [ [ "PaintbyExamplePipeLoader", "PaintbyExampleSampler" ], { "title_aux": "ComfyUI-Paint-by-Example" } ], "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet": [ [ "ACN_AdvancedControlNetApply", "ACN_ControlNetLoaderWithLoraAdvanced", "ACN_DefaultUniversalWeights", "ACN_SparseCtrlIndexMethodNode", "ACN_SparseCtrlLoaderAdvanced", "ACN_SparseCtrlMergedLoaderAdvanced", "ACN_SparseCtrlRGBPreprocessor", "ACN_SparseCtrlSpreadMethodNode", "ControlNetLoaderAdvanced", "CustomControlNetWeights", "CustomT2IAdapterWeights", "DiffControlNetLoaderAdvanced", "LatentKeyframe", "LatentKeyframeBatchedGroup", "LatentKeyframeGroup", "LatentKeyframeTiming", "LoadImagesFromDirectory", "ScaledSoftControlNetWeights", "ScaledSoftMaskedUniversalWeights", "SoftControlNetWeights", "SoftT2IAdapterWeights", "TimestepKeyframe" ], { "title_aux": "ComfyUI-Advanced-ControlNet" } ], "https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved": [ [ "ADE_AdjustPEFullStretch", "ADE_AdjustPEManual", "ADE_AdjustPESweetspotStretch", "ADE_AnimateDiffCombine", "ADE_AnimateDiffKeyframe", "ADE_AnimateDiffLoRALoader", "ADE_AnimateDiffLoaderGen1", "ADE_AnimateDiffLoaderV1Advanced", "ADE_AnimateDiffLoaderWithContext", "ADE_AnimateDiffModelSettings", "ADE_AnimateDiffModelSettingsAdvancedAttnStrengths", "ADE_AnimateDiffModelSettingsSimple", "ADE_AnimateDiffModelSettings_Release", "ADE_AnimateDiffSamplingSettings", "ADE_AnimateDiffSettings", "ADE_AnimateDiffUniformContextOptions", "ADE_AnimateDiffUnload", "ADE_ApplyAnimateDiffModel", "ADE_ApplyAnimateDiffModelSimple", "ADE_BatchedContextOptions", "ADE_CustomCFG", "ADE_CustomCFGKeyframe", "ADE_EmptyLatentImageLarge", "ADE_IterationOptsDefault", "ADE_IterationOptsFreeInit", "ADE_LoadAnimateDiffModel", "ADE_LoopedUniformContextOptions", "ADE_LoopedUniformViewOptions", "ADE_MaskedLoadLora", "ADE_MultivalDynamic", "ADE_MultivalScaledMask", "ADE_NoiseLayerAdd", "ADE_NoiseLayerAddWeighted", "ADE_NoiseLayerReplace", "ADE_RawSigmaSchedule", "ADE_SigmaSchedule", "ADE_SigmaScheduleSplitAndCombine", "ADE_SigmaScheduleWeightedAverage", "ADE_SigmaScheduleWeightedAverageInterp", "ADE_StandardStaticContextOptions", "ADE_StandardStaticViewOptions", "ADE_StandardUniformContextOptions", "ADE_StandardUniformViewOptions", "ADE_UseEvolvedSampling", "ADE_ViewsOnlyContextOptions", "AnimateDiffLoaderV1", "CheckpointLoaderSimpleWithNoiseSelect" ], { "title_aux": "AnimateDiff Evolved" } ], "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite": [ [ "VHS_BatchManager", "VHS_DuplicateImages", "VHS_DuplicateLatents", "VHS_DuplicateMasks", "VHS_GetImageCount", "VHS_GetLatentCount", "VHS_GetMaskCount", "VHS_LoadAudio", "VHS_LoadImages", "VHS_LoadImagesPath", "VHS_LoadVideo", "VHS_LoadVideoPath", "VHS_MergeImages", "VHS_MergeLatents", "VHS_MergeMasks", "VHS_PruneOutputs", "VHS_SelectEveryNthImage", "VHS_SelectEveryNthLatent", "VHS_SelectEveryNthMask", "VHS_SplitImages", "VHS_SplitLatents", "VHS_SplitMasks", "VHS_VAEDecodeBatched", "VHS_VAEEncodeBatched", "VHS_VideoCombine" ], { "title_aux": "ComfyUI-VideoHelperSuite" } ], "https://github.com/LEv145/images-grid-comfy-plugin": [ [ "GridAnnotation", "ImageCombine", "ImagesGridByColumns", "ImagesGridByRows", "LatentCombine" ], { "title_aux": "ImagesGrid" } ], "https://github.com/LarryJane491/Image-Captioning-in-ComfyUI": [ [ "LoRA Caption Load", "LoRA Caption Save" ], { "title_aux": "Image-Captioning-in-ComfyUI" } ], "https://github.com/LarryJane491/Lora-Training-in-Comfy": [ [ "Lora Training in Comfy (Advanced)", "Lora Training in ComfyUI", "Tensorboard Access" ], { "title_aux": "Lora-Training-in-Comfy" } ], "https://github.com/Layer-norm/comfyui-lama-remover": [ [ "LamaRemover", "LamaRemoverIMG" ], { "title_aux": "Comfyui lama remover" } ], "https://github.com/Lerc/canvas_tab": [ [ "Canvas_Tab", "Send_To_Editor" ], { "author": "Lerc", "description": "This extension provides a full page image editor with mask support. There are two nodes, one to receive images from the editor and one to send images to the editor.", "nickname": "Canvas Tab", "title": "Canvas Tab", "title_aux": "Canvas Tab" } ], "https://github.com/Limitex/ComfyUI-Calculation": [ [ "CenterCalculation", "CreateQRCode" ], { "title_aux": "ComfyUI-Calculation" } ], "https://github.com/Limitex/ComfyUI-Diffusers": [ [ "CreateIntListNode", "DiffusersClipTextEncode", "DiffusersModelMakeup", "DiffusersPipelineLoader", "DiffusersSampler", "DiffusersSchedulerLoader", "DiffusersVaeLoader", "LcmLoraLoader", "StreamDiffusionCreateStream", "StreamDiffusionFastSampler", "StreamDiffusionSampler", "StreamDiffusionWarmup" ], { "title_aux": "ComfyUI-Diffusers" } ], "https://github.com/Loewen-Hob/rembg-comfyui-node-better": [ [ "Image Remove Background (rembg)" ], { "title_aux": "Rembg Background Removal Node for ComfyUI" } ], "https://github.com/LonicaMewinsky/ComfyUI-MakeFrame": [ [ "BreakFrames", "BreakGrid", "GetKeyFrames", "MakeGrid", "RandomImageFromDir" ], { "title_aux": "ComfyBreakAnim" } ], "https://github.com/LonicaMewinsky/ComfyUI-RawSaver": [ [ "SaveTifImage" ], { "title_aux": "ComfyUI-RawSaver" } ], "https://github.com/LyazS/comfyui-anime-seg": [ [ "Anime Character Seg" ], { "title_aux": "Anime Character Segmentation node for comfyui" } ], "https://github.com/M1kep/ComfyLiterals": [ [ "Checkpoint", "Float", "Int", "KepStringLiteral", "Lora", "Operation", "String" ], { "title_aux": "ComfyLiterals" } ], "https://github.com/M1kep/ComfyUI-KepOpenAI": [ [ "KepOpenAI_ImageWithPrompt" ], { "title_aux": "ComfyUI-KepOpenAI" } ], "https://github.com/M1kep/ComfyUI-OtherVAEs": [ [ "OtherVAE_Taesd" ], { "title_aux": "ComfyUI-OtherVAEs" } ], "https://github.com/M1kep/Comfy_KepKitchenSink": [ [ "KepRotateImage" ], { "title_aux": "Comfy_KepKitchenSink" } ], "https://github.com/M1kep/Comfy_KepListStuff": [ [ "Empty Images", "Image Overlay", "ImageListLoader", "Join Float Lists", "Join Image Lists", "KepStringList", "KepStringListFromNewline", "Kep_JoinListAny", "Kep_RepeatList", "Kep_ReverseList", "Kep_VariableImageBuilder", "List Length", "Range(Num Steps) - Float", "Range(Num Steps) - Int", "Range(Step) - Float", "Range(Step) - Int", "Stack Images", "XYAny", "XYImage" ], { "title_aux": "Comfy_KepListStuff" } ], "https://github.com/M1kep/Comfy_KepMatteAnything": [ [ "MatteAnything_DinoBoxes", "MatteAnything_GenerateVITMatte", "MatteAnything_InitSamPredictor", "MatteAnything_LoadDINO", "MatteAnything_LoadVITMatteModel", "MatteAnything_SAMLoader", "MatteAnything_SAMMaskFromBoxes", "MatteAnything_ToTrimap" ], { "title_aux": "Comfy_KepMatteAnything" } ], "https://github.com/M1kep/KepPromptLang": [ [ "Build Gif", "Special CLIP Loader" ], { "title_aux": "KepPromptLang" } ], "https://github.com/MNeMoNiCuZ/ComfyUI-mnemic-nodes": [ [ "Save Text File_mne" ], { "title_aux": "ComfyUI-mnemic-nodes" } ], "https://github.com/Mamaaaamooooo/batchImg-rembg-ComfyUI-nodes": [ [ "Image Remove Background (rembg)" ], { "title_aux": "Batch Rembg for ComfyUI" } ], "https://github.com/ManglerFTW/ComfyI2I": [ [ "Color Transfer", "Combine and Paste", "Inpaint Segments", "Mask Ops" ], { "author": "ManglerFTW", "title": "ComfyI2I", "title_aux": "ComfyI2I" } ], "https://github.com/MarkoCa1/ComfyUI_Segment_Mask": [ [ "AutomaticMask(segment anything)" ], { "title_aux": "ComfyUI_Segment_Mask" } ], "https://github.com/Miosp/ComfyUI-FBCNN": [ [ "JPEG artifacts removal FBCNN" ], { "title_aux": "ComfyUI-FBCNN" } ], "https://github.com/MitoshiroPJ/comfyui_slothful_attention": [ [ "NearSightedAttention", "NearSightedAttentionSimple", "NearSightedTile", "SlothfulAttention" ], { "title_aux": "ComfyUI Slothful Attention" } ], "https://github.com/MrForExample/ComfyUI-3D-Pack": [ [], { "nodename_pattern": "^\\[Comfy3D\\]", "title_aux": "ComfyUI-3D-Pack" } ], "https://github.com/MrForExample/ComfyUI-AnimateAnyone-Evolved": [ [], { "nodename_pattern": "^\\[AnimateAnyone\\]", "title_aux": "ComfyUI-AnimateAnyone-Evolved" } ], "https://github.com/NicholasMcCarthy/ComfyUI_TravelSuite": [ [ "LatentTravel" ], { "title_aux": "ComfyUI_TravelSuite" } ], "https://github.com/NimaNzrii/comfyui-photoshop": [ [ "PhotoshopToComfyUI" ], { "title_aux": "comfyui-photoshop" } ], "https://github.com/NimaNzrii/comfyui-popup_preview": [ [ "PreviewPopup" ], { "title_aux": "comfyui-popup_preview" } ], "https://github.com/Niutonian/ComfyUi-NoodleWebcam": [ [ "WebcamNode" ], { "title_aux": "ComfyUi-NoodleWebcam" } ], "https://github.com/Nlar/ComfyUI_CartoonSegmentation": [ [ "AnimeSegmentation", "KenBurnsConfigLoader", "KenBurns_Processor", "LoadImageFilename" ], { "author": "Nels Larsen", "description": "This extension offers a front end to the Cartoon Segmentation Project (https://github.com/CartoonSegmentation/CartoonSegmentation)", "nickname": "CfyCS", "title": "ComfyUI_CartoonSegmentation", "title_aux": "ComfyUI_CartoonSegmentation" } ], "https://github.com/NotHarroweD/Harronode": [ [ "Harronode" ], { "author": "HarroweD and quadmoon (https://github.com/traugdor)", "description": "This extension to ComfyUI will build a prompt for the Harrlogos LoRA for SDXL.", "nickname": "Harronode", "nodename_pattern": "Harronode", "title": "Harrlogos Prompt Builder Node", "title_aux": "Harronode" } ], "https://github.com/Nourepide/ComfyUI-Allor": [ [ "AlphaChanelAdd", "AlphaChanelAddByMask", "AlphaChanelAsMask", "AlphaChanelRemove", "AlphaChanelRestore", "ClipClamp", "ClipVisionClamp", "ClipVisionOutputClamp", "ConditioningClamp", "ControlNetClamp", "GligenClamp", "ImageBatchCopy", "ImageBatchFork", "ImageBatchGet", "ImageBatchJoin", "ImageBatchPermute", "ImageBatchRemove", "ImageClamp", "ImageCompositeAbsolute", "ImageCompositeAbsoluteByContainer", "ImageCompositeRelative", "ImageCompositeRelativeByContainer", "ImageContainer", "ImageContainerInheritanceAdd", "ImageContainerInheritanceMax", "ImageContainerInheritanceScale", "ImageContainerInheritanceSum", "ImageDrawArc", "ImageDrawArcByContainer", "ImageDrawChord", "ImageDrawChordByContainer", "ImageDrawEllipse", "ImageDrawEllipseByContainer", "ImageDrawLine", "ImageDrawLineByContainer", "ImageDrawPieslice", "ImageDrawPiesliceByContainer", "ImageDrawPolygon", "ImageDrawRectangle", "ImageDrawRectangleByContainer", "ImageDrawRectangleRounded", "ImageDrawRectangleRoundedByContainer", "ImageEffectsAdjustment", "ImageEffectsGrayscale", "ImageEffectsLensBokeh", "ImageEffectsLensChromaticAberration", "ImageEffectsLensOpticAxis", "ImageEffectsLensVignette", "ImageEffectsLensZoomBurst", "ImageEffectsNegative", "ImageEffectsSepia", "ImageFilterBilateralBlur", "ImageFilterBlur", "ImageFilterBoxBlur", "ImageFilterContour", "ImageFilterDetail", "ImageFilterEdgeEnhance", "ImageFilterEdgeEnhanceMore", "ImageFilterEmboss", "ImageFilterFindEdges", "ImageFilterGaussianBlur", "ImageFilterGaussianBlurAdvanced", "ImageFilterMax", "ImageFilterMedianBlur", "ImageFilterMin", "ImageFilterMode", "ImageFilterRank", "ImageFilterSharpen", "ImageFilterSmooth", "ImageFilterSmoothMore", "ImageFilterStackBlur", "ImageNoiseBeta", "ImageNoiseBinomial", "ImageNoiseBytes", "ImageNoiseGaussian", "ImageSegmentation", "ImageSegmentationCustom", "ImageSegmentationCustomAdvanced", "ImageText", "ImageTextMultiline", "ImageTextMultilineOutlined", "ImageTextOutlined", "ImageTransformCropAbsolute", "ImageTransformCropCorners", "ImageTransformCropRelative", "ImageTransformPaddingAbsolute", "ImageTransformPaddingRelative", "ImageTransformResizeAbsolute", "ImageTransformResizeClip", "ImageTransformResizeRelative", "ImageTransformRotate", "ImageTransformTranspose", "LatentClamp", "MaskClamp", "ModelClamp", "StyleModelClamp", "UpscaleModelClamp", "VaeClamp" ], { "title_aux": "Allor Plugin" } ], "https://github.com/Nuked88/ComfyUI-N-Nodes": [ [ "CLIPTextEncodeAdvancedNSuite [n-suite]", "DynamicPrompt [n-suite]", "Float Variable [n-suite]", "FrameInterpolator [n-suite]", "GPT Loader Simple [n-suite]", "GPT Sampler [n-suite]", "ImagePadForOutpaintAdvanced [n-suite]", "Integer Variable [n-suite]", "Llava Clip Loader [n-suite]", "LoadFramesFromFolder [n-suite]", "LoadVideo [n-suite]", "SaveVideo [n-suite]", "SetMetadataForSaveVideo [n-suite]", "String Variable [n-suite]" ], { "title_aux": "ComfyUI-N-Nodes" } ], "https://github.com/Off-Live/ComfyUI-off-suite": [ [ "Apply CLAHE", "Cached Image Load From URL", "Crop Center wigh SEGS", "Crop Center with SEGS", "Dilate Mask for Each Face", "GW Number Formatting", "Image Crop Fit", "Image Resize Fit", "OFF SEGS to Image", "Paste Face Segment to Image", "Query Gender and Age", "SEGS to Face Crop Data", "Safe Mask to Image", "VAE Encode For Inpaint V2", "Watermarking" ], { "title_aux": "ComfyUI-off-suite" } ], "https://github.com/Onierous/QRNG_Node_ComfyUI/raw/main/qrng_node.py": [ [ "QRNG_Node_CSV" ], { "title_aux": "QRNG_Node_ComfyUI" } ], "https://github.com/PCMonsterx/ComfyUI-CSV-Loader": [ [ "Load Artists CSV", "Load Artmovements CSV", "Load Characters CSV", "Load Colors CSV", "Load Composition CSV", "Load Lighting CSV", "Load Negative CSV", "Load Positive CSV", "Load Settings CSV", "Load Styles CSV" ], { "title_aux": "ComfyUI-CSV-Loader" } ], "https://github.com/ParmanBabra/ComfyUI-Malefish-Custom-Scripts": [ [ "CSVPromptsLoader", "CombinePrompt", "MultiLoraLoader", "RandomPrompt" ], { "title_aux": "ComfyUI-Malefish-Custom-Scripts" } ], "https://github.com/Pfaeff/pfaeff-comfyui": [ [ "AstropulsePixelDetector", "BackgroundRemover", "ImagePadForBetterOutpaint", "Inpainting", "InpaintingPipelineLoader" ], { "title_aux": "pfaeff-comfyui" } ], "https://github.com/QaisMalkawi/ComfyUI-QaisHelper": [ [ "Bool Binary Operation", "Bool Unary Operation", "Item Debugger", "Item Switch", "Nearest SDXL Resolution", "SDXL Resolution", "Size Swapper" ], { "title_aux": "ComfyUI-Qais-Helper" } ], "https://github.com/RenderRift/ComfyUI-RenderRiftNodes": [ [ "AnalyseMetadata", "DateIntegerNode", "DisplayMetaOptions", "LoadImageWithMeta", "MetadataOverlayNode", "VideoPathMetaExtraction" ], { "title_aux": "ComfyUI-RenderRiftNodes" } ], "https://github.com/Ryuukeisyou/comfyui_face_parsing": [ [ "BBoxListItemSelect(FaceParsing)", "BBoxResize(FaceParsing)", "ColorAdjust(FaceParsing)", "FaceBBoxDetect(FaceParsing)", "FaceBBoxDetectorLoader(FaceParsing)", "FaceParse(FaceParsing)", "FaceParsingModelLoader(FaceParsing)", "FaceParsingProcessorLoader(FaceParsing)", "FaceParsingResultsParser(FaceParsing)", "GuidedFilter(FaceParsing)", "ImageCropWithBBox(FaceParsing)", "ImageInsertWithBBox(FaceParsing)", "ImageListSelect(FaceParsing)", "ImagePadWithBBox(FaceParsing)", "ImageResizeCalculator(FaceParsing)", "ImageResizeWithBBox(FaceParsing)", "ImageSize(FaceParsing)", "LatentCropWithBBox(FaceParsing)", "LatentInsertWithBBox(FaceParsing)", "LatentSize(FaceParsing)", "MaskComposite(FaceParsing)", "MaskListComposite(FaceParsing)", "MaskListSelect(FaceParsing)", "MaskToBBox(FaceParsing)", "SkinDetectTraditional(FaceParsing)" ], { "title_aux": "comfyui_face_parsing" } ], "https://github.com/Ryuukeisyou/comfyui_image_io_helpers": [ [ "ImageLoadAsMaskByPath(ImageIOHelpers)", "ImageLoadByPath(ImageIOHelpers)", "ImageLoadFromBase64(ImageIOHelpers)", "ImageSaveAsBase64(ImageIOHelpers)", "ImageSaveToPath(ImageIOHelpers)" ], { "title_aux": "comfyui_image_io_helpers" } ], "https://github.com/SLAPaper/ComfyUI-Image-Selector": [ [ "ImageDuplicator", "ImageSelector", "LatentDuplicator", "LatentSelector" ], { "title_aux": "ComfyUI-Image-Selector" } ], "https://github.com/SOELexicon/ComfyUI-LexMSDBNodes": [ [ "MSSqlSelectNode", "MSSqlTableNode" ], { "title_aux": "LexMSDBNodes" } ], "https://github.com/SOELexicon/ComfyUI-LexTools": [ [ "AgeClassifierNode", "ArtOrHumanClassifierNode", "DocumentClassificationNode", "FoodCategoryClassifierNode", "ImageAspectPadNode", "ImageCaptioning", "ImageFilterByFloatScoreNode", "ImageFilterByIntScoreNode", "ImageQualityScoreNode", "ImageRankingNode", "ImageScaleToMin", "MD5ImageHashNode", "SamplerPropertiesNode", "ScoreConverterNode", "SeedIncrementerNode", "SegformerNode", "SegformerNodeMasks", "SegformerNodeMergeSegments", "StepCfgIncrementNode" ], { "title_aux": "ComfyUI-LexTools" } ], "https://github.com/SadaleNet/CLIPTextEncodeA1111-ComfyUI/raw/master/custom_nodes/clip_text_encoder_a1111.py": [ [ "CLIPTextEncodeA1111", "RerouteTextForCLIPTextEncodeA1111" ], { "title_aux": "ComfyUI A1111-like Prompt Custom Node Solution" } ], "https://github.com/Scholar01/ComfyUI-Keyframe": [ [ "KeyframeApply", "KeyframeInterpolationPart", "KeyframePart" ], { "title_aux": "SComfyUI-Keyframe" } ], "https://github.com/SeargeDP/SeargeSDXL": [ [ "SeargeAdvancedParameters", "SeargeCheckpointLoader", "SeargeConditionMixing", "SeargeConditioningMuxer2", "SeargeConditioningMuxer5", "SeargeConditioningParameters", "SeargeControlnetAdapterV2", "SeargeControlnetModels", "SeargeCustomAfterUpscaling", "SeargeCustomAfterVaeDecode", "SeargeCustomPromptMode", "SeargeDebugPrinter", "SeargeEnablerInputs", "SeargeFloatConstant", "SeargeFloatMath", "SeargeFloatPair", "SeargeFreeU", "SeargeGenerated1", "SeargeGenerationParameters", "SeargeHighResolution", "SeargeImage2ImageAndInpainting", "SeargeImageAdapterV2", "SeargeImageSave", "SeargeImageSaving", "SeargeInput1", "SeargeInput2", "SeargeInput3", "SeargeInput4", "SeargeInput5", "SeargeInput6", "SeargeInput7", "SeargeIntegerConstant", "SeargeIntegerMath", "SeargeIntegerPair", "SeargeIntegerScaler", "SeargeLatentMuxer3", "SeargeLoraLoader", "SeargeLoras", "SeargeMagicBox", "SeargeModelSelector", "SeargeOperatingMode", "SeargeOutput1", "SeargeOutput2", "SeargeOutput3", "SeargeOutput4", "SeargeOutput5", "SeargeOutput6", "SeargeOutput7", "SeargeParameterProcessor", "SeargePipelineStart", "SeargePipelineTerminator", "SeargePreviewImage", "SeargePromptAdapterV2", "SeargePromptCombiner", "SeargePromptStyles", "SeargePromptText", "SeargeSDXLBasePromptEncoder", "SeargeSDXLImage2ImageSampler", "SeargeSDXLImage2ImageSampler2", "SeargeSDXLPromptEncoder", "SeargeSDXLRefinerPromptEncoder", "SeargeSDXLSampler", "SeargeSDXLSampler2", "SeargeSDXLSamplerV3", "SeargeSamplerAdvanced", "SeargeSamplerInputs", "SeargeSaveFolderInputs", "SeargeSeparator", "SeargeStylePreprocessor", "SeargeTextInputV2", "SeargeUpscaleModelLoader", "SeargeUpscaleModels", "SeargeVAELoader" ], { "title_aux": "SeargeSDXL" } ], "https://github.com/Ser-Hilary/SDXL_sizing/raw/main/conditioning_sizing_for_SDXL.py": [ [ "get_aspect_from_image", "get_aspect_from_ints", "sizing_node", "sizing_node_basic", "sizing_node_unparsed" ], { "title_aux": "SDXL_sizing" } ], "https://github.com/ShmuelRonen/ComfyUI-SVDResizer": [ [ "SVDRsizer" ], { "title_aux": "ComfyUI-SVDResizer" } ], "https://github.com/Shraknard/ComfyUI-Remover": [ [ "Remover" ], { "title_aux": "ComfyUI-Remover" } ], "https://github.com/Siberpone/lazy-pony-prompter": [ [ "LPP_Deleter", "LPP_Derpibooru", "LPP_E621", "LPP_Loader_Derpibooru", "LPP_Loader_E621", "LPP_Saver" ], { "title_aux": "Lazy Pony Prompter" } ], "https://github.com/Smuzzies/comfyui_chatbox_overlay/raw/main/chatbox_overlay.py": [ [ "Chatbox Overlay" ], { "title_aux": "Chatbox Overlay node for ComfyUI" } ], "https://github.com/SoftMeng/ComfyUI_Mexx_Poster": [ [ "ComfyUI_Mexx_Poster" ], { "title_aux": "ComfyUI_Mexx_Poster" } ], "https://github.com/SoftMeng/ComfyUI_Mexx_Styler": [ [ "MexxSDXLPromptStyler", "MexxSDXLPromptStylerAdvanced" ], { "title_aux": "ComfyUI_Mexx_Styler" } ], "https://github.com/SpaceKendo/ComfyUI-svd_txt2vid": [ [ "SVD_txt2vid_ConditioningwithLatent" ], { "title_aux": "Text to video for Stable Video Diffusion in ComfyUI" } ], "https://github.com/Stability-AI/stability-ComfyUI-nodes": [ [ "ColorBlend", "ControlLoraSave", "GetImageSize" ], { "title_aux": "stability-ComfyUI-nodes" } ], "https://github.com/StartHua/ComfyUI_Seg_VITON": [ [ "segformer_agnostic", "segformer_clothes", "segformer_remove_bg", "stabel_vition" ], { "title_aux": "ComfyUI_Seg_VITON" } ], "https://github.com/StartHua/Comfyui_joytag": [ [ "CXH_JoyTag" ], { "title_aux": "Comfyui_joytag" } ], "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes": [ [ "CR 8 Channel In", "CR 8 Channel Out", "CR Apply ControlNet", "CR Apply LoRA Stack", "CR Apply Model Merge", "CR Apply Multi Upscale", "CR Apply Multi-ControlNet", "CR Arabic Text RTL", "CR Aspect Ratio", "CR Aspect Ratio Banners", "CR Aspect Ratio SDXL", "CR Aspect Ratio Social Media", "CR Batch Images From List", "CR Batch Process Switch", "CR Binary Pattern", "CR Binary To Bit List", "CR Bit Schedule", "CR Central Schedule", "CR Checker Pattern", "CR Clamp Value", "CR Clip Input Switch", "CR Color Bars", "CR Color Gradient", "CR Color Panel", "CR Color Tint", "CR Combine Prompt", "CR Combine Schedules", "CR Comic Panel Templates", "CR Composite Text", "CR Conditioning Input Switch", "CR Conditioning Mixer", "CR ControlNet Input Switch", "CR Current Frame", "CR Cycle Images", "CR Cycle Images Simple", "CR Cycle LoRAs", "CR Cycle Models", "CR Cycle Text", "CR Cycle Text Simple", "CR Data Bus In", "CR Data Bus Out", "CR Debatch Frames", "CR Diamond Panel", "CR Draw Perspective Text", "CR Draw Pie", "CR Draw Shape", "CR Draw Text", "CR Encode Scheduled Prompts", "CR Feathered Border", "CR Float Range List", "CR Float To Integer", "CR Float To String", "CR Font File List", "CR Get Parameter From Prompt", "CR Gradient Float", "CR Gradient Integer", "CR Half Drop Panel", "CR Halftone Filter", "CR Halftone Grid", "CR Hires Fix Process Switch", "CR Image Border", "CR Image Grid Panel", "CR Image Input Switch", "CR Image Input Switch (4 way)", "CR Image List", "CR Image List Simple", "CR Image Output", "CR Image Panel", "CR Image Pipe Edit", "CR Image Pipe In", "CR Image Pipe Out", "CR Image Size", "CR Img2Img Process Switch", "CR Increment Float", "CR Increment Integer", "CR Index", "CR Index Increment", "CR Index Multiply", "CR Index Reset", "CR Input Text List", "CR Integer Multiple", "CR Integer Range List", "CR Integer To String", "CR Interpolate Latents", "CR Intertwine Lists", "CR Keyframe List", "CR Latent Batch Size", "CR Latent Input Switch", "CR LoRA List", "CR LoRA Stack", "CR Load Animation Frames", "CR Load Flow Frames", "CR Load GIF As List", "CR Load Image List", "CR Load Image List Plus", "CR Load LoRA", "CR Load Prompt Style", "CR Load Schedule From File", "CR Load Scheduled ControlNets", "CR Load Scheduled LoRAs", "CR Load Scheduled Models", "CR Load Text List", "CR Mask Text", "CR Math Operation", "CR Model Input Switch", "CR Model List", "CR Model Merge Stack", "CR Module Input", "CR Module Output", "CR Module Pipe Loader", "CR Multi Upscale Stack", "CR Multi-ControlNet Stack", "CR Multiline Text", "CR Output Flow Frames", "CR Output Schedule To File", "CR Overlay Text", "CR Overlay Transparent Image", "CR Page Layout", "CR Pipe Switch", "CR Polygons", "CR Prompt List", "CR Prompt List Keyframes", "CR Prompt Scheduler", "CR Prompt Text", "CR Radial Gradient", "CR Random Hex Color", "CR Random LoRA Stack", "CR Random Multiline Colors", "CR Random Multiline Values", "CR Random Panel Codes", "CR Random RGB", "CR Random RGB Gradient", "CR Random Shape Pattern", "CR Random Weight LoRA", "CR Repeater", "CR SD1.5 Aspect Ratio", "CR SDXL Aspect Ratio", "CR SDXL Base Prompt Encoder", "CR SDXL Prompt Mix Presets", "CR SDXL Prompt Mixer", "CR SDXL Style Text", "CR Save Text To File", "CR Schedule Input Switch", "CR Schedule To ScheduleList", "CR Seamless Checker", "CR Seed", "CR Seed to Int", "CR Select Font", "CR Select ISO Size", "CR Select Model", "CR Select Resize Method", "CR Set Switch From String", "CR Set Value On Binary", "CR Set Value On Boolean", "CR Set Value on String", "CR Simple Banner", "CR Simple Binary Pattern", "CR Simple Binary Pattern Simple", "CR Simple Image Compare", "CR Simple List", "CR Simple Meme Template", "CR Simple Prompt List", "CR Simple Prompt List Keyframes", "CR Simple Prompt Scheduler", "CR Simple Schedule", "CR Simple Text Panel", "CR Simple Text Scheduler", "CR Simple Text Watermark", "CR Simple Titles", "CR Simple Value Scheduler", "CR Split String", "CR Starburst Colors", "CR Starburst Lines", "CR String To Boolean", "CR String To Combo", "CR String To Number", "CR Style Bars", "CR Switch Model and CLIP", "CR Text", "CR Text Blacklist", "CR Text Concatenate", "CR Text Cycler", "CR Text Input Switch", "CR Text Input Switch (4 way)", "CR Text Length", "CR Text List", "CR Text List Simple", "CR Text List To String", "CR Text Operation", "CR Text Replace", "CR Text Scheduler", "CR Thumbnail Preview", "CR Trigger", "CR Upscale Image", "CR VAE Decode", "CR VAE Input Switch", "CR Value", "CR Value Cycler", "CR Value Scheduler", "CR Vignette Filter", "CR XY From Folder", "CR XY Index", "CR XY Interpolate", "CR XY List", "CR XY Product", "CR XY Save Grid Image", "CR XYZ Index", "CR_Aspect Ratio For Print" ], { "author": "Suzie1", "description": "175 custom nodes for artists, designers and animators.", "nickname": "Comfyroll Studio", "title": "Comfyroll Studio", "title_aux": "ComfyUI_Comfyroll_CustomNodes" } ], "https://github.com/Sxela/ComfyWarp": [ [ "ExtractOpticalFlow", "LoadFrame", "LoadFrameFromDataset", "LoadFrameFromFolder", "LoadFramePairFromDataset", "LoadFrameSequence", "MakeFrameDataset", "MixConsistencyMaps", "OffsetNumber", "ResizeToFit", "SaveFrame", "WarpFrame" ], { "title_aux": "ComfyWarp" } ], "https://github.com/TGu-97/ComfyUI-TGu-utils": [ [ "MPNReroute", "MPNSwitch", "PNSwitch" ], { "title_aux": "TGu Utilities" } ], "https://github.com/THtianhao/ComfyUI-FaceChain": [ [ "FC CropAndPaste", "FC CropBottom", "FC CropToOrigin", "FC FaceDetectCrop", "FC FaceFusion", "FC FaceSegAndReplace", "FC FaceSegment", "FC MaskOP", "FC RemoveCannyFace", "FC ReplaceByMask", "FC StyleLoraLoad" ], { "title_aux": "ComfyUI-FaceChain" } ], "https://github.com/THtianhao/ComfyUI-Portrait-Maker": [ [ "PM_BoxCropImage", "PM_ColorTransfer", "PM_ExpandMaskBox", "PM_FaceFusion", "PM_FaceShapMatch", "PM_FaceSkin", "PM_GetImageInfo", "PM_ImageResizeTarget", "PM_ImageScaleShort", "PM_MakeUpTransfer", "PM_MaskDilateErode", "PM_MaskMerge2Image", "PM_PortraitEnhancement", "PM_RatioMerge2Image", "PM_ReplaceBoxImg", "PM_RetinaFace", "PM_Similarity", "PM_SkinRetouching", "PM_SuperColorTransfer", "PM_SuperMakeUpTransfer" ], { "title_aux": "ComfyUI-Portrait-Maker" } ], "https://github.com/TRI3D-LC/tri3d-comfyui-nodes": [ [ "tri3d-adjust-neck", "tri3d-atr-parse", "tri3d-atr-parse-batch", "tri3d-clipdrop-bgremove-api", "tri3d-dwpose", "tri3d-extract-hand", "tri3d-extract-parts-batch", "tri3d-extract-parts-batch2", "tri3d-extract-parts-mask-batch", "tri3d-face-recognise", "tri3d-float-to-image", "tri3d-fuzzification", "tri3d-image-mask-2-box", "tri3d-image-mask-box-2-image", "tri3d-interaction-canny", "tri3d-load-pose-json", "tri3d-pose-adaption", "tri3d-pose-to-image", "tri3d-position-hands", "tri3d-position-parts-batch", "tri3d-recolor-mask", "tri3d-recolor-mask-LAB_space", "tri3d-recolor-mask-LAB_space_manual", "tri3d-recolor-mask-RGB_space", "tri3d-skin-feathered-padded-mask", "tri3d-swap-pixels" ], { "title_aux": "tri3d-comfyui-nodes" } ], "https://github.com/Taremin/comfyui-prompt-extranetworks": [ [ "PromptExtraNetworks" ], { "title_aux": "ComfyUI Prompt ExtraNetworks" } ], "https://github.com/Taremin/comfyui-string-tools": [ [ "StringToolsBalancedChoice", "StringToolsConcat", "StringToolsRandomChoice", "StringToolsString", "StringToolsText" ], { "title_aux": "ComfyUI String Tools" } ], "https://github.com/TeaCrab/ComfyUI-TeaNodes": [ [ "TC_ColorFill", "TC_EqualizeCLAHE", "TC_ImageResize", "TC_ImageScale", "TC_RandomColorFill", "TC_SizeApproximation" ], { "title_aux": "ComfyUI-TeaNodes" } ], "https://github.com/TemryL/ComfyS3": [ [ "DownloadFileS3", "LoadImageS3", "SaveImageS3", "SaveVideoFilesS3", "UploadFileS3" ], { "title_aux": "ComfyS3" } ], "https://github.com/TheBarret/ZSuite": [ [ "ZSuite: Prompter", "ZSuite: RF Noise", "ZSuite: SeedMod" ], { "title_aux": "ZSuite" } ], "https://github.com/TinyTerra/ComfyUI_tinyterraNodes": [ [ "ttN busIN", "ttN busOUT", "ttN compareInput", "ttN concat", "ttN debugInput", "ttN float", "ttN hiresfixScale", "ttN imageOutput", "ttN imageREMBG", "ttN int", "ttN multiModelMerge", "ttN pipe2BASIC", "ttN pipe2DETAILER", "ttN pipeEDIT", "ttN pipeEncodeConcat", "ttN pipeIN", "ttN pipeKSampler", "ttN pipeKSamplerAdvanced", "ttN pipeKSamplerSDXL", "ttN pipeLoader", "ttN pipeLoaderSDXL", "ttN pipeLoraStack", "ttN pipeOUT", "ttN seed", "ttN seedDebug", "ttN text", "ttN text3BOX_3WAYconcat", "ttN text7BOX_concat", "ttN textDebug", "ttN xyPlot" ], { "author": "tinyterra", "description": "This extension offers various pipe nodes, fullscreen image viewer based on node history, dynamic widgets, interface customization, and more.", "nickname": "ttNodes", "nodename_pattern": "^ttN ", "title": "tinyterraNodes", "title_aux": "tinyterraNodes" } ], "https://github.com/TripleHeadedMonkey/ComfyUI_MileHighStyler": [ [ "menus" ], { "title_aux": "ComfyUI_MileHighStyler" } ], "https://github.com/Tropfchen/ComfyUI-Embedding_Picker": [ [ "EmbeddingPicker" ], { "title_aux": "Embedding Picker" } ], "https://github.com/Tropfchen/ComfyUI-yaResolutionSelector": [ [ "YARS", "YARSAdv" ], { "title_aux": "YARS: Yet Another Resolution Selector" } ], "https://github.com/Trung0246/ComfyUI-0246": [ [ "0246.Beautify", "0246.BoxRange", "0246.CastReroute", "0246.Cloud", "0246.Convert", "0246.Count", "0246.Highway", "0246.HighwayBatch", "0246.Hold", "0246.Hub", "0246.Junction", "0246.JunctionBatch", "0246.Loop", "0246.Merge", "0246.Meta", "0246.Pick", "0246.RandomInt", "0246.Script", "0246.ScriptNode", "0246.ScriptPile", "0246.ScriptRule", "0246.Stringify", "0246.Switch" ], { "author": "Trung0246", "description": "Random nodes for ComfyUI I made to solve my struggle with ComfyUI (ex: pipe, process). Have varying quality.", "nickname": "ComfyUI-0246", "title": "ComfyUI-0246", "title_aux": "ComfyUI-0246" } ], "https://github.com/Ttl/ComfyUi_NNLatentUpscale": [ [ "NNLatentUpscale" ], { "title_aux": "ComfyUI Neural network latent upscale custom node" } ], "https://github.com/Umikaze-job/select_folder_path_easy": [ [ "SelectFolderPathEasy" ], { "title_aux": "select_folder_path_easy" } ], "https://github.com/WASasquatch/ASTERR": [ [ "ASTERR", "SaveASTERR" ], { "title_aux": "ASTERR" } ], "https://github.com/WASasquatch/ComfyUI_Preset_Merger": [ [ "Preset_Model_Merge" ], { "title_aux": "ComfyUI Preset Merger" } ], "https://github.com/WASasquatch/FreeU_Advanced": [ [ "FreeU (Advanced)", "FreeU_V2 (Advanced)" ], { "title_aux": "FreeU_Advanced" } ], "https://github.com/WASasquatch/PPF_Noise_ComfyUI": [ [ "Blend Latents (PPF Noise)", "Cross-Hatch Power Fractal (PPF Noise)", "Images as Latents (PPF Noise)", "Perlin Power Fractal Latent (PPF Noise)" ], { "title_aux": "PPF_Noise_ComfyUI" } ], "https://github.com/WASasquatch/PowerNoiseSuite": [ [ "Blend Latents (PPF Noise)", "Cross-Hatch Power Fractal (PPF Noise)", "Cross-Hatch Power Fractal Settings (PPF Noise)", "Images as Latents (PPF Noise)", "Latent Adjustment (PPF Noise)", "Latents to CPU (PPF Noise)", "Linear Cross-Hatch Power Fractal (PPF Noise)", "Perlin Power Fractal Latent (PPF Noise)", "Perlin Power Fractal Settings (PPF Noise)", "Power KSampler Advanced (PPF Noise)", "Power-Law Noise (PPF Noise)" ], { "title_aux": "Power Noise Suite for ComfyUI" } ], "https://github.com/WASasquatch/WAS_Extras": [ [ "BLVAEEncode", "CLIPTextEncodeList", "CLIPTextEncodeSequence2", "ConditioningBlend", "DebugInput", "KSamplerSeq", "KSamplerSeq2", "VAEEncodeForInpaint (WAS)", "VividSharpen" ], { "title_aux": "WAS_Extras" } ], "https://github.com/WASasquatch/was-node-suite-comfyui": [ [ "BLIP Analyze Image", "BLIP Model Loader", "Blend Latents", "Boolean To Text", "Bounded Image Blend", "Bounded Image Blend with Mask", "Bounded Image Crop", "Bounded Image Crop with Mask", "Bus Node", "CLIP Input Switch", "CLIP Vision Input Switch", "CLIPSeg Batch Masking", "CLIPSeg Masking", "CLIPSeg Model Loader", "CLIPTextEncode (BlenderNeko Advanced + NSP)", "CLIPTextEncode (NSP)", "Cache Node", "Checkpoint Loader", "Checkpoint Loader (Simple)", "Conditioning Input Switch", "Constant Number", "Control Net Model Input Switch", "Convert Masks to Images", "Create Grid Image", "Create Grid Image from Batch", "Create Morph Image", "Create Morph Image from Path", "Create Video from Path", "Debug Number to Console", "Dictionary to Console", "Diffusers Hub Model Down-Loader", "Diffusers Model Loader", "Export API", "Image Analyze", "Image Aspect Ratio", "Image Batch", "Image Blank", "Image Blend", "Image Blend by Mask", "Image Blending Mode", "Image Bloom Filter", "Image Bounds", "Image Bounds to Console", "Image Canny Filter", "Image Chromatic Aberration", "Image Color Palette", "Image Crop Face", "Image Crop Location", "Image Crop Square Location", "Image Displacement Warp", "Image Dragan Photography Filter", "Image Edge Detection Filter", "Image Film Grain", "Image Filter Adjustments", "Image Flip", "Image Generate Gradient", "Image Gradient Map", "Image High Pass Filter", "Image History Loader", "Image Input Switch", "Image Levels Adjustment", "Image Load", "Image Lucy Sharpen", "Image Median Filter", "Image Mix RGB Channels", "Image Monitor Effects Filter", "Image Nova Filter", "Image Padding", "Image Paste Crop", "Image Paste Crop by Location", "Image Paste Face", "Image Perlin Noise", "Image Perlin Power Fractal", "Image Pixelate", "Image Power Noise", "Image Rembg (Remove Background)", "Image Remove Background (Alpha)", "Image Remove Color", "Image Resize", "Image Rotate", "Image Rotate Hue", "Image SSAO (Ambient Occlusion)", "Image SSDO (Direct Occlusion)", "Image Save", "Image Seamless Texture", "Image Select Channel", "Image Select Color", "Image Shadows and Highlights", "Image Size to Number", "Image Stitch", "Image Style Filter", "Image Threshold", "Image Tiled", "Image Transpose", "Image Voronoi Noise Filter", "Image fDOF Filter", "Image to Latent Mask", "Image to Noise", "Image to Seed", "Images to Linear", "Images to RGB", "Inset Image Bounds", "Integer place counter", "KSampler (WAS)", "KSampler Cycle", "Latent Batch", "Latent Input Switch", "Latent Noise Injection", "Latent Size to Number", "Latent Upscale by Factor (WAS)", "Load Cache", "Load Image Batch", "Load Lora", "Load Text File", "Logic Boolean", "Logic Boolean Primitive", "Logic Comparison AND", "Logic Comparison OR", "Logic Comparison XOR", "Logic NOT", "Lora Input Switch", "Lora Loader", "Mask Arbitrary Region", "Mask Batch", "Mask Batch to Mask", "Mask Ceiling Region", "Mask Crop Dominant Region", "Mask Crop Minority Region", "Mask Crop Region", "Mask Dilate Region", "Mask Dominant Region", "Mask Erode Region", "Mask Fill Holes", "Mask Floor Region", "Mask Gaussian Region", "Mask Invert", "Mask Minority Region", "Mask Paste Region", "Mask Smooth Region", "Mask Threshold Region", "Masks Add", "Masks Combine Batch", "Masks Combine Regions", "Masks Subtract", "MiDaS Depth Approximation", "MiDaS Mask Image", "MiDaS Model Loader", "Model Input Switch", "Number Counter", "Number Input Condition", "Number Input Switch", "Number Multiple Of", "Number Operation", "Number PI", "Number to Float", "Number to Int", "Number to Seed", "Number to String", "Number to Text", "Prompt Multiple Styles Selector", "Prompt Styles Selector", "Random Number", "SAM Image Mask", "SAM Model Loader", "SAM Parameters", "SAM Parameters Combine", "Samples Passthrough (Stat System)", "Save Text File", "Seed", "String to Text", "Tensor Batch to Image", "Text Add Token by Input", "Text Add Tokens", "Text Compare", "Text Concatenate", "Text Contains", "Text Dictionary Convert", "Text Dictionary Get", "Text Dictionary Keys", "Text Dictionary New", "Text Dictionary To Text", "Text Dictionary Update", "Text File History Loader", "Text Find and Replace", "Text Find and Replace Input", "Text Find and Replace by Dictionary", "Text Input Switch", "Text List", "Text List Concatenate", "Text List to Text", "Text Load Line From File", "Text Multiline", "Text Parse A1111 Embeddings", "Text Parse Noodle Soup Prompts", "Text Parse Tokens", "Text Random Line", "Text Random Prompt", "Text Shuffle", "Text String", "Text String Truncate", "Text to Conditioning", "Text to Console", "Text to Number", "Text to String", "True Random.org Number Generator", "Upscale Model Loader", "Upscale Model Switch", "VAE Input Switch", "Video Dump Frames", "Write to GIF", "Write to Video", "unCLIP Checkpoint Loader" ], { "title_aux": "WAS Node Suite" } ], "https://github.com/WebDev9000/WebDev9000-Nodes": [ [ "IgnoreBraces", "SettingsSwitch" ], { "title_aux": "WebDev9000-Nodes" } ], "https://github.com/YMC-GitHub/ymc-node-suite-comfyui": [ [ "canvas-util-cal-size", "conditioning-util-input-switch", "cutoff-region-util", "hks-util-cal-denoise-step", "img-util-get-image-size", "img-util-switch-input-image", "io-image-save", "io-text-save", "io-util-file-list-get", "io-util-file-list-get-text", "number-util-random-num", "pipe-util-to-basic-pipe", "region-util-get-by-center-and-size", "region-util-get-by-lt", "region-util-get-crop-location-from-center-size-text", "region-util-get-pad-out-location-by-size", "text-preset-colors", "text-util-join-text", "text-util-loop-text", "text-util-path-list", "text-util-prompt-add-prompt", "text-util-prompt-adv-dup", "text-util-prompt-adv-search", "text-util-prompt-del", "text-util-prompt-dup", "text-util-prompt-join", "text-util-prompt-search", "text-util-prompt-shuffle", "text-util-prompt-std", "text-util-prompt-unweight", "text-util-random-text", "text-util-search-text", "text-util-show-text", "text-util-switch-text", "xyz-util-txt-to-int" ], { "title_aux": "ymc-node-suite-comfyui" } ], "https://github.com/YOUR-WORST-TACO/ComfyUI-TacoNodes": [ [ "Example", "TacoAnimatedLoader", "TacoGifMaker", "TacoImg2ImgAnimatedLoader", "TacoImg2ImgAnimatedProcessor", "TacoLatent" ], { "title_aux": "ComfyUI-TacoNodes" } ], "https://github.com/YinBailiang/MergeBlockWeighted_fo_ComfyUI": [ [ "MergeBlockWeighted" ], { "title_aux": "MergeBlockWeighted_fo_ComfyUI" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-ArtGallery": [ [ "ArtGallery_Zho", "ArtistsImage_Zho", "CamerasImage_Zho", "FilmsImage_Zho", "MovementsImage_Zho", "StylesImage_Zho" ], { "title_aux": "ComfyUI-ArtGallery" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Gemini": [ [ "ConcatText_Zho", "DisplayText_Zho", "Gemini_API_Chat_Zho", "Gemini_API_S_Chat_Zho", "Gemini_API_S_Vsion_ImgURL_Zho", "Gemini_API_S_Zho", "Gemini_API_Vsion_ImgURL_Zho", "Gemini_API_Zho" ], { "title_aux": "ComfyUI-Gemini" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-InstantID": [ [ "IDBaseModelLoader_fromhub", "IDBaseModelLoader_local", "IDControlNetLoader", "IDGenerationNode", "ID_Prompt_Styler", "InsightFaceLoader_Zho", "Ipadapter_instantidLoader" ], { "title_aux": "ComfyUI-InstantID" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-PhotoMaker-ZHO": [ [ "BaseModel_Loader_fromhub", "BaseModel_Loader_local", "LoRALoader", "NEW_PhotoMaker_Generation", "PhotoMakerAdapter_Loader_fromhub", "PhotoMakerAdapter_Loader_local", "PhotoMaker_Generation", "Prompt_Styler", "Ref_Image_Preprocessing" ], { "title_aux": "ComfyUI PhotoMaker (ZHO)" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Q-Align": [ [ "QAlign_Zho" ], { "title_aux": "ComfyUI-Q-Align" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Qwen-VL-API": [ [ "QWenVL_API_S_Multi_Zho", "QWenVL_API_S_Zho" ], { "title_aux": "ComfyUI-Qwen-VL-API" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-SVD-ZHO": [ [ "SVD_Aspect_Ratio_Zho", "SVD_Steps_MotionStrength_Seed_Zho", "SVD_Styler_Zho" ], { "title_aux": "ComfyUI-SVD-ZHO (WIP)" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-SegMoE": [ [ "SMoE_Generation_Zho", "SMoE_ModelLoader_Zho" ], { "title_aux": "ComfyUI SegMoE" } ], "https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite": [ [ "AlphaChanelAddByMask", "ImageCompositeBy_BG_Zho", "ImageCompositeBy_Zho", "ImageComposite_BG_Zho", "ImageComposite_Zho", "RGB_Image_Zho", "Text_Image_Frame_Zho", "Text_Image_Multiline_Zho", "Text_Image_Zho" ], { "title_aux": "ComfyUI-Text_Image-Composite [WIP]" } ], "https://github.com/ZHO-ZHO-ZHO/comfyui-portrait-master-zh-cn": [ [ "PortraitMaster_\u4e2d\u6587\u7248" ], { "title_aux": "comfyui-portrait-master-zh-cn" } ], "https://github.com/ZaneA/ComfyUI-ImageReward": [ [ "ImageRewardLoader", "ImageRewardScore" ], { "title_aux": "ImageReward" } ], "https://github.com/Zuellni/ComfyUI-ExLlama": [ [ "ZuellniExLlamaGenerator", "ZuellniExLlamaLoader", "ZuellniTextPreview", "ZuellniTextReplace" ], { "title_aux": "ComfyUI-ExLlama" } ], "https://github.com/Zuellni/ComfyUI-PickScore-Nodes": [ [ "ZuellniPickScoreImageProcessor", "ZuellniPickScoreLoader", "ZuellniPickScoreSelector", "ZuellniPickScoreTextProcessor" ], { "title_aux": "ComfyUI PickScore Nodes" } ], "https://github.com/a1lazydog/ComfyUI-AudioScheduler": [ [ "AmplitudeToGraph", "AmplitudeToNumber", "AudioToAmplitudeGraph", "AudioToFFTs", "BatchAmplitudeSchedule", "ClipAmplitude", "GateNormalizedAmplitude", "LoadAudio", "NormalizeAmplitude", "NormalizedAmplitudeDrivenString", "NormalizedAmplitudeToGraph", "NormalizedAmplitudeToNumber", "TransientAmplitudeBasic" ], { "title_aux": "ComfyUI-AudioScheduler" } ], "https://github.com/abdozmantar/ComfyUI-InstaSwap": [ [ "InstaSwapFaceSwap", "InstaSwapLoadFaceModel", "InstaSwapSaveFaceModel" ], { "title_aux": "InstaSwap Face Swap Node for ComfyUI" } ], "https://github.com/abyz22/image_control": [ [ "abyz22_Convertpipe", "abyz22_Editpipe", "abyz22_FirstNonNull", "abyz22_FromBasicPipe_v2", "abyz22_Frompipe", "abyz22_ImpactWildcardEncode", "abyz22_ImpactWildcardEncode_GetPrompt", "abyz22_Ksampler", "abyz22_Padding Image", "abyz22_SaveImage", "abyz22_SetQueue", "abyz22_ToBasicPipe", "abyz22_Topipe", "abyz22_blend_onecolor", "abyz22_blendimages", "abyz22_bypass", "abyz22_drawmask", "abyz22_lamaInpaint", "abyz22_lamaPreprocessor", "abyz22_makecircles", "abyz22_setimageinfo", "abyz22_smallhead" ], { "title_aux": "image_control" } ], "https://github.com/adbrasi/ComfyUI-TrashNodes-DownloadHuggingface": [ [ "DownloadLinkChecker", "ShowFileNames" ], { "title_aux": "ComfyUI-TrashNodes-DownloadHuggingface" } ], "https://github.com/adieyal/comfyui-dynamicprompts": [ [ "DPCombinatorialGenerator", "DPFeelingLucky", "DPJinja", "DPMagicPrompt", "DPOutput", "DPRandomGenerator" ], { "title_aux": "DynamicPrompts Custom Nodes" } ], "https://github.com/adriflex/ComfyUI_Blender_Texdiff": [ [ "ViewportColor", "ViewportDepth" ], { "title_aux": "ComfyUI_Blender_Texdiff" } ], "https://github.com/aegis72/aegisflow_utility_nodes": [ [ "Add Text To Image", "Aegisflow CLIP Pass", "Aegisflow Conditioning Pass", "Aegisflow Image Pass", "Aegisflow Latent Pass", "Aegisflow Mask Pass", "Aegisflow Model Pass", "Aegisflow Pos/Neg Pass", "Aegisflow SDXL Tuple Pass", "Aegisflow VAE Pass", "Aegisflow controlnet preprocessor bus", "Apply Instagram Filter", "Brightness_Contrast_Ally", "Flatten Colors", "Gaussian Blur_Ally", "GlitchThis Effect", "Hue Rotation", "Image Flip_ally", "Placeholder Tuple", "Swap Color Mode", "aegisflow Multi_Pass", "aegisflow Multi_Pass XL", "af_pipe_in_15", "af_pipe_in_xl", "af_pipe_out_15", "af_pipe_out_xl" ], { "title_aux": "AegisFlow Utility Nodes" } ], "https://github.com/aegis72/comfyui-styles-all": [ [ "menus" ], { "title_aux": "ComfyUI-styles-all" } ], "https://github.com/ai-liam/comfyui_liam_util": [ [ "LiamLoadImage" ], { "title_aux": "LiamUtil" } ], "https://github.com/aianimation55/ComfyUI-FatLabels": [ [ "FatLabels" ], { "title_aux": "Comfy UI FatLabels" } ], "https://github.com/alexopus/ComfyUI-Image-Saver": [ [ "Cfg Literal (Image Saver)", "Checkpoint Loader with Name (Image Saver)", "Float Literal (Image Saver)", "Image Saver", "Int Literal (Image Saver)", "Sampler Selector (Image Saver)", "Scheduler Selector (Image Saver)", "Seed Generator (Image Saver)", "String Literal (Image Saver)", "Width/Height Literal (Image Saver)" ], { "title_aux": "ComfyUI Image Saver" } ], "https://github.com/alpertunga-bile/prompt-generator-comfyui": [ [ "Prompt Generator" ], { "title_aux": "prompt-generator" } ], "https://github.com/alsritter/asymmetric-tiling-comfyui": [ [ "Asymmetric_Tiling_KSampler" ], { "title_aux": "asymmetric-tiling-comfyui" } ], "https://github.com/alt-key-project/comfyui-dream-project": [ [ "Analyze Palette [Dream]", "Beat Curve [Dream]", "Big Float Switch [Dream]", "Big Image Switch [Dream]", "Big Int Switch [Dream]", "Big Latent Switch [Dream]", "Big Palette Switch [Dream]", "Big Text Switch [Dream]", "Boolean To Float [Dream]", "Boolean To Int [Dream]", "Build Prompt [Dream]", "CSV Curve [Dream]", "CSV Generator [Dream]", "Calculation [Dream]", "Common Frame Dimensions [Dream]", "Compare Palettes [Dream]", "FFMPEG Video Encoder [Dream]", "File Count [Dream]", "Finalize Prompt [Dream]", "Float Input [Dream]", "Float to Log Entry [Dream]", "Frame Count Calculator [Dream]", "Frame Counter (Directory) [Dream]", "Frame Counter (Simple) [Dream]", "Frame Counter Info [Dream]", "Frame Counter Offset [Dream]", "Frame Counter Time Offset [Dream]", "Image Brightness Adjustment [Dream]", "Image Color Shift [Dream]", "Image Contrast Adjustment [Dream]", "Image Motion [Dream]", "Image Sequence Blend [Dream]", "Image Sequence Loader [Dream]", "Image Sequence Saver [Dream]", "Image Sequence Tweening [Dream]", "Int Input [Dream]", "Int to Log Entry [Dream]", "Laboratory [Dream]", "Linear Curve [Dream]", "Log Entry Joiner [Dream]", "Log File [Dream]", "Noise from Area Palettes [Dream]", "Noise from Palette [Dream]", "Palette Color Align [Dream]", "Palette Color Shift [Dream]", "Sample Image Area as Palette [Dream]", "Sample Image as Palette [Dream]", "Saw Curve [Dream]", "Sine Curve [Dream]", "Smooth Event Curve [Dream]", "String Input [Dream]", "String Tokenizer [Dream]", "String to Log Entry [Dream]", "Text Input [Dream]", "Triangle Curve [Dream]", "Triangle Event Curve [Dream]", "WAV Curve [Dream]" ], { "title_aux": "Dream Project Animation Nodes" } ], "https://github.com/alt-key-project/comfyui-dream-video-batches": [ [ "Blended Transition [DVB]", "Calculation [DVB]", "Create Frame Set [DVB]", "Divide [DVB]", "Fade From Black [DVB]", "Fade To Black [DVB]", "Float Input [DVB]", "For Each Done [DVB]", "For Each Filename [DVB]", "Frame Set Append [DVB]", "Frame Set Frame Dimensions Scaled [DVB]", "Frame Set Index Offset [DVB]", "Frame Set Merger [DVB]", "Frame Set Reindex [DVB]", "Frame Set Repeat [DVB]", "Frame Set Reverse [DVB]", "Frame Set Split Beginning [DVB]", "Frame Set Split End [DVB]", "Frame Set Splitter [DVB]", "Generate Inbetween Frames [DVB]", "Int Input [DVB]", "Linear Camera Pan [DVB]", "Linear Camera Roll [DVB]", "Linear Camera Zoom [DVB]", "Load Image From Path [DVB]", "Multiply [DVB]", "Sine Camera Pan [DVB]", "Sine Camera Roll [DVB]", "Sine Camera Zoom [DVB]", "String Input [DVB]", "Text Input [DVB]", "Trace Memory Allocation [DVB]", "Unwrap Frame Set [DVB]" ], { "title_aux": "Dream Video Batches" } ], "https://github.com/an90ray/ComfyUI_RErouter_CustomNodes": [ [ "CLIPTextEncode (RE)", "CLIPTextEncodeSDXL (RE)", "CLIPTextEncodeSDXLRefiner (RE)", "Int (RE)", "RErouter <=", "RErouter =>", "String (RE)" ], { "title_aux": "ComfyUI_RErouter_CustomNodes" } ], "https://github.com/andersxa/comfyui-PromptAttention": [ [ "CLIPAttentionMaskEncode" ], { "title_aux": "CLIP Directional Prompt Attention" } ], "https://github.com/antrobot1234/antrobots-comfyUI-nodepack": [ [ "composite", "crop", "paste", "preview_mask", "scale" ], { "title_aux": "antrobots ComfyUI Nodepack" } ], "https://github.com/asagi4/ComfyUI-CADS": [ [ "CADS" ], { "title_aux": "ComfyUI-CADS" } ], "https://github.com/asagi4/comfyui-prompt-control": [ [ "EditableCLIPEncode", "FilterSchedule", "LoRAScheduler", "PCApplySettings", "PCPromptFromSchedule", "PCScheduleSettings", "PCSplitSampling", "PromptControlSimple", "PromptToSchedule", "ScheduleToCond", "ScheduleToModel" ], { "title_aux": "ComfyUI prompt control" } ], "https://github.com/asagi4/comfyui-utility-nodes": [ [ "MUForceCacheClear", "MUJinjaRender", "MUSimpleWildcard" ], { "title_aux": "asagi4/comfyui-utility-nodes" } ], "https://github.com/aszc-dev/ComfyUI-CoreMLSuite": [ [ "Core ML Converter", "Core ML LCM Converter", "Core ML LoRA Loader", "CoreMLModelAdapter", "CoreMLSampler", "CoreMLSamplerAdvanced", "CoreMLUNetLoader" ], { "title_aux": "Core ML Suite for ComfyUI" } ], "https://github.com/avatechai/avatar-graph-comfyui": [ [ "ApplyMeshTransformAsShapeKey", "B_ENUM", "B_VECTOR3", "B_VECTOR4", "Combine Points", "CreateShapeFlow", "ExportBlendshapes", "ExportGLTF", "Extract Boundary Points", "Image Alpha Mask Merge", "ImageBridge", "LoadImageFromRequest", "LoadImageWithAlpha", "LoadValueFromRequest", "SAM MultiLayer", "Save Image With Workflow" ], { "author": "Avatech Limited", "description": "Include nodes for sam + bpy operation, that allows workflow creations for generative 2d character rig.", "nickname": "Avatar Graph", "title": "Avatar Graph", "title_aux": "avatar-graph-comfyui" } ], "https://github.com/azure-dragon-ai/ComfyUI-ClipScore-Nodes": [ [ "HaojihuiClipScoreFakeImageProcessor", "HaojihuiClipScoreImageProcessor", "HaojihuiClipScoreImageScore", "HaojihuiClipScoreLoader", "HaojihuiClipScoreRealImageProcessor", "HaojihuiClipScoreTextProcessor" ], { "title_aux": "ComfyUI-ClipScore-Nodes" } ], "https://github.com/badjeff/comfyui_lora_tag_loader": [ [ "LoraTagLoader" ], { "title_aux": "LoRA Tag Loader for ComfyUI" } ], "https://github.com/banodoco/steerable-motion": [ [ "BatchCreativeInterpolation" ], { "title_aux": "Steerable Motion" } ], "https://github.com/bash-j/mikey_nodes": [ [ "AddMetaData", "Batch Crop Image", "Batch Crop Resize Inplace", "Batch Load Images", "Batch Resize Image for SDXL", "Checkpoint Loader Simple Mikey", "CinematicLook", "Empty Latent Ratio Custom SDXL", "Empty Latent Ratio Select SDXL", "EvalFloats", "FaceFixerOpenCV", "FileNamePrefix", "FileNamePrefixDateDirFirst", "Float to String", "HaldCLUT", "Image Caption", "ImageBorder", "ImageOverlay", "ImagePaste", "Int to String", "LMStudioPrompt", "Load Image Based on Number", "LoraSyntaxProcessor", "Mikey Sampler", "Mikey Sampler Base Only", "Mikey Sampler Base Only Advanced", "Mikey Sampler Tiled", "Mikey Sampler Tiled Base Only", "MikeySamplerTiledAdvanced", "MikeySamplerTiledAdvancedBaseOnly", "OobaPrompt", "PresetRatioSelector", "Prompt With SDXL", "Prompt With Style", "Prompt With Style V2", "Prompt With Style V3", "Range Float", "Range Integer", "Ratio Advanced", "Resize Image for SDXL", "Save Image If True", "Save Image With Prompt Data", "Save Images Mikey", "Save Images No Display", "SaveMetaData", "SearchAndReplace", "Seed String", "Style Conditioner", "Style Conditioner Base Only", "Text2InputOr3rdOption", "TextCombinations", "TextCombinations3", "TextConcat", "TextPreserve", "Upscale Tile Calculator", "Wildcard Processor", "WildcardAndLoraSyntaxProcessor", "WildcardOobaPrompt" ], { "title_aux": "Mikey Nodes" } ], "https://github.com/bedovyy/ComfyUI_NAIDGenerator": [ [ "GenerateNAID", "Img2ImgOptionNAID", "InpaintingOptionNAID", "MaskImageToNAID", "ModelOptionNAID", "PromptToNAID" ], { "title_aux": "ComfyUI_NAIDGenerator" } ], "https://github.com/biegert/ComfyUI-CLIPSeg/raw/main/custom_nodes/clipseg.py": [ [ "CLIPSeg", "CombineSegMasks" ], { "title_aux": "CLIPSeg" } ], "https://github.com/bilal-arikan/ComfyUI_TextAssets": [ [ "LoadTextAsset" ], { "title_aux": "ComfyUI_TextAssets" } ], "https://github.com/blepping/ComfyUI-bleh": [ [ "BlehDeepShrink", "BlehDiscardPenultimateSigma", "BlehHyperTile", "BlehInsaneChainSampler", "BlehModelPatchConditional" ], { "title_aux": "ComfyUI-bleh" } ], "https://github.com/bmad4ever/comfyui_ab_samplercustom": [ [ "AB SamplerCustom (experimental)" ], { "title_aux": "comfyui_ab_sampler" } ], "https://github.com/bmad4ever/comfyui_bmad_nodes": [ [ "AdaptiveThresholding", "Add String To Many", "AddAlpha", "AdjustRect", "AnyToAny", "BoundingRect (contours)", "BuildColorRangeAdvanced (hsv)", "BuildColorRangeHSV (hsv)", "CLAHE", "CLIPEncodeMultiple", "CLIPEncodeMultipleAdvanced", "ChameleonMask", "CheckpointLoader (dirty)", "CheckpointLoaderSimple (dirty)", "Color (RGB)", "Color (hexadecimal)", "Color Clip", "Color Clip (advanced)", "Color Clip ADE20k", "ColorDictionary", "ColorDictionary (custom)", "Conditioning (combine multiple)", "Conditioning (combine selective)", "Conditioning Grid (cond)", "Conditioning Grid (string)", "Conditioning Grid (string) Advanced", "Contour To Mask", "Contours", "ControlNetHadamard", "ControlNetHadamard (manual)", "ConvertImg", "CopyMakeBorder", "CreateRequestMetadata", "DistanceTransform", "Draw Contour(s)", "EqualizeHistogram", "ExtendColorList", "ExtendCondList", "ExtendFloatList", "ExtendImageList", "ExtendIntList", "ExtendLatentList", "ExtendMaskList", "ExtendModelList", "ExtendStringList", "FadeMaskEdges", "Filter Contour", "FindComplementaryColor", "FindThreshold", "FlatLatentsIntoSingleGrid", "Framed Mask Grab Cut", "Framed Mask Grab Cut 2", "FromListGet1Color", "FromListGet1Cond", "FromListGet1Float", "FromListGet1Image", "FromListGet1Int", "FromListGet1Latent", "FromListGet1Mask", "FromListGet1Model", "FromListGet1String", "FromListGetColors", "FromListGetConds", "FromListGetFloats", "FromListGetImages", "FromListGetInts", "FromListGetLatents", "FromListGetMasks", "FromListGetModels", "FromListGetStrings", "Get Contour from list", "Get Models", "Get Prompt", "HypernetworkLoader (dirty)", "ImageBatchToList", "InRange (hsv)", "Inpaint", "Input/String to Int Array", "KMeansColor", "Load 64 Encoded Image", "LoraLoader (dirty)", "MaskGrid N KSamplers Advanced", "MaskOuterBlur", "Merge Latent Batch Gridwise", "MonoMerge", "MorphologicOperation", "MorphologicSkeletoning", "NaiveAutoKMeansColor", "OtsuThreshold", "RGB to HSV", "Rect Grab Cut", "Remap", "RemapBarrelDistortion", "RemapFromInsideParabolas", "RemapFromQuadrilateral (homography)", "RemapInsideParabolas", "RemapInsideParabolasAdvanced", "RemapPinch", "RemapReverseBarrelDistortion", "RemapStretch", "RemapToInnerCylinder", "RemapToOuterCylinder", "RemapToQuadrilateral", "RemapWarpPolar", "Repeat Into Grid (image)", "Repeat Into Grid (latent)", "RequestInputs", "SampleColorHSV", "Save Image (api)", "SeamlessClone", "SeamlessClone (simple)", "SetRequestStateToComplete", "String", "String to Float", "String to Integer", "ToColorList", "ToCondList", "ToFloatList", "ToImageList", "ToIntList", "ToLatentList", "ToMaskList", "ToModelList", "ToStringList", "UnGridify (image)", "VAEEncodeBatch" ], { "title_aux": "Bmad Nodes" } ], "https://github.com/bmad4ever/comfyui_lists_cartesian_product": [ [ "AnyListCartesianProduct" ], { "title_aux": "Lists Cartesian Product" } ], "https://github.com/bradsec/ComfyUI_ResolutionSelector": [ [ "ResolutionSelector" ], { "title_aux": "ResolutionSelector for ComfyUI" } ], "https://github.com/braintacles/braintacles-comfyui-nodes": [ [ "CLIPTextEncodeSDXL-Multi-IO", "CLIPTextEncodeSDXL-Pipe", "Empty Latent Image from Aspect-Ratio", "Random Find and Replace", "VAE Decode Pipe", "VAE Decode Tiled Pipe", "VAE Encode Pipe", "VAE Encode Tiled Pipe" ], { "title_aux": "braintacles-nodes" } ], "https://github.com/brianfitzgerald/style_aligned_comfy": [ [ "StyleAlignedBatchAlign", "StyleAlignedReferenceSampler", "StyleAlignedSampleReferenceLatents" ], { "title_aux": "StyleAligned for ComfyUI" } ], "https://github.com/bronkula/comfyui-fitsize": [ [ "FS: Crop Image Into Even Pieces", "FS: Fit Image And Resize", "FS: Fit Size From Image", "FS: Fit Size From Int", "FS: Image Region To Mask", "FS: Load Image And Resize To Fit", "FS: Pick Image From Batch", "FS: Pick Image From Batches", "FS: Pick Image From List" ], { "title_aux": "comfyui-fitsize" } ], "https://github.com/bruefire/ComfyUI-SeqImageLoader": [ [ "VFrame Loader With Mask Editor", "Video Loader With Mask Editor" ], { "title_aux": "ComfyUI Sequential Image Loader" } ], "https://github.com/budihartono/comfyui_otonx_nodes": [ [ "OTX Integer Multiple Inputs 4", "OTX Integer Multiple Inputs 5", "OTX Integer Multiple Inputs 6", "OTX KSampler Feeder", "OTX Versatile Multiple Inputs 4", "OTX Versatile Multiple Inputs 5", "OTX Versatile Multiple Inputs 6" ], { "title_aux": "Otonx's Custom Nodes" } ], "https://github.com/bvhari/ComfyUI_ImageProcessing": [ [ "BilateralFilter", "Brightness", "Gamma", "Hue", "Saturation", "SigmoidCorrection", "UnsharpMask" ], { "title_aux": "ImageProcessing" } ], "https://github.com/bvhari/ComfyUI_LatentToRGB": [ [ "LatentToRGB" ], { "title_aux": "LatentToRGB" } ], "https://github.com/bvhari/ComfyUI_PerpWeight": [ [ "CLIPTextEncodePerpWeight" ], { "title_aux": "ComfyUI_PerpWeight" } ], "https://github.com/catscandrive/comfyui-imagesubfolders/raw/main/loadImageWithSubfolders.py": [ [ "LoadImagewithSubfolders" ], { "title_aux": "Image loader with subfolders" } ], "https://github.com/ccvv804/ComfyUI-DiffusersStableCascade-LowVRAM": [ [ "DiffusersStableCascade" ], { "title_aux": "ComfyUI StableCascade using diffusers for Low VRAM" } ], "https://github.com/celsojr2013/comfyui_simpletools/raw/main/google_translator.py": [ [ "GoogleTranslator" ], { "title_aux": "ComfyUI SimpleTools Suit" } ], "https://github.com/ceruleandeep/ComfyUI-LLaVA-Captioner": [ [ "LlavaCaptioner" ], { "title_aux": "ComfyUI LLaVA Captioner" } ], "https://github.com/chaojie/ComfyUI-DragNUWA": [ [ "BrushMotion", "CompositeMotionBrush", "CompositeMotionBrushWithoutModel", "DragNUWA Run", "DragNUWA Run MotionBrush", "Get First Image", "Get Last Image", "InstantCameraMotionBrush", "InstantObjectMotionBrush", "Load CheckPoint DragNUWA", "Load MotionBrush From Optical Flow", "Load MotionBrush From Optical Flow Directory", "Load MotionBrush From Optical Flow Without Model", "Load MotionBrush From Tracking Points", "Load MotionBrush From Tracking Points Without Model", "Load Pose KeyPoints", "Loop", "LoopEnd_IMAGE", "LoopStart_IMAGE", "Split Tracking Points" ], { "title_aux": "ComfyUI-DragNUWA" } ], "https://github.com/chaojie/ComfyUI-DynamiCrafter": [ [ "DynamiCrafter Simple", "DynamiCrafterLoader" ], { "title_aux": "ComfyUI-DynamiCrafter" } ], "https://github.com/chaojie/ComfyUI-I2VGEN-XL": [ [ "I2VGEN-XL Simple", "Modelscope Pipeline Loader" ], { "title_aux": "ComfyUI-I2VGEN-XL" } ], "https://github.com/chaojie/ComfyUI-LightGlue": [ [ "LightGlue Loader", "LightGlue Simple", "LightGlue Simple Multi" ], { "title_aux": "ComfyUI-LightGlue" } ], "https://github.com/chaojie/ComfyUI-Moore-AnimateAnyone": [ [ "Moore-AnimateAnyone Denoising Unet", "Moore-AnimateAnyone Image Encoder", "Moore-AnimateAnyone Pipeline Loader", "Moore-AnimateAnyone Pose Guider", "Moore-AnimateAnyone Reference Unet", "Moore-AnimateAnyone Simple", "Moore-AnimateAnyone VAE" ], { "title_aux": "ComfyUI-Moore-AnimateAnyone" } ], "https://github.com/chaojie/ComfyUI-Motion-Vector-Extractor": [ [ "Motion Vector Extractor", "VideoCombineThenPath" ], { "title_aux": "ComfyUI-Motion-Vector-Extractor" } ], "https://github.com/chaojie/ComfyUI-MotionCtrl": [ [ "Load Motion Camera Preset", "Load Motion Traj Preset", "Load Motionctrl Checkpoint", "Motionctrl Cond", "Motionctrl Sample", "Motionctrl Sample Simple", "Select Image Indices" ], { "title_aux": "ComfyUI-MotionCtrl" } ], "https://github.com/chaojie/ComfyUI-MotionCtrl-SVD": [ [ "Load Motionctrl-SVD Camera Preset", "Load Motionctrl-SVD Checkpoint", "Motionctrl-SVD Sample Simple" ], { "title_aux": "ComfyUI-MotionCtrl-SVD" } ], "https://github.com/chaojie/ComfyUI-Panda3d": [ [ "Panda3dAmbientLight", "Panda3dAttachNewNode", "Panda3dBase", "Panda3dDirectionalLight", "Panda3dLoadDepthModel", "Panda3dLoadModel", "Panda3dLoadTexture", "Panda3dModelMerge", "Panda3dTest", "Panda3dTextureMerge" ], { "title_aux": "ComfyUI-Panda3d" } ], "https://github.com/chaojie/ComfyUI-Pymunk": [ [ "PygameRun", "PygameSurface", "PymunkDynamicBox", "PymunkDynamicCircle", "PymunkRun", "PymunkShapeMerge", "PymunkSpace", "PymunkStaticLine" ], { "title_aux": "ComfyUI-Pymunk" } ], "https://github.com/chaojie/ComfyUI-RAFT": [ [ "Load MotionBrush", "RAFT Run", "Save MotionBrush", "VizMotionBrush" ], { "title_aux": "ComfyUI-RAFT" } ], "https://github.com/chflame163/ComfyUI_LayerStyle": [ [ "LayerColor: Brightness & Contrast", "LayerColor: ColorAdapter", "LayerColor: Exposure", "LayerColor: Gamma", "LayerColor: HSV", "LayerColor: LAB", "LayerColor: LUT Apply", "LayerColor: RGB", "LayerColor: YUV", "LayerFilter: ChannelShake", "LayerFilter: ColorMap", "LayerFilter: GaussianBlur", "LayerFilter: MotionBlur", "LayerFilter: Sharp & Soft", "LayerFilter: SkinBeauty", "LayerFilter: SoftLight", "LayerFilter: WaterColor", "LayerMask: CreateGradientMask", "LayerMask: MaskBoxDetect", "LayerMask: MaskByDifferent", "LayerMask: MaskEdgeShrink", "LayerMask: MaskEdgeUltraDetail", "LayerMask: MaskGradient", "LayerMask: MaskGrow", "LayerMask: MaskInvert", "LayerMask: MaskMotionBlur", "LayerMask: MaskPreview", "LayerMask: MaskStroke", "LayerMask: PixelSpread", "LayerMask: RemBgUltra", "LayerMask: SegmentAnythingUltra", "LayerStyle: ColorOverlay", "LayerStyle: DropShadow", "LayerStyle: GradientOverlay", "LayerStyle: InnerGlow", "LayerStyle: InnerShadow", "LayerStyle: OuterGlow", "LayerStyle: Stroke", "LayerUtility: ColorImage", "LayerUtility: ColorPicker", "LayerUtility: CropByMask", "LayerUtility: ExtendCanvas", "LayerUtility: GetColorTone", "LayerUtility: GetImageSize", "LayerUtility: GradientImage", "LayerUtility: ImageBlend", "LayerUtility: ImageBlendAdvance", "LayerUtility: ImageChannelMerge", "LayerUtility: ImageChannelSplit", "LayerUtility: ImageMaskScaleAs", "LayerUtility: ImageOpacity", "LayerUtility: ImageScaleRestore", "LayerUtility: ImageShift", "LayerUtility: LayerImageTransform", "LayerUtility: LayerMaskTransform", "LayerUtility: PrintInfo", "LayerUtility: RestoreCropBox", "LayerUtility: TextImage", "LayerUtility: XY to Percent" ], { "title_aux": "ComfyUI Layer Style" } ], "https://github.com/chflame163/ComfyUI_MSSpeech_TTS": [ [ "Input Trigger", "MicrosoftSpeech_TTS", "Play Sound", "Play Sound (loop)" ], { "title_aux": "ComfyUI_MSSpeech_TTS" } ], "https://github.com/chflame163/ComfyUI_WordCloud": [ [ "ComfyWordCloud", "LoadTextFile", "RGB_Picker" ], { "title_aux": "ComfyUI_WordCloud" } ], "https://github.com/chibiace/ComfyUI-Chibi-Nodes": [ [ "ConditionText", "ConditionTextMulti", "ImageAddText", "ImageSimpleResize", "ImageSizeInfo", "ImageTool", "Int2String", "LoadEmbedding", "LoadImageExtended", "Loader", "Prompts", "RandomResolutionLatent", "SaveImages", "SeedGenerator", "SimpleSampler", "TextSplit", "Textbox", "Wildcards" ], { "title_aux": "ComfyUI-Chibi-Nodes" } ], "https://github.com/chrisgoringe/cg-image-picker": [ [ "Preview Chooser", "Preview Chooser Fabric" ], { "author": "chrisgoringe", "description": "Custom nodes that preview images and pause the workflow to allow the user to select one or more to progress", "nickname": "Image Chooser", "title": "Image Chooser", "title_aux": "Image chooser" } ], "https://github.com/chrisgoringe/cg-noise": [ [ "Hijack", "KSampler Advanced with Variations", "KSampler with Variations", "UnHijack" ], { "title_aux": "Variation seeds" } ], "https://github.com/chrisgoringe/cg-use-everywhere": [ [ "Seed Everywhere" ], { "nodename_pattern": "(^(Prompts|Anything) Everywhere|Simple String)", "title_aux": "Use Everywhere (UE Nodes)" } ], "https://github.com/city96/ComfyUI_ColorMod": [ [ "ColorModEdges", "ColorModPivot", "LoadImageHighPrec", "PreviewImageHighPrec", "SaveImageHighPrec" ], { "title_aux": "ComfyUI_ColorMod" } ], "https://github.com/city96/ComfyUI_DiT": [ [ "DiTCheckpointLoader", "DiTCheckpointLoaderSimple", "DiTLabelCombine", "DiTLabelSelect", "DiTSampler" ], { "title_aux": "ComfyUI_DiT [WIP]" } ], "https://github.com/city96/ComfyUI_ExtraModels": [ [ "DiTCondLabelEmpty", "DiTCondLabelSelect", "DitCheckpointLoader", "ExtraVAELoader", "PixArtCheckpointLoader", "PixArtDPMSampler", "PixArtLoraLoader", "PixArtResolutionSelect", "PixArtT5TextEncode", "T5TextEncode", "T5v11Loader" ], { "title_aux": "Extra Models for ComfyUI" } ], "https://github.com/city96/ComfyUI_NetDist": [ [ "CombineImageBatch", "FetchRemote", "LoadCurrentWorkflowJSON", "LoadDiskWorkflowJSON", "LoadImageUrl", "LoadLatentNumpy", "LoadLatentUrl", "RemoteChainEnd", "RemoteChainStart", "RemoteQueueSimple", "RemoteQueueWorker", "SaveDiskWorkflowJSON", "SaveImageUrl", "SaveLatentNumpy" ], { "title_aux": "ComfyUI_NetDist" } ], "https://github.com/city96/SD-Advanced-Noise": [ [ "LatentGaussianNoise", "MathEncode" ], { "title_aux": "SD-Advanced-Noise" } ], "https://github.com/city96/SD-Latent-Interposer": [ [ "LatentInterposer" ], { "title_aux": "Latent-Interposer" } ], "https://github.com/city96/SD-Latent-Upscaler": [ [ "LatentUpscaler" ], { "title_aux": "SD-Latent-Upscaler" } ], "https://github.com/civitai/comfy-nodes": [ [ "CivitAI_Checkpoint_Loader", "CivitAI_Lora_Loader" ], { "title_aux": "comfy-nodes" } ], "https://github.com/comfyanonymous/ComfyUI": [ [ "BasicScheduler", "CLIPLoader", "CLIPMergeSimple", "CLIPSave", "CLIPSetLastLayer", "CLIPTextEncode", "CLIPTextEncodeControlnet", "CLIPTextEncodeSDXL", "CLIPTextEncodeSDXLRefiner", "CLIPVisionEncode", "CLIPVisionLoader", "Canny", "CheckpointLoader", "CheckpointLoaderSimple", "CheckpointSave", "ConditioningAverage", "ConditioningCombine", "ConditioningConcat", "ConditioningSetArea", "ConditioningSetAreaPercentage", "ConditioningSetAreaStrength", "ConditioningSetMask", "ConditioningSetTimestepRange", "ConditioningZeroOut", "ControlNetApply", "ControlNetApplyAdvanced", "ControlNetLoader", "CropMask", "DiffControlNetLoader", "DiffusersLoader", "DualCLIPLoader", "EmptyImage", "EmptyLatentImage", "ExponentialScheduler", "FeatherMask", "FlipSigmas", "FreeU", "FreeU_V2", "GLIGENLoader", "GLIGENTextBoxApply", "GrowMask", "HyperTile", "HypernetworkLoader", "ImageBatch", "ImageBlend", "ImageBlur", "ImageColorToMask", "ImageCompositeMasked", "ImageCrop", "ImageFromBatch", "ImageInvert", "ImageOnlyCheckpointLoader", "ImageOnlyCheckpointSave", "ImagePadForOutpaint", "ImageQuantize", "ImageScale", "ImageScaleBy", "ImageScaleToTotalPixels", "ImageSharpen", "ImageToMask", "ImageUpscaleWithModel", "InpaintModelConditioning", "InvertMask", "JoinImageWithAlpha", "KSampler", "KSamplerAdvanced", "KSamplerSelect", "KarrasScheduler", "LatentAdd", "LatentBatch", "LatentBatchSeedBehavior", "LatentBlend", "LatentComposite", "LatentCompositeMasked", "LatentCrop", "LatentFlip", "LatentFromBatch", "LatentInterpolate", "LatentMultiply", "LatentRotate", "LatentSubtract", "LatentUpscale", "LatentUpscaleBy", "LoadImage", "LoadImageMask", "LoadLatent", "LoraLoader", "LoraLoaderModelOnly", "MaskComposite", "MaskToImage", "ModelMergeAdd", "ModelMergeBlocks", "ModelMergeSimple", "ModelMergeSubtract", "ModelSamplingContinuousEDM", "ModelSamplingDiscrete", "PatchModelAddDownscale", "PerpNeg", "PhotoMakerEncode", "PhotoMakerLoader", "PolyexponentialScheduler", "PorterDuffImageComposite", "PreviewImage", "RebatchImages", "RebatchLatents", "RepeatImageBatch", "RepeatLatentBatch", "RescaleCFG", "SDTurboScheduler", "SD_4XUpscale_Conditioning", "SVD_img2vid_Conditioning", "SamplerCustom", "SamplerDPMPP_2M_SDE", "SamplerDPMPP_SDE", "SaveAnimatedPNG", "SaveAnimatedWEBP", "SaveImage", "SaveLatent", "SelfAttentionGuidance", "SetLatentNoiseMask", "SolidMask", "SplitImageWithAlpha", "SplitSigmas", "StableCascade_EmptyLatentImage", "StableCascade_StageB_Conditioning", "StableZero123_Conditioning", "StableZero123_Conditioning_Batched", "StyleModelApply", "StyleModelLoader", "TomePatchModel", "UNETLoader", "UpscaleModelLoader", "VAEDecode", "VAEDecodeTiled", "VAEEncode", "VAEEncodeForInpaint", "VAEEncodeTiled", "VAELoader", "VAESave", "VPScheduler", "VideoLinearCFGGuidance", "unCLIPCheckpointLoader", "unCLIPConditioning" ], { "title_aux": "ComfyUI" } ], "https://github.com/comfyanonymous/ComfyUI_experiments": [ [ "ModelMergeBlockNumber", "ModelMergeSDXL", "ModelMergeSDXLDetailedTransformers", "ModelMergeSDXLTransformers", "ModelSamplerTonemapNoiseTest", "ReferenceOnlySimple", "RescaleClassifierFreeGuidanceTest", "TonemapNoiseWithRescaleCFG" ], { "title_aux": "ComfyUI_experiments" } ], "https://github.com/concarne000/ConCarneNode": [ [ "BingImageGrabber", "Zephyr" ], { "title_aux": "ConCarneNode" } ], "https://github.com/coreyryanhanson/ComfyQR": [ [ "comfy-qr-by-image-size", "comfy-qr-by-module-size", "comfy-qr-by-module-split", "comfy-qr-mask_errors" ], { "title_aux": "ComfyQR" } ], "https://github.com/coreyryanhanson/ComfyQR-scanning-nodes": [ [ "comfy-qr-read", "comfy-qr-validate" ], { "title_aux": "ComfyQR-scanning-nodes" } ], "https://github.com/cubiq/ComfyUI_IPAdapter_plus": [ [ "IPAdapterApply", "IPAdapterApplyEncoded", "IPAdapterApplyFaceID", "IPAdapterBatchEmbeds", "IPAdapterEncoder", "IPAdapterLoadEmbeds", "IPAdapterModelLoader", "IPAdapterSaveEmbeds", "IPAdapterTilesMasked", "InsightFaceLoader", "PrepImageForClipVision", "PrepImageForInsightFace" ], { "title_aux": "ComfyUI_IPAdapter_plus" } ], "https://github.com/cubiq/ComfyUI_InstantID": [ [ "ApplyInstantID", "FaceKeypointsPreprocessor", "InstantIDFaceAnalysis", "InstantIDModelLoader" ], { "title_aux": "ComfyUI InstantID (Native Support)" } ], "https://github.com/cubiq/ComfyUI_SimpleMath": [ [ "SimpleMath", "SimpleMathDebug" ], { "title_aux": "Simple Math" } ], "https://github.com/cubiq/ComfyUI_essentials": [ [ "BatchCount+", "CLIPTextEncodeSDXL+", "ConsoleDebug+", "DebugTensorShape+", "DrawText+", "ExtractKeyframes+", "GetImageSize+", "ImageApplyLUT+", "ImageCASharpening+", "ImageCompositeFromMaskBatch+", "ImageCrop+", "ImageDesaturate+", "ImageEnhanceDifference+", "ImageExpandBatch+", "ImageFlip+", "ImageFromBatch+", "ImagePosterize+", "ImageRemoveBackground+", "ImageResize+", "ImageSeamCarving+", "KSamplerVariationsStochastic+", "KSamplerVariationsWithNoise+", "MaskBatch+", "MaskBlur+", "MaskExpandBatch+", "MaskFlip+", "MaskFromBatch+", "MaskFromColor+", "MaskPreview+", "ModelCompile+", "RemBGSession+", "SDXLEmptyLatentSizePicker+", "SimpleMath+", "TransitionMask+" ], { "title_aux": "ComfyUI Essentials" } ], "https://github.com/dagthomas/comfyui_dagthomas": [ [ "CSL", "CSVPromptGenerator", "PromptGenerator" ], { "title_aux": "SDXL Auto Prompter" } ], "https://github.com/daniel-lewis-ab/ComfyUI-Llama": [ [ "Call LLM Advanced", "Call LLM Basic", "LLM_Create_Completion Advanced", "LLM_Detokenize", "LLM_Embed", "LLM_Eval", "LLM_Load_State", "LLM_Reset", "LLM_Sample", "LLM_Save_State", "LLM_Token_BOS", "LLM_Token_EOS", "LLM_Tokenize", "Load LLM Model Advanced", "Load LLM Model Basic" ], { "title_aux": "ComfyUI-Llama" } ], "https://github.com/daniel-lewis-ab/ComfyUI-TTS": [ [ "Load_Piper_Model", "Piper_Speak_Text" ], { "title_aux": "ComfyUI-TTS" } ], "https://github.com/darkpixel/darkprompts": [ [ "DarkCombine", "DarkFaceIndexShuffle", "DarkLoRALoader", "DarkPrompt" ], { "title_aux": "DarkPrompts" } ], "https://github.com/davask/ComfyUI-MarasIT-Nodes": [ [ "MarasitBusNode" ], { "title_aux": "MarasIT Nodes" } ], "https://github.com/dave-palt/comfyui_DSP_imagehelpers": [ [ "dsp-imagehelpers-concat" ], { "title_aux": "comfyui_DSP_imagehelpers" } ], "https://github.com/dawangraoming/ComfyUI_ksampler_gpu/raw/main/ksampler_gpu.py": [ [ "KSamplerAdvancedGPU", "KSamplerGPU" ], { "title_aux": "KSampler GPU" } ], "https://github.com/daxthin/DZ-FaceDetailer": [ [ "DZ_Face_Detailer" ], { "title_aux": "DZ-FaceDetailer" } ], "https://github.com/deroberon/StableZero123-comfyui": [ [ "SDZero ImageSplit", "Stablezero123", "Stablezero123WithDepth" ], { "title_aux": "StableZero123-comfyui" } ], "https://github.com/deroberon/demofusion-comfyui": [ [ "Batch Unsampler", "Demofusion", "Demofusion From Single File", "Iterative Mixing KSampler" ], { "title_aux": "demofusion-comfyui" } ], "https://github.com/dfl/comfyui-clip-with-break": [ [ "AdvancedCLIPTextEncodeWithBreak", "CLIPTextEncodeWithBreak" ], { "author": "dfl", "description": "CLIP text encoder that does BREAK prompting like A1111", "nickname": "CLIP with BREAK", "title": "CLIP with BREAK syntax", "title_aux": "comfyui-clip-with-break" } ], "https://github.com/digitaljohn/comfyui-propost": [ [ "ProPostApplyLUT", "ProPostDepthMapBlur", "ProPostFilmGrain", "ProPostRadialBlur", "ProPostVignette" ], { "title_aux": "ComfyUI-ProPost" } ], "https://github.com/dimtoneff/ComfyUI-PixelArt-Detector": [ [ "PixelArtAddDitherPattern", "PixelArtDetectorConverter", "PixelArtDetectorSave", "PixelArtDetectorToImage", "PixelArtLoadPalettes" ], { "title_aux": "ComfyUI PixelArt Detector" } ], "https://github.com/diontimmer/ComfyUI-Vextra-Nodes": [ [ "Add Text To Image", "Apply Instagram Filter", "Create Solid Color", "Flatten Colors", "Generate Noise Image", "GlitchThis Effect", "Hue Rotation", "Load Picture Index", "Pixel Sort", "Play Sound At Execution", "Prettify Prompt Using distilgpt2", "Swap Color Mode" ], { "title_aux": "ComfyUI-Vextra-Nodes" } ], "https://github.com/djbielejeski/a-person-mask-generator": [ [ "APersonMaskGenerator" ], { "title_aux": "a-person-mask-generator" } ], "https://github.com/dmarx/ComfyUI-AudioReactive": [ [ "OpAbs", "OpBandpass", "OpClamp", "OpHarmonic", "OpModulo", "OpNormalize", "OpNovelty", "OpPercussive", "OpPow", "OpPow2", "OpPredominant_pulse", "OpQuantize", "OpRms", "OpSmoosh", "OpSmooth", "OpSqrt", "OpStretch", "OpSustain", "OpThreshold" ], { "title_aux": "ComfyUI-AudioReactive" } ], "https://github.com/dmarx/ComfyUI-Keyframed": [ [ "Example", "KfAddCurveToPGroup", "KfAddCurveToPGroupx10", "KfApplyCurveToCond", "KfConditioningAdd", "KfConditioningAddx10", "KfCurveConstant", "KfCurveDraw", "KfCurveFromString", "KfCurveFromYAML", "KfCurveInverse", "KfCurveToAcnLatentKeyframe", "KfCurvesAdd", "KfCurvesAddx10", "KfCurvesDivide", "KfCurvesMultiply", "KfCurvesMultiplyx10", "KfCurvesSubtract", "KfDebug_Clip", "KfDebug_Cond", "KfDebug_Curve", "KfDebug_Float", "KfDebug_Image", "KfDebug_Int", "KfDebug_Latent", "KfDebug_Model", "KfDebug_Passthrough", "KfDebug_Segs", "KfDebug_String", "KfDebug_Vae", "KfDrawSchedule", "KfEvaluateCurveAtT", "KfGetCurveFromPGroup", "KfGetScheduleConditionAtTime", "KfGetScheduleConditionSlice", "KfKeyframedCondition", "KfKeyframedConditionWithText", "KfPGroupCurveAdd", "KfPGroupCurveMultiply", "KfPGroupDraw", "KfPGroupProd", "KfPGroupSum", "KfSetCurveLabel", "KfSetKeyframe", "KfSinusoidalAdjustAmplitude", "KfSinusoidalAdjustFrequency", "KfSinusoidalAdjustPhase", "KfSinusoidalAdjustWavelength", "KfSinusoidalEntangledZeroOneFromFrequencyx2", "KfSinusoidalEntangledZeroOneFromFrequencyx3", "KfSinusoidalEntangledZeroOneFromFrequencyx4", "KfSinusoidalEntangledZeroOneFromFrequencyx5", "KfSinusoidalEntangledZeroOneFromFrequencyx6", "KfSinusoidalEntangledZeroOneFromFrequencyx7", "KfSinusoidalEntangledZeroOneFromFrequencyx8", "KfSinusoidalEntangledZeroOneFromFrequencyx9", "KfSinusoidalEntangledZeroOneFromWavelengthx2", "KfSinusoidalEntangledZeroOneFromWavelengthx3", "KfSinusoidalEntangledZeroOneFromWavelengthx4", "KfSinusoidalEntangledZeroOneFromWavelengthx5", "KfSinusoidalEntangledZeroOneFromWavelengthx6", "KfSinusoidalEntangledZeroOneFromWavelengthx7", "KfSinusoidalEntangledZeroOneFromWavelengthx8", "KfSinusoidalEntangledZeroOneFromWavelengthx9", "KfSinusoidalGetAmplitude", "KfSinusoidalGetFrequency", "KfSinusoidalGetPhase", "KfSinusoidalGetWavelength", "KfSinusoidalWithFrequency", "KfSinusoidalWithWavelength" ], { "title_aux": "ComfyUI-Keyframed" } ], "https://github.com/drago87/ComfyUI_Dragos_Nodes": [ [ "file_padding", "image_info", "lora_loader", "vae_loader" ], { "title_aux": "ComfyUI_Dragos_Nodes" } ], "https://github.com/drustan-hawk/primitive-types": [ [ "float", "int", "string", "string_multiline" ], { "title_aux": "primitive-types" } ], "https://github.com/ealkanat/comfyui_easy_padding": [ [ "comfyui-easy-padding" ], { "title_aux": "ComfyUI Easy Padding" } ], "https://github.com/edenartlab/eden_comfy_pipelines": [ [ "CLIP_Interrogator", "Eden_Bool", "Eden_Compare", "Eden_DebugPrint", "Eden_Float", "Eden_Int", "Eden_String", "Filepicker", "IMG_blender", "IMG_padder", "IMG_scaler", "IMG_unpadder", "If ANY execute A else B", "LatentTypeConversion", "SaveImageAdvanced", "VAEDecode_to_folder" ], { "title_aux": "eden_comfy_pipelines" } ], "https://github.com/evanspearman/ComfyMath": [ [ "CM_BoolBinaryOperation", "CM_BoolToInt", "CM_BoolUnaryOperation", "CM_BreakoutVec2", "CM_BreakoutVec3", "CM_BreakoutVec4", "CM_ComposeVec2", "CM_ComposeVec3", "CM_ComposeVec4", "CM_FloatBinaryCondition", "CM_FloatBinaryOperation", "CM_FloatToInt", "CM_FloatToNumber", "CM_FloatUnaryCondition", "CM_FloatUnaryOperation", "CM_IntBinaryCondition", "CM_IntBinaryOperation", "CM_IntToBool", "CM_IntToFloat", "CM_IntToNumber", "CM_IntUnaryCondition", "CM_IntUnaryOperation", "CM_NearestSDXLResolution", "CM_NumberBinaryCondition", "CM_NumberBinaryOperation", "CM_NumberToFloat", "CM_NumberToInt", "CM_NumberUnaryCondition", "CM_NumberUnaryOperation", "CM_SDXLResolution", "CM_Vec2BinaryCondition", "CM_Vec2BinaryOperation", "CM_Vec2ScalarOperation", "CM_Vec2ToScalarBinaryOperation", "CM_Vec2ToScalarUnaryOperation", "CM_Vec2UnaryCondition", "CM_Vec2UnaryOperation", "CM_Vec3BinaryCondition", "CM_Vec3BinaryOperation", "CM_Vec3ScalarOperation", "CM_Vec3ToScalarBinaryOperation", "CM_Vec3ToScalarUnaryOperation", "CM_Vec3UnaryCondition", "CM_Vec3UnaryOperation", "CM_Vec4BinaryCondition", "CM_Vec4BinaryOperation", "CM_Vec4ScalarOperation", "CM_Vec4ToScalarBinaryOperation", "CM_Vec4ToScalarUnaryOperation", "CM_Vec4UnaryCondition", "CM_Vec4UnaryOperation" ], { "title_aux": "ComfyMath" } ], "https://github.com/fearnworks/ComfyUI_FearnworksNodes/raw/main/fw_nodes.py": [ [ "Count Files in Directory (FW)", "Count Tokens (FW)", "Token Count Ranker(FW)", "Trim To Tokens (FW)" ], { "title_aux": "Fearnworks Custom Nodes" } ], "https://github.com/fexli/fexli-util-node-comfyui": [ [ "FEBCPrompt", "FEBatchGenStringBCDocker", "FEColor2Image", "FEColorOut", "FEDataInsertor", "FEDataPacker", "FEDataUnpacker", "FEDeepClone", "FEDictPacker", "FEDictUnpacker", "FEEncLoraLoader", "FEExtraInfoAdd", "FEGenStringBCDocker", "FEGenStringGPT", "FEImageNoiseGenerate", "FEImagePadForOutpaint", "FEImagePadForOutpaintByImage", "FEOperatorIf", "FEPythonStrOp", "FERandomLoraSelect", "FERandomPrompt", "FERandomizedColor2Image", "FERandomizedColorOut", "FERerouteWithName", "FESaveEncryptImage", "FETextCombine", "FETextInput" ], { "title_aux": "fexli-util-node-comfyui" } ], "https://github.com/filipemeneses/comfy_pixelization": [ [ "Pixelization" ], { "title_aux": "Pixelization" } ], "https://github.com/filliptm/ComfyUI_Fill-Nodes": [ [ "FL_ImageCaptionSaver", "FL_ImageRandomizer" ], { "title_aux": "ComfyUI_Fill-Nodes" } ], "https://github.com/fitCorder/fcSuite/raw/main/fcSuite.py": [ [ "fcFloat", "fcFloatMatic", "fcHex", "fcInteger" ], { "title_aux": "fcSuite" } ], "https://github.com/florestefano1975/comfyui-portrait-master": [ [ "PortraitMaster" ], { "title_aux": "comfyui-portrait-master" } ], "https://github.com/florestefano1975/comfyui-prompt-composer": [ [ "PromptComposerCustomLists", "PromptComposerEffect", "PromptComposerGrouping", "PromptComposerMerge", "PromptComposerStyler", "PromptComposerTextSingle", "promptComposerTextMultiple" ], { "title_aux": "comfyui-prompt-composer" } ], "https://github.com/flowtyone/ComfyUI-Flowty-LDSR": [ [ "LDSRModelLoader", "LDSRUpscale", "LDSRUpscaler" ], { "title_aux": "ComfyUI-Flowty-LDSR" } ], "https://github.com/flyingshutter/As_ComfyUI_CustomNodes": [ [ "BatchIndex_AS", "CropImage_AS", "ImageMixMasked_As", "ImageToMask_AS", "Increment_AS", "Int2Any_AS", "LatentAdd_AS", "LatentMixMasked_As", "LatentMix_AS", "LatentToImages_AS", "LoadLatent_AS", "MapRange_AS", "MaskToImage_AS", "Math_AS", "NoiseImage_AS", "Number2Float_AS", "Number2Int_AS", "Number_AS", "SaveLatent_AS", "TextToImage_AS", "TextWildcardList_AS" ], { "title_aux": "As_ComfyUI_CustomNodes" } ], "https://github.com/foxtrot-roger/comfyui-rf-nodes": [ [ "LogBool", "LogFloat", "LogInt", "LogNumber", "LogString", "LogVec2", "LogVec3", "RF_AtIndexString", "RF_BoolToString", "RF_FloatToString", "RF_IntToString", "RF_JsonStyleLoader", "RF_MergeLines", "RF_NumberToString", "RF_OptionsString", "RF_RangeFloat", "RF_RangeInt", "RF_RangeNumber", "RF_SavePromptInfo", "RF_SplitLines", "RF_TextConcatenate", "RF_TextInput", "RF_TextReplace", "RF_Timestamp", "RF_ToString", "RF_Vec2ToString", "RF_Vec3ToString", "TextLine" ], { "title_aux": "RF Nodes" } ], "https://github.com/gemell1/ComfyUI_GMIC": [ [ "GmicCliWrapper" ], { "title_aux": "ComfyUI_GMIC" } ], "https://github.com/giriss/comfy-image-saver": [ [ "Cfg Literal", "Checkpoint Selector", "Int Literal", "Sampler Selector", "Save Image w/Metadata", "Scheduler Selector", "Seed Generator", "String Literal", "Width/Height Literal" ], { "title_aux": "Save Image with Generation Metadata" } ], "https://github.com/glibsonoran/Plush-for-ComfyUI": [ [ "DalleImage", "Enhancer", "ImgTextSwitch", "Plush-Exif Wrangler", "mulTextSwitch" ], { "title_aux": "Plush-for-ComfyUI" } ], "https://github.com/glifxyz/ComfyUI-GlifNodes": [ [ "GlifConsistencyDecoder", "GlifPatchConsistencyDecoderTiled", "SDXLAspectRatio" ], { "title_aux": "ComfyUI-GlifNodes" } ], "https://github.com/glowcone/comfyui-base64-to-image": [ [ "LoadImageFromBase64" ], { "title_aux": "Load Image From Base64 URI" } ], "https://github.com/godspede/ComfyUI_Substring": [ [ "SubstringTheory" ], { "title_aux": "ComfyUI Substring" } ], "https://github.com/gokayfem/ComfyUI_VLM_nodes": [ [ "Joytag", "JsonToText", "KeywordExtraction", "LLMLoader", "LLMPromptGenerator", "LLMSampler", "LLava Loader Simple", "LLavaPromptGenerator", "LLavaSamplerAdvanced", "LLavaSamplerSimple", "LlavaClipLoader", "MoonDream", "PromptGenerateAPI", "SimpleText", "Suggester", "ViewText" ], { "title_aux": "VLM_nodes" } ], "https://github.com/guoyk93/yk-node-suite-comfyui": [ [ "YKImagePadForOutpaint", "YKMaskToImage" ], { "title_aux": "y.k.'s ComfyUI node suite" } ], "https://github.com/hhhzzyang/Comfyui_Lama": [ [ "LamaApply", "LamaModelLoader", "YamlConfigLoader" ], { "title_aux": "Comfyui-Lama" } ], "https://github.com/hinablue/ComfyUI_3dPoseEditor": [ [ "Hina.PoseEditor3D" ], { "title_aux": "ComfyUI 3D Pose Editor" } ], "https://github.com/hustille/ComfyUI_Fooocus_KSampler": [ [ "KSampler With Refiner (Fooocus)" ], { "title_aux": "ComfyUI_Fooocus_KSampler" } ], "https://github.com/hustille/ComfyUI_hus_utils": [ [ "3way Prompt Styler", "Batch State", "Date Time Format", "Debug Extra", "Fetch widget value", "Text Hash" ], { "title_aux": "hus' utils for ComfyUI" } ], "https://github.com/hylarucoder/ComfyUI-Eagle-PNGInfo": [ [ "EagleImageNode", "SDXLPromptStyler", "SDXLPromptStylerAdvanced", "SDXLResolutionPresets" ], { "title_aux": "Eagle PNGInfo" } ], "https://github.com/idrirap/ComfyUI-Lora-Auto-Trigger-Words": [ [ "FusionText", "LoraListNames", "LoraLoaderAdvanced", "LoraLoaderStackedAdvanced", "LoraLoaderStackedVanilla", "LoraLoaderVanilla", "LoraTagsOnly", "Randomizer", "TagsFormater", "TagsSelector", "TextInputBasic" ], { "title_aux": "ComfyUI-Lora-Auto-Trigger-Words" } ], "https://github.com/imb101/ComfyUI-FaceSwap": [ [ "FaceSwapNode" ], { "title_aux": "FaceSwap" } ], "https://github.com/jags111/ComfyUI_Jags_Audiotools": [ [ "BatchJoinAudio", "BatchToList", "BitCrushAudioFX", "BulkVariation", "ChorusAudioFX", "ClippingAudioFX", "CompressorAudioFX", "ConcatAudioList", "ConvolutionAudioFX", "CutAudio", "DelayAudioFX", "DistortionAudioFX", "DuplicateAudio", "GainAudioFX", "GenerateAudioSample", "GenerateAudioWave", "GetAudioFromFolderIndex", "GetSingle", "GetStringByIndex", "HighShelfFilter", "HighpassFilter", "ImageToSpectral", "InvertAudioFX", "JoinAudio", "LadderFilter", "LimiterAudioFX", "ListToBatch", "LoadAudioDir", "LoadAudioFile", "LoadAudioModel (DD)", "LoadVST3", "LowShelfFilter", "LowpassFilter", "MP3CompressorAudioFX", "MixAudioTensors", "NoiseGateAudioFX", "OTTAudioFX", "PeakFilter", "PhaserEffectAudioFX", "PitchShiftAudioFX", "PlotSpectrogram", "PreviewAudioFile", "PreviewAudioTensor", "ResampleAudio", "ReverbAudioFX", "ReverseAudio", "SaveAudioTensor", "SequenceVariation", "SliceAudio", "SoundPlayer", "StretchAudio", "samplerate" ], { "author": "jags111", "description": "This extension offers various audio generation tools", "nickname": "Audiotools", "title": "Jags_Audiotools", "title_aux": "ComfyUI_Jags_Audiotools" } ], "https://github.com/jags111/ComfyUI_Jags_VectorMagic": [ [ "CircularVAEDecode", "JagsCLIPSeg", "JagsClipseg", "JagsCombineMasks", "SVG", "YoloSEGdetectionNode", "YoloSegNode", "color_drop", "my unique name", "xy_Tiling_KSampler" ], { "author": "jags111", "description": "This extension offers various vector manipulation and generation tools", "nickname": "Jags_VectorMagic", "title": "Jags_VectorMagic", "title_aux": "ComfyUI_Jags_VectorMagic" } ], "https://github.com/jags111/efficiency-nodes-comfyui": [ [ "AnimateDiff Script", "Apply ControlNet Stack", "Control Net Stacker", "Eff. Loader SDXL", "Efficient Loader", "HighRes-Fix Script", "Image Overlay", "Join XY Inputs of Same Type", "KSampler (Efficient)", "KSampler Adv. (Efficient)", "KSampler SDXL (Eff.)", "LatentUpscaler", "LoRA Stack to String converter", "LoRA Stacker", "Manual XY Entry Info", "NNLatentUpscale", "Noise Control Script", "Pack SDXL Tuple", "Tiled Upscaler Script", "Unpack SDXL Tuple", "XY Input: Add/Return Noise", "XY Input: Aesthetic Score", "XY Input: CFG Scale", "XY Input: Checkpoint", "XY Input: Clip Skip", "XY Input: Control Net", "XY Input: Control Net Plot", "XY Input: Denoise", "XY Input: LoRA", "XY Input: LoRA Plot", "XY Input: LoRA Stacks", "XY Input: Manual XY Entry", "XY Input: Prompt S/R", "XY Input: Refiner On/Off", "XY Input: Sampler/Scheduler", "XY Input: Seeds++ Batch", "XY Input: Steps", "XY Input: VAE", "XY Plot" ], { "title_aux": "Efficiency Nodes for ComfyUI Version 2.0+" } ], "https://github.com/jamal-alkharrat/ComfyUI_rotate_image": [ [ "RotateImage" ], { "title_aux": "ComfyUI_rotate_image" } ], "https://github.com/jamesWalker55/comfyui-various": [ [], { "nodename_pattern": "^JW", "title_aux": "Various ComfyUI Nodes by Type" } ], "https://github.com/jesenzhang/ComfyUI_StreamDiffusion": [ [ "StreamDiffusion_Loader", "StreamDiffusion_Sampler" ], { "title_aux": "ComfyUI_StreamDiffusion" } ], "https://github.com/jitcoder/lora-info": [ [ "ImageFromURL", "LoraInfo" ], { "title_aux": "LoraInfo" } ], "https://github.com/jjkramhoeft/ComfyUI-Jjk-Nodes": [ [ "JjkConcat", "JjkShowText", "JjkText", "SDXLRecommendedImageSize" ], { "title_aux": "ComfyUI-Jjk-Nodes" } ], "https://github.com/jojkaart/ComfyUI-sampler-lcm-alternative": [ [ "LCMScheduler", "SamplerLCMAlternative", "SamplerLCMCycle" ], { "title_aux": "ComfyUI-sampler-lcm-alternative" } ], "https://github.com/jordoh/ComfyUI-Deepface": [ [ "DeepfaceExtractFaces", "DeepfaceVerify" ], { "title_aux": "ComfyUI Deepface" } ], "https://github.com/jtrue/ComfyUI-JaRue": [ [ "Text2Image_jru", "YouTube2Prompt_jru" ], { "nodename_pattern": "_jru$", "title_aux": "ComfyUI-JaRue" } ], "https://github.com/ka-puna/comfyui-yanc": [ [ "YANC.ConcatStrings", "YANC.FormatDatetimeString", "YANC.GetWidgetValueString", "YANC.IntegerCaster", "YANC.MultilineString", "YANC.TruncateString" ], { "title_aux": "comfyui-yanc" } ], "https://github.com/kadirnar/ComfyUI-Transformers": [ [ "DepthEstimationPipeline", "ImageClassificationPipeline", "ImageSegmentationPipeline", "ObjectDetectionPipeline" ], { "title_aux": "ComfyUI-Transformers" } ], "https://github.com/kenjiqq/qq-nodes-comfyui": [ [ "Any List", "Axis Pack", "Axis Unpack", "Image Accumulator End", "Image Accumulator Start", "Load Lines From Text File", "Slice List", "Text Splitter", "XY Grid Helper" ], { "title_aux": "qq-nodes-comfyui" } ], "https://github.com/kft334/Knodes": [ [ "Image(s) To Websocket (Base64)", "ImageOutput", "Load Image (Base64)", "Load Images (Base64)" ], { "title_aux": "Knodes" } ], "https://github.com/kijai/ComfyUI-CCSR": [ [ "CCSR_Model_Select", "CCSR_Upscale" ], { "title_aux": "ComfyUI-CCSR" } ], "https://github.com/kijai/ComfyUI-DDColor": [ [ "DDColor_Colorize" ], { "title_aux": "ComfyUI-DDColor" } ], "https://github.com/kijai/ComfyUI-DiffusersStableCascade": [ [ "DiffusersStableCascade" ], { "title_aux": "ComfyUI StableCascade using diffusers" } ], "https://github.com/kijai/ComfyUI-KJNodes": [ [ "AddLabel", "BatchCLIPSeg", "BatchCropFromMask", "BatchCropFromMaskAdvanced", "BatchUncrop", "BatchUncropAdvanced", "BboxToInt", "ColorMatch", "ColorToMask", "CondPassThrough", "ConditioningMultiCombine", "ConditioningSetMaskAndCombine", "ConditioningSetMaskAndCombine3", "ConditioningSetMaskAndCombine4", "ConditioningSetMaskAndCombine5", "CreateAudioMask", "CreateFadeMask", "CreateFadeMaskAdvanced", "CreateFluidMask", "CreateGradientMask", "CreateMagicMask", "CreateShapeMask", "CreateTextMask", "CreateVoronoiMask", "CrossFadeImages", "DummyLatentOut", "EmptyLatentImagePresets", "FilterZeroMasksAndCorrespondingImages", "FlipSigmasAdjusted", "FloatConstant", "GLIGENTextBoxApplyBatch", "GenerateNoise", "GetImageRangeFromBatch", "GetImagesFromBatchIndexed", "GetLatentsFromBatchIndexed", "GrowMaskWithBlur", "INTConstant", "ImageBatchRepeatInterleaving", "ImageBatchTestPattern", "ImageConcanate", "ImageGrabPIL", "ImageGridComposite2x2", "ImageGridComposite3x3", "ImageTransformByNormalizedAmplitude", "ImageUpscaleWithModelBatched", "InjectNoiseToLatent", "InsertImageBatchByIndexes", "NormalizeLatent", "NormalizedAmplitudeToMask", "OffsetMask", "OffsetMaskByNormalizedAmplitude", "ReferenceOnlySimple3", "ReplaceImagesInBatch", "ResizeMask", "ReverseImageBatch", "RoundMask", "SaveImageWithAlpha", "ScaleBatchPromptSchedule", "SomethingToString", "SoundReactive", "SplitBboxes", "StableZero123_BatchSchedule", "StringConstant", "VRAM_Debug", "WidgetToString" ], { "title_aux": "KJNodes for ComfyUI" } ], "https://github.com/kijai/ComfyUI-Marigold": [ [ "ColorizeDepthmap", "MarigoldDepthEstimation", "RemapDepth", "SaveImageOpenEXR" ], { "title_aux": "Marigold depth estimation in ComfyUI" } ], "https://github.com/kijai/ComfyUI-SVD": [ [ "SVDimg2vid" ], { "title_aux": "ComfyUI-SVD" } ], "https://github.com/kinfolk0117/ComfyUI_GradientDeepShrink": [ [ "GradientPatchModelAddDownscale", "GradientPatchModelAddDownscaleAdvanced" ], { "title_aux": "ComfyUI_GradientDeepShrink" } ], "https://github.com/kinfolk0117/ComfyUI_Pilgram": [ [ "Pilgram" ], { "title_aux": "ComfyUI_Pilgram" } ], "https://github.com/kinfolk0117/ComfyUI_SimpleTiles": [ [ "DynamicTileMerge", "DynamicTileSplit", "TileCalc", "TileMerge", "TileSplit" ], { "title_aux": "SimpleTiles" } ], "https://github.com/kinfolk0117/ComfyUI_TiledIPAdapter": [ [ "TiledIPAdapter" ], { "title_aux": "TiledIPAdapter" } ], "https://github.com/knuknX/ComfyUI-Image-Tools": [ [ "BatchImagePathLoader", "ImageBgRemoveProcessor", "ImageCheveretoUploader", "ImageStandardResizeProcessor", "JSONMessageNotifyTool", "PreviewJSONNode", "SingleImagePathLoader", "SingleImageUrlLoader" ], { "title_aux": "ComfyUI-Image-Tools" } ], "https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI": [ [ "LLLiteLoader" ], { "title_aux": "ControlNet-LLLite-ComfyUI" } ], "https://github.com/komojini/ComfyUI_SDXL_DreamBooth_LoRA_CustomNodes": [ [ "S3 Bucket LoRA", "S3Bucket_Load_LoRA", "XL DreamBooth LoRA", "XLDB_LoRA" ], { "title_aux": "ComfyUI_SDXL_DreamBooth_LoRA_CustomNodes" } ], "https://github.com/komojini/komojini-comfyui-nodes": [ [ "BatchCreativeInterpolationNodeDynamicSettings", "CachedGetter", "DragNUWAImageCanvas", "FlowBuilder", "FlowBuilder (adv)", "FlowBuilder (advanced)", "FlowBuilder (advanced) Setter", "FlowBuilderSetter", "FlowBuilderSetter (adv)", "Getter", "ImageCropByRatio", "ImageCropByRatioAndResize", "ImageGetter", "ImageMerger", "ImagesCropByRatioAndResizeBatch", "KSamplerAdvancedCacheable", "KSamplerCacheable", "Setter", "UltimateVideoLoader", "UltimateVideoLoader (simple)", "YouTubeVideoLoader" ], { "title_aux": "komojini-comfyui-nodes" } ], "https://github.com/kwaroran/abg-comfyui": [ [ "Remove Image Background (abg)" ], { "title_aux": "abg-comfyui" } ], "https://github.com/laksjdjf/LCMSampler-ComfyUI": [ [ "SamplerLCM", "TAESDLoader" ], { "title_aux": "LCMSampler-ComfyUI" } ], "https://github.com/laksjdjf/LoRA-Merger-ComfyUI": [ [ "LoraLoaderFromWeight", "LoraLoaderWeightOnly", "LoraMerge", "LoraSave" ], { "title_aux": "LoRA-Merger-ComfyUI" } ], "https://github.com/laksjdjf/attention-couple-ComfyUI": [ [ "Attention couple" ], { "title_aux": "attention-couple-ComfyUI" } ], "https://github.com/laksjdjf/cd-tuner_negpip-ComfyUI": [ [ "CDTuner", "Negapip", "Negpip" ], { "title_aux": "cd-tuner_negpip-ComfyUI" } ], "https://github.com/laksjdjf/pfg-ComfyUI": [ [ "PFG" ], { "title_aux": "pfg-ComfyUI" } ], "https://github.com/lilly1987/ComfyUI_node_Lilly": [ [ "CheckpointLoaderSimpleText", "LoraLoaderText", "LoraLoaderTextRandom", "Random_Sampler", "VAELoaderDecode" ], { "title_aux": "simple wildcard for ComfyUI" } ], "https://github.com/lldacing/comfyui-easyapi-nodes": [ [ "Base64ToImage", "Base64ToMask", "ImageToBase64", "ImageToBase64Advanced", "LoadImageFromURL", "LoadImageToBase64", "LoadMaskFromURL", "MaskImageToBase64", "MaskToBase64", "MaskToBase64Image", "SamAutoMaskSEGS" ], { "title_aux": "comfyui-easyapi-nodes" } ], "https://github.com/longgui0318/comfyui-mask-util": [ [ "Mask Region Info", "Mask Selection Of Masks", "Split Masks" ], { "title_aux": "comfyui-mask-util" } ], "https://github.com/lordgasmic/ComfyUI-Wildcards/raw/master/wildcards.py": [ [ "CLIPTextEncodeWithWildcards" ], { "title_aux": "Wildcards" } ], "https://github.com/lrzjason/ComfyUIJasonNode/raw/main/SDXLMixSampler.py": [ [ "SDXLMixSampler" ], { "title_aux": "ComfyUIJasonNode" } ], "https://github.com/ltdrdata/ComfyUI-Impact-Pack": [ [ "AddMask", "BasicPipeToDetailerPipe", "BasicPipeToDetailerPipeSDXL", "BboxDetectorCombined", "BboxDetectorCombined_v2", "BboxDetectorForEach", "BboxDetectorSEGS", "BitwiseAndMask", "BitwiseAndMaskForEach", "CLIPSegDetectorProvider", "CfgScheduleHookProvider", "CombineRegionalPrompts", "CoreMLDetailerHookProvider", "DenoiseScheduleHookProvider", "DenoiseSchedulerDetailerHookProvider", "DetailerForEach", "DetailerForEachDebug", "DetailerForEachDebugPipe", "DetailerForEachPipe", "DetailerForEachPipeForAnimateDiff", "DetailerHookCombine", "DetailerPipeToBasicPipe", "EditBasicPipe", "EditDetailerPipe", "EditDetailerPipeSDXL", "EmptySegs", "FaceDetailer", "FaceDetailerPipe", "FromBasicPipe", "FromBasicPipe_v2", "FromDetailerPipe", "FromDetailerPipeSDXL", "FromDetailerPipe_v2", "ImageListToImageBatch", "ImageMaskSwitch", "ImageReceiver", "ImageSender", "ImpactAssembleSEGS", "ImpactCombineConditionings", "ImpactCompare", "ImpactConcatConditionings", "ImpactConditionalBranch", "ImpactConditionalBranchSelMode", "ImpactConditionalStopIteration", "ImpactControlBridge", "ImpactControlNetApplyAdvancedSEGS", "ImpactControlNetApplySEGS", "ImpactControlNetClearSEGS", "ImpactConvertDataType", "ImpactDecomposeSEGS", "ImpactDilateMask", "ImpactDilateMaskInSEGS", "ImpactDilate_Mask_SEG_ELT", "ImpactDummyInput", "ImpactEdit_SEG_ELT", "ImpactFloat", "ImpactFrom_SEG_ELT", "ImpactGaussianBlurMask", "ImpactGaussianBlurMaskInSEGS", "ImpactHFTransformersClassifierProvider", "ImpactIfNone", "ImpactImageBatchToImageList", "ImpactImageInfo", "ImpactInt", "ImpactInversedSwitch", "ImpactIsNotEmptySEGS", "ImpactKSamplerAdvancedBasicPipe", "ImpactKSamplerBasicPipe", "ImpactLatentInfo", "ImpactLogger", "ImpactLogicalOperators", "ImpactMakeImageBatch", "ImpactMakeImageList", "ImpactMakeTileSEGS", "ImpactMinMax", "ImpactNeg", "ImpactNodeSetMuteState", "ImpactQueueTrigger", "ImpactQueueTriggerCountdown", "ImpactRemoteBoolean", "ImpactRemoteInt", "ImpactSEGSClassify", "ImpactSEGSConcat", "ImpactSEGSLabelFilter", "ImpactSEGSOrderedFilter", "ImpactSEGSPicker", "ImpactSEGSRangeFilter", "ImpactSEGSToMaskBatch", "ImpactSEGSToMaskList", "ImpactScaleBy_BBOX_SEG_ELT", "ImpactSegsAndMask", "ImpactSegsAndMaskForEach", "ImpactSetWidgetValue", "ImpactSimpleDetectorSEGS", "ImpactSimpleDetectorSEGSPipe", "ImpactSimpleDetectorSEGS_for_AD", "ImpactSleep", "ImpactStringSelector", "ImpactSwitch", "ImpactValueReceiver", "ImpactValueSender", "ImpactWildcardEncode", "ImpactWildcardProcessor", "IterativeImageUpscale", "IterativeLatentUpscale", "KSamplerAdvancedProvider", "KSamplerProvider", "LatentPixelScale", "LatentReceiver", "LatentSender", "LatentSwitch", "MMDetDetectorProvider", "MMDetLoader", "MaskDetailerPipe", "MaskListToMaskBatch", "MaskPainter", "MaskToSEGS", "MaskToSEGS_for_AnimateDiff", "MasksToMaskList", "MediaPipeFaceMeshToSEGS", "NoiseInjectionDetailerHookProvider", "NoiseInjectionHookProvider", "ONNXDetectorProvider", "ONNXDetectorSEGS", "PixelKSampleHookCombine", "PixelKSampleUpscalerProvider", "PixelKSampleUpscalerProviderPipe", "PixelTiledKSampleUpscalerProvider", "PixelTiledKSampleUpscalerProviderPipe", "PreviewBridge", "PreviewBridgeLatent", "PreviewDetailerHookProvider", "ReencodeLatent", "ReencodeLatentPipe", "RegionalPrompt", "RegionalSampler", "RegionalSamplerAdvanced", "RemoveImageFromSEGS", "RemoveNoiseMask", "SAMDetectorCombined", "SAMDetectorSegmented", "SAMLoader", "SEGSDetailer", "SEGSDetailerForAnimateDiff", "SEGSLabelFilterDetailerHookProvider", "SEGSOrderedFilterDetailerHookProvider", "SEGSPaste", "SEGSPreview", "SEGSPreviewCNet", "SEGSRangeFilterDetailerHookProvider", "SEGSSwitch", "SEGSToImageList", "SegmDetectorCombined", "SegmDetectorCombined_v2", "SegmDetectorForEach", "SegmDetectorSEGS", "Segs Mask", "Segs Mask ForEach", "SegsMaskCombine", "SegsToCombinedMask", "SetDefaultImageForSEGS", "StepsScheduleHookProvider", "SubtractMask", "SubtractMaskForEach", "TiledKSamplerProvider", "ToBasicPipe", "ToBinaryMask", "ToDetailerPipe", "ToDetailerPipeSDXL", "TwoAdvancedSamplersForMask", "TwoSamplersForMask", "TwoSamplersForMaskUpscalerProvider", "TwoSamplersForMaskUpscalerProviderPipe", "UltralyticsDetectorProvider", "UnsamplerDetailerHookProvider", "UnsamplerHookProvider" ], { "author": "Dr.Lt.Data", "description": "This extension offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler.", "nickname": "Impact Pack", "title": "Impact Pack", "title_aux": "ComfyUI Impact Pack" } ], "https://github.com/ltdrdata/ComfyUI-Inspire-Pack": [ [ "AnimeLineArt_Preprocessor_Provider_for_SEGS //Inspire", "ApplyRegionalIPAdapters //Inspire", "BindImageListPromptList //Inspire", "CLIPTextEncodeWithWeight //Inspire", "CacheBackendData //Inspire", "CacheBackendDataList //Inspire", "CacheBackendDataNumberKey //Inspire", "CacheBackendDataNumberKeyList //Inspire", "Canny_Preprocessor_Provider_for_SEGS //Inspire", "ChangeImageBatchSize //Inspire", "CheckpointLoaderSimpleShared //Inspire", "Color_Preprocessor_Provider_for_SEGS //Inspire", "ConcatConditioningsWithMultiplier //Inspire", "DWPreprocessor_Provider_for_SEGS //Inspire", "FakeScribblePreprocessor_Provider_for_SEGS //Inspire", "FloatRange //Inspire", "FromIPAdapterPipe //Inspire", "GlobalSampler //Inspire", "GlobalSeed //Inspire", "HEDPreprocessor_Provider_for_SEGS //Inspire", "HyperTile //Inspire", "IPAdapterModelHelper //Inspire", "ImageBatchSplitter //Inspire", "InpaintPreprocessor_Provider_for_SEGS //Inspire", "KSampler //Inspire", "KSamplerAdvanced //Inspire", "KSamplerAdvancedPipe //Inspire", "KSamplerAdvancedProgress //Inspire", "KSamplerPipe //Inspire", "KSamplerProgress //Inspire", "LatentBatchSplitter //Inspire", "LeRes_DepthMap_Preprocessor_Provider_for_SEGS //Inspire", "LineArt_Preprocessor_Provider_for_SEGS //Inspire", "ListCounter //Inspire", "LoadImage //Inspire", "LoadImageListFromDir //Inspire", "LoadImagesFromDir //Inspire", "LoadPromptsFromDir //Inspire", "LoadPromptsFromFile //Inspire", "LoadSinglePromptFromFile //Inspire", "LoraBlockInfo //Inspire", "LoraLoaderBlockWeight //Inspire", "MakeBasicPipe //Inspire", "Manga2Anime_LineArt_Preprocessor_Provider_for_SEGS //Inspire", "MediaPipeFaceMeshDetectorProvider //Inspire", "MediaPipe_FaceMesh_Preprocessor_Provider_for_SEGS //Inspire", "MeshGraphormerDepthMapPreprocessorProvider_for_SEGS //Inspire", "MiDaS_DepthMap_Preprocessor_Provider_for_SEGS //Inspire", "OpenPose_Preprocessor_Provider_for_SEGS //Inspire", "PromptBuilder //Inspire", "PromptExtractor //Inspire", "RandomGeneratorForList //Inspire", "RegionalConditioningColorMask //Inspire", "RegionalConditioningSimple //Inspire", "RegionalIPAdapterColorMask //Inspire", "RegionalIPAdapterEncodedColorMask //Inspire", "RegionalIPAdapterEncodedMask //Inspire", "RegionalIPAdapterMask //Inspire", "RegionalPromptColorMask //Inspire", "RegionalPromptSimple //Inspire", "RegionalSeedExplorerColorMask //Inspire", "RegionalSeedExplorerMask //Inspire", "RemoveBackendData //Inspire", "RemoveBackendDataNumberKey //Inspire", "RemoveControlNet //Inspire", "RemoveControlNetFromRegionalPrompts //Inspire", "RetrieveBackendData //Inspire", "RetrieveBackendDataNumberKey //Inspire", "SeedExplorer //Inspire", "ShowCachedInfo //Inspire", "TilePreprocessor_Provider_for_SEGS //Inspire", "ToIPAdapterPipe //Inspire", "UnzipPrompt //Inspire", "WildcardEncode //Inspire", "XY Input: Lora Block Weight //Inspire", "ZipPrompt //Inspire", "Zoe_DepthMap_Preprocessor_Provider_for_SEGS //Inspire" ], { "author": "Dr.Lt.Data", "description": "This extension provides various nodes to support Lora Block Weight and the Impact Pack.", "nickname": "Inspire Pack", "nodename_pattern": "Inspire$", "title": "Inspire Pack", "title_aux": "ComfyUI Inspire Pack" } ], "https://github.com/m-sokes/ComfyUI-Sokes-Nodes": [ [ "Custom Date Format | sokes \ud83e\uddac", "Latent Switch x9 | sokes \ud83e\uddac" ], { "title_aux": "ComfyUI Sokes Nodes" } ], "https://github.com/m957ymj75urz/ComfyUI-Custom-Nodes/raw/main/clip-text-encode-split/clip_text_encode_split.py": [ [ "RawText", "RawTextCombine", "RawTextEncode", "RawTextReplace" ], { "title_aux": "m957ymj75urz/ComfyUI-Custom-Nodes" } ], "https://github.com/mape/ComfyUI-mape-Helpers": [ [ "mape Variable" ], { "author": "mape", "description": "Various QoL improvements like prompt tweaking, variable assignment, image preview, fuzzy search, error reporting, organizing and node navigation.", "nickname": "\ud83d\udfe1 mape's helpers", "title": "mape's helpers", "title_aux": "mape's ComfyUI Helpers" } ], "https://github.com/marhensa/sdxl-recommended-res-calc": [ [ "RecommendedResCalc" ], { "title_aux": "Recommended Resolution Calculator" } ], "https://github.com/martijnat/comfyui-previewlatent": [ [ "PreviewLatent", "PreviewLatentAdvanced", "PreviewLatentXL" ], { "title_aux": "comfyui-previewlatent" } ], "https://github.com/massao000/ComfyUI_aspect_ratios": [ [ "Aspect Ratios Node" ], { "title_aux": "ComfyUI_aspect_ratios" } ], "https://github.com/matan1905/ComfyUI-Serving-Toolkit": [ [ "DiscordServing", "ServingInputNumber", "ServingInputText", "ServingOutput", "WebSocketServing" ], { "title_aux": "ComfyUI Serving toolkit" } ], "https://github.com/mav-rik/facerestore_cf": [ [ "CropFace", "FaceRestoreCFWithModel", "FaceRestoreModelLoader" ], { "title_aux": "Facerestore CF (Code Former)" } ], "https://github.com/mbrostami/ComfyUI-HF": [ [ "GPT2Node" ], { "title_aux": "ComfyUI-HF" } ], "https://github.com/mcmonkeyprojects/sd-dynamic-thresholding": [ [ "DynamicThresholdingFull", "DynamicThresholdingSimple" ], { "title_aux": "Stable Diffusion Dynamic Thresholding (CFG Scale Fix)" } ], "https://github.com/meap158/ComfyUI-Background-Replacement": [ [ "BackgroundReplacement", "ImageComposite" ], { "title_aux": "ComfyUI-Background-Replacement" } ], "https://github.com/meap158/ComfyUI-GPU-temperature-protection": [ [ "GPUTemperatureProtection" ], { "title_aux": "GPU temperature protection" } ], "https://github.com/meap158/ComfyUI-Prompt-Expansion": [ [ "PromptExpansion" ], { "title_aux": "ComfyUI-Prompt-Expansion" } ], "https://github.com/melMass/comfy_mtb": [ [ "Animation Builder (mtb)", "Any To String (mtb)", "Batch Float (mtb)", "Batch Float Assemble (mtb)", "Batch Float Fill (mtb)", "Batch Make (mtb)", "Batch Merge (mtb)", "Batch Shake (mtb)", "Batch Shape (mtb)", "Batch Transform (mtb)", "Bbox (mtb)", "Bbox From Mask (mtb)", "Blur (mtb)", "Color Correct (mtb)", "Colored Image (mtb)", "Concat Images (mtb)", "Crop (mtb)", "Debug (mtb)", "Deep Bump (mtb)", "Export With Ffmpeg (mtb)", "Face Swap (mtb)", "Film Interpolation (mtb)", "Fit Number (mtb)", "Float To Number (mtb)", "Get Batch From History (mtb)", "Image Compare (mtb)", "Image Premultiply (mtb)", "Image Remove Background Rembg (mtb)", "Image Resize Factor (mtb)", "Image Tile Offset (mtb)", "Int To Bool (mtb)", "Int To Number (mtb)", "Interpolate Clip Sequential (mtb)", "Latent Lerp (mtb)", "Load Face Analysis Model (mtb)", "Load Face Enhance Model (mtb)", "Load Face Swap Model (mtb)", "Load Film Model (mtb)", "Load Image From Url (mtb)", "Load Image Sequence (mtb)", "Mask To Image (mtb)", "Math Expression (mtb)", "Model Patch Seamless (mtb)", "Pick From Batch (mtb)", "Qr Code (mtb)", "Restore Face (mtb)", "Save Gif (mtb)", "Save Image Grid (mtb)", "Save Image Sequence (mtb)", "Save Tensors (mtb)", "Sharpen (mtb)", "Smart Step (mtb)", "Stack Images (mtb)", "String Replace (mtb)", "Styles Loader (mtb)", "Text To Image (mtb)", "Transform Image (mtb)", "Uncrop (mtb)", "Unsplash Image (mtb)", "Vae Decode (mtb)" ], { "nodename_pattern": "\\(mtb\\)$", "title_aux": "MTB Nodes" } ], "https://github.com/mihaiiancu/ComfyUI_Inpaint": [ [ "InpaintMediapipe" ], { "title_aux": "mihaiiancu/Inpaint" } ], "https://github.com/mikkel/ComfyUI-text-overlay": [ [ "Image Text Overlay" ], { "title_aux": "ComfyUI - Text Overlay Plugin" } ], "https://github.com/mikkel/comfyui-mask-boundingbox": [ [ "Mask Bounding Box" ], { "title_aux": "ComfyUI - Mask Bounding Box" } ], "https://github.com/mlinmg/ComfyUI-LaMA-Preprocessor": [ [ "LaMaPreprocessor", "lamaPreprocessor" ], { "title_aux": "LaMa Preprocessor [WIP]" } ], "https://github.com/modusCell/ComfyUI-dimension-node-modusCell": [ [ "DimensionProviderFree modusCell", "DimensionProviderRatio modusCell", "String Concat modusCell" ], { "title_aux": "Preset Dimensions" } ], "https://github.com/mpiquero7164/ComfyUI-SaveImgPrompt": [ [ "Save IMG Prompt" ], { "title_aux": "SaveImgPrompt" } ], "https://github.com/nagolinc/ComfyUI_FastVAEDecorder_SDXL": [ [ "FastLatentToImage" ], { "title_aux": "ComfyUI_FastVAEDecorder_SDXL" } ], "https://github.com/natto-maki/ComfyUI-NegiTools": [ [ "NegiTools_CompositeImages", "NegiTools_DepthEstimationByMarigold", "NegiTools_DetectFaceRotationForInpainting", "NegiTools_ImageProperties", "NegiTools_LatentProperties", "NegiTools_NoiseImageGenerator", "NegiTools_OpenAiDalle3", "NegiTools_OpenAiGpt", "NegiTools_OpenAiGpt4v", "NegiTools_OpenAiTranslate", "NegiTools_OpenPoseToPointList", "NegiTools_PointListToMask", "NegiTools_RandomImageLoader", "NegiTools_SaveImageToDirectory", "NegiTools_SeedGenerator", "NegiTools_StereoImageGenerator", "NegiTools_StringFunction" ], { "title_aux": "ComfyUI-NegiTools" } ], "https://github.com/nicolai256/comfyUI_Nodes_nicolai256/raw/main/yugioh-presets.py": [ [ "yugioh_Presets" ], { "title_aux": "comfyUI_Nodes_nicolai256" } ], "https://github.com/ningxiaoxiao/comfyui-NDI": [ [ "NDI_LoadImage", "NDI_SendImage" ], { "title_aux": "comfyui-NDI" } ], "https://github.com/nkchocoai/ComfyUI-PromptUtilities": [ [ "PromptUtilitiesConstString", "PromptUtilitiesConstStringMultiLine", "PromptUtilitiesFormatString", "PromptUtilitiesJoinStringList", "PromptUtilitiesLoadPreset", "PromptUtilitiesLoadPresetAdvanced", "PromptUtilitiesRandomPreset", "PromptUtilitiesRandomPresetAdvanced" ], { "title_aux": "ComfyUI-PromptUtilities" } ], "https://github.com/nkchocoai/ComfyUI-SizeFromPresets": [ [ "EmptyLatentImageFromPresetsSD15", "EmptyLatentImageFromPresetsSDXL", "RandomEmptyLatentImageFromPresetsSD15", "RandomEmptyLatentImageFromPresetsSDXL", "RandomSizeFromPresetsSD15", "RandomSizeFromPresetsSDXL", "SizeFromPresetsSD15", "SizeFromPresetsSDXL" ], { "title_aux": "ComfyUI-SizeFromPresets" } ], "https://github.com/nkchocoai/ComfyUI-TextOnSegs": [ [ "CalcMaxFontSize", "ExtractDominantColor", "GetComplementaryColor", "SegsToRegion", "TextOnSegsFloodFill" ], { "title_aux": "ComfyUI-TextOnSegs" } ], "https://github.com/noembryo/ComfyUI-noEmbryo": [ [ "PromptTermList1", "PromptTermList2", "PromptTermList3", "PromptTermList4", "PromptTermList5", "PromptTermList6" ], { "author": "noEmbryo", "description": "Some useful nodes for ComfyUI", "nickname": "noEmbryo", "title": "noEmbryo nodes for ComfyUI", "title_aux": "noEmbryo nodes" } ], "https://github.com/nosiu/comfyui-instantId-faceswap": [ [ "FaceEmbed", "FaceSwapGenerationInpaint", "FaceSwapSetupPipeline", "LCMLora" ], { "title_aux": "ComfyUI InstantID Faceswapper" } ], "https://github.com/noxinias/ComfyUI_NoxinNodes": [ [ "NoxinChime", "NoxinPromptLoad", "NoxinPromptSave", "NoxinScaledResolution", "NoxinSimpleMath", "NoxinSplitPrompt" ], { "title_aux": "ComfyUI_NoxinNodes" } ], "https://github.com/ntc-ai/ComfyUI-DARE-LoRA-Merge": [ [ "Apply LoRA", "DARE Merge LoRA Stack", "Save LoRA" ], { "title_aux": "ComfyUI - Apply LoRA Stacker with DARE" } ], "https://github.com/ntdviet/comfyui-ext/raw/main/custom_nodes/gcLatentTunnel/gcLatentTunnel.py": [ [ "gcLatentTunnel" ], { "title_aux": "ntdviet/comfyui-ext" } ], "https://github.com/omar92/ComfyUI-QualityOfLifeSuit_Omar92": [ [ "CLIPStringEncode _O", "Chat completion _O", "ChatGPT Simple _O", "ChatGPT _O", "ChatGPT compact _O", "Chat_Completion _O", "Chat_Message _O", "Chat_Message_fromString _O", "Concat Text _O", "ConcatRandomNSP_O", "Debug String _O", "Debug Text _O", "Debug Text route _O", "Edit_image _O", "Equation1param _O", "Equation2params _O", "GetImage_(Width&Height) _O", "GetLatent_(Width&Height) _O", "ImageScaleFactor _O", "ImageScaleFactorSimple _O", "LatentUpscaleFactor _O", "LatentUpscaleFactorSimple _O", "LatentUpscaleMultiply", "Note _O", "RandomNSP _O", "Replace Text _O", "String _O", "Text _O", "Text2Image _O", "Trim Text _O", "VAEDecodeParallel _O", "combine_chat_messages _O", "compine_chat_messages _O", "concat Strings _O", "create image _O", "create_image _O", "debug Completeion _O", "debug messages_O", "float _O", "floatToInt _O", "floatToText _O", "int _O", "intToFloat _O", "load_openAI _O", "replace String _O", "replace String advanced _O", "saveTextToFile _O", "seed _O", "selectLatentFromBatch _O", "string2Image _O", "trim String _O", "variation_image _O" ], { "title_aux": "Quality of life Suit:V2" } ], "https://github.com/ostris/ostris_nodes_comfyui": [ [ "LLM Pipe Loader - Ostris", "LLM Prompt Upsampling - Ostris", "One Seed - Ostris", "Text Box - Ostris" ], { "nodename_pattern": "- Ostris$", "title_aux": "Ostris Nodes ComfyUI" } ], "https://github.com/ownimage/ComfyUI-ownimage": [ [ "Caching Image Loader" ], { "title_aux": "ComfyUI-ownimage" } ], "https://github.com/oyvindg/ComfyUI-TrollSuite": [ [ "BinaryImageMask", "ImagePadding", "LoadLastImage", "RandomMask", "TransparentImage" ], { "title_aux": "ComfyUI-TrollSuite" } ], "https://github.com/palant/extended-saveimage-comfyui": [ [ "SaveImageExtended" ], { "title_aux": "Extended Save Image for ComfyUI" } ], "https://github.com/palant/image-resize-comfyui": [ [ "ImageResize" ], { "title_aux": "Image Resize for ComfyUI" } ], "https://github.com/pants007/comfy-pants": [ [ "CLIPTextEncodeAIO", "Image Make Square" ], { "title_aux": "pants" } ], "https://github.com/paulo-coronado/comfy_clip_blip_node": [ [ "CLIPTextEncodeBLIP", "CLIPTextEncodeBLIP-2", "Example" ], { "title_aux": "comfy_clip_blip_node" } ], "https://github.com/picturesonpictures/comfy_PoP": [ [ "AdaptiveCannyDetector_PoP", "AnyAspectRatio", "ConditioningMultiplier_PoP", "ConditioningNormalizer_PoP", "DallE3_PoP", "LoadImageResizer_PoP", "LoraStackLoader10_PoP", "LoraStackLoader_PoP", "VAEDecoderPoP", "VAEEncoderPoP" ], { "title_aux": "comfy_PoP" } ], "https://github.com/pkpkTech/ComfyUI-SaveAVIF": [ [ "SaveAvif" ], { "title_aux": "ComfyUI-SaveAVIF" } ], "https://github.com/pkpkTech/ComfyUI-TemporaryLoader": [ [ "LoadTempCheckpoint", "LoadTempLoRA", "LoadTempMultiLoRA" ], { "title_aux": "ComfyUI-TemporaryLoader" } ], "https://github.com/pythongosssss/ComfyUI-Custom-Scripts": [ [ "CheckpointLoader|pysssss", "ConstrainImageforVideo|pysssss", "ConstrainImage|pysssss", "LoadText|pysssss", "LoraLoader|pysssss", "MathExpression|pysssss", "MultiPrimitive|pysssss", "PlaySound|pysssss", "Repeater|pysssss", "ReroutePrimitive|pysssss", "SaveText|pysssss", "ShowText|pysssss", "StringFunction|pysssss" ], { "title_aux": "pythongosssss/ComfyUI-Custom-Scripts" } ], "https://github.com/pythongosssss/ComfyUI-WD14-Tagger": [ [ "WD14Tagger|pysssss" ], { "title_aux": "ComfyUI WD 1.4 Tagger" } ], "https://github.com/ramyma/A8R8_ComfyUI_nodes": [ [ "Base64ImageInput", "Base64ImageOutput" ], { "title_aux": "A8R8 ComfyUI Nodes" } ], "https://github.com/rcfcu2000/zhihuige-nodes-comfyui": [ [ "Combine ZHGMasks", "Cover ZHGMasks", "From ZHG pip", "GroundingDinoModelLoader (zhihuige)", "GroundingDinoPIPESegment (zhihuige)", "GroundingDinoSAMSegment (zhihuige)", "InvertMask (zhihuige)", "SAMModelLoader (zhihuige)", "To ZHG pip", "ZHG FaceIndex", "ZHG GetMaskArea", "ZHG Image Levels", "ZHG SaveImage", "ZHG SmoothEdge", "ZHG UltimateSDUpscale" ], { "title_aux": "zhihuige-nodes-comfyui" } ], "https://github.com/rcsaquino/comfyui-custom-nodes": [ [ "BackgroundRemover | rcsaquino", "VAELoader | rcsaquino", "VAEProcessor | rcsaquino" ], { "title_aux": "rcsaquino/comfyui-custom-nodes" } ], "https://github.com/receyuki/comfyui-prompt-reader-node": [ [ "SDBatchLoader", "SDLoraLoader", "SDLoraSelector", "SDParameterExtractor", "SDParameterGenerator", "SDPromptMerger", "SDPromptReader", "SDPromptSaver", "SDTypeConverter" ], { "author": "receyuki", "description": "ComfyUI node version of the SD Prompt Reader", "nickname": "SD Prompt Reader", "title": "SD Prompt Reader", "title_aux": "comfyui-prompt-reader-node" } ], "https://github.com/redhottensors/ComfyUI-Prediction": [ [ "SamplerCustomPrediction" ], { "title_aux": "ComfyUI-Prediction" } ], "https://github.com/rgthree/rgthree-comfy": [ [], { "author": "rgthree", "description": "A bunch of nodes I created that I also find useful.", "nickname": "rgthree", "nodename_pattern": " \\(rgthree\\)$", "title": "Comfy Nodes", "title_aux": "rgthree's ComfyUI Nodes" } ], "https://github.com/richinsley/Comfy-LFO": [ [ "LFO_Pulse", "LFO_Sawtooth", "LFO_Sine", "LFO_Square", "LFO_Triangle" ], { "title_aux": "Comfy-LFO" } ], "https://github.com/ricklove/comfyui-ricklove": [ [ "RL_Crop_Resize", "RL_Crop_Resize_Batch", "RL_Depth16", "RL_Finetune_Analyze", "RL_Finetune_Analyze_Batch", "RL_Finetune_Variable", "RL_Image_Shadow", "RL_Image_Threshold_Channels", "RL_Internet_Search", "RL_LoadImageSequence", "RL_Optical_Flow_Dip", "RL_SaveImageSequence", "RL_Uncrop", "RL_Warp_Image", "RL_Zoe_Depth_Map_Preprocessor", "RL_Zoe_Depth_Map_Preprocessor_Raw_Infer", "RL_Zoe_Depth_Map_Preprocessor_Raw_Process" ], { "title_aux": "comfyui-ricklove" } ], "https://github.com/rklaffehn/rk-comfy-nodes": [ [ "RK_CivitAIAddHashes", "RK_CivitAIMetaChecker" ], { "title_aux": "rk-comfy-nodes" } ], "https://github.com/romeobuilderotti/ComfyUI-PNG-Metadata": [ [ "SetMetadataAll", "SetMetadataString" ], { "title_aux": "ComfyUI PNG Metadata" } ], "https://github.com/rui40000/RUI-Nodes": [ [ "ABCondition", "CharacterCount" ], { "title_aux": "RUI-Nodes" } ], "https://github.com/s1dlx/comfy_meh/raw/main/meh.py": [ [ "MergingExecutionHelper" ], { "title_aux": "comfy_meh" } ], "https://github.com/seanlynch/comfyui-optical-flow": [ [ "Apply optical flow", "Compute optical flow", "Visualize optical flow" ], { "title_aux": "ComfyUI Optical Flow" } ], "https://github.com/seanlynch/srl-nodes": [ [ "SRL Conditional Interrrupt", "SRL Eval", "SRL Filter Image List", "SRL Format String" ], { "title_aux": "SRL's nodes" } ], "https://github.com/sergekatzmann/ComfyUI_Nimbus-Pack": [ [ "ImageResizeAndCropNode", "ImageSquareAdapterNode" ], { "title_aux": "ComfyUI_Nimbus-Pack" } ], "https://github.com/shadowcz007/comfyui-consistency-decoder": [ [ "VAEDecodeConsistencyDecoder", "VAELoaderConsistencyDecoder" ], { "title_aux": "Consistency Decoder" } ], "https://github.com/shadowcz007/comfyui-mixlab-nodes": [ [ "3DImage", "AppInfo", "AreaToMask", "CenterImage", "CharacterInText", "ChatGPTOpenAI", "CkptNames_", "Color", "DynamicDelayProcessor", "EmbeddingPrompt", "EnhanceImage", "FaceToMask", "FeatheredMask", "FloatSlider", "FloatingVideo", "Font", "GamePal", "GetImageSize_", "GradientImage", "GridOutput", "ImageColorTransfer", "ImageCropByAlpha", "IntNumber", "JoinWithDelimiter", "LaMaInpainting", "LimitNumber", "LoadImagesFromPath", "LoadImagesFromURL", "LoraNames_", "MergeLayers", "MirroredImage", "MultiplicationNode", "NewLayer", "NoiseImage", "OutlineMask", "PromptImage", "PromptSimplification", "PromptSlide", "RandomPrompt", "ResizeImageMixlab", "SamplerNames_", "SaveImageToLocal", "ScreenShare", "Seed_", "ShowLayer", "ShowTextForGPT", "SmoothMask", "SpeechRecognition", "SpeechSynthesis", "SplitImage", "SplitLongMask", "SvgImage", "SwitchByIndex", "TESTNODE_", "TESTNODE_TOKEN", "TextImage", "TextInput_", "TextToNumber", "TransparentImage", "VAEDecodeConsistencyDecoder", "VAELoaderConsistencyDecoder" ], { "title_aux": "comfyui-mixlab-nodes" } ], "https://github.com/shadowcz007/comfyui-ultralytics-yolo": [ [ "DetectByLabel" ], { "title_aux": "comfyui-ultralytics-yolo" } ], "https://github.com/shiimizu/ComfyUI-PhotoMaker-Plus": [ [ "PhotoMakerEncodePlus", "PhotoMakerStyles", "PrepImagesForClipVisionFromPath" ], { "title_aux": "ComfyUI PhotoMaker Plus" } ], "https://github.com/shiimizu/ComfyUI-TiledDiffusion": [ [ "NoiseInversion", "TiledDiffusion", "VAEDecodeTiled_TiledDiffusion", "VAEEncodeTiled_TiledDiffusion" ], { "title_aux": "Tiled Diffusion & VAE for ComfyUI" } ], "https://github.com/shiimizu/ComfyUI_smZNodes": [ [ "smZ CLIPTextEncode", "smZ Settings" ], { "title_aux": "smZNodes" } ], "https://github.com/shingo1228/ComfyUI-SDXL-EmptyLatentImage": [ [ "SDXL Empty Latent Image" ], { "title_aux": "ComfyUI-SDXL-EmptyLatentImage" } ], "https://github.com/shingo1228/ComfyUI-send-eagle-slim": [ [ "Send Eagle with text", "Send Webp Image to Eagle" ], { "title_aux": "ComfyUI-send-Eagle(slim)" } ], "https://github.com/shockz0rz/ComfyUI_InterpolateEverything": [ [ "OpenposePreprocessorInterpolate" ], { "title_aux": "InterpolateEverything" } ], "https://github.com/shockz0rz/comfy-easy-grids": [ [ "FloatToText", "GridFloatList", "GridFloats", "GridIntList", "GridInts", "GridLoras", "GridStringList", "GridStrings", "ImageGridCommander", "IntToText", "SaveImageGrid", "TextConcatenator" ], { "title_aux": "comfy-easy-grids" } ], "https://github.com/siliconflow/onediff_comfy_nodes": [ [ "CompareModel", "ControlNetGraphLoader", "ControlNetGraphSaver", "ControlNetSpeedup", "ModelGraphLoader", "ModelGraphSaver", "ModelSpeedup", "ModuleDeepCacheSpeedup", "OneDiffCheckpointLoaderSimple", "SVDSpeedup", "ShowImageDiff", "VaeGraphLoader", "VaeGraphSaver", "VaeSpeedup" ], { "title_aux": "OneDiff Nodes" } ], "https://github.com/sipherxyz/comfyui-art-venture": [ [ "AV_CheckpointMerge", "AV_CheckpointModelsToParametersPipe", "AV_CheckpointSave", "AV_ControlNetEfficientLoader", "AV_ControlNetEfficientLoaderAdvanced", "AV_ControlNetEfficientStacker", "AV_ControlNetEfficientStackerSimple", "AV_ControlNetLoader", "AV_ControlNetPreprocessor", "AV_LoraListLoader", "AV_LoraListStacker", "AV_LoraLoader", "AV_ParametersPipeToCheckpointModels", "AV_ParametersPipeToPrompts", "AV_PromptsToParametersPipe", "AV_SAMLoader", "AV_VAELoader", "AspectRatioSelector", "BLIPCaption", "BLIPLoader", "BooleanPrimitive", "ColorBlend", "ColorCorrect", "DeepDanbooruCaption", "DependenciesEdit", "Fooocus_KSampler", "Fooocus_KSamplerAdvanced", "GetBoolFromJson", "GetFloatFromJson", "GetIntFromJson", "GetObjectFromJson", "GetSAMEmbedding", "GetTextFromJson", "ISNetLoader", "ISNetSegment", "ImageAlphaComposite", "ImageApplyChannel", "ImageExtractChannel", "ImageGaussianBlur", "ImageMuxer", "ImageRepeat", "ImageScaleDown", "ImageScaleDownBy", "ImageScaleDownToSize", "ImageScaleToMegapixels", "LaMaInpaint", "LoadImageAsMaskFromUrl", "LoadImageFromUrl", "LoadJsonFromUrl", "MergeModels", "NumberScaler", "OverlayInpaintedImage", "OverlayInpaintedLatent", "PrepareImageAndMaskForInpaint", "QRCodeGenerator", "RandomFloat", "RandomInt", "SAMEmbeddingToImage", "SDXLAspectRatioSelector", "SDXLPromptStyler", "SeedSelector", "StringToInt", "StringToNumber" ], { "title_aux": "comfyui-art-venture" } ], "https://github.com/skfoo/ComfyUI-Coziness": [ [ "LoraTextExtractor-b1f83aa2", "MultiLoraLoader-70bf3d77" ], { "title_aux": "ComfyUI-Coziness" } ], "https://github.com/smagnetize/kb-comfyui-nodes": [ [ "SingleImageDataUrlLoader" ], { "title_aux": "kb-comfyui-nodes" } ], "https://github.com/space-nuko/ComfyUI-Disco-Diffusion": [ [ "DiscoDiffusion_DiscoDiffusion", "DiscoDiffusion_DiscoDiffusionExtraSettings", "DiscoDiffusion_GuidedDiffusionLoader", "DiscoDiffusion_OpenAICLIPLoader" ], { "title_aux": "Disco Diffusion" } ], "https://github.com/space-nuko/ComfyUI-OpenPose-Editor": [ [ "Nui.OpenPoseEditor" ], { "title_aux": "OpenPose Editor" } ], "https://github.com/space-nuko/nui-suite": [ [ "Nui.DynamicPromptsTextGen", "Nui.FeelingLuckyTextGen", "Nui.OutputString" ], { "title_aux": "nui suite" } ], "https://github.com/spacepxl/ComfyUI-HQ-Image-Save": [ [ "LoadEXR", "LoadLatentEXR", "SaveEXR", "SaveLatentEXR", "SaveTiff" ], { "title_aux": "ComfyUI-HQ-Image-Save" } ], "https://github.com/spacepxl/ComfyUI-Image-Filters": [ [ "AdainImage", "AdainLatent", "AlphaClean", "AlphaMatte", "BatchAverageImage", "BatchAverageUnJittered", "BatchNormalizeImage", "BatchNormalizeLatent", "BlurImageFast", "BlurMaskFast", "ClampOutliers", "ConvertNormals", "DifferenceChecker", "DilateErodeMask", "EnhanceDetail", "ExposureAdjust", "GuidedFilterAlpha", "ImageConstant", "ImageConstantHSV", "JitterImage", "Keyer", "LatentStats", "NormalMapSimple", "OffsetLatentImage", "RemapRange", "Tonemap", "UnJitterImage", "UnTonemap" ], { "title_aux": "ComfyUI-Image-Filters" } ], "https://github.com/spacepxl/ComfyUI-RAVE": [ [ "ConditioningDebug", "ImageGridCompose", "ImageGridDecompose", "KSamplerRAVE", "LatentGridCompose", "LatentGridDecompose" ], { "title_aux": "ComfyUI-RAVE" } ], "https://github.com/spinagon/ComfyUI-seam-carving": [ [ "SeamCarving" ], { "title_aux": "ComfyUI-seam-carving" } ], "https://github.com/spinagon/ComfyUI-seamless-tiling": [ [ "CircularVAEDecode", "MakeCircularVAE", "OffsetImage", "SeamlessTile" ], { "title_aux": "Seamless tiling Node for ComfyUI" } ], "https://github.com/spro/comfyui-mirror": [ [ "LatentMirror" ], { "title_aux": "Latent Mirror node for ComfyUI" } ], "https://github.com/ssitu/ComfyUI_UltimateSDUpscale": [ [ "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale" ], { "title_aux": "UltimateSDUpscale" } ], "https://github.com/ssitu/ComfyUI_fabric": [ [ "FABRICPatchModel", "FABRICPatchModelAdv", "KSamplerAdvFABRICAdv", "KSamplerFABRIC", "KSamplerFABRICAdv" ], { "title_aux": "ComfyUI fabric" } ], "https://github.com/ssitu/ComfyUI_restart_sampling": [ [ "KRestartSampler", "KRestartSamplerAdv", "KRestartSamplerSimple" ], { "title_aux": "Restart Sampling" } ], "https://github.com/ssitu/ComfyUI_roop": [ [ "RoopImproved", "roop" ], { "title_aux": "ComfyUI roop" } ], "https://github.com/storyicon/comfyui_segment_anything": [ [ "GroundingDinoModelLoader (segment anything)", "GroundingDinoSAMSegment (segment anything)", "InvertMask (segment anything)", "IsMaskEmpty", "SAMModelLoader (segment anything)" ], { "title_aux": "segment anything" } ], "https://github.com/strimmlarn/ComfyUI_Strimmlarns_aesthetic_score": [ [ "AesthetlcScoreSorter", "CalculateAestheticScore", "LoadAesteticModel", "ScoreToNumber" ], { "title_aux": "ComfyUI_Strimmlarns_aesthetic_score" } ], "https://github.com/styler00dollar/ComfyUI-deepcache": [ [ "DeepCache" ], { "title_aux": "ComfyUI-deepcache" } ], "https://github.com/styler00dollar/ComfyUI-sudo-latent-upscale": [ [ "SudoLatentUpscale" ], { "title_aux": "ComfyUI-sudo-latent-upscale" } ], "https://github.com/syllebra/bilbox-comfyui": [ [ "BilboXLut", "BilboXPhotoPrompt", "BilboXVignette" ], { "title_aux": "BilboX's ComfyUI Custom Nodes" } ], "https://github.com/sylym/comfy_vid2vid": [ [ "CheckpointLoaderSimpleSequence", "DdimInversionSequence", "KSamplerSequence", "LoadImageMaskSequence", "LoadImageSequence", "LoraLoaderSequence", "SetLatentNoiseSequence", "TrainUnetSequence", "VAEEncodeForInpaintSequence" ], { "title_aux": "Vid2vid" } ], "https://github.com/szhublox/ambw_comfyui": [ [ "Auto Merge Block Weighted", "CLIPMergeSimple", "CheckpointSave", "ModelMergeBlocks", "ModelMergeSimple" ], { "title_aux": "Auto-MBW" } ], "https://github.com/taabata/Comfy_Syrian_Falcon_Nodes/raw/main/SyrianFalconNodes.py": [ [ "CompositeImage", "KSamplerAlternate", "KSamplerPromptEdit", "KSamplerPromptEditAndAlternate", "LoopBack", "QRGenerate", "WordAsImage" ], { "title_aux": "Syrian Falcon Nodes" } ], "https://github.com/taabata/LCM_Inpaint-Outpaint_Comfy": [ [ "ComfyNodesToSaveCanvas", "FloatNumber", "FreeU_LCM", "ImageOutputToComfyNodes", "ImageShuffle", "ImageSwitch", "LCMGenerate", "LCMGenerate_ReferenceOnly", "LCMGenerate_SDTurbo", "LCMGenerate_img2img", "LCMGenerate_img2img_IPAdapter", "LCMGenerate_img2img_controlnet", "LCMGenerate_inpaintv2", "LCMGenerate_inpaintv3", "LCMLoader", "LCMLoader_RefInpaint", "LCMLoader_ReferenceOnly", "LCMLoader_SDTurbo", "LCMLoader_controlnet", "LCMLoader_controlnet_inpaint", "LCMLoader_img2img", "LCMLoraLoader_inpaint", "LCMLoraLoader_ipadapter", "LCMLora_inpaint", "LCMLora_ipadapter", "LCMT2IAdapter", "LCM_IPAdapter", "LCM_IPAdapter_inpaint", "LCM_outpaint_prep", "LoadImageNode_LCM", "Loader_SegmindVega", "OutpaintCanvasTool", "SaveImage_Canvas", "SaveImage_LCM", "SaveImage_Puzzle", "SaveImage_PuzzleV2", "SegmindVega", "SettingsSwitch", "stitch" ], { "title_aux": "LCM_Inpaint-Outpaint_Comfy" } ], "https://github.com/talesofai/comfyui-browser": [ [ "DifyTextGenerator //Browser", "LoadImageByUrl //Browser", "SelectInputs //Browser", "UploadToRemote //Browser", "XyzPlot //Browser" ], { "title_aux": "ComfyUI Browser" } ], "https://github.com/theUpsider/ComfyUI-Logic": [ [ "Bool", "Compare", "DebugPrint", "Float", "If ANY execute A else B", "Int", "String" ], { "title_aux": "ComfyUI-Logic" } ], "https://github.com/theUpsider/ComfyUI-Styles_CSV_Loader": [ [ "Load Styles CSV" ], { "title_aux": "Styles CSV Loader Extension for ComfyUI" } ], "https://github.com/thecooltechguy/ComfyUI-MagicAnimate": [ [ "MagicAnimate", "MagicAnimateModelLoader" ], { "title_aux": "ComfyUI-MagicAnimate" } ], "https://github.com/thecooltechguy/ComfyUI-Stable-Video-Diffusion": [ [ "SVDDecoder", "SVDModelLoader", "SVDSampler", "SVDSimpleImg2Vid" ], { "title_aux": "ComfyUI Stable Video Diffusion" } ], "https://github.com/thedyze/save-image-extended-comfyui": [ [ "SaveImageExtended" ], { "title_aux": "Save Image Extended for ComfyUI" } ], "https://github.com/tocubed/ComfyUI-AudioReactor": [ [ "AudioFrameTransformBeats", "AudioFrameTransformShadertoy", "AudioLoadPath", "Shadertoy" ], { "title_aux": "ComfyUI-AudioReactor" } ], "https://github.com/toyxyz/ComfyUI_toyxyz_test_nodes": [ [ "CaptureWebcam", "LatentDelay", "LoadWebcamImage", "SaveImagetoPath" ], { "title_aux": "ComfyUI_toyxyz_test_nodes" } ], "https://github.com/trojblue/trNodes": [ [ "JpgConvertNode", "trColorCorrection", "trLayering", "trRouter", "trRouterLonger" ], { "title_aux": "trNodes" } ], "https://github.com/trumanwong/ComfyUI-NSFW-Detection": [ [ "NSFWDetection" ], { "title_aux": "ComfyUI-NSFW-Detection" } ], "https://github.com/ttulttul/ComfyUI-Iterative-Mixer": [ [ "Batch Unsampler", "Iterative Mixing KSampler", "Iterative Mixing KSampler Advanced", "IterativeMixingSampler", "IterativeMixingScheduler", "IterativeMixingSchedulerAdvanced", "Latent Batch Comparison Plot", "Latent Batch Statistics Plot", "MixingMaskGenerator" ], { "title_aux": "ComfyUI Iterative Mixing Nodes" } ], "https://github.com/ttulttul/ComfyUI-Tensor-Operations": [ [ "Image Match Normalize", "Latent Match Normalize" ], { "title_aux": "ComfyUI-Tensor-Operations" } ], "https://github.com/tudal/Hakkun-ComfyUI-nodes/raw/main/hakkun_nodes.py": [ [ "Any Converter", "Calculate Upscale", "Image Resize To Height", "Image Resize To Width", "Image size to string", "Load Random Image", "Load Text", "Multi Text Merge", "Prompt Parser", "Random Line", "Random Line 4" ], { "title_aux": "Hakkun-ComfyUI-nodes" } ], "https://github.com/tusharbhutt/Endless-Nodes": [ [ "ESS Aesthetic Scoring", "ESS Aesthetic Scoring Auto", "ESS Combo Parameterizer", "ESS Combo Parameterizer & Prompts", "ESS Eight Input Random", "ESS Eight Input Text Switch", "ESS Float to Integer", "ESS Float to Number", "ESS Float to String", "ESS Float to X", "ESS Global Envoy", "ESS Image Reward", "ESS Image Reward Auto", "ESS Image Saver with JSON", "ESS Integer to Float", "ESS Integer to Number", "ESS Integer to String", "ESS Integer to X", "ESS Number to Float", "ESS Number to Integer", "ESS Number to String", "ESS Number to X", "ESS Parameterizer", "ESS Parameterizer & Prompts", "ESS Six Float Output", "ESS Six Input Random", "ESS Six Input Text Switch", "ESS Six Integer IO Switch", "ESS Six Integer IO Widget", "ESS String to Float", "ESS String to Integer", "ESS String to Num", "ESS String to X", "\u267e\ufe0f\ud83c\udf0a\u2728 Image Saver with JSON" ], { "author": "BiffMunky", "description": "A small set of nodes I created for various numerical and text inputs. Features image saver with ability to have JSON saved to separate folder, parameter collection nodes, two aesthetic scoring models, switches for text and numbers, and conversion of string to numeric and vice versa.", "nickname": "\u267e\ufe0f\ud83c\udf0a\u2728", "title": "Endless \ufe0f\ud83c\udf0a\u2728 Nodes", "title_aux": "Endless \ufe0f\ud83c\udf0a\u2728 Nodes" } ], "https://github.com/twri/sdxl_prompt_styler": [ [ "SDXLPromptStyler", "SDXLPromptStylerAdvanced" ], { "title_aux": "SDXL Prompt Styler" } ], "https://github.com/uarefans/ComfyUI-Fans": [ [ "Fans Prompt Styler Negative", "Fans Prompt Styler Positive", "Fans Styler", "Fans Text Concatenate" ], { "title_aux": "ComfyUI-Fans" } ], "https://github.com/vanillacode314/SimpleWildcardsComfyUI": [ [ "SimpleConcat", "SimpleWildcard" ], { "author": "VanillaCode314", "description": "A simple wildcard node for ComfyUI. Can also be used a style prompt node.", "nickname": "Simple Wildcard", "title": "Simple Wildcard", "title_aux": "Simple Wildcard" } ], "https://github.com/vienteck/ComfyUI-Chat-GPT-Integration": [ [ "ChatGptPrompt" ], { "title_aux": "ComfyUI-Chat-GPT-Integration" } ], "https://github.com/violet-chen/comfyui-psd2png": [ [ "Psd2Png" ], { "title_aux": "comfyui-psd2png" } ], "https://github.com/wallish77/wlsh_nodes": [ [ "Alternating KSampler (WLSH)", "Build Filename String (WLSH)", "CLIP +/- w/Text Unified (WLSH)", "CLIP Positive-Negative (WLSH)", "CLIP Positive-Negative XL (WLSH)", "CLIP Positive-Negative XL w/Text (WLSH)", "CLIP Positive-Negative w/Text (WLSH)", "Checkpoint Loader w/Name (WLSH)", "Empty Latent by Pixels (WLSH)", "Empty Latent by Ratio (WLSH)", "Empty Latent by Size (WLSH)", "Generate Border Mask (WLSH)", "Grayscale Image (WLSH)", "Image Load with Metadata (WLSH)", "Image Save with Prompt (WLSH)", "Image Save with Prompt File (WLSH)", "Image Save with Prompt/Info (WLSH)", "Image Save with Prompt/Info File (WLSH)", "Image Scale By Factor (WLSH)", "Image Scale by Shortside (WLSH)", "KSamplerAdvanced (WLSH)", "Multiply Integer (WLSH)", "Outpaint to Image (WLSH)", "Prompt Weight (WLSH)", "Quick Resolution Multiply (WLSH)", "Resolutions by Ratio (WLSH)", "SDXL Quick Empty Latent (WLSH)", "SDXL Quick Image Scale (WLSH)", "SDXL Resolutions (WLSH)", "SDXL Steps (WLSH)", "Save Positive Prompt(WLSH)", "Save Prompt (WLSH)", "Save Prompt/Info (WLSH)", "Seed and Int (WLSH)", "Seed to Number (WLSH)", "Simple Pattern Replace (WLSH)", "Simple String Combine (WLSH)", "Time String (WLSH)", "Upscale by Factor with Model (WLSH)", "VAE Encode for Inpaint w/Padding (WLSH)" ], { "title_aux": "wlsh_nodes" } ], "https://github.com/whatbirdisthat/cyberdolphin": [ [ "\ud83d\udc2c Gradio ChatInterface", "\ud83d\udc2c OpenAI Advanced", "\ud83d\udc2c OpenAI Compatible", "\ud83d\udc2c OpenAI DALL\u00b7E", "\ud83d\udc2c OpenAI Simple" ], { "title_aux": "cyberdolphin" } ], "https://github.com/whmc76/ComfyUI-Openpose-Editor-Plus": [ [ "CDL.OpenPoseEditorPlus" ], { "title_aux": "ComfyUI-Openpose-Editor-Plus" } ], "https://github.com/wmatson/easy-comfy-nodes": [ [ "EZAssocDictNode", "EZAssocImgNode", "EZAssocStrNode", "EZEmptyDictNode", "EZHttpPostNode", "EZLoadImgBatchFromUrlsNode", "EZLoadImgFromUrlNode", "EZRemoveImgBackground", "EZS3Uploader", "EZVideoCombiner" ], { "title_aux": "easy-comfy-nodes" } ], "https://github.com/wolfden/ComfyUi_PromptStylers": [ [ "SDXLPromptStylerAll", "SDXLPromptStylerHorror", "SDXLPromptStylerMisc", "SDXLPromptStylerbyArtist", "SDXLPromptStylerbyCamera", "SDXLPromptStylerbyComposition", "SDXLPromptStylerbyCyberpunkSurrealism", "SDXLPromptStylerbyDepth", "SDXLPromptStylerbyEnvironment", "SDXLPromptStylerbyFantasySetting", "SDXLPromptStylerbyFilter", "SDXLPromptStylerbyFocus", "SDXLPromptStylerbyImpressionism", "SDXLPromptStylerbyLighting", "SDXLPromptStylerbyMileHigh", "SDXLPromptStylerbyMood", "SDXLPromptStylerbyMythicalCreature", "SDXLPromptStylerbyOriginal", "SDXLPromptStylerbyQuantumRealism", "SDXLPromptStylerbySteamPunkRealism", "SDXLPromptStylerbySubject", "SDXLPromptStylerbySurrealism", "SDXLPromptStylerbyTheme", "SDXLPromptStylerbyTimeofDay", "SDXLPromptStylerbyWyvern", "SDXLPromptbyCelticArt", "SDXLPromptbyContemporaryNordicArt", "SDXLPromptbyFashionArt", "SDXLPromptbyGothicRevival", "SDXLPromptbyIrishFolkArt", "SDXLPromptbyRomanticNationalismArt", "SDXLPromptbySportsArt", "SDXLPromptbyStreetArt", "SDXLPromptbyVikingArt", "SDXLPromptbyWildlifeArt" ], { "title_aux": "SDXL Prompt Styler (customized version by wolfden)" } ], "https://github.com/wolfden/ComfyUi_String_Function_Tree": [ [ "StringFunction" ], { "title_aux": "ComfyUi_String_Function_Tree" } ], "https://github.com/wsippel/comfyui_ws/raw/main/sdxl_utility.py": [ [ "SDXLResolutionPresets" ], { "title_aux": "SDXLResolutionPresets" } ], "https://github.com/wutipong/ComfyUI-TextUtils": [ [ "Text Utils - Join N-Elements of String List", "Text Utils - Join String List", "Text Utils - Join Strings", "Text Utils - Split String to List" ], { "title_aux": "ComfyUI-TextUtils" } ], "https://github.com/wwwins/ComfyUI-Simple-Aspect-Ratio": [ [ "SimpleAspectRatio" ], { "title_aux": "ComfyUI-Simple-Aspect-Ratio" } ], "https://github.com/xXAdonesXx/NodeGPT": [ [ "AppendAgent", "Assistant", "Chat", "ChatGPT", "CombineInput", "Conditioning", "CostumeAgent_1", "CostumeAgent_2", "CostumeMaster_1", "Critic", "DisplayString", "DisplayTextAsImage", "EVAL", "Engineer", "Executor", "GroupChat", "Image_generation_Conditioning", "LM_Studio", "LoadAPIconfig", "LoadTXT", "MemGPT", "Memory_Excel", "Model_1", "Ollama", "Output2String", "Planner", "Scientist", "TextCombine", "TextGeneration", "TextGenerator", "TextInput", "TextOutput", "UserProxy", "llama-cpp", "llava", "oobaboogaOpenAI" ], { "title_aux": "NodeGPT" } ], "https://github.com/xiaoxiaodesha/hd_node": [ [ "Combine HDMasks", "Cover HDMasks", "HD FaceIndex", "HD GetMaskArea", "HD Image Levels", "HD SmoothEdge", "HD UltimateSDUpscale" ], { "title_aux": "hd-nodes-comfyui" } ], "https://github.com/yffyhk/comfyui_auto_danbooru": [ [ "GetDanbooru", "TagEncode" ], { "title_aux": "comfyui_auto_danbooru" } ], "https://github.com/yolain/ComfyUI-Easy-Use": [ [ "dynamicThresholdingFull", "easy LLLiteLoader", "easy XYInputs: CFG Scale", "easy XYInputs: Checkpoint", "easy XYInputs: ControlNet", "easy XYInputs: Denoise", "easy XYInputs: Lora", "easy XYInputs: ModelMergeBlocks", "easy XYInputs: NegativeCond", "easy XYInputs: NegativeCondList", "easy XYInputs: PositiveCond", "easy XYInputs: PositiveCondList", "easy XYInputs: PromptSR", "easy XYInputs: Sampler/Scheduler", "easy XYInputs: Seeds++ Batch", "easy XYInputs: Steps", "easy XYPlot", "easy XYPlotAdvanced", "easy a1111Loader", "easy boolean", "easy cleanGpuUsed", "easy comfyLoader", "easy compare", "easy controlnetLoader", "easy controlnetLoaderADV", "easy convertAnything", "easy detailerFix", "easy float", "easy fooocusInpaintLoader", "easy fullLoader", "easy fullkSampler", "easy globalSeed", "easy hiresFix", "easy if", "easy imageInsetCrop", "easy imagePixelPerfect", "easy imageRemoveBG", "easy imageSave", "easy imageScaleDown", "easy imageScaleDownBy", "easy imageScaleDownToSize", "easy imageSize", "easy imageSizeByLongerSide", "easy imageSizeBySide", "easy imageSwitch", "easy imageToMask", "easy int", "easy isSDXL", "easy joinImageBatch", "easy kSampler", "easy kSamplerDownscaleUnet", "easy kSamplerInpainting", "easy kSamplerSDTurbo", "easy kSamplerTiled", "easy latentCompositeMaskedWithCond", "easy latentNoisy", "easy loraStack", "easy negative", "easy pipeIn", "easy pipeOut", "easy pipeToBasicPipe", "easy portraitMaster", "easy poseEditor", "easy positive", "easy preDetailerFix", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG", "easy preSamplingSdTurbo", "easy promptList", "easy rangeFloat", "easy rangeInt", "easy samLoaderPipe", "easy seed", "easy showAnything", "easy showLoaderSettingsNames", "easy showSpentTime", "easy string", "easy stylesSelector", "easy svdLoader", "easy ultralyticsDetectorPipe", "easy unSampler", "easy wildcards", "easy xyAny", "easy zero123Loader" ], { "title_aux": "ComfyUI Easy Use" } ], "https://github.com/yolanother/DTAIComfyImageSubmit": [ [ "DTSimpleSubmitImage", "DTSubmitImage" ], { "title_aux": "Comfy AI DoubTech.ai Image Sumission Node" } ], "https://github.com/yolanother/DTAIComfyLoaders": [ [ "DTCLIPLoader", "DTCLIPVisionLoader", "DTCheckpointLoader", "DTCheckpointLoaderSimple", "DTControlNetLoader", "DTDiffControlNetLoader", "DTDiffusersLoader", "DTGLIGENLoader", "DTLoadImage", "DTLoadImageMask", "DTLoadLatent", "DTLoraLoader", "DTLorasLoader", "DTStyleModelLoader", "DTUpscaleModelLoader", "DTVAELoader", "DTunCLIPCheckpointLoader" ], { "title_aux": "Comfy UI Online Loaders" } ], "https://github.com/yolanother/DTAIComfyPromptAgent": [ [ "DTPromptAgent", "DTPromptAgentString" ], { "title_aux": "Comfy UI Prompt Agent" } ], "https://github.com/yolanother/DTAIComfyQRCodes": [ [ "QRCode" ], { "title_aux": "Comfy UI QR Codes" } ], "https://github.com/yolanother/DTAIComfyVariables": [ [ "DTCLIPTextEncode", "DTSingleLineStringVariable", "DTSingleLineStringVariableNoClip", "FloatVariable", "IntVariable", "StringFormat", "StringFormatSingleLine", "StringVariable" ], { "title_aux": "Variables for Comfy UI" } ], "https://github.com/yolanother/DTAIImageToTextNode": [ [ "DTAIImageToTextNode", "DTAIImageUrlToTextNode" ], { "title_aux": "Image to Text Node" } ], "https://github.com/youyegit/tdxh_node_comfyui": [ [ "TdxhBoolNumber", "TdxhClipVison", "TdxhControlNetApply", "TdxhControlNetProcessor", "TdxhFloatInput", "TdxhImageToSize", "TdxhImageToSizeAdvanced", "TdxhImg2ImgLatent", "TdxhIntInput", "TdxhLoraLoader", "TdxhOnOrOff", "TdxhReference", "TdxhStringInput", "TdxhStringInputTranslator" ], { "title_aux": "tdxh_node_comfyui" } ], "https://github.com/yuvraj108c/ComfyUI-Pronodes": [ [ "LoadYoutubeVideoNode" ], { "title_aux": "ComfyUI-Pronodes" } ], "https://github.com/yuvraj108c/ComfyUI-Whisper": [ [ "Add Subtitles To Background", "Add Subtitles To Frames", "Apply Whisper", "Resize Cropped Subtitles" ], { "title_aux": "ComfyUI Whisper" } ], "https://github.com/zcfrank1st/Comfyui-Toolbox": [ [ "PreviewJson", "PreviewVideo", "SaveJson", "TestJsonPreview" ], { "title_aux": "Comfyui-Toolbox" } ], "https://github.com/zcfrank1st/Comfyui-Yolov8": [ [ "Yolov8Detection", "Yolov8Segmentation" ], { "title_aux": "ComfyUI Yolov8" } ], "https://github.com/zcfrank1st/comfyui_visual_anagrams": [ [ "VisualAnagramsAnimate", "VisualAnagramsSample" ], { "title_aux": "comfyui_visual_anagram" } ], "https://github.com/zer0TF/cute-comfy": [ [ "Cute.Placeholder" ], { "title_aux": "Cute Comfy" } ], "https://github.com/zfkun/ComfyUI_zfkun": [ [ "ZFLoadImagePath", "ZFPreviewText", "ZFPreviewTextMultiline", "ZFShareScreen", "ZFTextTranslation" ], { "title_aux": "ComfyUI_zfkun" } ], "https://github.com/zhongpei/ComfyUI-InstructIR": [ [ "InstructIRProcess", "LoadInstructIRModel" ], { "title_aux": "ComfyUI for InstructIR" } ], "https://github.com/zhongpei/Comfyui_image2prompt": [ [ "Image2Text", "LoadImage2TextModel" ], { "title_aux": "Comfyui_image2prompt" } ], "https://github.com/zhuanqianfish/ComfyUI-EasyNode": [ [ "EasyCaptureNode", "EasyVideoOutputNode", "SendImageWebSocket" ], { "title_aux": "EasyCaptureNode for ComfyUI" } ], "https://raw.githubusercontent.com/throttlekitty/SDXLCustomAspectRatio/main/SDXLAspectRatio.py": [ [ "SDXLAspectRatio" ], { "title_aux": "SDXLCustomAspectRatio" } ] } """; } ================================================ FILE: StabilityMatrix.Core/Models/Configs/ApiOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Configs; /// /// Configuration options for API services /// public record ApiOptions { /// /// Base URL for Lykos Authentication API /// public Uri LykosAuthApiBaseUrl { get; init; } = new("https://auth.lykos.ai"); /// /// Base URL for Lykos Analytics API /// public Uri LykosAnalyticsApiBaseUrl { get; init; } = new("https://analytics.lykos.ai"); /// /// Base URL for Lykos Account API /// public Uri LykosAccountApiBaseUrl { get; init; } = new("https://account.lykos.ai/"); /// /// Base URL for PromptGen API /// public Uri LykosPromptGenApiBaseUrl { get; init; } = new("https://promptgen.lykos.ai/api"); } ================================================ FILE: StabilityMatrix.Core/Models/Configs/DebugOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Configs; public class DebugOptions { /// /// Sets up LiteDB to use a temporary database file on each run /// public bool TempDatabase { get; set; } /// /// Always show the one-click install page on launch /// public bool ShowOneClickInstall { get; set; } /// /// Override the default update manifest url /// (https://cdn.lykos.ai/update.json) /// public string? UpdateManifestUrl { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/ConnectedModelInfo.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.OpenModelsDb; namespace StabilityMatrix.Core.Models; public class ConnectedModelInfo : IEquatable { [JsonIgnore] public const string FileExtension = ".cm-info.json"; public int? ModelId { get; set; } public string ModelName { get; set; } public string ModelDescription { get; set; } public bool Nsfw { get; set; } public string[] Tags { get; set; } public CivitModelType ModelType { get; set; } public int? VersionId { get; set; } public string VersionName { get; set; } public string? VersionDescription { get; set; } public string? BaseModel { get; set; } public CivitFileMetadata? FileMetadata { get; set; } 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; } public string? ThumbnailImageUrl { get; set; } public InferenceDefaults? InferenceDefaults { get; set; } public ConnectedModelSource? Source { get; set; } = ConnectedModelSource.Civitai; public ConnectedModelInfo() { } public ConnectedModelInfo( CivitModel civitModel, CivitModelVersion civitModelVersion, CivitFile civitFile, DateTimeOffset importedAt ) { ModelId = civitModel.Id; ModelName = civitModel.Name; ModelDescription = civitModel.Description ?? string.Empty; Nsfw = civitModel.Nsfw; Tags = civitModel.Tags; ModelType = civitModel.Type; VersionId = civitModelVersion.Id; VersionName = civitModelVersion.Name; VersionDescription = civitModelVersion.Description; ImportedAt = importedAt; BaseModel = civitModelVersion.BaseModel; FileMetadata = civitFile.Metadata; Hashes = civitFile.Hashes; TrainedWords = civitModelVersion.TrainedWords; Stats = civitModel.Stats; Source = ConnectedModelSource.Civitai; } public ConnectedModelInfo( CivitModel civitModel, CivitModelVersion civitModelVersion, CivitFile civitFile, DateTimeOffset importedAt, InferenceDefaults? inferenceDefaults ) { ModelId = civitModel.Id; ModelName = civitModel.Name; ModelDescription = civitModel.Description ?? string.Empty; Nsfw = civitModel.Nsfw; Tags = civitModel.Tags; ModelType = civitModel.Type; VersionId = civitModelVersion.Id; VersionName = civitModelVersion.Name; VersionDescription = civitModelVersion.Description; ImportedAt = importedAt; BaseModel = civitModelVersion.BaseModel; FileMetadata = civitFile.Metadata; Hashes = civitFile.Hashes; TrainedWords = civitModelVersion.TrainedWords; Stats = civitModel.Stats; Source = ConnectedModelSource.Civitai; InferenceDefaults = inferenceDefaults; } public ConnectedModelInfo( OpenModelDbKeyedModel model, OpenModelDbResource resource, DateTimeOffset importedAt ) { ModelName = model.Id; ModelDescription = model.Description ?? string.Empty; VersionName = model.Name ?? string.Empty; Tags = model.Tags?.ToArray() ?? []; ImportedAt = importedAt; Hashes = new CivitFileHashes { SHA256 = resource.Sha256 }; ThumbnailImageUrl = resource.Urls?.FirstOrDefault(); ModelType = CivitModelType.Upscaler; Source = ConnectedModelSource.OpenModelDb; } public static ConnectedModelInfo? FromJson(string json) { return JsonSerializer.Deserialize( json, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull } ); } /// /// Saves the model info to a json file in the specified directory. /// Overwrites existing files. /// /// Path of directory to save file /// Model file name without extensions public async Task SaveJsonToDirectory(string directoryPath, string modelFileName) { var name = modelFileName + FileExtension; var json = JsonSerializer.Serialize(this); await File.WriteAllTextAsync(Path.Combine(directoryPath, name), json); } [JsonIgnore] public string TrainedWordsString => TrainedWords != null ? string.Join(", ", TrainedWords) : string.Empty; public bool Equals(ConnectedModelInfo? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; return Comparer.Equals(this, other); } public override bool Equals(object? obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((ConnectedModelInfo)obj); } public override int GetHashCode() { return Comparer.GetHashCode(this); } public static bool operator ==(ConnectedModelInfo? left, ConnectedModelInfo? right) { return Equals(left, right); } public static bool operator !=(ConnectedModelInfo? left, ConnectedModelInfo? right) { return !Equals(left, right); } private sealed class ConnectedModelInfoEqualityComparer : IEqualityComparer { public bool Equals(ConnectedModelInfo? x, ConnectedModelInfo? y) { if (ReferenceEquals(x, y)) return true; if (x is null) return false; if (y is null) return false; if (x.GetType() != y.GetType()) return false; return x.ModelId == y.ModelId && x.ModelName == y.ModelName && x.ModelDescription == y.ModelDescription && x.Nsfw == y.Nsfw && x.Tags?.SequenceEqual(y.Tags ?? []) is null or true && x.ModelType == y.ModelType && x.VersionId == y.VersionId && x.VersionName == y.VersionName && x.VersionDescription == y.VersionDescription && x.BaseModel == y.BaseModel && x.FileMetadata == y.FileMetadata && x.ImportedAt.Equals(y.ImportedAt) && x.Hashes == y.Hashes && x.TrainedWords?.SequenceEqual(y.TrainedWords ?? []) is null or true && x.Stats == y.Stats && x.UserTitle == y.UserTitle && x.ThumbnailImageUrl == y.ThumbnailImageUrl && x.InferenceDefaults == y.InferenceDefaults && x.Source == y.Source; } public int GetHashCode(ConnectedModelInfo obj) { var hashCode = new HashCode(); hashCode.Add(obj.ModelId); hashCode.Add(obj.ModelName); hashCode.Add(obj.ModelDescription); hashCode.Add(obj.Nsfw); hashCode.Add(obj.Tags); hashCode.Add((int)obj.ModelType); hashCode.Add(obj.VersionId); hashCode.Add(obj.VersionName); hashCode.Add(obj.VersionDescription); hashCode.Add(obj.BaseModel); hashCode.Add(obj.FileMetadata); hashCode.Add(obj.ImportedAt); hashCode.Add(obj.Hashes); hashCode.Add(obj.TrainedWords); hashCode.Add(obj.Stats); hashCode.Add(obj.UserTitle); hashCode.Add(obj.ThumbnailImageUrl); hashCode.Add(obj.InferenceDefaults); hashCode.Add(obj.Source); return hashCode.ToHashCode(); } } public static IEqualityComparer Comparer { get; } = new ConnectedModelInfoEqualityComparer(); } [JsonSourceGenerationOptions( GenerationMode = JsonSourceGenerationMode.Default, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull )] [JsonSerializable(typeof(ConnectedModelInfo))] internal partial class ConnectedModelInfoSerializerContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Models/ConnectedModelSource.cs ================================================ namespace StabilityMatrix.Core.Models; public enum ConnectedModelSource { Civitai, OpenModelDb, Other } ================================================ FILE: StabilityMatrix.Core/Models/CustomVersion.cs ================================================ namespace StabilityMatrix.Core.Models; public class CustomVersion : IComparable { public int Major { get; set; } public int Minor { get; set; } public int Patch { get; set; } public string? PreRelease { get; set; } public CustomVersion() { } public CustomVersion(string versionString) { var parts = versionString.Split(new[] { '-', '.' }, StringSplitOptions.None); Major = int.Parse(parts[0]); Minor = int.Parse(parts[1]); Patch = int.Parse(parts[2]); PreRelease = parts.Length > 3 ? string.Join(".", parts.Skip(3)) : null; } public int CompareTo(CustomVersion? other) { var result = Major.CompareTo(other?.Major); if (result != 0) return result; result = Minor.CompareTo(other?.Minor); if (result != 0) return result; result = Patch.CompareTo(other?.Patch); if (result != 0) return result; switch (PreRelease) { case null when other?.PreRelease == null: return 0; case null: return 1; } if (other?.PreRelease == null) return -1; return string.Compare(PreRelease, other.PreRelease, StringComparison.Ordinal); } public static bool operator <(CustomVersion v1, CustomVersion v2) { return v1.CompareTo(v2) < 0; } public static bool operator >(CustomVersion v1, CustomVersion v2) { return v1.CompareTo(v2) > 0; } public static bool operator <=(CustomVersion v1, CustomVersion v2) { return v1.CompareTo(v2) <= 0; } public static bool operator >=(CustomVersion v1, CustomVersion v2) { return v1.CompareTo(v2) >= 0; } public override string ToString() { return $"{Major}.{Minor}.{Patch}" + (PreRelease != null ? $"-{PreRelease}" : string.Empty); } } ================================================ FILE: StabilityMatrix.Core/Models/Database/CivitBaseModelTypeCacheEntry.cs ================================================ namespace StabilityMatrix.Core.Models.Database; public class CivitBaseModelTypeCacheEntry { public required string Id { get; set; } public List ModelTypes { get; set; } = []; public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Models/Database/GitCommit.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Database; public class GitCommit { public string? Sha { get; set; } [JsonIgnore] public string ShortSha => string.IsNullOrWhiteSpace(Sha) ? string.Empty : Sha[..7]; } ================================================ FILE: StabilityMatrix.Core/Models/Database/GithubCacheEntry.cs ================================================ using LiteDB; using Octokit; namespace StabilityMatrix.Core.Models.Database; public class GithubCacheEntry { [BsonId] public string CacheKey { get; set; } public DateTimeOffset LastUpdated { get; set; } = DateTimeOffset.UtcNow; public IEnumerable AllReleases { get; set; } = new List(); public IEnumerable Branches { get; set; } = new List(); public IEnumerable Commits { get; set; } = new List(); } ================================================ FILE: StabilityMatrix.Core/Models/Database/InferenceProjectEntry.cs ================================================ using System.Text.Json.Nodes; using LiteDB; namespace StabilityMatrix.Core.Models.Database; public record InferenceProjectEntry { [BsonId] public required Guid Id { get; init; } /// /// Full path to the project file (.smproj) /// public required string FilePath { get; init; } /// /// Whether the project is open in the editor /// public bool IsOpen { get; set; } /// /// Whether the project is selected in the editor /// public bool IsSelected { get; set; } /// /// Current index of the tab /// public int CurrentTabIndex { get; set; } = -1; /// /// The current dock layout state /// public JsonObject? DockLayout { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Database/LocalImageFile.cs ================================================ using System.Text.Json; using DynamicData.Tests; using MetadataExtractor.Formats.Exif; using SkiaSharp; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using JsonSerializer = System.Text.Json.JsonSerializer; using Size = System.Drawing.Size; namespace StabilityMatrix.Core.Models.Database; /// /// Represents a locally indexed image file. /// public record LocalImageFile { public required string AbsolutePath { get; init; } /// /// Type of the model file. /// public LocalImageFileType ImageType { get; init; } /// /// Creation time of the file. /// public DateTimeOffset CreatedAt { get; init; } /// /// Last modified time of the file. /// public DateTimeOffset LastModifiedAt { get; init; } /// /// Generation parameters metadata of the file. /// public GenerationParameters? GenerationParameters { get; init; } /// /// Dimensions of the image /// public Size? ImageSize { get; init; } /// /// File name of the relative path. /// public string FileName => Path.GetFileName(AbsolutePath); /// /// File name of the relative path without extension. /// public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(AbsolutePath); public ( string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes, string? CivitParameters ) ReadMetadata() { if (AbsolutePath.EndsWith("webp")) { var paramsJson = ImageMetadata.ReadTextChunkFromWebp( AbsolutePath, ExifDirectoryBase.TagImageDescription ); var smProj = ImageMetadata.ReadTextChunkFromWebp(AbsolutePath, ExifDirectoryBase.TagSoftware); return (null, paramsJson, smProj, null, null); } using var stream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read); using var reader = new BinaryReader(stream); var parameters = ImageMetadata.ReadTextChunk(reader, "parameters"); var parametersJson = ImageMetadata.ReadTextChunk(reader, "parameters-json"); var smProject = ImageMetadata.ReadTextChunk(reader, "smproj"); var comfyNodes = ImageMetadata.ReadTextChunk(reader, "prompt"); var civitParameters = ImageMetadata.ReadTextChunk(reader, "user_comment"); return ( string.IsNullOrEmpty(parameters) ? null : parameters, string.IsNullOrEmpty(parametersJson) ? null : parametersJson, string.IsNullOrEmpty(smProject) ? null : smProject, string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes, string.IsNullOrEmpty(civitParameters) ? null : civitParameters ); } public static LocalImageFile FromPath(FilePath filePath) { // TODO: Support other types const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage; if (filePath.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { var paramsJson = ImageMetadata.ReadTextChunkFromWebp( filePath, ExifDirectoryBase.TagImageDescription ); GenerationParameters? parameters = null; try { parameters = string.IsNullOrWhiteSpace(paramsJson) ? null : JsonSerializer.Deserialize(paramsJson); } catch (JsonException) { // just don't load params I guess, no logger here <_< } filePath.Info.Refresh(); return new LocalImageFile { AbsolutePath = filePath, ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, GenerationParameters = parameters, ImageSize = new Size(parameters?.Width ?? 0, parameters?.Height ?? 0), }; } if (filePath.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase)) { // Get metadata using var stream = filePath.Info.OpenRead(); using var reader = new BinaryReader(stream); var imageSize = ImageMetadata.GetImageSize(reader); var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json"); GenerationParameters? genParams; if (!string.IsNullOrWhiteSpace(metadata)) { genParams = JsonSerializer.Deserialize(metadata); } else { metadata = ImageMetadata.ReadTextChunk(reader, "parameters"); if (string.IsNullOrWhiteSpace(metadata)) // if still empty, try civitai metadata (user_comment) { metadata = ImageMetadata.ReadTextChunk(reader, "user_comment"); } GenerationParameters.TryParse(metadata, out genParams); } filePath.Info.Refresh(); return new LocalImageFile { AbsolutePath = filePath, ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, GenerationParameters = genParams, ImageSize = imageSize, }; } using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); using var ms = new SKManagedStream(fs); var codec = SKCodec.Create(ms); return new LocalImageFile { AbsolutePath = filePath, ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, ImageSize = new Size { Height = codec.Info.Height, Width = codec.Info.Width }, }; } public static readonly HashSet SupportedImageExtensions = [ ".png", ".jpg", ".jpeg", ".gif", ".webp", ]; } ================================================ FILE: StabilityMatrix.Core/Models/Database/LocalImageFileType.cs ================================================ namespace StabilityMatrix.Core.Models.Database; [Flags] public enum LocalImageFileType : ulong { // Source Automatic = 1 << 1, Comfy = 1 << 2, Inference = 1 << 3, // Generation Type TextToImage = 1 << 10, ImageToImage = 1 << 11 } ================================================ FILE: StabilityMatrix.Core/Models/Database/LocalModelFile.cs ================================================ using System.Diagnostics.CodeAnalysis; using LiteDB; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Database; /// /// Represents a locally indexed model file. /// public record LocalModelFile { private sealed class RelativePathConnectedModelInfoEqualityComparer : IEqualityComparer { public bool Equals(LocalModelFile? x, LocalModelFile? y) { if (ReferenceEquals(x, y)) return true; if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; return x.RelativePath == y.RelativePath && Equals(x.ConnectedModelInfo, y.ConnectedModelInfo) && x.HasUpdate == y.HasUpdate && x.HasEarlyAccessUpdateOnly == y.HasEarlyAccessUpdateOnly; } public int GetHashCode(LocalModelFile obj) { return HashCode.Combine( obj.RelativePath, obj.ConnectedModelInfo, obj.HasUpdate, obj.HasEarlyAccessUpdateOnly ); } } public static IEqualityComparer RelativePathConnectedModelInfoComparer { get; } = new RelativePathConnectedModelInfoEqualityComparer(); public virtual bool Equals(LocalModelFile? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return RelativePath == other.RelativePath && ConnectedModelInfo == other.ConnectedModelInfo && HasUpdate == other.HasUpdate && HasEarlyAccessUpdateOnly == other.HasEarlyAccessUpdateOnly; } public override int GetHashCode() { return HashCode.Combine(RelativePath, ConnectedModelInfo, HasUpdate, HasEarlyAccessUpdateOnly); } /// /// Relative path to the file from the root model directory. /// [BsonId] public required string RelativePath { get; init; } /// /// Type of the model file. /// public required SharedFolderType SharedFolderType { get; set; } /// /// Optional connected model information. /// public ConnectedModelInfo? ConnectedModelInfo { get; set; } /// /// Optional preview image relative path. /// public string? PreviewImageRelativePath { get; set; } /// /// Optional preview image full path. Takes priority over . /// public string? PreviewImageFullPath { get; set; } /// /// Optional full path to the model's configuration (.yaml) file. /// public string? ConfigFullPath { get; set; } /// /// Whether or not an update is available for this model /// public bool HasUpdate { get; set; } /// /// Whether updates are limited to Early Access versions only. /// public bool HasEarlyAccessUpdateOnly { get; set; } /// /// Last time this model was checked for an update /// public DateTimeOffset LastUpdateCheck { get; set; } /// /// The latest CivitModel info /// [BsonRef("CivitModels")] public CivitModel? LatestModelInfo { get; set; } /// /// File name of the relative path. /// [BsonIgnore] public string FileName => Path.GetFileName(RelativePath); /// /// File name of the relative path without extension. /// [BsonIgnore] public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath); /// /// Relative file path from the shared folder type model directory. /// [BsonIgnore] public string RelativePathFromSharedFolder => Path.GetRelativePath(SharedFolderType.GetStringValue(), RelativePath); /// /// Blake3 hash of the file. /// public string? HashBlake3 => ConnectedModelInfo?.Hashes?.BLAKE3; [BsonIgnore] public bool IsSafetensorFile => Path.GetExtension(RelativePath) == ".safetensors"; public string? HashSha256 => ConnectedModelInfo?.Hashes.SHA256; [BsonIgnore] public string? PreviewImageFullPathGlobal => PreviewImageFullPath ?? GetPreviewImageFullPath(GlobalConfig.ModelsDir); [BsonIgnore] public Uri? PreviewImageUriGlobal => PreviewImageFullPathGlobal == null ? null : new Uri(PreviewImageFullPathGlobal); [BsonIgnore] public string DisplayModelName => ConnectedModelInfo?.ModelName ?? FileNameWithoutExtension; [BsonIgnore] public string DisplayModelVersion => ConnectedModelInfo?.VersionName ?? string.Empty; [BsonIgnore] public string DisplayModelFileName => FileName; [BsonIgnore] public string DisplayConfigFileName => Path.GetFileName(ConfigFullPath) ?? string.Empty; [BsonIgnore] [MemberNotNullWhen(true, nameof(ConnectedModelInfo))] public bool HasConnectedModel => ConnectedModelInfo != null; [BsonIgnore] [MemberNotNullWhen(true, nameof(ConnectedModelInfo))] public bool HasCivitMetadata => HasConnectedModel && ConnectedModelInfo.ModelId != null && ConnectedModelInfo.Source == ConnectedModelSource.Civitai; [BsonIgnore] [MemberNotNullWhen(true, nameof(ConnectedModelInfo))] public bool HasOpenModelDbMetadata => HasConnectedModel && ConnectedModelInfo.Source == ConnectedModelSource.OpenModelDb; public string GetFullPath(string rootModelDirectory) { return Path.Combine(rootModelDirectory, RelativePath); } public string? GetPreviewImageFullPath(string rootModelDirectory) { if (PreviewImageFullPath != null) return PreviewImageFullPath; return PreviewImageRelativePath == null ? null : Path.Combine(rootModelDirectory, PreviewImageRelativePath); } public string GetConnectedModelInfoFullPath(string rootModelDirectory) { var modelNameNoExt = Path.GetFileNameWithoutExtension(RelativePath); var modelParentDir = Path.GetDirectoryName(GetFullPath(rootModelDirectory)) ?? ""; return Path.Combine(modelParentDir, $"{modelNameNoExt}.cm-info.json"); } public IEnumerable GetDeleteFullPaths(string rootModelDirectory) { if (GetFullPath(rootModelDirectory) is { } filePath && File.Exists(filePath)) { yield return filePath; } if ( HasConnectedModel && GetConnectedModelInfoFullPath(rootModelDirectory) is { } cmInfoPath && File.Exists(cmInfoPath) ) { yield return cmInfoPath; } var previewImagePath = GetPreviewImageFullPath(rootModelDirectory); if (File.Exists(previewImagePath)) { yield return previewImagePath; } } public static readonly HashSet SupportedCheckpointExtensions = [ ".safetensors", ".pt", ".ckpt", ".pth", ".bin", ".sft", ".gguf", ]; public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".webp"]; public static readonly HashSet SupportedMetadataExtensions = [".json"]; } ================================================ FILE: StabilityMatrix.Core/Models/Database/LocalModelFolder.cs ================================================ namespace StabilityMatrix.Core.Models.Database; public class LocalModelFolder { public required string RelativePath { get; set; } public Dictionary Files { get; set; } = []; public Dictionary Folders { get; set; } = []; } ================================================ FILE: StabilityMatrix.Core/Models/Database/PyPiCacheEntry.cs ================================================ using LiteDB; namespace StabilityMatrix.Core.Models.Database; public class PyPiCacheEntry { [BsonId] public required string CacheKey { get; set; } public required List Versions { get; set; } public DateTimeOffset LastUpdated { get; set; } = DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Models/DimensionStringComparer.cs ================================================ using System.Collections; using System.Text.RegularExpressions; namespace StabilityMatrix.Core.Models; public partial class DimensionStringComparer : IComparer, IComparer { public static readonly DimensionStringComparer Instance = new(); /// /// Compares two dimension strings (like "1024 x 768") by the first numeric value. /// /// First dimension string to compare /// Second dimension string to compare /// /// A negative value if x comes before y; /// zero if x equals y; /// a positive value if x comes after y /// public int Compare(object? x, object? y) { // Handle null cases if (x == null && y == null) return 0; if (x == null) return -1; if (y == null) return 1; if (x is not string xStr || y is not string yStr) throw new ArgumentException("Both arguments must be strings."); // Extract the first number from each string var firstX = ExtractFirstNumber(xStr); var firstY = ExtractFirstNumber(yStr); // Compare the first numbers return firstX.CompareTo(firstY); } public int Compare(string? x, string? y) { // Handle null cases if (x == null && y == null) return 0; if (x == null) return -1; if (y == null) return 1; // Extract the first number from each string var firstX = ExtractFirstNumber(x); var firstY = ExtractFirstNumber(y); // Compare the first numbers return firstX.CompareTo(firstY); } /// /// Extracts the first numeric value from a dimension string. /// /// String in format like "1024 x 768" /// The first numeric value or 0 if no number is found private static int ExtractFirstNumber(string dimensionString) { // Use regex to find the first number in the string var match = NumberRegex().Match(dimensionString); if (match.Success && int.TryParse(match.Value, out var result)) { return result; } return 0; // Return 0 if no number found } [GeneratedRegex(@"\d+")] private static partial Regex NumberRegex(); } ================================================ FILE: StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs ================================================ namespace StabilityMatrix.Core.Models; public class DownloadPackageVersionOptions { public string? BranchName { get; set; } public string? CommitHash { get; set; } public string? VersionTag { get; set; } public bool IsLatest { get; set; } public bool IsPrerelease { get; set; } public string GetReadableVersionString() => !string.IsNullOrWhiteSpace(VersionTag) ? VersionTag : $"{BranchName}@{CommitHash?[..7]}"; public string ReadableVersionString => GetReadableVersionString(); } ================================================ FILE: StabilityMatrix.Core/Models/EnvVarKeyPair.cs ================================================ namespace StabilityMatrix.Core.Models; public class EnvVarKeyPair { public string Key { get; set; } public string Value { get; set; } public EnvVarKeyPair(string key = "", string value = "") { Key = key; Value = value; } } ================================================ FILE: StabilityMatrix.Core/Models/ExtraPackageCommand.cs ================================================ namespace StabilityMatrix.Core.Models; public class ExtraPackageCommand { public required string CommandName { get; set; } public required Func Command { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/FDS/ComfyUiSelfStartSettings.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2024 Stability AI // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // from https://github.com/Stability-AI/StableSwarmUI/blob/master/src/BuiltinExtensions/ComfyUIBackend/ComfyUISelfStartBackend.cs using FreneticUtilities.FreneticDataSyntax; namespace StabilityMatrix.Core.Models.FDS; public class ComfyUiSelfStartSettings : AutoConfiguration { [ConfigComment( "The location of the 'main.py' file. Can be an absolute or relative path, but must end with 'main.py'.\nIf you used the installer, this should be 'dlbackend/ComfyUI/main.py'." )] public string StartScript = ""; [ConfigComment("Any arguments to include in the launch script.")] public string ExtraArgs = ""; [ConfigComment( "If unchecked, the system will automatically add some relevant arguments to the comfy launch. If checked, automatic args (other than port) won't be added." )] public bool DisableInternalArgs = false; [ConfigComment("If checked, will automatically keep the comfy backend up to date when launching.")] public bool AutoUpdate = true; [ConfigComment( "If checked, tells Comfy to generate image previews. If unchecked, previews will not be generated, and images won't show up until they're done." )] public bool EnablePreviews = true; [ConfigComment("Which GPU to use, if multiple are available.")] public int GPU_ID = 0; [ConfigComment("How many extra requests may queue up on this backend while one is processing.")] public int OverQueue = 1; [ConfigComment( "Whether the Comfy backend should automatically update nodes within Swarm's managed nodes folder.\nYou can update every launch, never update automatically, or force-update (bypasses some common git issues)." )] public string UpdateManagedNodes = "true"; } ================================================ FILE: StabilityMatrix.Core/Models/FDS/StableSwarmSettings.cs ================================================ // The MIT License (MIT) // // Copyright (c) 2024 Stability AI // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // from https://raw.githubusercontent.com/Stability-AI/StableSwarmUI/master/src/Core/Settings.cs using FreneticUtilities.FreneticDataSyntax; namespace StabilityMatrix.Core.Models.FDS; /// Central default settings list. public class StableSwarmSettings : AutoConfiguration { [ConfigComment("Settings related to file paths.")] public PathsData Paths = new(); [ConfigComment("Settings related to networking and the webserver.")] public NetworkData Network = new(); [ConfigComment("Restrictions to apply to default users.")] public UserRestriction DefaultUserRestriction = new(); [ConfigComment( "Default settings for users (unless the user modifies them, if so permitted).\n(NOTE: Usually, don't edit this. Go to the 'User' tab to edit your User-Settings)." )] public User DefaultUser = new(); [ConfigComment("Settings related to backends.")] public BackendData Backends = new(); [ConfigComment( "If this is set to 'true', hides the installer page. If 'false', the installer page will be shown." )] public bool IsInstalled = false; [ConfigComment( "Ratelimit, in milliseconds, between Nvidia GPU status queries. Default is 1000 ms (1 second)." )] public long NvidiaQueryRateLimitMS = 1000; [ConfigComment( "How to launch the UI. If 'none', just quietly launch.\nIf 'web', launch your web-browser to the page.\nIf 'webinstall', launch web-browser to the install page.\nIf 'electron', launch the UI in an electron window (NOT YET IMPLEMENTED)." )] [ManualSettingsOptions(Impl = null, Vals = new string[] { "none", "web", "webinstall", "electron" })] public string LaunchMode = "webinstall"; [ConfigComment("The minimum tier of logs that should be visible in the console.\nDefault is 'info'.")] public string LogLevel = "Info"; [ConfigComment("Settings related to the User Interface.")] public UIData UI = new(); [ConfigComment("Settings related to webhooks.")] public WebHooksData WebHooks = new(); /// Settings related to backends. public class BackendData : AutoConfiguration { [ConfigComment("How many times to retry initializing a backend before giving up. Default is 3.")] public int MaxBackendInitAttempts = 3; [ConfigComment( "Safety check, the maximum duration all requests can be waiting for a backend before the system declares a backend handling failure." )] public int MaxTimeoutMinutes = 20; [ConfigComment( "The maximum duration an individual request can be waiting on a backend to be available before giving up.\n" + "Not to be confused with 'MaxTimeoutMinutes' which requires backends be unresponsive for that duration, this duration includes requests that are merely waiting because other requests are queued." + "\nDefaults to 60 * 24 * 7 = 1 week (ultra-long max queue duration)." )] public int PerRequestTimeoutMinutes = 60 * 24 * 7; [ConfigComment( "The maximum number of pending requests to continue forcing orderly processing of.\nOver this limit, requests may start going out of order." )] public int MaxRequestsForcedOrder = 20; [ConfigComment( "How many minutes to wait after the last generation before automatically freeing up VRAM (to prevent issues with other programs).\nThis has the downside of a small added bit of time to load back onto VRAM at next usage.\nUse a decimal number to free after seconds.\nDefaults to 10 minutes." )] public double ClearVRAMAfterMinutes = 10; [ConfigComment( "How many minutes to wait after the last generation before automatically freeing up system RAM (to prevent issues with other programs).\nThis has the downside of causing models to fully load from data drive at next usage.\nUse a decimal number to free after seconds.\nDefaults to 60 minutes (one hour)." )] public double ClearSystemRAMAfterMinutes = 60; } /// Settings related to networking and the webserver. public class NetworkData : AutoConfiguration { [ConfigComment( "What web host address to use. `localhost` means your PC only." + "\nLinux users may use `0.0.0.0` to mean accessible to anyone that can connect to your PC (ie LAN users, or the public if your firewall is open)." + "\nWindows users may use `*` for that, though it may require additional Windows firewall configuration." + "\nAdvanced server users may wish to manually specify a host bind address here." )] public string Host = "localhost"; [ConfigComment("What web port to use. Default is '7801'.")] public int Port = 7801; [ConfigComment( "If true, if the port is already in use, the server will try to find another port to use instead.\nIf false, the server will fail to start if the port is already in use." )] public bool PortCanChange = true; [ConfigComment( "Backends are automatically assigned unique ports. This value selects which port number to start the assignment from.\nDefault is '7820'." )] public int BackendStartingPort = 7820; } /// Settings related to file paths. public class PathsData : AutoConfiguration { [ConfigComment( "Root path for model files. Use a full-formed path (starting with '/' or a Windows drive like 'C:') to use an absolute path.\nDefaults to 'Models'." )] public string ModelRoot = "Models"; [ConfigComment( "The model folder to use within 'ModelRoot'.\nDefaults to 'Stable-Diffusion'.\nAbsolute paths work too." )] public string SDModelFolder = "Stable-Diffusion"; [ConfigComment( "The LoRA (or related adapter type) model folder to use within 'ModelRoot'.\nDefaults to 'Lora'.\nAbsolute paths work too." )] public string SDLoraFolder = "Lora"; [ConfigComment( "The VAE (autoencoder) model folder to use within 'ModelRoot'.\nDefaults to 'VAE'.\nAbsolute paths work too." )] public string SDVAEFolder = "VAE"; [ConfigComment( "The Embedding (eg textual inversion) model folder to use within 'ModelRoot'.\nDefaults to 'Embeddings'.\nAbsolute paths work too." )] public string SDEmbeddingFolder = "Embeddings"; [ConfigComment( "The ControlNets model folder to use within 'ModelRoot'.\nDefaults to 'controlnet'.\nAbsolute paths work too." )] public string SDControlNetsFolder = "controlnet"; [ConfigComment( "The CLIP Vision model folder to use within 'ModelRoot'.\nDefaults to 'clip_vision'.\nAbsolute paths work too." )] public string SDClipVisionFolder = "clip_vision"; [ConfigComment("Root path for data (user configs, etc).\nDefaults to 'Data'")] public string DataPath = "Data"; [ConfigComment("Root path for output files (images, etc).\nDefaults to 'Output'")] public string OutputPath = "Output"; [ConfigComment("The folder for wildcard (.txt) files, under Data.\nDefaults to 'Wildcards'")] public string WildcardsFolder = "Wildcards"; [ConfigComment( "When true, output paths always have the username as a folder.\nWhen false, this will be skipped.\nKeep this on in multi-user environments." )] public bool AppendUserNameToOutputPath = true; } /// Settings to control restrictions on users. public class UserRestriction : AutoConfiguration { [ConfigComment("How many directories deep a user's custom OutPath can be.\nDefault is 5.")] public int MaxOutPathDepth = 5; [ConfigComment("Which user-settings the user is allowed to modify.\nDefault is all of them.")] public List AllowedSettings = new() { "*" }; [ConfigComment( "If true, the user is treated as a full admin.\nThis includes the ability to modify these settings." )] public bool Admin = false; [ConfigComment("If true, user may load models.\nIf false, they may only use already-loaded models.")] public bool CanChangeModels = true; [ConfigComment( "What models are allowed, as a path regex.\nDirectory-separator is always '/'. Can be '.*' for all, 'MyFolder/.*' for only within that folder, etc.\nDefault is all." )] public string AllowedModels = ".*"; [ConfigComment("Generic permission flags. '*' means all.\nDefault is all.")] public List PermissionFlags = new() { "*" }; [ConfigComment("How many images can try to be generating at the same time on this user.")] public int MaxT2ISimultaneous = 32; } /// Settings per-user. public class User : AutoConfiguration { public class OutPath : AutoConfiguration { [ConfigComment( "Builder for output file paths. Can use auto-filling placeholders like '[model]' for the model name, '[prompt]' for a snippet of prompt text, etc.\n" + "Full details in the docs: https://github.com/Stability-AI/StableSwarmUI/blob/master/docs/User%20Settings.md#path-format" )] public string Format = "raw/[year]-[month]-[day]/[hour][minute]-[prompt]-[model]-[seed]"; [ConfigComment("How long any one part can be.\nDefault is 40 characters.")] public int MaxLenPerPart = 40; } [ConfigComment("Settings related to output path building.")] public OutPath OutPathBuilder = new(); public class FileFormatData : AutoConfiguration { [ConfigComment("What format to save images in.\nDefault is '.jpg' (at 100% quality).")] public string ImageFormat = "JPG"; [ConfigComment("Whether to store metadata into saved images.\nDefaults enabled.")] public bool SaveMetadata = true; [ConfigComment( "If set to non-0, adds DPI metadata to saved images.\n'72' is a good value for compatibility with some external software." )] public int DPI = 0; [ConfigComment( "If set to true, a '.txt' file will be saved alongside images with the image metadata easily viewable.\nThis can work even if saving in the image is disabled. Defaults disabled." )] public bool SaveTextFileMetadata = false; } [ConfigComment("Settings related to saved file format.")] public FileFormatData FileFormat = new(); [ConfigComment("Whether your files save to server data drive or not.")] public bool SaveFiles = true; [ConfigComment("If true, folders will be discard from starred image paths.")] public bool StarNoFolders = false; [ConfigComment("What theme to use. Default is 'dark_dreams'.")] public string Theme = "dark_dreams"; [ConfigComment( "If enabled, batch size will be reset to 1 when parameters are loaded.\nThis can prevent accidents that might thrash your GPU or cause compatibility issues, especially for example when importing a comfy workflow.\nYou can still set the batch size at will in the GUI." )] public bool ResetBatchSizeToOne = false; public enum HintFormatOptions { BUTTON, HOVER, NONE } [ConfigComment("The format for parameter hints to display as.\nDefault is 'BUTTON'.")] [SettingsOptions(Impl = typeof(SettingsOptionsAttribute.ForEnum))] public string HintFormat = "BUTTON"; public class VAEsData : AutoConfiguration { [ConfigComment( "What VAE to use with SDXL models by default. Use 'None' to use the one in the model." )] [ManualSettingsOptions(Impl = null, Vals = new string[] { "None" })] public string DefaultSDXLVAE = "None"; [ConfigComment( "What VAE to use with SDv1 models by default. Use 'None' to use the one in the model." )] [ManualSettingsOptions(Impl = null, Vals = new string[] { "None" })] public string DefaultSDv1VAE = "None"; } [ConfigComment("Options to override default VAEs with.")] public VAEsData VAEs = new(); [ConfigComment( "When generating live previews, this is how many simultaneous generation requests can be waiting at one time." )] public int MaxSimulPreviews = 1; [ConfigComment( "Set to a number above 1 to allow generations of multiple images to automatically generate square mini-grids when they're done." )] public int MaxImagesInMiniGrid = 1; [ConfigComment("How many images the history view should stop trying to load after.")] public int MaxImagesInHistory = 1000; [ConfigComment( "If true, the Image History view will cache small preview thumbnails of images.\nThis should make things run faster. You can turn it off if you don't want that." )] public bool ImageHistoryUsePreviews = true; [ConfigComment( "Delay, in seconds, betweeen Generate Forever updates.\nIf the delay hits and a generation is still waiting, it will be skipped.\nDefault is 0.1 seconds." )] public double GenerateForeverDelay = 0.1; [ConfigComment("What language to display the UI in.\nDefault is 'en' (English).")] public string Language = "en"; } /// UI-related settings. public class UIData : AutoConfiguration { [ConfigComment( "Optionally specify a (raw HTML) welcome message here. If specified, will override the automatic welcome messages." )] public string OverrideWelcomeMessage = ""; } /// Webhook settings. public class WebHooksData : AutoConfiguration { [ConfigComment( "Webhook to call (empty JSON POST) when queues are starting up from idle.\nLeave empty to disable any webhook.\nCall must return before the first generation starts." )] public string QueueStartWebhook = ""; [ConfigComment( "Webhook to call (empty JSON POST) when all queues are done and the server is going idle.\nLeave empty to disable any webhook.\nCall must return before queuing may restart." )] public string QueueEndWebhook = ""; [ConfigComment( "How long to wait (in seconds) after all queues are done before sending the queue end webhook.\nThis is useful to prevent rapid start+end calls." )] public double QueueEndDelay = 1; } } [AttributeUsage(AttributeTargets.Field)] public class SettingsOptionsAttribute : Attribute { public abstract class AbstractImpl { public abstract string[] GetOptions { get; } } public class ForEnum : AbstractImpl where T : Enum { public override string[] GetOptions => Enum.GetNames(typeof(T)); } public Type Impl; public virtual string[] Options => (Activator.CreateInstance(Impl) as AbstractImpl).GetOptions; } [AttributeUsage(AttributeTargets.Field)] public class ManualSettingsOptionsAttribute : SettingsOptionsAttribute { public string[] Vals; public override string[] Options => Vals; } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs ================================================ using System.Collections; using System.ComponentModel; using System.Text.Json.Serialization; using JetBrains.Annotations; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Models.FileInterfaces; [PublicAPI] [Localizable(false)] [JsonConverter(typeof(StringJsonConverter))] public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable { private DirectoryInfo? info; [JsonIgnore] public DirectoryInfo Info => info ??= new DirectoryInfo(FullPath); [JsonIgnore] FileSystemInfo IPathObject.Info => Info; [JsonIgnore] public bool IsSymbolicLink { get { Info.Refresh(); return Info.Exists && Info.Attributes.HasFlag(FileAttributes.ReparsePoint); } } /// /// Gets a value indicating whether the directory exists. /// [JsonIgnore] public bool Exists => Info.Exists; /// [JsonIgnore] public string Name => Info.Name; /// /// Get the parent directory. /// [JsonIgnore] public DirectoryPath? Parent => Info.Parent == null ? null : new DirectoryPath(Info.Parent); public DirectoryPath([Localizable(false)] string path) : base(path) { } public DirectoryPath(FileSystemPath path) : base(path) { } public DirectoryPath(DirectoryInfo info) : base(info.FullName) { // Additionally set the info field this.info = info; } public DirectoryPath([Localizable(false)] params string[] paths) : base(paths) { } public DirectoryPath RelativeTo(DirectoryPath path) { return new DirectoryPath(Path.GetRelativePath(path.FullPath, FullPath)); } /// public long GetSize() { Info.Refresh(); return Info.EnumerateFiles("*", EnumerationOptionConstants.AllDirectories).Sum(file => file.Length); } /// /// Gets the size of the directory. /// /// /// Whether to include files and subdirectories that are symbolic links / reparse points. /// public long GetSize(bool includeSymbolicLinks) { if (includeSymbolicLinks) return GetSize(); Info.Refresh(); var files = Info.GetFiles() .Where(file => !file.Attributes.HasFlag(FileAttributes.ReparsePoint)) .Sum(file => file.Length); var subDirs = Info.GetDirectories() .Where(dir => !dir.Attributes.HasFlag(FileAttributes.ReparsePoint)) .Sum( dir => dir.EnumerateFiles("*", EnumerationOptionConstants.AllDirectories) .Sum(file => file.Length) ); return files + subDirs; } /// /// Gets the size of the directory asynchronously. /// /// /// Whether to include files and subdirectories that are symbolic links / reparse points. /// public Task GetSizeAsync(bool includeSymbolicLinks) { return Task.Run(() => GetSize(includeSymbolicLinks)); } /// /// Creates the directory. /// public void Create() => Directory.CreateDirectory(FullPath); /// /// Deletes the directory. /// public void Delete() => Info.Delete(); /// Deletes the directory asynchronously. public Task DeleteAsync() => Task.Run(Delete); /// /// Deletes the directory. /// /// Whether to delete subdirectories and files. public void Delete(bool recursive) => Info.Delete(recursive); /// /// Deletes the directory asynchronously. /// public Task DeleteAsync(bool recursive) => Task.Run(() => Delete(recursive)); void IPathObject.Delete() => Info.Delete(true); Task IPathObject.DeleteAsync() => DeleteAsync(true); private void ThrowIfNotExists() { if (!Exists) { throw new DirectoryNotFoundException($"Directory not found: {FullPath}"); } } public void CopyTo(DirectoryPath destinationDir, bool recursive = true) { ThrowIfNotExists(); // Cache directories before we start copying var dirs = EnumerateDirectories().ToList(); destinationDir.Create(); // Get the files in the source directory and copy to the destination directory foreach (var file in EnumerateFiles()) { var targetFilePath = destinationDir.JoinFile(file.Name); file.CopyTo(targetFilePath); } // If recursive and copying subdirectories, recursively call this method if (recursive) { foreach (var subDir in dirs) { var targetDirectory = destinationDir.JoinDir(subDir.Name); subDir.CopyTo(targetDirectory); } } } public async Task CopyToAsync(DirectoryPath destinationDir, bool recursive = true) { ThrowIfNotExists(); // Cache directories before we start copying var dirs = EnumerateDirectories().ToList(); destinationDir.Create(); // Get the files in the source directory and copy to the destination directory foreach (var file in EnumerateFiles()) { var targetFilePath = destinationDir.JoinFile(file.Name); await file.CopyToAsync(targetFilePath).ConfigureAwait(false); } // If recursive and copying subdirectories, recursively call this method if (recursive) { foreach (var subDir in dirs) { var targetDirectory = destinationDir.JoinDir(subDir.Name); await subDir.CopyToAsync(targetDirectory).ConfigureAwait(false); } } } /// /// Move the directory to a destination path. /// public DirectoryPath MoveTo(DirectoryPath destinationDir) { Info.MoveTo(destinationDir.FullPath); // Return the new path return destinationDir; } /// /// Move the file to a target path. /// public async Task MoveToAsync(DirectoryPath destinationDir) { await Task.Run(() => Info.MoveTo(destinationDir.FullPath)).ConfigureAwait(false); // Return the new path return destinationDir; } /// /// Move the directory to a destination path as a subfolder with the current name. /// public async Task MoveToDirectoryAsync(DirectoryPath destinationParentDir) { await Task.Run(() => Info.MoveTo(destinationParentDir.JoinDir(Name))).ConfigureAwait(false); // Return the new path return destinationParentDir.JoinDir(this); } /// /// Join with other paths to form a new directory path. /// public DirectoryPath JoinDir([Localizable(false)] params DirectoryPath[] paths) => new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray()))); /// /// Join with other paths to form a new file path. /// public FilePath JoinFile([Localizable(false)] params FilePath[] paths) => new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray()))); /// /// Returns an enumerable collection of files that matches /// a specified search pattern and search subdirectory option. /// public IEnumerable EnumerateFiles( string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly ) => Info.EnumerateFiles(searchPattern, searchOption).Select(file => new FilePath(file)); /// /// Returns an enumerable collection of files. Allows passing of . /// public IEnumerable EnumerateFiles( string searchPattern, EnumerationOptions enumerationOptions ) => Info.EnumerateFiles(searchPattern, enumerationOptions).Select(file => new FilePath(file)); /// /// Returns an enumerable collection of directories. Allows passing of . /// public IEnumerable EnumerateDirectories( string searchPattern, EnumerationOptions enumerationOptions ) => Info.EnumerateDirectories(searchPattern, enumerationOptions) .Select(directory => new DirectoryPath(directory)); /// /// Returns an enumerable collection of directories /// public IEnumerable EnumerateDirectories( string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly ) => Info.EnumerateDirectories(searchPattern, searchOption) .Select(directory => new DirectoryPath(directory)); /// /// Return a new with the given file name. /// public DirectoryPath WithName(string directoryName) { if (Path.GetDirectoryName(FullPath) is { } directory && !string.IsNullOrWhiteSpace(directory)) { return new DirectoryPath(directory, directoryName); } return new DirectoryPath(directoryName); } public override string ToString() => FullPath; /// public IEnumerator GetEnumerator() { return Info.EnumerateFileSystemInfos("*", EnumerationOptionConstants.TopLevelOnly) .Select( fsInfo => fsInfo switch { FileInfo file => new FilePath(file), DirectoryInfo directory => new DirectoryPath(directory), _ => throw new InvalidOperationException("Unknown file system info type") } ) .GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } // DirectoryPath + DirectoryPath = DirectoryPath public static DirectoryPath operator +(DirectoryPath path, DirectoryPath other) => new(Path.Combine(path, other.FullPath)); // DirectoryPath + FilePath = FilePath public static FilePath operator +(DirectoryPath path, FilePath other) => new(Path.Combine(path, other.FullPath)); // DirectoryPath + FileInfo = FilePath public static FilePath operator +(DirectoryPath path, FileInfo other) => new(Path.Combine(path, other.FullName)); // DirectoryPath + string = string public static string operator +(DirectoryPath path, [Localizable(false)] string other) => Path.Combine(path, other); // Implicit conversions to and from string public static implicit operator string(DirectoryPath path) => path.FullPath; public static implicit operator DirectoryPath([Localizable(false)] string path) => new(path); // Implicit conversions to and from DirectoryInfo public static implicit operator DirectoryInfo(DirectoryPath path) => path.Info; public static implicit operator DirectoryPath(DirectoryInfo path) => new(path); } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/FilePath.Fluent.cs ================================================ namespace StabilityMatrix.Core.Models.FileInterfaces; public partial class FilePath { /// /// Return a new with the given file name. /// public FilePath WithName(string fileName) { if ( Path.GetDirectoryName(FullPath) is { } directory && !string.IsNullOrWhiteSpace(directory) ) { return new FilePath(directory, fileName); } return new FilePath(fileName); } } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs ================================================ using System.ComponentModel; using System.Text; using System.Text.Json.Serialization; using JetBrains.Annotations; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.FileInterfaces; [PublicAPI] [Localizable(false)] [JsonConverter(typeof(StringJsonConverter))] public partial class FilePath : FileSystemPath, IPathObject { private FileInfo? _info; [JsonIgnore] public FileInfo Info => _info ??= new FileInfo(FullPath); [JsonIgnore] FileSystemInfo IPathObject.Info => Info; [JsonIgnore] public bool IsSymbolicLink { get { Info.Refresh(); return Info.Attributes.HasFlag(FileAttributes.ReparsePoint); } } [JsonIgnore] public bool Exists => Info.Exists; [JsonIgnore] public string Name => Info.Name; [JsonIgnore] public string NameWithoutExtension => Path.GetFileNameWithoutExtension(Info.Name); /// [JsonIgnore] public string Extension => Info.Extension; /// /// Get the directory of the file. /// [JsonIgnore] public DirectoryPath? Directory { get { try { return Info.Directory == null ? null : new DirectoryPath(Info.Directory); } catch (DirectoryNotFoundException) { return null; } } } public FilePath([Localizable(false)] string path) : base(path) { } public FilePath(FileInfo fileInfo) : base(fileInfo.FullName) { _info = fileInfo; } public FilePath(FileSystemPath path) : base(path) { } public FilePath([Localizable(false)] params string[] paths) : base(paths) { } public FilePath RelativeTo(DirectoryPath path) { return new FilePath(Path.GetRelativePath(path.FullPath, FullPath)); } public long GetSize() { Info.Refresh(); return Info.Length; } public long GetSize(bool includeSymbolicLinks) { if (!includeSymbolicLinks && IsSymbolicLink) return 0; return GetSize(); } public Task GetSizeAsync(bool includeSymbolicLinks) { return Task.Run(() => GetSize(includeSymbolicLinks)); } /// Creates an empty file. public void Create() => File.Create(FullPath).Close(); /// Deletes the file public void Delete() => File.Delete(FullPath); /// Deletes the file asynchronously public Task DeleteAsync(CancellationToken ct = default) { return Task.Run(() => File.Delete(FullPath), ct); } // Methods specific to files /// Read text public string ReadAllText() => File.ReadAllText(FullPath); /// Read text asynchronously public Task ReadAllTextAsync(CancellationToken ct = default) { return File.ReadAllTextAsync(FullPath, ct); } /// Write text public void WriteAllText(string text, Encoding? encoding = null) => File.WriteAllText(FullPath, text, encoding ?? new UTF8Encoding(false)); /// Write text asynchronously public Task WriteAllTextAsync(string text, CancellationToken ct = default, Encoding? encoding = null) { return File.WriteAllTextAsync(FullPath, text, encoding ?? new UTF8Encoding(false), ct); } /// Read bytes public byte[] ReadAllBytes() => File.ReadAllBytes(FullPath); /// Read bytes asynchronously public Task ReadAllBytesAsync(CancellationToken ct = default) { return File.ReadAllBytesAsync(FullPath, ct); } /// Write bytes public void WriteAllBytes(byte[] bytes) => File.WriteAllBytes(FullPath, bytes); /// Write bytes asynchronously public Task WriteAllBytesAsync(byte[] bytes, CancellationToken ct = default) { return File.WriteAllBytesAsync(FullPath, bytes, ct); } /// /// Rename the file. /// public FilePath Rename([Localizable(false)] string fileName) { if (Path.GetDirectoryName(FullPath) is { } directory && !string.IsNullOrWhiteSpace(directory)) { var target = Path.Combine(directory, fileName); Info.MoveTo(target, true); return new FilePath(target); } throw new InvalidOperationException("Cannot rename a file path that is empty or has no directory"); } /// /// Move the file to a directory. /// public FilePath MoveTo(FilePath destinationFile) { Info.MoveTo(destinationFile.FullPath, true); // Return the new path return destinationFile; } /// /// Move the file to a directory. /// public async Task MoveToDirectoryAsync(DirectoryPath directory) { await Task.Run(() => Info.MoveTo(directory.JoinFile(Name), true)).ConfigureAwait(false); // Return the new path return directory.JoinFile(this); } /// /// Move the file to a target path. /// public async Task MoveToAsync(FilePath destinationFile) { await Task.Run(() => Info.MoveTo(destinationFile.FullPath)).ConfigureAwait(false); // Return the new path return destinationFile; } /// /// Move the file to a target path with auto increment if the file already exists. /// /// The new path, possibly with incremented file name public async Task MoveToWithIncrementAsync(FilePath destinationFile, int maxTries = 100) { await Task.Yield(); var targetFile = destinationFile; for (var i = 1; i < maxTries; i++) { if (!targetFile.Exists) { return await MoveToAsync(targetFile).ConfigureAwait(false); } targetFile = destinationFile.WithName( destinationFile.NameWithoutExtension + $" ({i})" + destinationFile.Extension ); } throw new IOException($"Could not move file to {destinationFile} because it already exists."); } /// /// Copy the file to a target path. /// public FilePath CopyTo(FilePath destinationFile, bool overwrite = false) { Info.CopyTo(destinationFile.FullPath, overwrite); // Return the new path return destinationFile; } /// /// Copy the file to a target path asynchronously. /// public async Task CopyToAsync(FilePath destinationFile, bool overwrite = false) { await using var sourceStream = Info.OpenRead(); await using var destinationStream = destinationFile.Info.OpenWrite(); await sourceStream.CopyToAsync(destinationStream).ConfigureAwait(false); // Return the new path return destinationFile; } /// /// Copy the file to a target path asynchronously with a specified the file share mode. /// public async Task CopyToAsync( FilePath destinationFile, FileShare sourceShare, bool overwrite = false ) { await using var sourceStream = Info.Open(FileMode.Open, FileAccess.Read, sourceShare); await using var destinationStream = destinationFile.Info.OpenWrite(); await sourceStream.CopyToAsync(destinationStream).ConfigureAwait(false); // Return the new path return destinationFile; } // Implicit conversions to and from string public static implicit operator string(FilePath path) => path.FullPath; public static implicit operator FilePath([Localizable(false)] string path) => new(path); } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs ================================================ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; namespace StabilityMatrix.Core.Models.FileInterfaces; [PublicAPI] [Localizable(false)] public class FileSystemPath : IEquatable, IFormattable { public string FullPath { get; } protected FileSystemPath(string path) { FullPath = path; } protected FileSystemPath(FileSystemPath path) : this(path.FullPath) { } protected FileSystemPath(params string[] paths) : this(Path.Combine(paths)) { } /// public override string ToString() { return FullPath; } /// string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { return ToString(format, formatProvider); } /// /// Overridable IFormattable.ToString method. /// By default, returns . /// protected virtual string ToString(string? format, IFormatProvider? formatProvider) { return FullPath; } public static bool operator ==(FileSystemPath? left, FileSystemPath? right) { return Equals(left, right); } public static bool operator !=(FileSystemPath? left, FileSystemPath? right) { return !Equals(left, right); } /// public bool Equals(FileSystemPath? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return string.Equals( GetNormalizedPath(FullPath), GetNormalizedPath(other.FullPath), OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal ); } /// public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (GetType() != obj.GetType()) return false; return Equals((FileSystemPath)obj); } /// /// Normalize a path to a consistent format for comparison. /// /// Path to normalize. /// Normalized path. [return: NotNullIfNotNull(nameof(path))] private static string? GetNormalizedPath(string? path) { // Return null or empty paths as-is if (string.IsNullOrEmpty(path)) { return path; } if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri)) { if (uri.IsAbsoluteUri) { path = uri.LocalPath; } } // Get full path if possible, ignore errors like invalid chars or too long try { path = Path.GetFullPath(path); } catch (SystemException) { } return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } /// public override int GetHashCode() { return HashCode.Combine(GetType().GetHashCode(), FullPath.GetHashCode()); } // Implicit conversions to and from string public static implicit operator string(FileSystemPath path) => path.FullPath; public static implicit operator FileSystemPath(string path) => new(path); } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/IPathObject.cs ================================================ namespace StabilityMatrix.Core.Models.FileInterfaces; public interface IPathObject { /// Full path of the file system object. string FullPath { get; } /// Info of the file system object. FileSystemInfo Info { get; } /// Name of the file system object. string Name { get; } /// Whether the file system object is a symbolic link or junction. bool IsSymbolicLink { get; } /// Gets the size of the file system object. long GetSize(); /// Gets the size of the file system object asynchronously. Task GetSizeAsync() => Task.Run(GetSize); /// Whether the file system object exists. bool Exists { get; } /// Deletes the file system object void Delete(); /// Deletes the file system object asynchronously. public Task DeleteAsync() => Task.Run(Delete); } ================================================ FILE: StabilityMatrix.Core/Models/FileInterfaces/TempDirectoryPath.cs ================================================ namespace StabilityMatrix.Core.Models.FileInterfaces; public class TempDirectoryPath : DirectoryPath, IDisposable { public TempDirectoryPath() : base(Path.GetTempPath(), Path.GetRandomFileName()) { Directory.CreateDirectory(FullPath); } public void Dispose() { ForceDeleteDirectory(FullPath); GC.SuppressFinalize(this); } private static void ForceDeleteDirectory(string directoryPath) { if (!Directory.Exists(directoryPath)) { return; } var files = Directory.GetFiles(directoryPath); var directories = Directory.GetDirectories(directoryPath); foreach (var file in files) { File.SetAttributes(file, FileAttributes.Normal); File.Delete(file); } foreach (var dir in directories) { ForceDeleteDirectory(dir); } File.SetAttributes(directoryPath, FileAttributes.Normal); Directory.Delete(directoryPath, false); } } ================================================ FILE: StabilityMatrix.Core/Models/FileSizeType.cs ================================================ using System.Globalization; namespace StabilityMatrix.Core.Models; public class FileSizeType { public double SizeInKB { get; private set; } public string HumanReadableRepresentation { get; private set; } public FileSizeType(double sizeInKB) { SizeInKB = sizeInKB; HumanReadableRepresentation = ConvertToHumanReadable(); } private string ConvertToHumanReadable() { var sizeUnits = new string[] { "KB", "MB", "GB", "TB" }; var size = SizeInKB; var unitIndex = 0; while (size >= 1024 && unitIndex < sizeUnits.Length - 1) { size /= 1024; unitIndex++; } return string.Format("{0} {1}", size.ToString("0.##", CultureInfo.InvariantCulture), sizeUnits[unitIndex]); } public override string ToString() { return HumanReadableRepresentation; } } ================================================ FILE: StabilityMatrix.Core/Models/GenerationParameters.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Models; [JsonSerializable(typeof(GenerationParameters))] public record GenerationParameters { public string? PositivePrompt { get; set; } public string? NegativePrompt { get; set; } public int Steps { get; set; } public string? Sampler { get; set; } public double CfgScale { get; set; } public ulong Seed { get; set; } public int Height { get; set; } public int Width { get; set; } public string? ModelHash { get; set; } public string? ModelName { get; set; } public int FrameCount { get; set; } public int MotionBucketId { get; set; } public int VideoQuality { get; set; } public bool Lossless { get; set; } public int Fps { get; set; } public double OutputFps { get; set; } public double MinCfg { get; set; } public double AugmentationLevel { get; set; } public string? VideoOutputMethod { get; set; } public int? ModelVersionId { get; set; } public List? ExtraNetworkModelVersionIds { get; set; } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; public static bool TryParse( string? text, [NotNullWhen(true)] out GenerationParameters? generationParameters ) { if (string.IsNullOrWhiteSpace(text)) { generationParameters = null; return false; } try { generationParameters = Parse(text); } catch (Exception) { generationParameters = null; return false; } return true; } public static GenerationParameters Parse(string text) { var lines = text.Split('\n'); if (lines.LastOrDefault() is not { } lastLine) { throw new ValidationException("Fields line not found"); } if (lastLine.StartsWith("Steps:") != true) { lines = text.Split("\r\n"); lastLine = lines.LastOrDefault() ?? string.Empty; if (lastLine.StartsWith("Steps:") != true) { throw new ValidationException("Unable to locate starting marker of last line"); } } // Join lines before last line, split at 'Negative prompt: ' var joinedLines = string.Join("\n", lines[..^1]).Trim(); // Apparently there is no space after the colon if value is empty, so check and add space here if (joinedLines.EndsWith("Negative prompt:")) { joinedLines += ' '; } var splitFirstPart = joinedLines.Split("Negative prompt: ", 2); var positivePrompt = splitFirstPart.ElementAtOrDefault(0)?.Trim(); var negativePrompt = splitFirstPart.ElementAtOrDefault(1)?.Trim(); // Parse last line var lineFields = ParseLine(lastLine); var generationParameters = new GenerationParameters { PositivePrompt = positivePrompt, NegativePrompt = negativePrompt, Steps = int.Parse(lineFields.GetValueOrDefault("Steps", "0")), Sampler = lineFields.GetValueOrDefault("Sampler"), CfgScale = double.Parse(lineFields.GetValueOrDefault("CFG scale", "0")), Seed = ulong.Parse(lineFields.GetValueOrDefault("Seed", "0")), ModelHash = lineFields.GetValueOrDefault("Model hash"), ModelName = lineFields.GetValueOrDefault("Model"), }; if (lineFields.ContainsKey("Civitai resources")) { // [{"type":"checkpoint","modelVersionId":290640,"modelName":"Pony Diffusion V6 XL","modelVersionName":"V6 (start with this one)"},{"type":"lora","weight":0.8,"modelVersionId":333590,"modelName":"Not Artists Styles for Pony Diffusion V6 XL","modelVersionName":"Anime 2"}] var civitaiResources = lineFields["Civitai resources"]; if (!string.IsNullOrWhiteSpace(civitaiResources)) { var resources = JsonSerializer.Deserialize>( civitaiResources, JsonOptions ); if (resources is not null) { generationParameters.ModelName ??= resources .FirstOrDefault(x => x.Type == "checkpoint") ?.ModelName; generationParameters.ModelVersionId ??= resources .FirstOrDefault(x => x.Type == "checkpoint") ?.ModelVersionId; foreach (var lora in resources.Where(x => x.Type == "lora")) { generationParameters.ExtraNetworkModelVersionIds ??= []; generationParameters.ExtraNetworkModelVersionIds.Add(lora.ModelVersionId); } } } } if (lineFields.GetValueOrDefault("Size") is { } size) { var split = size.Split('x', 2); if (split.Length == 2) { generationParameters = generationParameters with { Width = int.Parse(split[0]), Height = int.Parse(split[1]), }; } } return generationParameters; } /// /// Parse A1111 metadata fields in a single line where /// fields are separated by commas and key-value pairs are separated by colons. /// i.e. "key1: value1, key2: value2" /// internal static Dictionary ParseLine(string line) { var dict = new Dictionary(); var quoteStack = new Stack(); // the Range for the key Range? currentKeyRange = null; // the start of the key or value Index currentStart = 0; for (var i = 0; i < line.Length; i++) { var c = line[i]; switch (c) { case '"': // if we are in a " quote, pop the stack if (quoteStack.Count > 0 && quoteStack.Peek() == '"') { quoteStack.Pop(); } else { // start of a new quoted section quoteStack.Push(c); } break; case '[': case '{': case '(': case '<': quoteStack.Push(c); break; case ']': if (quoteStack.Count > 0 && quoteStack.Peek() == '[') { quoteStack.Pop(); } break; case '}': if (quoteStack.Count > 0 && quoteStack.Peek() == '{') { quoteStack.Pop(); } break; case ')': if (quoteStack.Count > 0 && quoteStack.Peek() == '(') { quoteStack.Pop(); } break; case '>': if (quoteStack.Count > 0 && quoteStack.Peek() == '<') { quoteStack.Pop(); } break; case ':': // : marks the end of the key // if we already have a key, ignore this colon as it is part of the value // if we are not in a quote, we have a key if (!currentKeyRange.HasValue && quoteStack.Count == 0) { currentKeyRange = new Range(currentStart, i); currentStart = i + 1; } break; case ',': // , marks the end of a key-value pair // if we are not in a quote, we have a value if (quoteStack.Count != 0) { break; } if (!currentKeyRange.HasValue) { // unexpected comma, reset and start from current position currentStart = i + 1; break; } try { // extract the key and value var key = new string(line.AsSpan()[currentKeyRange!.Value].Trim()); var value = new string(line.AsSpan()[currentStart..i].Trim()); // check duplicates and prefer the first occurrence if (!string.IsNullOrWhiteSpace(key) && !dict.ContainsKey(key)) { dict[key] = value; } } catch (Exception) { // ignore individual key-value pair errors } currentKeyRange = null; currentStart = i + 1; break; default: break; } // end of switch } // end of for // if we have a key-value pair at the end of the string if (currentKeyRange.HasValue) { try { var key = new string(line.AsSpan()[currentKeyRange!.Value].Trim()); var value = new string(line.AsSpan()[currentStart..].Trim()); if (!string.IsNullOrWhiteSpace(key) && !dict.ContainsKey(key)) { dict[key] = value; } } catch (Exception) { // ignore individual key-value pair errors } } return dict; } /// /// Converts current string to and . /// /// public (ComfySampler sampler, ComfyScheduler scheduler)? GetComfySamplers() { if (Sampler is not { } source) return null; var scheduler = source switch { _ when source.Contains("Karras") => ComfyScheduler.Karras, _ when source.Contains("Exponential") => ComfyScheduler.Exponential, _ => ComfyScheduler.Normal, }; var sampler = source switch { "LMS" => ComfySampler.LMS, "DDIM" => ComfySampler.DDIM, "UniPC" => ComfySampler.UniPC, "DPM fast" => ComfySampler.DpmFast, "DPM adaptive" => ComfySampler.DpmAdaptive, "Heun" => ComfySampler.Heun, _ when source.StartsWith("DPM2 a") => ComfySampler.Dpm2Ancestral, _ when source.StartsWith("DPM2") => ComfySampler.Dpm2, _ when source.StartsWith("DPM++ 2M SDE") => ComfySampler.Dpmpp2MSde, _ when source.StartsWith("DPM++ 2M") => ComfySampler.Dpmpp2M, _ when source.StartsWith("DPM++ 3M SDE") => ComfySampler.Dpmpp3MSde, _ when source.StartsWith("DPM++ 3M") => ComfySampler.Dpmpp3M, _ when source.StartsWith("DPM++ SDE") => ComfySampler.DpmppSde, _ when source.StartsWith("DPM++ 2S a") => ComfySampler.Dpmpp2SAncestral, _ => default, }; return (sampler, scheduler); } /// /// Return a sample parameters for UI preview /// public static GenerationParameters GetSample() { return new GenerationParameters { PositivePrompt = "(cat:1.2), by artist, detailed, [shaded]", NegativePrompt = "blurry, jpg artifacts", Steps = 30, CfgScale = 7, Width = 640, Height = 896, Seed = 124825529, ModelName = "ExampleMix7", ModelHash = "b899d188a1ac7356bfb9399b2277d5b21712aa360f8f9514fba6fcce021baff7", Sampler = "DPM++ 2M Karras", }; } } ================================================ FILE: StabilityMatrix.Core/Models/GitVersion.cs ================================================ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text; namespace StabilityMatrix.Core.Models; /// /// Union of either Tag or Branch + CommitSha. /// [Localizable(false)] public record GitVersion : IFormattable, IUtf8SpanParsable { public string? Tag { get; init; } public string? Branch { get; init; } public string? CommitSha { get; init; } /// public override string ToString() { return ToString(null, null); } /// /// /// - The "O" format specifier can be used to format for round-trip serialization with full commit SHAs. /// - The "G" format specifier uses abbreviated commit SHAs (first 7 characters). /// "O" is used by default. /// public string ToString(string? format, IFormatProvider? formatProvider) { switch (format) { case "G": { if (!string.IsNullOrEmpty(Tag)) { return Tag; } if (!string.IsNullOrEmpty(Branch) && !string.IsNullOrEmpty(CommitSha)) { return $"{Branch}@{CommitSha[..7]}"; } if (!string.IsNullOrEmpty(Branch)) { return Branch; } return !string.IsNullOrEmpty(CommitSha) ? CommitSha[..7] : ""; } case "O": case null: { if (!string.IsNullOrEmpty(Tag)) { return Tag; } if (!string.IsNullOrEmpty(Branch) && !string.IsNullOrEmpty(CommitSha)) { return $"{Branch}@{CommitSha}"; } if (!string.IsNullOrEmpty(Branch)) { return Branch; } return !string.IsNullOrEmpty(CommitSha) ? CommitSha : ""; } default: throw new FormatException($"The {format} format specifier is not supported."); } } public static bool TryParse( ReadOnlySpan utf8Text, IFormatProvider? provider, [MaybeNullWhen(false)] out GitVersion result ) { return TryParse(utf8Text, provider, out result, false); } private static bool TryParse( ReadOnlySpan utf8Source, IFormatProvider? provider, [MaybeNullWhen(false)] out GitVersion result, bool throwOnFailure ) { result = null; try { var source = Encoding.UTF8.GetString(utf8Source); if (string.IsNullOrEmpty(source)) { return false; } if (source.Contains('@')) { var parts = source.Split('@'); if (parts.Length == 2) { var branch = parts[0]; var commitSha = parts[1]; result = new GitVersion { Branch = branch, CommitSha = commitSha }; return true; } } else { result = new GitVersion { Tag = source }; return true; } } catch { if (throwOnFailure) { throw; } return false; } return false; } public static GitVersion Parse(ReadOnlySpan utf8Source, IFormatProvider? provider) { if (TryParse(utf8Source, provider, out var result)) { return result; } throw new FormatException("Invalid GitVersion format."); } } ================================================ FILE: StabilityMatrix.Core/Models/GlobalConfig.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Models; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class GlobalConfig { private static DirectoryPath? libraryDir; private static DirectoryPath? modelsDir; /// /// Absolute path to the library directory. /// Needs to be set by SettingsManager.TryFindLibrary() before being accessed. /// /// public static DirectoryPath LibraryDir { get { if (libraryDir is null) { throw new NullReferenceException( "GlobalConfig.LibraryDir was not set before being accessed." ); } return libraryDir; } set => libraryDir = value; } /// /// Full path to the %APPDATA% directory. /// Usually C:\Users\{username}\AppData\Roaming /// public static DirectoryPath AppDataDir { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); /// /// Full path to the fixed home directory. /// Currently %APPDATA%\StabilityMatrix /// public static DirectoryPath HomeDir { get; set; } = AppDataDir.JoinDir("StabilityMatrix"); public static DirectoryPath ModelsDir { get { if (modelsDir is null) { throw new NullReferenceException("GlobalConfig.ModelsDir was not set before being accessed."); } return modelsDir; } set => modelsDir = value; } } ================================================ FILE: StabilityMatrix.Core/Models/GlobalEncryptedSerializer.cs ================================================ using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Text.Json; using DeviceId; namespace StabilityMatrix.Core.Models; /// /// Encrypted MessagePack Serializer that uses a global key derived from the computer's SID. /// Header contains additional random entropy as a salt that is used in decryption. /// public static class GlobalEncryptedSerializer { internal static KeyInfo KeyInfoV1 => new(FormatVersion.V1, 32, 16, 300); internal static KeyInfo KeyInfoV2 => new(FormatVersion.V2, 32, 16, 300); private static byte[] HeaderPrefixV2 => [0x4C, 0x4B, 0x1F, 0x45, 0x5C, 0x02, 0x00]; internal readonly record struct KeyInfo(FormatVersion Version, int KeySize, int SaltSize, int Iterations); internal enum FormatVersion : byte { /// /// Version 1 /// Original format, no header. /// File: [(16 bytes salt), (Encrypted json data)] /// V1 = 1, /// /// Version 2+ /// Header: [4C, 4B, 1F, 45, 5C, ??, 00] where ?? is the version byte. /// File: [(Header), (SaltSize bytes salt), (Encrypted json data)] /// V2 = 2 } [Serializable] [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct HeaderV2 { private unsafe fixed byte Prefix[7]; public int KeySize; public int SaltSize; public int Iterations; public HeaderV2() { unsafe { Prefix[0] = 0x4C; Prefix[1] = 0x4B; Prefix[2] = 0x1F; Prefix[3] = 0x45; Prefix[4] = 0x5C; Prefix[5] = 0x02; Prefix[6] = 0x00; } } } public static T Deserialize(ReadOnlySpan data) { // Header prefix, use v2 if (data.StartsWith(HeaderPrefixV2)) { var json = DeserializeToBytesV2(data); return JsonSerializer.Deserialize(json) ?? throw new InvalidOperationException("Deserialize returned null"); } // No header, use v1 else { var json = DeserializeToBytesV1(data); return JsonSerializer.Deserialize(json) ?? throw new InvalidOperationException("Deserialize returned null"); } } public static byte[] Serialize(T obj) { return Serialize(obj, KeyInfoV2); } internal static byte[] Serialize(T obj, KeyInfo keyInfo) { switch (keyInfo.Version) { case <= FormatVersion.V1: { var json = JsonSerializer.SerializeToUtf8Bytes(obj); return SerializeToBytesV1(json); } case >= FormatVersion.V2: { var json = JsonSerializer.SerializeToUtf8Bytes(obj); return SerializeToBytesV2(json, keyInfo); } } } private static byte[] SerializeToBytesV1(byte[] data) { // Get encrypted bytes and salt var password = GetComputerKeyPhrase(KeyInfoV1.Version); var (encrypted, salt) = EncryptBytes(data, password, KeyInfoV1); // Prepend salt to encrypted json var fileData = salt.Concat(encrypted); return fileData.ToArray(); } private static byte[] SerializeToBytesV2(byte[] data, KeyInfo keyInfo) { // Create header var headerSize = Marshal.SizeOf(); var header = new HeaderV2 { KeySize = keyInfo.KeySize, SaltSize = keyInfo.SaltSize, Iterations = keyInfo.Iterations }; var headerBytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref header, 1)); Debug.Assert(headerBytes.Length == headerSize); // Get salt + encrypted json var password = GetComputerKeyPhrase(keyInfo.Version); var (encrypted, salt) = EncryptBytes(data, password, keyInfo); Debug.Assert(salt.Length == keyInfo.SaltSize); // Write result as [header, salt, encrypted] var result = new byte[headerBytes.Length + salt.Length + encrypted.Length]; headerBytes.CopyTo(result.AsSpan(0, headerSize)); salt.CopyTo(result.AsSpan(headerSize, keyInfo.SaltSize)); encrypted.CopyTo(result.AsSpan(headerSize + keyInfo.SaltSize)); return result; } private static byte[] DeserializeToBytesV1(ReadOnlySpan data) { var keyInfo = KeyInfoV1; // Get salt from start of file var salt = data[..keyInfo.SaltSize].ToArray(); // Get encrypted json from rest of file var encryptedJson = data[keyInfo.SaltSize..]; var password = GetComputerKeyPhrase(keyInfo.Version); return DecryptBytes(encryptedJson, salt, password, keyInfo); } private static byte[] DeserializeToBytesV2(ReadOnlySpan data) { // Read header var headerSize = Marshal.SizeOf(); var header = MemoryMarshal.Read(data[..Marshal.SizeOf()]); // Read Salt var salt = data[headerSize..(headerSize + header.SaltSize)].ToArray(); // Rest of data is encrypted json var encryptedData = data[(headerSize + header.SaltSize)..]; var keyInfo = new KeyInfo(FormatVersion.V2, header.KeySize, header.SaltSize, header.Iterations); var password = GetComputerKeyPhrase(keyInfo.Version); return DecryptBytes(encryptedData, salt, password, keyInfo); } private static string? GetComputerSid(FormatVersion version) { return version switch { FormatVersion.V1 => new DeviceIdBuilder() .AddMachineName() .AddOsVersion() .OnWindows( windows => windows.AddProcessorId().AddMotherboardSerialNumber().AddSystemDriveSerialNumber() ) .OnLinux(linux => linux.AddMotherboardSerialNumber().AddSystemDriveSerialNumber()) .OnMac(mac => mac.AddSystemDriveSerialNumber().AddPlatformSerialNumber()) .ToString(), // v2: Removed OsVersion since it's updated often on macOS FormatVersion.V2 => new DeviceIdBuilder() .AddMachineName() .OnWindows( windows => windows.AddProcessorId().AddMotherboardSerialNumber().AddSystemDriveSerialNumber() ) .OnLinux(linux => linux.AddMotherboardSerialNumber().AddSystemDriveSerialNumber()) .OnMac(mac => mac.AddSystemDriveSerialNumber().AddPlatformSerialNumber()) .ToString(), _ => throw new ArgumentOutOfRangeException(nameof(version)) }; } private static SecureString GetComputerKeyPhrase(FormatVersion version) { var keySource = GetComputerSid(version); // If no sid, use username as fallback keySource ??= Environment.UserName; // XOR with fixed constant const string keyPhrase = "StabilityMatrix"; var result = new SecureString(); for (var i = 0; i < keySource.Length; i++) { result.AppendChar((char)(keySource[i] ^ keyPhrase[i % keyPhrase.Length])); } return result; } private static byte[] GenerateSalt(int length) { return RandomNumberGenerator.GetBytes(length); } private static byte[] DeriveKey(SecureString password, byte[] salt, int iterations, int keyLength) { var ptr = Marshal.SecureStringToBSTR(password); try { var length = Marshal.ReadInt32(ptr, -4); var passwordByteArray = new byte[length]; var handle = GCHandle.Alloc(passwordByteArray, GCHandleType.Pinned); try { for (var i = 0; i < length; i++) { passwordByteArray[i] = Marshal.ReadByte(ptr, i); } using var rfc2898 = new Rfc2898DeriveBytes( passwordByteArray, salt, iterations, HashAlgorithmName.SHA512 ); return rfc2898.GetBytes(keyLength); } finally { Array.Clear(passwordByteArray, 0, passwordByteArray.Length); handle.Free(); } } finally { Marshal.ZeroFreeBSTR(ptr); } } internal static (byte[] EncryptedData, byte[] Salt) EncryptBytes( byte[] data, SecureString password, KeyInfo keyInfo ) { var salt = GenerateSalt(keyInfo.SaltSize); var key = DeriveKey(password, salt, keyInfo.Iterations, keyInfo.KeySize); using var aes = Aes.Create(); aes.Key = key; aes.IV = salt; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; var transform = aes.CreateEncryptor(); return (transform.TransformFinalBlock(data, 0, data.Length), salt); } internal static byte[] DecryptBytes( ReadOnlySpan encryptedData, byte[] salt, SecureString password, KeyInfo keyInfo ) { var key = DeriveKey(password, salt, keyInfo.Iterations, keyInfo.KeySize); using var aes = Aes.Create(); aes.Key = key; aes.IV = salt; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; var transform = aes.CreateDecryptor(); return transform.TransformFinalBlock(encryptedData.ToArray(), 0, encryptedData.Length); } } ================================================ FILE: StabilityMatrix.Core/Models/HybridModelFile.cs ================================================ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Models; /// /// Model file union that may be remote or local. /// [Localizable(false)] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public record HybridModelFile : ISearchText, IDownloadableResource { /// /// Singleton instance of that represents use of a default model. /// public static HybridModelFile Default { get; } = FromRemote("@default"); /// /// Singleton instance of that represents no model. /// public static HybridModelFile None { get; } = FromRemote("@none"); public string? RemoteName { get; init; } public LocalModelFile? Local { get; init; } /// /// Downloadable model information. /// public RemoteResource? DownloadableResource { get; init; } public HybridModelType Type { get; init; } [MemberNotNullWhen(true, nameof(RemoteName))] [JsonIgnore] public bool IsRemote => RemoteName != null; [MemberNotNullWhen(true, nameof(DownloadableResource))] public bool IsDownloadable => DownloadableResource != null; [JsonIgnore] public string RelativePath => Type switch { HybridModelType.Local => Local!.RelativePathFromSharedFolder, HybridModelType.Remote => RemoteName!, HybridModelType.Downloadable => DownloadableResource!.Value.RelativeDirectory == null ? DownloadableResource!.Value.FileName : Path.Combine( DownloadableResource!.Value.RelativeDirectory, DownloadableResource!.Value.FileName ), HybridModelType.None => throw new InvalidOperationException(), _ => throw new ArgumentOutOfRangeException(), }; [JsonIgnore] public string FileName => Path.GetFileName(RelativePath); [JsonIgnore] public string ShortDisplayName { get { if (IsNone) { return "None"; } if (IsDefault) { return "Default"; } if (ReferenceEquals(this, RemoteModels.ControlNetReferenceOnlyModel)) { return "Reference Only"; } var fileName = Path.GetFileNameWithoutExtension(RelativePath); if ( !fileName.Equals("diffusion_pytorch_model", StringComparison.OrdinalIgnoreCase) && !fileName.Equals("pytorch_model", StringComparison.OrdinalIgnoreCase) && !fileName.Equals("ip_adapter", StringComparison.OrdinalIgnoreCase) ) { return Path.GetFileNameWithoutExtension(RelativePath); } // show a friendlier name when models have the same name like ip_adapter or diffusion_pytorch_model var directoryName = Path.GetDirectoryName(RelativePath); if (directoryName is null) return Path.GetFileNameWithoutExtension(RelativePath); var lastIndex = directoryName.LastIndexOf(Path.DirectorySeparatorChar); if (lastIndex < 0) return $"{fileName} ({directoryName})"; var parentDirectoryName = directoryName.Substring(lastIndex + 1); return $"{fileName} ({parentDirectoryName})"; } } [JsonIgnore] public string SortKey => Local?.ConnectedModelInfo != null ? $"{Local.ConnectedModelInfo.ModelName}{Local.ConnectedModelInfo.VersionName}" : ShortDisplayName; public static HybridModelFile FromLocal(LocalModelFile local) { return new HybridModelFile { Local = local, Type = HybridModelType.Local }; } public static HybridModelFile FromRemote(string remoteName) { return new HybridModelFile { RemoteName = remoteName, Type = HybridModelType.Remote }; } public static HybridModelFile FromDownloadable(RemoteResource resource) { return new HybridModelFile { DownloadableResource = resource, Type = HybridModelType.Downloadable }; } public string GetId() { return $"{RelativePath.NormalizePathSeparators()};{IsNone};{IsDefault}"; } /// /// Special Comparer that compares Remote Name and Local RelativePath, /// used for letting remote models not override local models with more metadata. /// Pls do not use for other stuff. /// private sealed class RemoteNameLocalEqualityComparer : IEqualityComparer { public bool Equals(HybridModelFile? x, HybridModelFile? y) { if (ReferenceEquals(x, y)) return true; if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; if (!Equals(x.RelativePath.NormalizePathSeparators(), y.RelativePath.NormalizePathSeparators())) return false; // This equality affects replacements of remote over local models // We want local and remote models to be considered equal if they have the same relative path // But 2 local models with the same path but different config paths should be considered different return !(x.Type == y.Type && x.Local?.ConfigFullPath != y.Local?.ConfigFullPath); } public int GetHashCode(HybridModelFile obj) { return HashCode.Combine(obj.IsNone, obj.IsDefault, obj.RelativePath); } } /// /// Actual general purpose equality comparer. /// Use this for general equality checks :) /// private sealed class EqualityComparer : IEqualityComparer { public bool Equals(HybridModelFile? x, HybridModelFile? y) { if (ReferenceEquals(x, y)) return true; if (ReferenceEquals(x, null)) return false; if (ReferenceEquals(y, null)) return false; if (x.GetType() != y.GetType()) return false; if (!Equals(x.RelativePath.NormalizePathSeparators(), y.RelativePath.NormalizePathSeparators())) return false; return Equals(x.Type, y.Type) && x.RemoteName == y.RemoteName && x.Local?.ConfigFullPath == y.Local?.ConfigFullPath && x.Local?.ConnectedModelInfo == y.Local?.ConnectedModelInfo; } public int GetHashCode(HybridModelFile obj) { return HashCode.Combine( obj.IsNone, obj.IsDefault, obj.RelativePath, obj.RemoteName, obj.Local?.ConfigFullPath, obj.Local?.ConnectedModelInfo ); } } /// /// Whether this instance is the default model. /// public bool IsDefault => ReferenceEquals(this, Default); /// /// Whether this instance is no model. /// public bool IsNone => ReferenceEquals(this, None); /// /// Actual general purpose equality comparer. /// Use this for general equality checks :) /// public static IEqualityComparer Comparer { get; } = new EqualityComparer(); /// /// Special Comparer that compares Remote Name and Local RelativePath, /// used for letting remote models not override local models with more metadata. /// Pls do not use for other stuff. /// public static IEqualityComparer RemoteLocalComparer { get; } = new RemoteNameLocalEqualityComparer(); [JsonIgnore] public string SearchText => SortKey; } ================================================ FILE: StabilityMatrix.Core/Models/HybridModelType.cs ================================================ namespace StabilityMatrix.Core.Models; public enum HybridModelType { None, Local, Remote, Downloadable } ================================================ FILE: StabilityMatrix.Core/Models/IContextAction.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models; [JsonDerivedType(typeof(CivitPostDownloadContextAction), "CivitPostDownload")] [JsonDerivedType(typeof(ModelPostDownloadContextAction), "ModelPostDownload")] public interface IContextAction { object? Context { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/IDownloadableResource.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models; /// /// Interface for items that may have a downloadable resource. /// public interface IDownloadableResource { /// /// Downloadable resource information. /// RemoteResource? DownloadableResource { get; } [MemberNotNullWhen(true, nameof(DownloadableResource))] bool IsDownloadable => DownloadableResource is not null; } ================================================ FILE: StabilityMatrix.Core/Models/IHandleNavigation.cs ================================================ namespace StabilityMatrix.Core.Models; public interface IHandleNavigation { bool GoBack(); } ================================================ FILE: StabilityMatrix.Core/Models/ISearchText.cs ================================================ namespace StabilityMatrix.Core.Models; public interface ISearchText { string SearchText { get; } } ================================================ FILE: StabilityMatrix.Core/Models/IndexCollection.cs ================================================ using System.Reactive.Linq; using DynamicData; using DynamicData.Binding; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models; public class IndexCollection where TKey : notnull { private readonly IImageIndexService imageIndexService; public string? RelativePath { get; set; } public SourceCache ItemsSource { get; } /// /// Observable Collection of indexed items /// public IObservableCollection Items { get; } = new ObservableCollectionExtended(); public IndexCollection( IImageIndexService imageIndexService, Func keySelector, Func>, IObservable>>? transform = null ) { this.imageIndexService = imageIndexService; ItemsSource = new SourceCache(keySelector); var source = ItemsSource.Connect().DeferUntilLoaded(); if (transform is not null) { source = transform(source); } source.Bind(Items).ObserveOn(SynchronizationContext.Current).Subscribe(); } public void Add(TObject item) { ItemsSource.AddOrUpdate(item); } public void Remove(TObject item) { ItemsSource.Remove(item); } public void RemoveKey(TKey key) { ItemsSource.RemoveKey(key); } } ================================================ FILE: StabilityMatrix.Core/Models/Inference/InferenceProjectType.cs ================================================ namespace StabilityMatrix.Core.Models.Inference; public enum InferenceProjectType { Unknown, TextToImage, ImageToImage, Inpainting, Upscale, ImageToVideo, FluxTextToImage, WanTextToVideo, WanImageToVideo, } ================================================ FILE: StabilityMatrix.Core/Models/Inference/LayerDiffuseMode.cs ================================================ using System.ComponentModel.DataAnnotations; namespace StabilityMatrix.Core.Models.Inference; public enum LayerDiffuseMode { /// /// The layer diffuse mode is not set. /// [Display(Name = "None")] None, /// /// Generate foreground only with transparency. SD1.5 /// [Display(Name = "(SD 1.5) Generate Foreground with Transparency")] GenerateForegroundWithTransparencySD15, /// /// Generate foreground only with transparency. SDXL /// [Display(Name = "(SDXL) Generate Foreground with Transparency")] GenerateForegroundWithTransparencySDXL, } ================================================ FILE: StabilityMatrix.Core/Models/Inference/ModelLoader.cs ================================================ using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.Inference; public enum ModelLoader { [StringValue("Default")] Default, [StringValue("GGUF")] Gguf, [StringValue("nf4")] Nf4, [StringValue("UNet")] Unet } ================================================ FILE: StabilityMatrix.Core/Models/Inference/ModuleApplyStepTemporaryArgs.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; namespace StabilityMatrix.Core.Models.Inference; public class ModuleApplyStepTemporaryArgs { /// /// Temporary Primary apply step, used by ControlNet ReferenceOnly which changes the latent. /// public PrimaryNodeConnection? Primary { get; set; } public VAENodeConnection? PrimaryVAE { get; set; } /// /// Used by Reference-Only ControlNet to indicate that has been batched. /// public bool IsPrimaryTempBatched { get; set; } /// /// When is true, this is the index of the temp batch to pick after sampling. /// public int PrimaryTempBatchPickIndex { get; set; } public Dictionary Models { get; set; } = new() { ["Base"] = new ModelConnections("Base"), ["Refiner"] = new ModelConnections("Refiner") }; public ModelConnections Base => Models["Base"]; public ModelConnections Refiner => Models["Refiner"]; public ConditioningConnections GetRefinerOrBaseConditioning() { return Refiner.Conditioning ?? Base.Conditioning ?? throw new NullReferenceException("No Refiner or Base Conditioning"); } public ModelNodeConnection GetRefinerOrBaseModel() { return Refiner.Model ?? Base.Model ?? throw new NullReferenceException("No Refiner or Base Model"); } public VAENodeConnection GetDefaultVAE() { return PrimaryVAE ?? Refiner.VAE ?? Base.VAE ?? throw new NullReferenceException("No VAE"); } } ================================================ FILE: StabilityMatrix.Core/Models/InferenceDefaults.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Models; public record InferenceDefaults { public ComfySampler? Sampler { get; set; } public ComfyScheduler? Scheduler { get; set; } public int Steps { get; set; } public double CfgScale { get; set; } public int Width { get; set; } public int Height { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/InferenceRunCustomPromptEventArgs.cs ================================================ using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Core.Models; public class InferenceQueueCustomPromptEventArgs : EventArgs { public ComfyNodeBuilder Builder { get; } = new(); public NodeDictionary Nodes => Builder.Nodes; public long? SeedOverride { get; init; } public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = []; } ================================================ FILE: StabilityMatrix.Core/Models/InstalledPackage.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models; /// /// Profile information for a user-installed package. /// public class InstalledPackage : IJsonOnDeserialized { // Unique ID for the installation public Guid Id { get; set; } // User defined name public string? DisplayName { get; set; } // Package name public string? PackageName { get; set; } // Package version public InstalledPackageVersion? Version { get; set; } /// /// Relative path from the library root. /// public string? LibraryPath { get; set; } /// /// Full path to the package, using LibraryPath and GlobalConfig.LibraryDir. /// [JsonIgnore] public string? FullPath => LibraryPath != null ? System.IO.Path.Combine(GlobalConfig.LibraryDir, LibraryPath) : null; public string? LaunchCommand { get; set; } public List? LaunchArgs { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; } public bool UpdateAvailable { get; set; } public bool DontCheckForUpdates { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] public TorchIndex? PreferredTorchIndex { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] [Obsolete("Use PreferredTorchIndex instead. (Kept for migration)")] public TorchIndex? PreferredTorchVersion { get; set; } [JsonConverter(typeof(JsonStringEnumConverter))] public SharedFolderMethod? PreferredSharedFolderMethod { get; set; } public bool UseSharedOutputFolder { get; set; } public List? ExtraExtensionManifestUrls { get; set; } public List? PipOverrides { get; set; } public string PythonVersion { get; set; } = PyInstallationManager.Python_3_10_11.StringValue; /// /// Get the launch args host option value. /// public string? GetLaunchArgsHost() { var hostOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "--host"); if (hostOption?.OptionValue != null) { return hostOption.OptionValue as string; } return hostOption?.DefaultValue as string; } /// /// Get the launch args port option value. /// public string? GetLaunchArgsPort() { var portOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "--port"); if (portOption?.OptionValue != null) { return portOption.OptionValue as string; } return portOption?.DefaultValue as string; } /// /// Get the path as a relative sub-path of the relative path. /// If not a sub-path, return null. /// public static string? GetSubPath(string relativeTo, string path) { var relativePath = System.IO.Path.GetRelativePath(relativeTo, path); // GetRelativePath returns the path if it's not relative if (relativePath == path) return null; // Further check if the path is a sub-path of the library var isSubPath = relativePath != "." && relativePath != ".." && !relativePath.StartsWith(".." + System.IO.Path.DirectorySeparatorChar) && !System.IO.Path.IsPathRooted(relativePath); return isSubPath ? relativePath : null; } public static IEqualityComparer Comparer { get; } = new PropertyComparer(p => p.Id); protected bool Equals(InstalledPackage other) { return Id.Equals(other.Id); } public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == this.GetType() && Equals((InstalledPackage)obj); } public override int GetHashCode() { return Id.GetHashCode(); } #region Migration / Obsolete // Old type absolute path [Obsolete("Use LibraryPath instead. (Kept for migration)")] public string? Path { get; set; } // Old type versions [Obsolete("Use Version instead. (Kept for migration)")] public string? PackageVersion { get; set; } [Obsolete("Use Version instead. (Kept for migration)")] public string? InstalledBranch { get; set; } [Obsolete("Use Version instead. (Kept for migration)")] public string? DisplayVersion { get; set; } /// /// Migrates the old Path to the new LibraryPath. /// If libraryDirectory is null, GlobalConfig.LibraryDir is used. /// /// True if the path was migrated, false otherwise. public bool TryPureMigratePath(string? libraryDirectory = null) { #pragma warning disable CS0618 var oldPath = Path; #pragma warning restore CS0618 if (oldPath == null) return false; // Check if the path is a sub-path of the library var library = libraryDirectory ?? GlobalConfig.LibraryDir; var relativePath = GetSubPath(library, oldPath); // If so we migrate without any IO operations if (relativePath != null) { LibraryPath = relativePath; #pragma warning disable CS0618 Path = null; #pragma warning restore CS0618 return true; } return false; } /// /// Check if the old Path can be migrated to the new LibraryPath. /// /// /// public bool CanPureMigratePath(string? libraryDirectory = null) { #pragma warning disable CS0618 var oldPath = Path; #pragma warning restore CS0618 if (oldPath == null) return false; // Check if the path is a sub-path of the library var library = libraryDirectory ?? GlobalConfig.LibraryDir; var relativePath = GetSubPath(library, oldPath); return relativePath != null; } /// /// Migrate the old Path to the new LibraryPath. /// If libraryDirectory is null, GlobalConfig.LibraryDir is used. /// Will move the package directory to Library/Packages if not relative. /// public async Task MigratePath(string? libraryDirectory = null) { #pragma warning disable CS0618 var oldPath = Path; #pragma warning restore CS0618 if (oldPath == null) return; var libDir = libraryDirectory ?? GlobalConfig.LibraryDir; // if old package Path is same as new library, return if (oldPath.Replace(DisplayName, "") == libDir) { // Update the paths #pragma warning disable CS0618 Path = null; #pragma warning restore CS0618 LibraryPath = System.IO.Path.Combine("Packages", DisplayName); return; } // Try using pure migration first if (TryPureMigratePath(libraryDirectory)) return; // If not, we need to move the package directory var packageFolderName = new DirectoryInfo(oldPath).Name; // Get the new Library/Packages path var library = libraryDirectory ?? GlobalConfig.LibraryDir; var newPackagesDir = System.IO.Path.Combine(library, "Packages"); // Get the new target path var newPackagePath = System.IO.Path.Combine(newPackagesDir, packageFolderName); // Ensure it is not already there, if so, add a suffix until it's not var suffix = 2; while (Directory.Exists(newPackagePath)) { newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}"); suffix++; } // Move the package directory await Task.Run(() => Utilities.CopyDirectory(oldPath, newPackagePath, true)); // Update the paths #pragma warning disable CS0618 Path = null; #pragma warning restore CS0618 LibraryPath = System.IO.Path.Combine("Packages", packageFolderName); } public void OnDeserialized() { #pragma warning disable CS0618 // Type or member is obsolete // handle TorchIndex migration PreferredTorchIndex ??= PreferredTorchVersion; // Handle version migration if (Version != null) return; if (string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)) { // release mode Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion, IsPrerelease = false }; } else if (!string.IsNullOrWhiteSpace(PackageVersion)) { Version = new InstalledPackageVersion { InstalledBranch = InstalledBranch, InstalledCommitSha = PackageVersion, IsPrerelease = false }; } #pragma warning restore CS0618 // Type or member is obsolete } #endregion } ================================================ FILE: StabilityMatrix.Core/Models/InstalledPackageVersion.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models; public class InstalledPackageVersion { public string? InstalledReleaseVersion { get; set; } public string? InstalledBranch { get; set; } public string? InstalledCommitSha { get; set; } public bool IsPrerelease { get; set; } [JsonIgnore] public bool IsReleaseMode => string.IsNullOrWhiteSpace(InstalledBranch); [JsonIgnore] public string DisplayVersion => ( IsReleaseMode ? InstalledReleaseVersion : string.IsNullOrWhiteSpace(InstalledCommitSha) ? InstalledBranch : $"{InstalledBranch}@{InstalledCommitSha[..7]}" ) ?? string.Empty; } ================================================ FILE: StabilityMatrix.Core/Models/LaunchOption.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models; public class LaunchOption { public required string Name { get; init; } public LaunchOptionType Type { get; init; } = LaunchOptionType.Bool; [JsonIgnore] public object? DefaultValue { get; init; } [JsonIgnore] public bool HasDefaultValue => DefaultValue != null; [JsonConverter(typeof(LaunchOptionValueJsonConverter))] public object? OptionValue { get; set; } /// /// Checks if the option has no user entered value, /// or that the value is the same as the default value. /// /// public bool IsEmptyOrDefault() { return Type switch { LaunchOptionType.Bool => OptionValue == null, LaunchOptionType.Int => OptionValue == null || (int?) OptionValue == (int?) DefaultValue, LaunchOptionType.String => OptionValue == null || (string?) OptionValue == (string?) DefaultValue, _ => throw new ArgumentOutOfRangeException() }; } /// /// Parses a string value to the correct type for the option. /// This returned object can be assigned to OptionValue. /// public static object? ParseValue(string? value, LaunchOptionType type) { return type switch { LaunchOptionType.Bool => bool.TryParse(value, out var boolValue) ? boolValue : null, LaunchOptionType.Int => int.TryParse(value, out var intValue) ? intValue : null, LaunchOptionType.String => value, _ => throw new ArgumentException($"Unknown option type {type}") }; } /// /// Convert the option value to a string that can be passed to a Process. /// /// /// public string? ToArgString() { // Convert to string switch (Type) { case LaunchOptionType.Bool: return (bool?) OptionValue == true ? Name : null; case LaunchOptionType.Int: return (int?) OptionValue != null ? $"{Name} {OptionValue}" : null; case LaunchOptionType.String: var valueString = (string?) OptionValue; // Special case empty string name to not do quoting (for custom launch args) if (Name == "") { return valueString; } return string.IsNullOrWhiteSpace(valueString) ? null : $"{Name} {ProcessRunner.Quote(valueString)}"; default: throw new ArgumentOutOfRangeException(); } } } ================================================ FILE: StabilityMatrix.Core/Models/LaunchOptionCard.cs ================================================ using System.Collections.Immutable; using System.Diagnostics; namespace StabilityMatrix.Core.Models; public readonly record struct LaunchOptionCard { public required string Title { get; init; } public required LaunchOptionType Type { get; init; } public required IReadOnlyList Options { get; init; } public string? Description { get; init; } public static LaunchOptionCard FromDefinition(LaunchOptionDefinition definition) { return new LaunchOptionCard { Title = definition.Name, Description = definition.Description, Type = definition.Type, Options = definition.Options.Select(s => { var option = new LaunchOption { Name = s, Type = definition.Type, DefaultValue = definition.DefaultValue }; return option; }).ToImmutableArray() }; } /// /// Yield LaunchOptionCards given definitions and launch args to load /// /// /// /// /// public static IEnumerable FromDefinitions( IEnumerable definitions, IEnumerable launchArgs) { // During card creation, store dict of options with initial values var initialOptions = new Dictionary(); // To dictionary ignoring duplicates var launchArgsDict = launchArgs .ToLookup(launchArg => launchArg.Name) .ToDictionary( group => group.Key, group => group.First() ); // Create cards foreach (var definition in definitions) { // Check that non-bool types have exactly one option if (definition.Type != LaunchOptionType.Bool && definition.Options.Count != 1) { throw new InvalidOperationException( $"Definition: '{definition.Name}' has {definition.Options.Count} options," + $" it must have exactly 1 option for non-bool types"); } // Store initial values if (definition.InitialValue != null) { // For bool types, initial value can be string (single/multiple options) or bool (single option) if (definition.Type == LaunchOptionType.Bool) { // For single option, check bool if (definition.Options.Count == 1 && definition.InitialValue is bool boolValue) { initialOptions[definition.Options.First()] = boolValue; } else { // For single/multiple options (string only) var option = definition.Options.FirstOrDefault(opt => opt.Equals(definition.InitialValue)); if (option == null) { throw new InvalidOperationException( $"Definition '{definition.Name}' has InitialValue of '{definition.InitialValue}', but it was not found in options:" + $" '{string.Join(",", definition.Options)}'"); } initialOptions[option] = true; } } else { // Otherwise store initial value for first option initialOptions[definition.Options.First()] = definition.InitialValue; } } // Create the new card var card = new LaunchOptionCard { Title = definition.Name, Description = definition.Description, Type = definition.Type, Options = definition.Options.Select(s => { // Parse defaults and user loaded values here var userOption = launchArgsDict.GetValueOrDefault(s); var userValue = userOption?.OptionValue; // If no user value, check set initial value if (userValue is null) { var initialValue = initialOptions.GetValueOrDefault(s); userValue ??= initialValue; Debug.WriteLineIf(initialValue != null, $"Using initial value {initialValue} for option {s}"); } var option = new LaunchOption { Name = s, Type = definition.Type, DefaultValue = definition.DefaultValue, OptionValue = userValue }; return option; }).ToImmutableArray() }; yield return card; } } } ================================================ FILE: StabilityMatrix.Core/Models/LaunchOptionDefinition.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models; /// /// Defines a launch option for a BasePackage. /// public record LaunchOptionDefinition { /// /// Name or title of the card. /// public required string Name { get; init; } /// /// Type of the option. "bool", "int", or "string" /// - "bool" can supply 1 or more flags in the Options list (e.g. ["--api", "--lowvram"]) /// - "int" and "string" should supply a single flag in the Options list (e.g. ["--width"], ["--api"]) /// public required LaunchOptionType Type { get; init; } /// /// Optional description of the option. /// public string? Description { get; init; } /// /// Server-side default for the option. (Ignored for launch and saving if value matches) /// Use `InitialValue` to provide a default that is set as the user value and used for launch. /// public object? DefaultValue { get; init; } /// /// Initial value for the option if no set value is available, set as the user value on save. /// Use `DefaultValue` to provide a server-side default that is ignored for launch and saving. /// public object? InitialValue { get; init; } /// /// Minimum number of selected options (for validation) /// public int? MinSelectedOptions { get; init; } /// /// Maximum number of selected options (for validation) /// public int? MaxSelectedOptions { get; init; } /// /// List of option flags like "--api", "--lowvram", etc. /// public List Options { get; init; } = new(); /// /// Constant for the Extras launch option. /// [JsonIgnore] public static LaunchOptionDefinition Extras => new() { Name = "Extra Launch Arguments", Type = LaunchOptionType.String, Options = new List {""} }; } ================================================ FILE: StabilityMatrix.Core/Models/LaunchOptionType.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models; [JsonConverter(typeof(JsonStringEnumConverter))] public enum LaunchOptionType { Bool, String, Int } ================================================ FILE: StabilityMatrix.Core/Models/LicenseInfo.cs ================================================ namespace StabilityMatrix.Core.Models; public class LicenseInfo { public string PackageName { get; set; } public string PackageVersion { get; set; } public string PackageUrl { get; set; } public string Copyright { get; set; } public List Authors { get; set; } public string Description { get; set; } public string LicenseUrl { get; set; } public string LicenseType { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/LoadState.cs ================================================ namespace StabilityMatrix.Core.Models; public enum LoadState { NotLoaded, Loading, Loaded, } ================================================ FILE: StabilityMatrix.Core/Models/ModelPostDownloadContextAction.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models; public class ModelPostDownloadContextAction : IContextAction { /// public object? Context { get; set; } [SuppressMessage("Performance", "CA1822:Mark members as static")] public void Invoke(IModelIndexService modelIndexService) { // Request reindex modelIndexService.BackgroundRefreshIndex(); } } ================================================ FILE: StabilityMatrix.Core/Models/ObservableHashSet.cs ================================================ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; namespace StabilityMatrix.Core.Models; /// /// Represents a observable hash set of values. /// /// The type of elements in the hash set. /// /// An that also implements – /// perfect when you need *unique* items **and** change notifications /// (e.g. for XAML bindings or ReactiveUI/DynamicData pipelines). /// public class ObservableHashSet : ObservableCollection, ISet { private readonly HashSet set; #region ░░░ Constructors ░░░ public ObservableHashSet() : this((IEqualityComparer?)null) { } public ObservableHashSet(IEqualityComparer? comparer) : base() { set = new HashSet(comparer ?? EqualityComparer.Default); } public ObservableHashSet(IEnumerable collection) : this(collection, null) { } public ObservableHashSet(IEnumerable collection, IEqualityComparer? comparer) : this(comparer) { foreach (var item in collection) Add(item); // guarantees uniqueness + raises events } #endregion #region ░░░ Overrides that enforce set semantics ░░░ /// /// Called by (and therefore by LINQ’s /// Add extension, by , etc.). /// We only insert if the value was *not* already present in the set. /// protected override void InsertItem(int index, T item) { if (set.Add(item)) // true == unique base.InsertItem(index, item); // fires events // duplicate → ignore silently (same behaviour as HashSet) } protected override void SetItem(int index, T item) { var existing = this[index]; // no-op if same reference/value if (EqualityComparer.Default.Equals(existing, item)) return; // attempting to “replace” with a value already in the set → ignore if (set.Contains(item)) return; set.Remove(existing); set.Add(item); base.SetItem(index, item); // fires events } protected override void RemoveItem(int index) { set.Remove(this[index]); base.RemoveItem(index); // fires events } protected override void ClearItems() { set.Clear(); base.ClearItems(); // fires events } #endregion #region ░░░ ISet explicit implementation ░░░ // Most operations delegate to the HashSet for the heavy lifting, // THEN synchronise the ObservableCollection so that UI bindings // get the right notifications. bool ISet.Add(T item) => !set.Contains(item) && AddAndReturnTrue(item); private bool AddAndReturnTrue(T item) { base.Add(item); return true; } void ISet.ExceptWith(IEnumerable other) { ArgumentNullException.ThrowIfNull(other); foreach (var item in other) _ = Remove(item); // Remove() already updates both collections } void ISet.IntersectWith(IEnumerable other) { ArgumentNullException.ThrowIfNull(other); var keep = new HashSet(other, set.Comparer); for (var i = Count - 1; i >= 0; i--) if (!keep.Contains(this[i])) RemoveItem(i); } void ISet.SymmetricExceptWith(IEnumerable other) { ArgumentNullException.ThrowIfNull(other); var toToggle = new HashSet(other, set.Comparer); foreach (var item in toToggle) if (!Remove(item)) Add(item); } void ISet.UnionWith(IEnumerable other) { ArgumentNullException.ThrowIfNull(other); foreach (var item in other) _ = ((ISet)this).Add(item); // uses our Add logic } // The pure-query methods just delegate to HashSet: bool ISet.IsSubsetOf(IEnumerable other) => set.IsSubsetOf(other); bool ISet.IsSupersetOf(IEnumerable other) => set.IsSupersetOf(other); bool ISet.IsProperSubsetOf(IEnumerable other) => set.IsProperSubsetOf(other); bool ISet.IsProperSupersetOf(IEnumerable other) => set.IsProperSupersetOf(other); bool ISet.Overlaps(IEnumerable other) => set.Overlaps(other); public bool SetEquals(IEnumerable other) => set.SetEquals(other); #endregion #region ░░░ Useful helpers ░░░ public new bool Contains(T item) => set.Contains(item); /// /// Returns a copy of the internal . /// Handy when you need fast look-ups without exposing mutability. /// public HashSet ToHashSet() => new(set, set.Comparer); public void AddRange(IEnumerable items) { ArgumentNullException.ThrowIfNull(items); // 1. Keep only values that are truly new for this set var newItems = new List(); foreach (var item in items) if (set.Add(item)) // true == not yet present newItems.Add(item); if (newItems.Count == 0) return; // nothing to do CheckReentrancy(); // ObservableCollection helper // 2. Append to the internal Items list *without* raising events yet int startIdx = Items.Count; foreach (var item in newItems) Items.Add(item); // 3. Fire a single consolidated notification OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); // choose either a single "Add" with the batch… OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, newItems, // the items added startIdx ) ); // starting index // …or, if you want absolute safety for all consumers, // you could raise "Reset" instead: // OnCollectionChanged(new NotifyCollectionChangedEventArgs( // NotifyCollectionChangedAction.Reset)); } #endregion } ================================================ FILE: StabilityMatrix.Core/Models/OrderedValue.cs ================================================ namespace StabilityMatrix.Core.Models; public readonly record struct OrderedValue(int Order, TValue Value) : IComparable>, IComparable { private sealed class OrderRelationalComparer : IComparer> { public int Compare(OrderedValue x, OrderedValue y) { return x.Order.CompareTo(y.Order); } } public static IComparer> OrderComparer { get; } = new OrderRelationalComparer(); public int CompareTo(OrderedValue other) { return Order.CompareTo(other.Order); } public int CompareTo(object? obj) { if (ReferenceEquals(null, obj)) return 1; return obj is OrderedValue other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(OrderedValue)}"); } public static bool operator <(OrderedValue left, OrderedValue right) { return left.CompareTo(right) < 0; } public static bool operator >(OrderedValue left, OrderedValue right) { return left.CompareTo(right) > 0; } public static bool operator <=(OrderedValue left, OrderedValue right) { return left.CompareTo(right) <= 0; } public static bool operator >=(OrderedValue left, OrderedValue right) { return left.CompareTo(right) >= 0; } } ================================================ FILE: StabilityMatrix.Core/Models/PackageDifficulty.cs ================================================ namespace StabilityMatrix.Core.Models; public enum PackageDifficulty { ReallyRecommended = -1, Recommended = 0, InferenceCompatible = 1, Simple = 2, Advanced = 3, Expert = 4, Nightmare = 5, UltraNightmare = 6, Impossible = 999 } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/ActionPackageStep.cs ================================================ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; /// /// A package step that wraps an async action, useful for ad-hoc operations /// that need to run within the PackageModificationRunner. /// public class ActionPackageStep( Func, Task> action, string progressTitle = "Working..." ) : IPackageStep { public string ProgressTitle => progressTitle; public async Task ExecuteAsync(IProgress? progress) { await action(progress ?? new Progress()).ConfigureAwait(false); } } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/AddInstalledPackageStep.cs ================================================ using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class AddInstalledPackageStep : IPackageStep { private readonly ISettingsManager settingsManager; private readonly InstalledPackage newInstalledPackage; public AddInstalledPackageStep( ISettingsManager settingsManager, InstalledPackage newInstalledPackage ) { this.settingsManager = settingsManager; this.newInstalledPackage = newInstalledPackage; } public async Task ExecuteAsync(IProgress? progress = null) { if (!string.IsNullOrWhiteSpace(newInstalledPackage.DisplayName)) { settingsManager.PackageInstallsInProgress.Remove(newInstalledPackage.DisplayName); } await using var transaction = settingsManager.BeginTransaction(); transaction.Settings.InstalledPackages.Add(newInstalledPackage); transaction.Settings.ActiveInstalledPackageId = newInstalledPackage.Id; } public string ProgressTitle => $"{newInstalledPackage.DisplayName} Installed"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/DownloadOpenArtWorkflowStep.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Models.Api.OpenArt; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class DownloadOpenArtWorkflowStep( IOpenArtApi openArtApi, OpenArtSearchResult workflow, ISettingsManager settingsManager ) : IPackageStep { public async Task ExecuteAsync(IProgress? progress = null) { var workflowData = await openArtApi .DownloadWorkflowAsync(new OpenArtDownloadRequest { WorkflowId = workflow.Id }) .ConfigureAwait(false); var workflowJson = JsonSerializer.SerializeToNode(workflow); Directory.CreateDirectory(settingsManager.WorkflowDirectory); var filePath = Path.Combine(settingsManager.WorkflowDirectory, $"{workflowData.Filename}.json"); var jsonObject = JsonNode.Parse(workflowData.Payload) as JsonObject; jsonObject?.Add("sm_workflow_data", workflowJson); await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(jsonObject)).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Downloaded OpenArt Workflow")); } public string ProgressTitle => "Downloading OpenArt Workflow"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/DownloadPackageVersionStep.cs ================================================ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class DownloadPackageVersionStep( BasePackage package, string installPath, DownloadPackageOptions options ) : ICancellablePackageStep { public Task ExecuteAsync( IProgress? progress = null, CancellationToken cancellationToken = default ) { return package.DownloadPackage(installPath, options, progress, cancellationToken); } public string ProgressTitle => "Downloading package..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/ICancellablePackageStep.cs ================================================ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public interface ICancellablePackageStep : IPackageStep { Task IPackageStep.ExecuteAsync(IProgress? progress) { return ExecuteAsync(progress, CancellationToken.None); } Task ExecuteAsync( IProgress? progress = null, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public interface IPackageModificationRunner { Task ExecuteSteps(IEnumerable steps); bool IsRunning { get; } [MemberNotNullWhen(true, nameof(Exception))] bool Failed { get; } Exception? Exception { get; } ProgressReport CurrentProgress { get; set; } IPackageStep? CurrentStep { get; set; } event EventHandler? ProgressChanged; event EventHandler? Completed; List ConsoleOutput { get; } Guid Id { get; } bool ShowDialogOnStart { get; init; } bool HideCloseButton { get; init; } bool CloseWhenFinished { get; init; } string? ModificationCompleteTitle { get; init; } string ModificationCompleteMessage { get; init; } string? ModificationFailedTitle { get; init; } string? ModificationFailedMessage { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/ImportModelsStep.cs ================================================ using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class ImportModelsStep( ModelFinder modelFinder, IDownloadService downloadService, IModelIndexService modelIndexService, IEnumerable files, DirectoryPath destinationFolder, bool isImportAsConnectedEnabled, bool moveFiles = false ) : IPackageStep { public async Task ExecuteAsync(IProgress? progress = null) { var copyPaths = files.ToDictionary(k => k, v => Path.Combine(destinationFolder, Path.GetFileName(v))); // remove files that are already in the folder foreach (var (source, destination) in copyPaths) { if (source == destination) { copyPaths.Remove(source); } } if (copyPaths.Count == 0) { progress?.Report(new ProgressReport(1f, message: "Import Complete")); return; } progress?.Report(new ProgressReport(0f, message: "Importing...")); var lastMessage = string.Empty; var transferProgress = new Progress(report => { var message = copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}"; progress?.Report( new ProgressReport( report.Progress ?? 0, message: message, printToConsole: message != lastMessage ) ); lastMessage = message; }); if (moveFiles) { var filesMoved = 0; foreach (var (source, destination) in copyPaths) { try { await FileTransfers.MoveFileAsync(source, destination).ConfigureAwait(false); filesMoved++; } catch (Exception) { // ignored } progress?.Report( new ProgressReport( filesMoved, copyPaths.Count, Path.GetFileName(source), $"{filesMoved}/{copyPaths.Count}" ) ); } } else { await FileTransfers.CopyFiles(copyPaths, transferProgress).ConfigureAwait(false); } // Hash files and convert them to connected model if found if (isImportAsConnectedEnabled) { var modelFilesCount = copyPaths.Count; var modelFiles = copyPaths.Values.Select(path => new FilePath(path)); // Holds tasks for model queries after hash var modelQueryTasks = new List>(); foreach (var (i, modelFile) in modelFiles.Enumerate()) { var hashProgress = new Progress(report => { var message = modelFilesCount > 1 ? $"Computing metadata for {modelFile.Name} ({i}/{modelFilesCount})" : $"Computing metadata for {modelFile.Name}"; progress?.Report( new ProgressReport( report.Progress ?? 0, message: message, printToConsole: message != lastMessage ) ); lastMessage = message; }); var hashBlake3 = await FileHash.GetBlake3Async(modelFile, hashProgress).ConfigureAwait(false); // Start a task to query the model in background var queryTask = Task.Run(async () => { var result = await modelFinder.LocalFindModel(hashBlake3).ConfigureAwait(false); result ??= await modelFinder.RemoteFindModel(hashBlake3).ConfigureAwait(false); if (result is null) return false; // Not found var (model, version, file) = result.Value; // Save connected model info json var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name); var modelInfo = new ConnectedModelInfo(model, version, file, DateTimeOffset.UtcNow); await modelInfo .SaveJsonToDirectory(destinationFolder, modelFileName) .ConfigureAwait(false); // If available, save thumbnail var image = version.Images?.FirstOrDefault(x => x.Type == "image"); if (image != null) { var imageExt = Path.GetExtension(image.Url).TrimStart('.'); if (imageExt is "jpg" or "jpeg" or "png") { var imageDownloadPath = Path.GetFullPath( Path.Combine(destinationFolder, $"{modelFileName}.preview.{imageExt}") ); await downloadService .DownloadToFileAsync(image.Url, imageDownloadPath) .ConfigureAwait(false); } } return true; }); modelQueryTasks.Add(queryTask); } // Set progress to indeterminate progress?.Report( new ProgressReport { IsIndeterminate = true, Progress = -1f, Message = "Checking connected model information", PrintToConsole = true } ); // Wait for all model queries to finish var modelQueryResults = await Task.WhenAll(modelQueryTasks).ConfigureAwait(false); var successCount = modelQueryResults.Count(r => r); var totalCount = modelQueryResults.Length; var failCount = totalCount - successCount; var progressText = successCount switch { 0 when failCount > 0 => "Import complete. No connected data found.", > 0 when failCount > 0 => $"Import complete. Found connected data for {successCount} of {totalCount} models.", 1 when failCount == 0 => "Import complete. Found connected data for 1 model.", _ => $"Import complete. Found connected data for all {totalCount} models." }; progress?.Report(new ProgressReport(1f, message: progressText)); } else { progress?.Report(new ProgressReport(1f, message: "Import Complete")); } await modelIndexService.RefreshIndex().ConfigureAwait(false); } public string ProgressTitle => "Importing Models"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/InstallExtensionStep.cs ================================================ using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class InstallExtensionStep( IPackageExtensionManager extensionManager, InstalledPackage installedPackage, PackageExtension packageExtension, PackageExtensionVersion? extensionVersion = null ) : IPackageStep { public Task ExecuteAsync(IProgress? progress = null) { return extensionManager.InstallExtensionAsync( packageExtension, installedPackage, extensionVersion, progress ); } public string ProgressTitle => $"Installing Extension {packageExtension.Title}"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/InstallNunchakuStep.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.PackageModification; public class InstallNunchakuStep(IPyInstallationManager pyInstallationManager) : IPackageStep { public required InstalledPackage InstalledPackage { get; init; } public required DirectoryPath WorkingDirectory { get; init; } public required GpuInfo? PreferredGpu { get; init; } public required IPackageExtensionManager ComfyExtensionManager { get; init; } public IReadOnlyDictionary? EnvironmentVariables { get; init; } public async Task ExecuteAsync(IProgress? progress = null) { if (Compat.IsMacOS || PreferredGpu?.ComputeCapabilityValue is null or < 7.5m) { throw new NotSupportedException( "Nunchaku is not supported on macOS or GPUs with compute capability < 7.5." ); } var venvDir = WorkingDirectory.JoinDir("venv"); var pyVersion = PyVersion.Parse(InstalledPackage.PythonVersion); if (pyVersion.StringValue == "0.0.0") { pyVersion = PyInstallationManager.Python_3_10_11; } var baseInstall = !string.IsNullOrWhiteSpace(InstalledPackage.PythonVersion) ? new PyBaseInstall( await pyInstallationManager.GetInstallationAsync(pyVersion).ConfigureAwait(false) ) : PyBaseInstall.Default; await using var venvRunner = baseInstall.CreateVenvRunner( venvDir, workingDirectory: WorkingDirectory, environmentVariables: EnvironmentVariables ); var torchInfo = await venvRunner.PipShow("torch").ConfigureAwait(false); var shortPythonVersionString = pyVersion.Minor switch { 10 => "cp310", 11 => "cp311", 12 => "cp312", 13 => "cp313", _ => throw new ArgumentOutOfRangeException("Invalid Python version"), }; var platform = Compat.IsWindows ? "win_amd64" : "linux_x86_64"; if (torchInfo is null) { throw new InvalidOperationException("Torch is not installed in the virtual environment."); } var torchVersion = torchInfo.Version switch { var v when v.StartsWith("2.7") => "2.7", var v when v.StartsWith("2.8") => "2.8", var v when v.StartsWith("2.9") => "2.9", var v when v.StartsWith("2.10") => "2.10", _ => throw new InvalidOperationException( "No compatible torch version found in the virtual environment." ), }; var downloadUrl = $"https://github.com/nunchaku-tech/nunchaku/releases/download/v1.0.2/nunchaku-1.0.2+torch{torchVersion}-{shortPythonVersionString}-{shortPythonVersionString}-{platform}.whl"; progress?.Report( new ProgressReport(-1f, message: "Installing Nunchaku backend", isIndeterminate: true) ); await venvRunner.PipInstall(downloadUrl, progress.AsProcessOutputHandler()).ConfigureAwait(false); progress?.Report( new ProgressReport(1f, message: "Nunchaku backend installed successfully", isIndeterminate: false) ); var extensions = await ComfyExtensionManager .GetManifestExtensionsAsync(ComfyExtensionManager.DefaultManifests) .ConfigureAwait(false); var nunchakuExtension = extensions.FirstOrDefault(e => e.Title.Equals("ComfyUI-nunchaku", StringComparison.OrdinalIgnoreCase) ); if (nunchakuExtension is null) return; var installedExtensions = await ComfyExtensionManager .GetInstalledExtensionsLiteAsync(InstalledPackage) .ConfigureAwait(false); var installedNunchakuExtension = installedExtensions.FirstOrDefault(e => e.Title.Equals("ComfyUI-nunchaku", StringComparison.OrdinalIgnoreCase) ); if (installedNunchakuExtension is not null) { var installedNunchakuExtensionWithVersion = await ComfyExtensionManager .GetInstalledExtensionInfoAsync(installedNunchakuExtension) .ConfigureAwait(false); installedNunchakuExtensionWithVersion = installedNunchakuExtensionWithVersion with { Definition = nunchakuExtension, }; await ComfyExtensionManager .UpdateExtensionAsync(installedNunchakuExtensionWithVersion, InstalledPackage, null, progress) .ConfigureAwait(false); } else { await ComfyExtensionManager .InstallExtensionAsync(nunchakuExtension, InstalledPackage, null, progress) .ConfigureAwait(false); } progress?.Report( new ProgressReport( 1f, message: "Nunchaku extension installed successfully.", isIndeterminate: false ) ); } public string ProgressTitle => "Installing nunchaku..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class InstallPackageStep( BasePackage basePackage, string installLocation, InstalledPackage installedPackage, InstallPackageOptions options ) : ICancellablePackageStep { public async Task ExecuteAsync( IProgress? progress = null, CancellationToken cancellationToken = default ) { await basePackage .InstallPackage( installLocation, installedPackage, options, progress, progress.AsProcessOutputHandler(setMessageAsOutput: false), cancellationToken ) .ConfigureAwait(false); } public string ProgressTitle => $"Installing {basePackage.DisplayName}..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/InstallSageAttentionStep.cs ================================================ using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; 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.PackageModification; public class InstallSageAttentionStep( IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager ) : IPackageStep { private const string PythonLibsDownloadUrl = "https://cdn.lykos.ai/python_libs_for_sage.zip"; public required InstalledPackage InstalledPackage { get; init; } public required DirectoryPath WorkingDirectory { get; init; } public required bool IsBlackwellGpu { get; init; } public IReadOnlyDictionary? EnvironmentVariables { get; init; } public async Task ExecuteAsync(IProgress? progress = null) { if (!Compat.IsWindows) { throw new PlatformNotSupportedException( "This method of installing Triton and SageAttention is only supported on Windows" ); } var venvDir = WorkingDirectory.JoinDir("venv"); var pyVersion = PyVersion.Parse(InstalledPackage.PythonVersion); if (pyVersion.StringValue == "0.0.0") { pyVersion = PyInstallationManager.Python_3_10_11; } var baseInstall = !string.IsNullOrWhiteSpace(InstalledPackage.PythonVersion) ? new PyBaseInstall( await pyInstallationManager.GetInstallationAsync(pyVersion).ConfigureAwait(false) ) : PyBaseInstall.Default; await using var venvRunner = baseInstall.CreateVenvRunner( venvDir, workingDirectory: WorkingDirectory, environmentVariables: EnvironmentVariables ); var torchInfo = await venvRunner.PipShow("torch").ConfigureAwait(false); var sageWheelUrl = string.Empty; if (torchInfo != null) { // Extract base version (before +) and CUDA index var versionString = torchInfo.Version; var plusIndex = versionString.IndexOf('+'); var baseVersionString = plusIndex >= 0 ? versionString[..plusIndex] : versionString; var cudaIndex = plusIndex >= 0 ? versionString[(plusIndex + 1)..] : string.Empty; // Try to parse base version for comparison if (Version.TryParse(baseVersionString, out var torchVersion)) { var minVersion = new Version(2, 9, 0); // New wheels work for torch >= 2.9.0 with cu128 or cu130 if (torchVersion >= minVersion) { if (cudaIndex.Contains("cu128")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post4/sageattention-2.2.0+cu128torch2.9.0andhigher.post4-cp39-abi3-win_amd64.whl"; } else if (cudaIndex.Contains("cu130")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post4/sageattention-2.2.0+cu130torch2.9.0andhigher.post4-cp39-abi3-win_amd64.whl"; } } else { // Fallback to old wheels for torch < 2.9.0 var shortPythonVersionString = pyVersion.Minor switch { 10 => "cp310", 11 => "cp311", 12 => "cp312", _ => throw new ArgumentOutOfRangeException("Invalid Python version"), }; if (versionString.Contains("2.5.1") && versionString.Contains("cu124")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post3/sageattention-2.2.0+cu124torch2.5.1.post3-cp39-abi3-win_amd64.whl"; } else if (versionString.Contains("2.6.0") && versionString.Contains("cu126")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post3/sageattention-2.2.0+cu126torch2.6.0.post3-cp39-abi3-win_amd64.whl"; } else if (versionString.Contains("2.7.0") && versionString.Contains("cu128")) { sageWheelUrl = $"https://github.com/woct0rdho/SageAttention/releases/download/v2.1.1-windows/sageattention-2.1.1+cu128torch2.7.0-{shortPythonVersionString}-{shortPythonVersionString}-win_amd64.whl"; } else if (versionString.Contains("2.7.1") && versionString.Contains("cu128")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post3/sageattention-2.2.0+cu128torch2.7.1.post3-cp39-abi3-win_amd64.whl"; } else if (versionString.Contains("2.8.0") && versionString.Contains("cu128")) { sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post3/sageattention-2.2.0+cu128torch2.8.0.post3-cp39-abi3-win_amd64.whl"; } } } } var pipArgs = new PipInstallArgs("triton-windows"); if (!string.IsNullOrWhiteSpace(sageWheelUrl)) { pipArgs = pipArgs.AddArg(sageWheelUrl); progress?.Report( new ProgressReport(-1f, message: "Installing Triton & SageAttention", isIndeterminate: true) ); await venvRunner.PipInstall(pipArgs, progress.AsProcessOutputHandler()).ConfigureAwait(false); return; } // no wheels, gotta build if (!prerequisiteHelper.IsVcBuildToolsInstalled) { throw new MissingPrerequisiteException( "Visual Studio 2022 Build Tools", "Could not find Visual Studio 2022 Build Tools. Please install them from the link below.", "https://aka.ms/vs/17/release/vs_BuildTools.exe" ); } var nvccPath = await Utilities.WhichAsync("nvcc").ConfigureAwait(false); if (string.IsNullOrWhiteSpace(nvccPath)) { var cuda126ExpectedPath = new DirectoryPath( @"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin" ); var cuda128ExpectedPath = new DirectoryPath( @"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.8\bin" ); if (!cuda126ExpectedPath.Exists && !cuda128ExpectedPath.Exists) { throw new MissingPrerequisiteException( "CUDA Toolkit", "Could not find CUDA Toolkit. Please install version 12.6 or newer from the link below.", "https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64" ); } nvccPath = cuda128ExpectedPath.Exists ? cuda128ExpectedPath.JoinFile("nvcc.exe").ToString() : cuda126ExpectedPath.JoinFile("nvcc.exe").ToString(); } venvRunner.UpdateEnvironmentVariables(env => { var cudaBinPath = Path.GetDirectoryName(nvccPath)!; var cudaHome = Path.GetDirectoryName(cudaBinPath)!; env = env.TryGetValue("PATH", out var pathValue) ? env.SetItem("PATH", $"{cudaBinPath}{Path.PathSeparator}{pathValue}") : env.Add("PATH", cudaBinPath); if (!env.ContainsKey("CUDA_HOME")) { env = env.Add("CUDA_HOME", cudaHome); } return env; }); progress?.Report(new ProgressReport(-1f, message: "Installing Triton", isIndeterminate: true)); await venvRunner.PipInstall(pipArgs, progress.AsProcessOutputHandler()).ConfigureAwait(false); venvRunner.UpdateEnvironmentVariables(env => env.SetItem("SETUPTOOLS_USE_DISTUTILS", "setuptools")); progress?.Report( new ProgressReport(-1f, message: "Downloading Python libraries", isIndeterminate: true) ); await AddMissingLibsToVenv(WorkingDirectory, progress).ConfigureAwait(false); var sageDir = WorkingDirectory.JoinDir("SageAttention"); if (!sageDir.Exists) { progress?.Report( new ProgressReport(-1f, message: "Downloading SageAttention", isIndeterminate: true) ); await prerequisiteHelper .RunGit( ["clone", "https://github.com/thu-ml/SageAttention.git", sageDir.ToString()], progress.AsProcessOutputHandler() ) .ConfigureAwait(false); } progress?.Report(new ProgressReport(-1f, message: "Installing SageAttention", isIndeterminate: true)); await venvRunner .PipInstall( [WorkingDirectory.JoinDir("SageAttention").ToString()], progress.AsProcessOutputHandler() ) .ConfigureAwait(false); } private async Task AddMissingLibsToVenv( DirectoryPath installedPackagePath, IProgress? progress = null ) { var venvLibsDir = installedPackagePath.JoinDir("venv", "libs"); var venvIncludeDir = installedPackagePath.JoinDir("venv", "include"); if ( venvLibsDir.Exists && venvIncludeDir.Exists && venvLibsDir.JoinFile("python3.lib").Exists && venvLibsDir.JoinFile("python310.lib").Exists ) { return; } var downloadPath = installedPackagePath.JoinFile("python_libs_for_sage.zip"); var venvDir = installedPackagePath.JoinDir("venv"); await downloadService .DownloadToFileAsync(PythonLibsDownloadUrl, downloadPath, progress) .ConfigureAwait(false); progress?.Report( new ProgressReport(-1f, message: "Extracting Python libraries", isIndeterminate: true) ); await ArchiveHelper.Extract7Z(downloadPath, venvDir, progress).ConfigureAwait(false); var includeFolder = venvDir.JoinDir("include"); var scriptsIncludeFolder = venvDir.JoinDir("Scripts").JoinDir("include"); await includeFolder.CopyToAsync(scriptsIncludeFolder).ConfigureAwait(false); await downloadPath.DeleteAsync().ConfigureAwait(false); } public string ProgressTitle => "Installing Triton and SageAttention"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class PackageModificationRunner : IPackageModificationRunner { public async Task ExecuteSteps(IEnumerable steps) { IProgress progress = new Progress(report => { CurrentProgress = report; if (!string.IsNullOrWhiteSpace(report.Message) && report.PrintToConsole) { ConsoleOutput.Add(report.Message); } if (!string.IsNullOrWhiteSpace(report.Title) && report.PrintToConsole) { ConsoleOutput.Add(report.Title); } OnProgressChanged(report); }); IsRunning = true; try { foreach (var step in steps) { CurrentStep = step; try { await step.ExecuteAsync(progress).ConfigureAwait(false); } catch (Exception e) { var failedMessage = string.IsNullOrWhiteSpace(ModificationFailedMessage) ? $"Error: {e}" : ModificationFailedMessage + $" ({e})"; progress.Report( new ProgressReport( 1f, title: ModificationFailedTitle, message: failedMessage, isIndeterminate: false ) ); Exception = e; Failed = true; return; } } if (!Failed) { progress.Report( new ProgressReport( 1f, title: ModificationCompleteTitle, message: ModificationCompleteMessage, isIndeterminate: false ) ); } } finally { IsRunning = false; OnCompleted(); } } public bool HideCloseButton { get; init; } public bool CloseWhenFinished { get; init; } = true; public bool ShowDialogOnStart { get; init; } public string? ModificationCompleteTitle { get; init; } = "Install Complete"; public required string ModificationCompleteMessage { get; init; } public string? ModificationFailedTitle { get; init; } = "Install Failed"; public string? ModificationFailedMessage { get; init; } public bool IsRunning { get; private set; } [MemberNotNullWhen(true, nameof(Exception))] public bool Failed { get; private set; } public Exception? Exception { get; set; } public ProgressReport CurrentProgress { get; set; } public IPackageStep? CurrentStep { get; set; } public List ConsoleOutput { get; } = new(); public Guid Id { get; } = Guid.NewGuid(); public event EventHandler? ProgressChanged; public event EventHandler? Completed; protected virtual void OnProgressChanged(ProgressReport e) => ProgressChanged?.Invoke(this, e); protected virtual void OnCompleted() => Completed?.Invoke(this, this); } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/PackageStep.cs ================================================ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public interface IPackageStep { Task ExecuteAsync(IProgress? progress = null); string ProgressTitle { get; } } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/PipStep.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.PackageModification; public class PipStep : IPackageStep { public required ProcessArgs Args { get; init; } public required DirectoryPath VenvDirectory { get; init; } public DirectoryPath? WorkingDirectory { get; init; } public IReadOnlyDictionary? EnvironmentVariables { get; init; } public PyBaseInstall? BaseInstall { get; set; } /// public string ProgressTitle => Args switch { _ when Args.Contains("install") => "Installing Pip Packages", _ when Args.Contains("uninstall") => "Uninstalling Pip Packages", _ when Args.Contains("-U") || Args.Contains("--upgrade") => "Updating Pip Packages", _ => "Running Pip" }; /// public async Task ExecuteAsync(IProgress? progress = null) { BaseInstall ??= PyBaseInstall.Default; await using var venvRunner = BaseInstall.CreateVenvRunner( VenvDirectory, workingDirectory: WorkingDirectory, environmentVariables: EnvironmentVariables ); if (BaseInstall.UsesUv && Args.Contains("install")) { var uvArgs = Args.ToString().Replace("install ", string.Empty); await venvRunner.PipInstall(uvArgs, progress.AsProcessOutputHandler()).ConfigureAwait(false); } else { venvRunner.RunDetached(Args.Prepend(["-m", "pip"]), progress.AsProcessOutputHandler()); } await ProcessRunner.WaitForExitConditionAsync(venvRunner.Process).ConfigureAwait(false); } } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/ProcessStep.cs ================================================ using System.Collections.Immutable; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.PackageModification; public class ProcessStep : ICancellablePackageStep { public required string FileName { get; init; } public ProcessArgs Args { get; init; } = ""; public DirectoryPath? WorkingDirectory { get; init; } public ImmutableDictionary EnvironmentVariables { get; init; } = ImmutableDictionary.Empty; public bool UseAnsiParsing { get; init; } = true; public string ProgressTitle { get; init; } = "Running Process"; /// public async Task ExecuteAsync( IProgress? progress = null, CancellationToken cancellationToken = default ) { progress?.Report( new ProgressReport { Message = "Running Process", IsIndeterminate = true, PrintToConsole = true } ); if (UseAnsiParsing) { using var process = ProcessRunner.StartAnsiProcess( fileName: FileName, arguments: Args.ToString(), workingDirectory: WorkingDirectory?.FullPath, environmentVariables: EnvironmentVariables, outputDataReceived: progress.AsProcessOutputHandler() ); await ProcessRunner .WaitForExitConditionAsync(process, cancelToken: cancellationToken) .ConfigureAwait(false); await process.WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false); } else { using var process = ProcessRunner.StartProcess( fileName: FileName, arguments: Args.ToString(), workingDirectory: WorkingDirectory?.FullPath, environmentVariables: EnvironmentVariables, outputDataReceived: progress is null ? null : output => { progress.Report( new ProgressReport { Message = output, IsIndeterminate = true, PrintToConsole = true } ); } ); await ProcessRunner .WaitForExitConditionAsync(process, cancelToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/ScanMetadataStep.cs ================================================ using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class ScanMetadataStep( DirectoryPath directoryPath, IMetadataImportService metadataImportService, bool updateExistingMetadata = false ) : IPackageStep { public Task ExecuteAsync(IProgress? progress = null) => updateExistingMetadata ? metadataImportService.UpdateExistingMetadata(directoryPath, progress) : metadataImportService.ScanDirectoryForMissingInfo(directoryPath, progress); public string ProgressTitle => "Updating Metadata"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/SetPackageInstallingStep.cs ================================================ using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class SetPackageInstallingStep : IPackageStep { private readonly ISettingsManager settingsManager; private readonly string packageName; public SetPackageInstallingStep(ISettingsManager settingsManager, string packageName) { this.settingsManager = settingsManager; this.packageName = packageName; } public Task ExecuteAsync(IProgress? progress = null) { settingsManager.PackageInstallsInProgress.Add(packageName); return Task.CompletedTask; } public string ProgressTitle => "Starting Package Installation"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/SetupModelFoldersStep.cs ================================================ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class SetupModelFoldersStep : IPackageStep { private readonly BasePackage package; private readonly SharedFolderMethod sharedFolderMethod; private readonly string installPath; public SetupModelFoldersStep( BasePackage package, SharedFolderMethod sharedFolderMethod, string installPath ) { this.package = package; this.sharedFolderMethod = sharedFolderMethod; this.installPath = installPath; } public async Task ExecuteAsync(IProgress? progress = null) { progress?.Report( new ProgressReport(-1f, "Setting up shared folder links...", isIndeterminate: true) ); await package.SetupModelFolders(installPath, sharedFolderMethod).ConfigureAwait(false); } public string ProgressTitle => "Setting up shared folder links..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/SetupOutputSharingStep.cs ================================================ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class SetupOutputSharingStep(BasePackage package, string installPath) : IPackageStep { public Task ExecuteAsync(IProgress? progress = null) { package.SetupOutputFolderLinks(installPath); return Task.CompletedTask; } public string ProgressTitle => "Setting up output sharing..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/SetupPrerequisitesStep.cs ================================================ using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.PackageModification; public class SetupPrerequisitesStep( IPrerequisiteHelper prerequisiteHelper, BasePackage package, PyVersion? pythonVersion = null ) : IPackageStep { public async Task ExecuteAsync(IProgress? progress = null) { await prerequisiteHelper .InstallPackageRequirements(package, pythonVersion, progress) .ConfigureAwait(false); } public string ProgressTitle => "Installing prerequisites..."; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/UninstallExtensionStep.cs ================================================ using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class UninstallExtensionStep( IPackageExtensionManager extensionManager, InstalledPackage installedPackage, InstalledPackageExtension packageExtension ) : IPackageStep { public Task ExecuteAsync(IProgress? progress = null) { return extensionManager.UninstallExtensionAsync(packageExtension, installedPackage, progress); } public string ProgressTitle => $"Uninstalling Extension {packageExtension.Title}"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/UpdateExtensionStep.cs ================================================ using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.PackageModification; public class UpdateExtensionStep( IPackageExtensionManager extensionManager, InstalledPackage installedPackage, InstalledPackageExtension installedExtension, PackageExtensionVersion? extensionVersion = null ) : IPackageStep { public Task ExecuteAsync(IProgress? progress = null) { return extensionManager.UpdateExtensionAsync( installedExtension, installedPackage, extensionVersion, progress ); } public string ProgressTitle => $"Updating Extension {installedExtension.Title}"; } ================================================ FILE: StabilityMatrix.Core/Models/PackageModification/UpdatePackageStep.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.PackageModification; public class UpdatePackageStep( ISettingsManager settingsManager, BasePackage basePackage, string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options ) : ICancellablePackageStep { public async Task ExecuteAsync( IProgress? progress = null, CancellationToken cancellationToken = default ) { var updateResult = await basePackage .Update( installLocation, installedPackage, options, progress, progress.AsProcessOutputHandler(), cancellationToken ) .ConfigureAwait(false); await using (settingsManager.BeginTransaction()) { installedPackage.Version = updateResult; installedPackage.UpdateAvailable = false; } } public string ProgressTitle => $"Updating {installedPackage.DisplayName}"; } ================================================ FILE: StabilityMatrix.Core/Models/PackagePair.cs ================================================ using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models; /// /// Pair of InstalledPackage and BasePackage /// public record PackagePair(InstalledPackage InstalledPackage, BasePackage BasePackage); ================================================ FILE: StabilityMatrix.Core/Models/PackagePrerequisite.cs ================================================ namespace StabilityMatrix.Core.Models; public enum PackagePrerequisite { Python310, Python31017, VcRedist, Git, HipSdk, Node, Dotnet, Tkinter, VcBuildTools } ================================================ FILE: StabilityMatrix.Core/Models/PackageType.cs ================================================ namespace StabilityMatrix.Core.Models; public enum PackageType { SdInference, SdTraining, Legacy, } ================================================ FILE: StabilityMatrix.Core/Models/PackageVersion.cs ================================================ namespace StabilityMatrix.Core.Models; public record PackageVersion { public required string TagName { get; set; } public string? ReleaseNotesMarkdown { get; set; } public bool IsPrerelease { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/PackageVersionType.cs ================================================ namespace StabilityMatrix.Core.Models; [Flags] public enum PackageVersionType { None = 0, GithubRelease = 1 << 0, Commit = 1 << 1 } ================================================ FILE: StabilityMatrix.Core/Models/Packages/A3WebUI.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class A3WebUI( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "stable-diffusion-webui"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI"; public override string Author => "AUTOMATIC1111"; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/LICENSE.txt"; public override string Blurb => "A browser interface based on Gradio library for Stable Diffusion"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override PackageType PackageType => PackageType.SdInference; // From https://github.com/AUTOMATIC1111/stable-diffusion-webui/tree/master/models public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = ["models/Stable-diffusion/sd"], [SharedFolderType.ESRGAN] = ["models/ESRGAN"], [SharedFolderType.GFPGAN] = ["models/GFPGAN"], [SharedFolderType.RealESRGAN] = ["models/RealESRGAN"], [SharedFolderType.SwinIR] = ["models/SwinIR"], [SharedFolderType.Lora] = ["models/Lora"], [SharedFolderType.LyCORIS] = ["models/LyCORIS"], [SharedFolderType.ApproxVAE] = ["models/VAE-approx"], [SharedFolderType.VAE] = ["models/VAE"], [SharedFolderType.DeepDanbooru] = ["models/deepbooru"], [SharedFolderType.Karlo] = ["models/karlo"], [SharedFolderType.Embeddings] = ["embeddings"], [SharedFolderType.Hypernetwork] = ["models/hypernetworks"], [SharedFolderType.ControlNet] = ["models/controlnet/ControlNet"], [SharedFolderType.Codeformer] = ["models/Codeformer"], [SharedFolderType.LDSR] = ["models/LDSR"], [SharedFolderType.AfterDetailer] = ["models/adetailer"], [SharedFolderType.T2IAdapter] = ["models/controlnet/T2IAdapter"], [SharedFolderType.IpAdapter] = ["models/controlnet/IpAdapter"], [SharedFolderType.IpAdapters15] = ["models/controlnet/DiffusersIpAdapters"], [SharedFolderType.IpAdaptersXl] = ["models/controlnet/DiffusersIpAdaptersXL"], [SharedFolderType.SVD] = ["models/svd"], [SharedFolderType.TextEncoders] = ["models/text_encoder"], [SharedFolderType.DiffusionModels] = ["models/Stable-diffusion/unet"], }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Extras] = ["outputs/extras-images"], [SharedOutputType.Saved] = ["log/images"], [SharedOutputType.Img2Img] = ["outputs/img2img-images"], [SharedOutputType.Text2Img] = ["outputs/txt2img-images"], [SharedOutputType.Img2ImgGrids] = ["outputs/img2img-grids"], [SharedOutputType.Text2ImgGrids] = ["outputs/txt2img-grids"], [SharedOutputType.SVD] = ["outputs/svd"], }; [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { MemoryLevel.Low => "--lowvram", MemoryLevel.Medium => "--medvram", _ => null, }, Options = ["--lowvram", "--medvram", "--medvram-sdxl"], }, new() { Name = "Xformers", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), Options = ["--xformers"], }, new() { Name = "API", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--api"], }, new() { Name = "Auto Launch Web UI", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--autolaunch"], }, new() { Name = "Skip Torch CUDA Check", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), Options = ["--skip-torch-cuda-test"], }, new() { Name = "Skip Python Version Check", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--skip-python-version-check"], }, new() { Name = "No Half", Type = LaunchOptionType.Bool, Description = "Do not switch the model to 16-bit floats", InitialValue = HardwareHelper.PreferRocm() || HardwareHelper.PreferDirectMLOrZluda() || Compat.IsMacOS, Options = ["--no-half"], }, new() { Name = "Skip SD Model Download", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--no-download-sd-model"], }, new() { Name = "Skip Install", Type = LaunchOptionType.Bool, Options = ["--skip-install"], }, LaunchOptionDefinition.Extras, ]; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.Symlink, SharedFolderMethod.None]; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.Rocm, TorchIndex.Mps]; public override string MainBranch => "dev"; public override string OutputFolderName => "outputs"; public override IPackageExtensionManager ExtensionManager => new A3WebUiExtensionManager(this); public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var isBlackwell = torchIndex is TorchIndex.Cuda && (SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu()); // 1. Configure the entire install process declaratively. var config = new PipInstallConfig { RequirementsFilePaths = ["requirements_versions.txt"], TorchVersion = torchIndex == TorchIndex.Mps ? "==2.3.1" : (isBlackwell ? "" : "==2.1.2"), TorchvisionVersion = torchIndex == TorchIndex.Mps ? "==0.18.1" : (isBlackwell ? "" : "==0.16.2"), XformersVersion = isBlackwell ? " " : "==0.0.23.post1", CudaIndex = isBlackwell ? "cu128" : "cu121", RocmIndex = "rocm5.6", ExtraPipArgs = [ "https://github.com/openai/CLIP/archive/d50d76daa670286dd6cacf3bcd80b5e4823fc8e1.zip", ], }; // 2. Execute the standardized installation process. await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); // 3. Perform any final, package-specific tasks. progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true)); var configPath = Path.Combine(installLocation, "config.json"); if (!File.Exists(configPath)) { var configJson = new JsonObject { { "show_progress_type", "TAESD" } }; await File.WriteAllTextAsync(configPath, configJson.ToString(), cancellationToken) .ConfigureAwait(false); } progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.UpdateEnvironmentVariables(GetEnvVars); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); } VenvRunner.RunDetached( [ Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments, .. ExtraLaunchArguments, ], HandleConsoleOutput, OnExit ); } public override IReadOnlyList ExtraLaunchArguments => settingsManager.IsLibraryDirSet ? ["--gradio-allowed-path", settingsManager.ImagesDirectory] : []; protected virtual ImmutableDictionary GetEnvVars(ImmutableDictionary env) { // Set the Stable Diffusion repository URL to a working fork // This is required because the original Stability-AI/stablediffusion repo was removed // See: https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/17212 env = env.SetItem("STABLE_DIFFUSION_REPO", "https://github.com/w-e-w/stablediffusion.git"); return env; } private class A3WebUiExtensionManager(A3WebUI package) : GitPackageExtensionManager(package.PrerequisiteHelper) { public override string RelativeInstallDirectory => "extensions"; public override IEnumerable DefaultManifests => [ new ExtensionManifest( new Uri( "https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json" ) ), ]; public override async Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ) { try { // Get json var content = await package .DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken) .ConfigureAwait(false); // Parse json var jsonManifest = JsonSerializer.Deserialize( content, A1111ExtensionManifestSerializerContext.Default.Options ); return jsonManifest?.GetPackageExtensions() ?? Enumerable.Empty(); } catch (Exception e) { Logger.Error(e, "Failed to get extensions from manifest"); return Enumerable.Empty(); } } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/AiToolkit.cs ================================================ using System.Collections.Immutable; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class AiToolkit( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private AnsiProcess? npmProcess; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "ai-toolkit"; public override string DisplayName { get; set; } = "AI-Toolkit"; public override string Author => "ostris"; public override string Blurb => "AI Toolkit is an all in one training suite for diffusion models"; public override string LicenseType => "MIT"; public override string LicenseUrl => "https://github.com/ostris/ai-toolkit/blob/main/LICENSE"; public override string LaunchCommand => string.Empty; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/aitoolkit/preview.webp"); public override string OutputFolderName => "output"; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda]; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override List LaunchOptions => []; public override Dictionary>? SharedOutputFolders => []; public override string MainBranch => "main"; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override TorchIndex GetRecommendedTorchVersion() => TorchIndex.Cuda; public override PackageType PackageType => PackageType.SdTraining; public override bool OfferInOneClickInstaller => false; public override bool ShouldIgnoreReleases => true; public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_11_13; public override IEnumerable Prerequisites => base.Prerequisites.Concat([PackagePrerequisite.Node]); public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); venvRunner.UpdateEnvironmentVariables(GetEnvVars); var isBlackwell = SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu(); var config = new PipInstallConfig { RequirementsFilePaths = ["requirements.txt"], TorchVersion = "==2.7.0", TorchvisionVersion = "==0.22.0", TorchaudioVersion = "==2.7.0", CudaIndex = isBlackwell ? "cu128" : "cu126", ExtraPipArgs = [Compat.IsWindows ? "triton-windows" : "triton"], UpgradePackages = true, }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing AI Toolkit UI...", isIndeterminate: true)); var uiDirectory = new DirectoryPath(installLocation, "ui"); var envVars = GetEnvVars(venvRunner.EnvironmentVariables); await PrerequisiteHelper .RunNpm("install", uiDirectory, progress?.AsProcessOutputHandler(), envVars) .ConfigureAwait(false); await PrerequisiteHelper .RunNpm("run update_db", uiDirectory, progress?.AsProcessOutputHandler(), envVars) .ConfigureAwait(false); await PrerequisiteHelper .RunNpm("run build", uiDirectory, progress?.AsProcessOutputHandler(), envVars) .ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.UpdateEnvironmentVariables(GetEnvVars); var uiDirectory = new DirectoryPath(installLocation, "ui"); var envVars = GetEnvVars(VenvRunner.EnvironmentVariables); npmProcess = PrerequisiteHelper.RunNpmDetached( "run start", uiDirectory, HandleConsoleOutput, envVars ); npmProcess.EnableRaisingEvents = true; if (Compat.IsWindows) { ProcessTracker.AttachExitHandlerJobToProcess(npmProcess); } return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Local: ", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } public override async Task WaitForShutdown() { if (npmProcess is { HasExited: false }) { npmProcess.Kill(true); try { await npmProcess .WaitForExitAsync(new CancellationTokenSource(5000).Token) .ConfigureAwait(false); } catch (OperationCanceledException e) { Logger.Warn(e, "Timed out waiting for npm to exit"); npmProcess.CancelStreamReaders(); } } npmProcess = null; } private ImmutableDictionary GetEnvVars(ImmutableDictionary env) { // set SETUPTOOLS_USE_DISTUTILS=setuptools to avoid job errors env = env.SetItem("SETUPTOOLS_USE_DISTUTILS", "setuptools"); var pathBuilder = new EnvPathBuilder(); if (env.TryGetValue("PATH", out var value)) { pathBuilder.AddPath(value); } pathBuilder.AddPath( Compat.IsWindows ? Environment.GetFolderPath(Environment.SpecialFolder.System) : Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs", "bin") ); pathBuilder.AddPath(Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs")); return env.SetItem("PATH", pathBuilder.ToString()); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using NLog; using Octokit; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.Database; 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; /// /// Base class for packages that are hosted on Github. /// Author and Name should be the Github username and repository name respectively. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] public abstract class BaseGitPackage : BasePackage { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); protected readonly IGithubApiCache GithubApi; protected readonly IDownloadService DownloadService; protected readonly IPrerequisiteHelper PrerequisiteHelper; protected readonly IPyInstallationManager PyInstallationManager; protected readonly IPipWheelService PipWheelService; public IPyVenvRunner? VenvRunner; public virtual string RepositoryName => Name; public virtual string RepositoryAuthor => Author; /// /// URL of the hosted web page on launch /// protected string WebUrl = string.Empty; public override string GithubUrl => $"https://github.com/{RepositoryAuthor}/{RepositoryName}"; public string DownloadLocation => Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip"); protected string GetDownloadUrl(DownloadPackageVersionOptions versionOptions) { if (!string.IsNullOrWhiteSpace(versionOptions.CommitHash)) { return $"https://github.com/{RepositoryAuthor}/{RepositoryName}/archive/{versionOptions.CommitHash}.zip"; } if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag)) { return $"https://api.github.com/repos/{RepositoryAuthor}/{RepositoryName}/zipball/{versionOptions.VersionTag}"; } if (!string.IsNullOrWhiteSpace(versionOptions.BranchName)) { return $"https://api.github.com/repos/{RepositoryAuthor}/{RepositoryName}/zipball/{versionOptions.BranchName}"; } throw new Exception("No download URL available"); } protected BaseGitPackage( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : base(settingsManager) { GithubApi = githubApi; DownloadService = downloadService; PrerequisiteHelper = prerequisiteHelper; PyInstallationManager = pyInstallationManager; PipWheelService = pipWheelService; } public override async Task GetLatestVersion( bool includePrerelease = false ) { if (ShouldIgnoreReleases) { var commits = await GithubApi .GetAllCommits(RepositoryAuthor, RepositoryName, MainBranch) .ConfigureAwait(false); return new DownloadPackageVersionOptions { IsLatest = true, IsPrerelease = false, BranchName = MainBranch, CommitHash = commits?.FirstOrDefault()?.Sha, }; } var releases = await GithubApi.GetAllReleases(RepositoryAuthor, RepositoryName).ConfigureAwait(false); var releaseList = releases.ToList(); if (releaseList.Count == 0) { return new DownloadPackageVersionOptions { IsLatest = true, IsPrerelease = false, BranchName = MainBranch, }; } var latestRelease = includePrerelease ? releaseList.First() : releaseList.First(x => !x.Prerelease); return new DownloadPackageVersionOptions { IsLatest = true, IsPrerelease = latestRelease.Prerelease, VersionTag = latestRelease.TagName!, }; } public override Task?> GetAllCommits(string branch, int page = 1, int perPage = 10) { return GithubApi.GetAllCommits(RepositoryAuthor, RepositoryName, branch, page, perPage); } public override async Task GetAllVersionOptions() { var packageVersionOptions = new PackageVersionOptions(); if (!ShouldIgnoreReleases) { var allReleases = await GithubApi .GetAllReleases(RepositoryAuthor, RepositoryName) .ConfigureAwait(false); var releasesList = allReleases.ToList(); if (releasesList.Any()) { packageVersionOptions.AvailableVersions = releasesList.Select(r => new PackageVersion { TagName = r.TagName!, ReleaseNotesMarkdown = r.Body, IsPrerelease = r.Prerelease, }); } } // Branch mode var allBranches = await GithubApi .GetAllBranches(RepositoryAuthor, RepositoryName) .ConfigureAwait(false); packageVersionOptions.AvailableBranches = allBranches.Select(b => new PackageVersion { TagName = $"{b.Name}", ReleaseNotesMarkdown = string.Empty, }); return packageVersionOptions; } /// /// Setup the virtual environment for the package. /// [MemberNotNull(nameof(VenvRunner))] public async Task SetupVenv( string installedPackagePath, string venvName = "venv", bool forceRecreate = false, Action? onConsoleOutput = null, PyVersion? pythonVersion = null ) { if (Interlocked.Exchange(ref VenvRunner, null) is { } oldRunner) { await oldRunner.DisposeAsync().ConfigureAwait(false); } var venvRunner = await SetupVenvPure( installedPackagePath, venvName, forceRecreate, onConsoleOutput, pythonVersion ) .ConfigureAwait(false); if (Interlocked.Exchange(ref VenvRunner, venvRunner) is { } oldRunner2) { await oldRunner2.DisposeAsync().ConfigureAwait(false); } Debug.Assert(VenvRunner != null, "VenvRunner != null"); return venvRunner; } /// /// Like , but does not set the property. /// Returns a new instance. /// public async Task SetupVenvPure( string installedPackagePath, string venvName = "venv", bool forceRecreate = false, Action? onConsoleOutput = null, PyVersion? pythonVersion = null ) { // Use either the specific version or the default one var baseInstall = pythonVersion.HasValue ? new PyBaseInstall( await PyInstallationManager.GetInstallationAsync(pythonVersion.Value).ConfigureAwait(false) ) : PyBaseInstall.Default; if (!PrerequisiteHelper.IsUvInstalled) { await PrerequisiteHelper.InstallUvIfNecessary().ConfigureAwait(false); } var venvRunner = await baseInstall .CreateVenvRunnerAsync( Path.Combine(installedPackagePath, venvName), workingDirectory: installedPackagePath, environmentVariables: SettingsManager.Settings.EnvironmentVariables, withDefaultTclTkEnv: Compat.IsWindows, withQueriedTclTkEnv: Compat.IsUnix ) .ConfigureAwait(false); if (forceRecreate || !venvRunner.Exists()) { await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false); } // Constrain setuptools<82 in uv's isolated build environments. // setuptools 82+ removed pkg_resources, breaking source builds that import it. var buildConstraintsPath = Path.Combine(installedPackagePath, venvName, "uv-build-constraints.txt"); await File.WriteAllTextAsync(buildConstraintsPath, "setuptools<82\n").ConfigureAwait(false); // Use relative path because uv splits UV_BUILD_CONSTRAINT on spaces (it's a list-type env var), // which breaks when the absolute path contains spaces. The working directory is installedPackagePath, // so the relative path resolves correctly. var relativeBuildConstraintsPath = Path.Combine(venvName, "uv-build-constraints.txt"); venvRunner.UpdateEnvironmentVariables(env => env.SetItem("UV_BUILD_CONSTRAINT", relativeBuildConstraintsPath) ); // ensure pip is installed await venvRunner.PipInstall("pip", onConsoleOutput).ConfigureAwait(false); if (!Compat.IsWindows) return venvRunner; try { await PrerequisiteHelper .AddMissingLibsToVenv(installedPackagePath, baseInstall) .ConfigureAwait(false); } catch (Exception e) { Logger.Warn(e, "Failed to add missing libs to venv"); } return venvRunner; } public override async Task> GetReleaseTags() { var allReleases = await GithubApi .GetAllReleases(RepositoryAuthor, RepositoryName) .ConfigureAwait(false); return allReleases; } public override async Task DownloadPackage( string installLocation, DownloadPackageOptions options, IProgress? progress = null, CancellationToken cancellationToken = default ) { var versionOptions = options.VersionOptions; const long fiveGigs = 5 * SystemInfo.Gibibyte; if (SystemInfo.GetDiskFreeSpaceBytes(installLocation) is < fiveGigs) { throw new ApplicationException( $"Not enough space to download {Name} to {installLocation}, need at least 5GB" ); } var gitArgs = new List { "clone" }; var branchArg = !string.IsNullOrWhiteSpace(versionOptions.VersionTag) ? versionOptions.VersionTag : versionOptions.BranchName; if (!string.IsNullOrWhiteSpace(branchArg)) { gitArgs.Add("--branch"); gitArgs.Add(branchArg); } gitArgs.Add(GithubUrl); gitArgs.Add(installLocation); await PrerequisiteHelper .RunGit(gitArgs.ToArray(), progress?.AsProcessOutputHandler()) .ConfigureAwait(false); if (!versionOptions.IsLatest && !string.IsNullOrWhiteSpace(versionOptions.CommitHash)) { await PrerequisiteHelper .RunGit( ["checkout", versionOptions.CommitHash], progress?.AsProcessOutputHandler(), installLocation ) .ConfigureAwait(false); } progress?.Report(new ProgressReport(100, message: "Download Complete")); } protected Task UnzipPackage(string installLocation, IProgress? progress = null) { using var zip = ZipFile.OpenRead(DownloadLocation); var zipDirName = string.Empty; var totalEntries = zip.Entries.Count; var currentEntry = 0; foreach (var entry in zip.Entries) { currentEntry++; if (string.IsNullOrWhiteSpace(entry.Name) && entry.FullName.EndsWith("/")) { if (string.IsNullOrWhiteSpace(zipDirName)) { zipDirName = entry.FullName; } var folderPath = Path.Combine( installLocation, entry.FullName.Replace(zipDirName, string.Empty) ); Directory.CreateDirectory(folderPath); continue; } var destinationPath = Path.GetFullPath( Path.Combine(installLocation, entry.FullName.Replace(zipDirName, string.Empty)) ); entry.ExtractToFile(destinationPath, true); progress?.Report( new ProgressReport( current: Convert.ToUInt64(currentEntry), total: Convert.ToUInt64(totalEntries) ) ); } return Task.CompletedTask; } public override async Task CheckForUpdates(InstalledPackage package) { var currentVersion = package.Version; if (currentVersion is null or { InstalledReleaseVersion: null, InstalledBranch: null }) { Logger.Warn( "Could not check updates for package {Name}, version is invalid: {@currentVersion}", Name, currentVersion ); return false; } try { if (currentVersion.IsReleaseMode) { var latestVersion = await GetLatestVersion(currentVersion.IsPrerelease).ConfigureAwait(false); UpdateAvailable = latestVersion.VersionTag != currentVersion.InstalledReleaseVersion; return UpdateAvailable; } var allCommits = ( await GetAllCommits(currentVersion.InstalledBranch!).ConfigureAwait(false) )?.ToList(); if (allCommits == null || allCommits.Count == 0) { Logger.Warn("No commits found for {Package}", package.PackageName); return false; } var latestCommitHash = allCommits.First().Sha; return latestCommitHash != currentVersion.InstalledCommitSha; } catch (ApiException e) { Logger.Warn(e, "Failed to check for package updates"); return false; } } public override async Task GetUpdate(InstalledPackage installedPackage) { var currentVersion = installedPackage.Version; if (currentVersion is null or { InstalledReleaseVersion: null, InstalledBranch: null }) { Logger.Warn( "Could not check updates for package {Name}, version is invalid: {@currentVersion}", Name, currentVersion ); return null; } var versionOptions = new DownloadPackageVersionOptions { IsLatest = true }; try { if (currentVersion.IsReleaseMode) { var latestVersion = await GetLatestVersion(currentVersion.IsPrerelease).ConfigureAwait(false); versionOptions.IsPrerelease = latestVersion.IsPrerelease; versionOptions.VersionTag = latestVersion.VersionTag; return versionOptions; } var allCommits = ( await GetAllCommits(currentVersion.InstalledBranch!).ConfigureAwait(false) )?.ToList(); if (allCommits == null || allCommits.Count == 0) { Logger.Warn("No commits found for {Package}", installedPackage.PackageName); return null; } var latestCommitHash = allCommits.First().Sha; versionOptions.CommitHash = latestCommitHash; versionOptions.BranchName = currentVersion.InstalledBranch; return versionOptions; } catch (ApiException e) { Logger.Warn(e, "Failed to check for package updates"); return null; } } public override async Task Update( string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (installedPackage.Version == null) throw new NullReferenceException("Version is null"); if (!Directory.Exists(Path.Combine(installedPackage.FullPath!, ".git"))) { Logger.Info("not a git repo, initializing..."); progress?.Report(new ProgressReport(-1f, "Initializing git repo", isIndeterminate: true)); await PrerequisiteHelper .RunGit("init", onConsoleOutput, installedPackage.FullPath) .ConfigureAwait(false); await PrerequisiteHelper .RunGit( new[] { "remote", "add", "origin", GithubUrl }, onConsoleOutput, installedPackage.FullPath ) .ConfigureAwait(false); } var sharedFolderMethodToUse = installedPackage.PreferredSharedFolderMethod ?? RecommendedSharedFolderMethod; // Temporarily remove symlinks if using Symlink method if (sharedFolderMethodToUse == SharedFolderMethod.Symlink) { if (SharedFolders is not null) { try { Helper.SharedFolders.RemoveLinksForPackage( SharedFolders, new DirectoryPath(installedPackage.FullPath!) ); } catch (Exception e) { Logger.Warn( e, "Failed to remove symlinks for package {Package}", installedPackage.PackageName ); } } if (SharedOutputFolders is not null && installedPackage.UseSharedOutputFolder) { try { Helper.SharedFolders.RemoveLinksForPackage( SharedOutputFolders, new DirectoryPath(installedPackage.FullPath!) ); } catch (Exception e) { Logger.Warn( e, "Failed to remove output symlinks for package {Package}", installedPackage.PackageName ); } } } var versionOptions = options.VersionOptions; if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag)) { progress?.Report(new ProgressReport(-1f, "Fetching tags...", isIndeterminate: true)); await PrerequisiteHelper .RunGit(new[] { "fetch", "--tags", "--force" }, onConsoleOutput, installedPackage.FullPath) .ConfigureAwait(false); progress?.Report( new ProgressReport(-1f, $"Checking out {versionOptions.VersionTag}", isIndeterminate: true) ); await PrerequisiteHelper .RunGit( new[] { "checkout", versionOptions.VersionTag, "--force" }, onConsoleOutput, installedPackage.FullPath ) .ConfigureAwait(false); await InstallPackage( installLocation, installedPackage, options.AsInstallOptions(), progress, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); return new InstalledPackageVersion { InstalledReleaseVersion = versionOptions.VersionTag, IsPrerelease = versionOptions.IsPrerelease, }; } // fetch progress?.Report(new ProgressReport(-1f, "Fetching data...", isIndeterminate: true)); await PrerequisiteHelper .RunGit(new[] { "fetch", "--force" }, onConsoleOutput, installedPackage.FullPath) .ConfigureAwait(false); if (versionOptions.IsLatest) { // checkout progress?.Report( new ProgressReport( -1f, $"Checking out {installedPackage.Version.InstalledBranch}...", isIndeterminate: true ) ); await PrerequisiteHelper .RunGit( new[] { "checkout", versionOptions.BranchName!, "--force" }, onConsoleOutput, installedPackage.FullPath ) .ConfigureAwait(false); // pull progress?.Report(new ProgressReport(-1f, "Pulling changes...", isIndeterminate: true)); // Try fast-forward-only first var ffOnly = await PrerequisiteHelper .GetGitOutput( ["pull", "--ff-only", "--autostash", "origin", installedPackage.Version.InstalledBranch!], installedPackage.FullPath! ) .ConfigureAwait(false); if (ffOnly.ExitCode != 0) { // Fallback to rebase to preserve local changes if any var rebaseRes = await PrerequisiteHelper .GetGitOutput( [ "pull", "--rebase", "--autostash", "origin", installedPackage.Version.InstalledBranch!, ], installedPackage.FullPath! ) .ConfigureAwait(false); rebaseRes.EnsureSuccessExitCode(); } } else { // checkout progress?.Report( new ProgressReport( -1f, $"Checking out {installedPackage.Version.InstalledBranch}...", isIndeterminate: true ) ); await PrerequisiteHelper .RunGit( new[] { "checkout", versionOptions.CommitHash!, "--force" }, onConsoleOutput, installedPackage.FullPath ) .ConfigureAwait(false); } await InstallPackage( installLocation, installedPackage, options.AsInstallOptions(), progress, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); return new InstalledPackageVersion { InstalledBranch = versionOptions.BranchName, InstalledCommitSha = versionOptions.CommitHash, IsPrerelease = versionOptions.IsPrerelease, }; } private async Task FixInfinityFolders(DirectoryPath rootDirectory, string infinityFolderName) { // Skip if first infinity not found if ( rootDirectory.JoinDir(infinityFolderName) is not { Exists: true, IsSymbolicLink: false } firstInfinity ) { return; } var depth = 0; var currentDir = rootDirectory; while (currentDir.JoinDir(infinityFolderName) is { Exists: true, IsSymbolicLink: false } newInfinity) { depth++; currentDir = newInfinity; } Logger.Info("Found {Depth} infinity folders from {FirstPath}", depth, firstInfinity.ToString()); // Move all items in infinity folder to root Logger.Info("Moving infinity folders content to root: {Path}", currentDir.ToString()); await FileTransfers.MoveAllFilesAndDirectories(currentDir, rootDirectory).ConfigureAwait(false); // Move any files from first infinity by enumeration just in case foreach (var file in firstInfinity.EnumerateFiles()) { await file.MoveToDirectoryAsync(rootDirectory).ConfigureAwait(false); } // Delete infinity folders chain from first Logger.Info("Deleting infinity folders: {Path}", currentDir.ToString()); await firstInfinity.DeleteAsync(true).ConfigureAwait(false); } private async Task FixForgeInfinity() { var modelsDir = new DirectoryPath(SettingsManager.ModelsDirectory); var rootDirectory = modelsDir.JoinDir("StableDiffusion").JoinDir("sd"); var infinityFolderName = "sd"; var firstInfinity = rootDirectory.JoinDir(infinityFolderName); var depth = 0; var currentDir = rootDirectory; while (currentDir.JoinDir(infinityFolderName) is { Exists: true, IsSymbolicLink: false } newInfinity) { depth++; currentDir = newInfinity; } if (depth <= 5) { Logger.Info("not really that infinity, aborting"); return; } Logger.Info("Found {Depth} infinity folders from {FirstPath}", depth, firstInfinity.ToString()); // Move all items in infinity folder to root Logger.Info("Moving infinity folders content to root: {Path}", currentDir.ToString()); await FileTransfers .MoveAllFilesAndDirectories(currentDir, rootDirectory, overwriteIfHashMatches: true) .ConfigureAwait(false); // Move any files from first infinity by enumeration just in case var leftoverFiles = firstInfinity.EnumerateFiles(searchOption: SearchOption.AllDirectories); foreach (var file in leftoverFiles) { await file.MoveToWithIncrementAsync(rootDirectory.JoinFile(file.Name)).ConfigureAwait(false); } if (!firstInfinity.EnumerateFiles(searchOption: SearchOption.AllDirectories).Any()) { // Delete infinity folders chain from first Logger.Info("Deleting infinity folders: {Path}", currentDir.ToString()); await firstInfinity.DeleteAsync(true).ConfigureAwait(false); } } public override async Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { if (sharedFolderMethod is SharedFolderMethod.Configuration && SharedFolderLayout is not null) { await SharedFoldersConfigHelper .UpdateConfigFileForSharedAsync( SharedFolderLayout, installDirectory.FullPath, SettingsManager.ModelsDirectory ) .ConfigureAwait(false); } else if (sharedFolderMethod is SharedFolderMethod.Symlink && SharedFolders is { } sharedFolders) { var modelsDir = new DirectoryPath(SettingsManager.ModelsDirectory); // fix infinity controlnet folders await FixInfinityFolders(modelsDir.JoinDir("ControlNet"), "ControlNet").ConfigureAwait(false); await FixForgeInfinity().ConfigureAwait(false); // fix duplicate links in models dir // see https://github.com/LykosAI/StabilityMatrix/issues/338 string[] duplicatePaths = [ Path.Combine("ControlNet", "ControlNet"), Path.Combine("IPAdapter", "base"), Path.Combine("IPAdapter", "sd15"), Path.Combine("IPAdapter", "sdxl"), ]; foreach (var duplicatePath in duplicatePaths) { var linkDir = modelsDir.JoinDir(duplicatePath); if (!linkDir.IsSymbolicLink) continue; Logger.Info("Removing duplicate junction at {Path}", linkDir.ToString()); await linkDir.DeleteAsync(false).ConfigureAwait(false); } await Helper .SharedFolders.UpdateLinksForPackage( sharedFolders, SettingsManager.ModelsDirectory, installDirectory ) .ConfigureAwait(false); } } public override Task UpdateModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) => SetupModelFolders(installDirectory, sharedFolderMethod); public override Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { // Auto handling for SharedFolderLayout if (sharedFolderMethod is SharedFolderMethod.Configuration && SharedFolderLayout is not null) { return SharedFoldersConfigHelper.UpdateConfigFileForDefaultAsync( SharedFolderLayout, installDirectory.FullPath ); } if (SharedFolders is not null && sharedFolderMethod is SharedFolderMethod.Symlink) { Helper.SharedFolders.RemoveLinksForPackage(SharedFolders, installDirectory); } return Task.CompletedTask; } public override Task SetupOutputFolderLinks(DirectoryPath installDirectory) { if (SharedOutputFolders is { } sharedOutputFolders) { return Helper.SharedFolders.UpdateLinksForPackage( sharedOutputFolders, SettingsManager.ImagesDirectory, installDirectory, recursiveDelete: true ); } return Task.CompletedTask; } public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory) { if (SharedOutputFolders is { } sharedOutputFolders) { Helper.SharedFolders.RemoveLinksForPackage(sharedOutputFolders, installDirectory); } return Task.CompletedTask; } // Send input to the running process. public virtual void SendInput(string input) { var process = VenvRunner?.Process; if (process == null) { Logger.Warn("No process running for {Name}", Name); return; } process.StandardInput.WriteLine(input); } public virtual async Task SendInputAsync(string input) { var process = VenvRunner?.Process; if (process == null) { Logger.Warn("No process running for {Name}", Name); return; } await process.StandardInput.WriteLineAsync(input).ConfigureAwait(false); } protected PipInstallArgs GetTorchPipArgs( TorchIndex torchIndex, string torchVersion = "", string torchvisionVersion = "", string torchaudioVersion = "", string xformersVersion = "", string cudaIndex = "cu130", string rocmIndex = "rocm6.4" ) { var pipArgs = new PipInstallArgs(); if (torchIndex == TorchIndex.DirectMl) { return pipArgs.WithTorchDirectML(); } pipArgs = pipArgs.WithTorch(torchVersion).WithTorchVision(torchvisionVersion); if (!string.IsNullOrEmpty(torchaudioVersion)) { pipArgs = pipArgs.WithTorchAudio(torchaudioVersion); } var extraIndex = torchIndex switch { TorchIndex.Cpu => "cpu", TorchIndex.Cuda => cudaIndex, TorchIndex.Rocm => rocmIndex, TorchIndex.Mps => "cpu", TorchIndex.Zluda => cudaIndex, _ => "cpu", }; pipArgs = pipArgs.WithTorchExtraIndex(extraIndex); if (torchIndex is TorchIndex.Cuda or TorchIndex.Zluda && !string.IsNullOrEmpty(xformersVersion)) { pipArgs = pipArgs.WithXFormers(xformersVersion); } return pipArgs; } /// /// Executes a standardized pip installation workflow: requirements first, then a forced torch install. /// protected async Task StandardPipInstallProcessAsync( IPyVenvRunner venvRunner, InstallPackageOptions options, InstalledPackage installedPackage, PipInstallConfig config, Action? onConsoleOutput = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Upgrading pip...", isIndeterminate: true)); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); if (config.PrePipInstallArgs.Any()) { await venvRunner .PipInstall(new PipInstallArgs([.. config.PrePipInstallArgs]), onConsoleOutput) .ConfigureAwait(false); } progress?.Report( new ProgressReport(-1f, "Installing package requirements...", isIndeterminate: true) ); var requirementsPipArgs = new PipInstallArgs([.. config.ExtraPipArgs]); if (config.UpgradePackages) { requirementsPipArgs = requirementsPipArgs.AddArg("--upgrade"); } foreach (var path in config.RequirementsFilePaths) { var requirementsFile = new FilePath(venvRunner.WorkingDirectory!, path); if (!requirementsFile.Exists) continue; var content = await requirementsFile.ReadAllTextAsync(cancellationToken).ConfigureAwait(false); requirementsPipArgs = requirementsPipArgs.WithParsedFromRequirementsTxt( content, config.RequirementsExcludePattern ); } if (installedPackage.PipOverrides != null) { requirementsPipArgs = requirementsPipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(requirementsPipArgs, onConsoleOutput).ConfigureAwait(false); if (config.SkipTorchInstall) return; progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var torchPipArgs = GetTorchPipArgs( torchIndex, config.TorchVersion, config.TorchvisionVersion, config.TorchaudioVersion, config.XformersVersion, config.CudaIndex, config.RocmIndex ); if (config.UpgradePackages) { torchPipArgs = torchPipArgs.AddArg("--upgrade"); } if (config.ForceReinstallTorch) { torchPipArgs = torchPipArgs.AddArg("--force-reinstall"); } if (installedPackage.PipOverrides != null) { torchPipArgs = torchPipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(torchPipArgs, onConsoleOutput).ConfigureAwait(false); if (config.PostInstallPipArgs.Any()) { var postInstallPipArgs = new PipInstallArgs([.. config.PostInstallPipArgs]); if (installedPackage.PipOverrides != null) { postInstallPipArgs = postInstallPipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(postInstallPipArgs, onConsoleOutput).ConfigureAwait(false); } } /// public override void Shutdown() { if (VenvRunner is not null) { VenvRunner.Dispose(); VenvRunner = null; } } /// public override async Task WaitForShutdown() { if (VenvRunner is not null) { await VenvRunner.DisposeAsync().ConfigureAwait(false); VenvRunner = null; } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/BasePackage.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Octokit; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; public abstract class BasePackage(ISettingsManager settingsManager) { protected readonly ISettingsManager SettingsManager = settingsManager; public string ByAuthor => $"By {Author}"; public abstract string Name { get; } public abstract string DisplayName { get; set; } public abstract string Author { get; } public abstract string Blurb { get; } public abstract string GithubUrl { get; } public abstract string LicenseType { get; } public abstract string LicenseUrl { get; } public virtual string Disclaimer => string.Empty; public virtual bool OfferInOneClickInstaller => true; /// /// Primary command to launch the package. 'Launch' buttons uses this. /// public abstract string LaunchCommand { get; } /// /// Optional commands (e.g. 'config') that are on the launch button split drop-down. /// public virtual IReadOnlyDictionary ExtraLaunchCommands { get; } = new Dictionary(); public abstract Uri PreviewImageUri { get; } public virtual bool ShouldIgnoreReleases => false; public virtual bool ShouldIgnoreBranches => false; public virtual bool UpdateAvailable { get; set; } public virtual bool IsInferenceCompatible => false; public abstract string OutputFolderName { get; } public abstract IEnumerable AvailableTorchIndices { get; } public virtual bool IsCompatible => GetRecommendedTorchVersion() != TorchIndex.Cpu; public abstract PackageDifficulty InstallerSortOrder { get; } public virtual PackageType PackageType => PackageType.SdInference; public virtual bool UsesVenv => true; public virtual bool InstallRequiresAdmin => false; public virtual string? AdminRequiredReason => null; public virtual PyVersion RecommendedPythonVersion => PyInstallationManager.Python_3_10_17; /// /// Minimum Python version required for updates. When set, updating a package with a lower /// installed Python version will prompt for venv recreation. Null means no minimum enforced. /// public virtual PyVersion? MinimumPythonVersion => null; /// /// Returns a list of extra commands that can be executed for this package. /// The function takes an InstalledPackage parameter to operate on a specific installation. /// public virtual List GetExtraCommands() => []; public abstract Task DownloadPackage( string installLocation, DownloadPackageOptions options, IProgress? progress = null, CancellationToken cancellationToken = default ); public abstract Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); public abstract Task CheckForUpdates(InstalledPackage package); public abstract Task Update( string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); public abstract Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); public virtual IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None }; public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; } public abstract Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ); public abstract Task UpdateModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ); public abstract Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ); public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory); public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory); public virtual TorchIndex GetRecommendedTorchVersion() { // if there's only one AvailableTorchVersion, return that if (AvailableTorchIndices.Count() == 1) { return AvailableTorchIndices.First(); } var preferNvidia = SettingsManager.Settings.PreferredGpu?.IsNvidia ?? HardwareHelper.HasNvidiaGpu(); if (AvailableTorchIndices.Contains(TorchIndex.Cuda) && preferNvidia) { return TorchIndex.Cuda; } var preferAmd = SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.HasAmdGpu(); if (AvailableTorchIndices.Contains(TorchIndex.Zluda) && preferAmd) { return TorchIndex.Zluda; } var preferIntel = SettingsManager.Settings.PreferredGpu?.IsIntel ?? HardwareHelper.HasIntelGpu(); if (AvailableTorchIndices.Contains(TorchIndex.Ipex) && preferIntel) { return TorchIndex.Ipex; } var preferRocm = Compat.IsLinux && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferRocm()); if (AvailableTorchIndices.Contains(TorchIndex.Rocm) && preferRocm) { return TorchIndex.Rocm; } var preferDirectMl = Compat.IsWindows && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferDirectMLOrZluda()); if (AvailableTorchIndices.Contains(TorchIndex.DirectMl) && preferDirectMl) { return TorchIndex.DirectMl; } if (Compat.IsMacOS && Compat.IsArm && AvailableTorchIndices.Contains(TorchIndex.Mps)) { return TorchIndex.Mps; } return TorchIndex.Cpu; } /// /// Shuts down the subprocess, canceling any pending streams. /// public abstract void Shutdown(); /// /// Shuts down the process, returning a Task to wait for output EOF. /// public abstract Task WaitForShutdown(); public abstract Task> GetReleaseTags(); public abstract List LaunchOptions { get; } public virtual IReadOnlyList ExtraLaunchArguments { get; } = Array.Empty(); /// /// Layout of the shared folders. For both Symlink and Config. /// public virtual SharedFolderLayout? SharedFolderLayout { get; } = new(); /// /// The shared folders that this package supports. /// Mapping of to the relative paths from the package root. /// (Legacy format for Symlink only, computed from SharedFolderLayout.) /// public virtual Dictionary>? SharedFolders => GetLegacySharedFolders(); private Dictionary>? GetLegacySharedFolders() { if (SharedFolderLayout is null) return null; // Keep track of unique paths since symbolic links can't do multiple targets // So we'll ignore duplicates once they appear here var addedPaths = new HashSet(); var result = new Dictionary>(); foreach (var rule in SharedFolderLayout.Rules) { // Ignore empty if (rule.TargetRelativePaths is not { Length: > 0 }) { continue; } // If there are multi SourceTypes <-> TargetRelativePaths: // We'll add a sub-path later var isMultiSource = rule.SourceTypes.Length > 1; foreach (var folderTypeKey in rule.SourceTypes) { var existingList = (ImmutableList) result.GetValueOrDefault(folderTypeKey, ImmutableList.Empty); var folderName = folderTypeKey.GetStringValue(); foreach (var path in rule.TargetRelativePaths) { var currentPath = path; if (isMultiSource) { // Add a sub-path for each source type currentPath = $"{path}/{folderName}"; } // Skip if the path is already in the list if (existingList.Contains(currentPath)) continue; // Skip if the path is already added globally if (!addedPaths.Add(currentPath)) continue; result[folderTypeKey] = existingList.Add(currentPath); } } } return result; } /// /// Represents a mapping of shared output types to their corresponding folder paths. /// This property defines where various output files, such as images or grids, /// are stored for the package. The dictionary keys represent specific /// output types, and the values are lists of associated folder paths. /// public abstract Dictionary>? SharedOutputFolders { get; } /// /// If defined, this package supports extensions using this manager. /// public virtual IPackageExtensionManager? ExtensionManager => null; /// /// True if this package supports extensions. /// [MemberNotNullWhen(true, nameof(ExtensionManager))] public virtual bool SupportsExtensions => ExtensionManager is not null; public abstract Task GetAllVersionOptions(); public abstract Task?> GetAllCommits( string branch, int page = 1, int perPage = 10 ); public abstract Task GetLatestVersion(bool includePrerelease = false); public abstract string MainBranch { get; } public event EventHandler? Exited; public event EventHandler? StartupComplete; public void OnExit(int exitCode) => Exited?.Invoke(this, exitCode); public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url); public virtual PackageVersionType AvailableVersionTypes => ShouldIgnoreReleases ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit; public virtual IEnumerable Prerequisites => [PackagePrerequisite.Git, PackagePrerequisite.Python310, PackagePrerequisite.VcRedist]; public abstract Task GetUpdate(InstalledPackage installedPackage); /// /// List of known vulnerabilities for this package /// public virtual IReadOnlyList KnownVulnerabilities { get; protected set; } = Array.Empty(); /// /// Whether this package has any known vulnerabilities /// public bool HasVulnerabilities => KnownVulnerabilities.Any(); /// /// Whether this package has any critical vulnerabilities /// public bool HasCriticalVulnerabilities => KnownVulnerabilities.Any(v => v.Severity == VulnerabilitySeverity.Critical); /// /// Check for any new vulnerabilities from external sources /// public virtual Task CheckForVulnerabilities(CancellationToken cancellationToken = default) { // Base implementation does nothing - derived classes should implement their own vulnerability checking return Task.CompletedTask; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Cogstudio.cs ================================================ using System.Text.RegularExpressions; using Injectio.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; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Cogstudio( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "Cogstudio"; public override string DisplayName { get; set; } = "Cogstudio"; public override string RepositoryName => "CogVideo"; public override string RepositoryAuthor => "THUDM"; public override string Author => "pinokiofactory"; public override string Blurb => "An advanced gradio web ui for generating and editing videos with CogVideo."; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/THUDM/CogVideo/blob/main/LICENSE"; public override string LaunchCommand => "inference/gradio_composite_demo/cogstudio.py"; public override Uri PreviewImageUri => new("https://raw.githubusercontent.com/pinokiofactory/cogstudio/main/img2vid.gif"); public override List LaunchOptions => new() { LaunchOptionDefinition.Extras }; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.None }; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Text2Vid] = new[] { "output" } }; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda }; public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override string OutputFolderName => "output"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { const string cogstudioUrl = "https://raw.githubusercontent.com/pinokiofactory/cogstudio/refs/heads/main/cogstudio.py"; progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Setting up Cogstudio files", isIndeterminate: true)); var gradioCompositeDemo = new FilePath(installLocation, "inference/gradio_composite_demo"); var cogstudioFile = new FilePath(gradioCompositeDemo, "cogstudio.py"); gradioCompositeDemo.Directory?.Create(); await DownloadService .DownloadToFileAsync(cogstudioUrl, cogstudioFile, cancellationToken: cancellationToken) .ConfigureAwait(false); progress?.Report( new ProgressReport( -1f, "Patching cogstudio.py to allow writing to the output folder", isIndeterminate: true ) ); var outputDir = new FilePath(installLocation, "output"); if (Compat.IsWindows) { outputDir = outputDir.ToString().Replace("\\", "\\\\"); } var cogstudioContent = await cogstudioFile.ReadAllTextAsync(cancellationToken).ConfigureAwait(false); cogstudioContent = cogstudioContent.Replace( "demo.launch()", $"demo.launch(allowed_paths=['{outputDir}'])" ); await cogstudioFile.WriteAllTextAsync(cogstudioContent, cancellationToken).ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing requirements", isIndeterminate: true)); var requirements = new FilePath(installLocation, "requirements.txt"); var pipArgs = new PipInstallArgs() .WithTorch("==2.3.1") .WithTorchVision("==0.18.1") .WithTorchAudio("==2.3.1") .WithTorchExtraIndex("cu121") .WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), excludePattern: Compat.IsWindows ? "torch.*|moviepy.*|SwissArmyTransformer.*" : "torch.*|moviepy.*" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } // SwissArmyTransformer is not available on Windows and DeepSpeed needs prebuilt wheels if (Compat.IsWindows) { await venvRunner .PipInstall( " https://github.com/daswer123/deepspeed-windows/releases/download/11.2/deepspeed-0.11.2+cuda121-cp310-cp310-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); await venvRunner .PipInstall("spandrel opencv-python scikit-video", onConsoleOutput) .ConfigureAwait(false); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("moviepy==2.0.0.dev2", onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/ComfyUI.cs ================================================ using System.Collections.Immutable; using System.Text.Json; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class ComfyUI( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "ComfyUI"; public override string DisplayName { get; set; } = "ComfyUI"; public override string Author => "comfyanonymous"; public override string LicenseType => "GPL-3.0"; public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE"; public override string Blurb => "A powerful and modular stable diffusion GUI and backend"; public override string LaunchCommand => "main.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/comfyui/preview.webp"); public override bool IsInferenceCompatible => true; public override string OutputFolderName => "output"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.InferenceCompatible; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_12_10; // https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11 public override SharedFolderLayout SharedFolderLayout => new() { RelativeConfigPath = "extra_model_paths.yaml", ConfigFileType = ConfigFileType.Yaml, ConfigSharingOptions = { RootKey = "stability_matrix", ConfigDefaultType = ConfigDefaultType.ClearRoot, }, Rules = [ new SharedFolderLayoutRule // Checkpoints { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["models/checkpoints"], ConfigDocumentPaths = ["checkpoints"], }, new SharedFolderLayoutRule // Diffusers { SourceTypes = [SharedFolderType.Diffusers], TargetRelativePaths = ["models/diffusers"], ConfigDocumentPaths = ["diffusers"], }, new SharedFolderLayoutRule // Loras { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["models/loras"], ConfigDocumentPaths = ["loras"], }, new SharedFolderLayoutRule // CLIP (Text Encoders) { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["models/clip"], ConfigDocumentPaths = ["clip"], }, new SharedFolderLayoutRule // CLIP Vision { SourceTypes = [SharedFolderType.ClipVision], TargetRelativePaths = ["models/clip_vision"], ConfigDocumentPaths = ["clip_vision"], }, new SharedFolderLayoutRule // Embeddings / Textual Inversion { SourceTypes = [SharedFolderType.Embeddings], TargetRelativePaths = ["models/embeddings"], ConfigDocumentPaths = ["embeddings"], }, new SharedFolderLayoutRule // VAE { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], ConfigDocumentPaths = ["vae"], }, new SharedFolderLayoutRule // VAE Approx { SourceTypes = [SharedFolderType.ApproxVAE], TargetRelativePaths = ["models/vae_approx"], ConfigDocumentPaths = ["vae_approx"], }, new SharedFolderLayoutRule // ControlNet / T2IAdapter { SourceTypes = [SharedFolderType.ControlNet, SharedFolderType.T2IAdapter], TargetRelativePaths = ["models/controlnet"], ConfigDocumentPaths = ["controlnet"], }, new SharedFolderLayoutRule // GLIGEN { SourceTypes = [SharedFolderType.GLIGEN], TargetRelativePaths = ["models/gligen"], ConfigDocumentPaths = ["gligen"], }, new SharedFolderLayoutRule // Upscalers { SourceTypes = [ SharedFolderType.ESRGAN, SharedFolderType.RealESRGAN, SharedFolderType.SwinIR, ], TargetRelativePaths = ["models/upscale_models"], ConfigDocumentPaths = ["upscale_models"], }, new SharedFolderLayoutRule // Hypernetworks { SourceTypes = [SharedFolderType.Hypernetwork], TargetRelativePaths = ["models/hypernetworks"], ConfigDocumentPaths = ["hypernetworks"], }, new SharedFolderLayoutRule // IP-Adapter Base, SD1.5, SDXL { SourceTypes = [ SharedFolderType.IpAdapter, SharedFolderType.IpAdapters15, SharedFolderType.IpAdaptersXl, ], TargetRelativePaths = ["models/ipadapter"], // Single target path ConfigDocumentPaths = ["ipadapter"], }, new SharedFolderLayoutRule // Prompt Expansion { SourceTypes = [SharedFolderType.PromptExpansion], TargetRelativePaths = ["models/prompt_expansion"], ConfigDocumentPaths = ["prompt_expansion"], }, new SharedFolderLayoutRule // Ultralytics { SourceTypes = [SharedFolderType.Ultralytics], // Might need specific UltralyticsBbox/Segm if symlinks differ TargetRelativePaths = ["models/ultralytics"], ConfigDocumentPaths = ["ultralytics"], }, // Config only rules for Ultralytics bbox/segm new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Ultralytics], SourceSubPath = "bbox", ConfigDocumentPaths = ["ultralytics_bbox"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Ultralytics], SourceSubPath = "segm", ConfigDocumentPaths = ["ultralytics_segm"], }, new SharedFolderLayoutRule // SAMs { SourceTypes = [SharedFolderType.Sams], TargetRelativePaths = ["models/sams"], ConfigDocumentPaths = ["sams"], }, new SharedFolderLayoutRule // Diffusion Models / Unet { SourceTypes = [SharedFolderType.DiffusionModels], TargetRelativePaths = ["models/diffusion_models"], ConfigDocumentPaths = ["diffusion_models"], }, ], }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = ["output"] }; public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", Options = ["--listen"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "8188", Options = ["--port"], }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { MemoryLevel.Low => "--lowvram", MemoryLevel.Medium => "--normalvram", _ => null, }, Options = ["--highvram", "--normalvram", "--lowvram", "--novram"], }, new() { Name = "Reserve VRAM", Type = LaunchOptionType.String, InitialValue = Compat.IsWindows && HardwareHelper.HasAmdGpu() ? "0.9" : null, Description = "Sets the amount of VRAM (in GB) you want to reserve for use by your OS/other software", Options = ["--reserve-vram"], }, new() { Name = "Preview Method", Type = LaunchOptionType.Bool, InitialValue = "--preview-method auto", Options = ["--preview-method auto", "--preview-method latent2rgb", "--preview-method taesd"], }, new() { Name = "Enable DirectML", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasWindowsRocmSupportedGpu() && HardwareHelper.PreferDirectMLOrZluda() && this is not ComfyZluda, Options = ["--directml"], }, new() { Name = "Use CPU only", Type = LaunchOptionType.Bool, InitialValue = !Compat.IsMacOS && !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(), Options = ["--cpu"], }, new() { Name = "Cross Attention Method", Type = LaunchOptionType.Bool, InitialValue = "--use-pytorch-cross-attention", Options = [ "--use-split-cross-attention", "--use-quad-cross-attention", "--use-pytorch-cross-attention", "--use-sage-attention", ], }, new() { Name = "Force Floating Point Precision", Type = LaunchOptionType.Bool, InitialValue = Compat.IsMacOS ? "--force-fp16" : null, Options = ["--force-fp32", "--force-fp16"], }, new() { Name = "VAE Precision", Type = LaunchOptionType.Bool, Options = ["--fp16-vae", "--fp32-vae", "--bf16-vae"], }, new() { Name = "Disable Xformers", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), Options = ["--disable-xformers"], }, new() { Name = "Disable upcasting of attention", Type = LaunchOptionType.Bool, Options = ["--dont-upcast-attention"], }, new() { Name = "Auto-Launch", Type = LaunchOptionType.Bool, Options = ["--auto-launch"], }, new() { Name = "Enable Manager", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--enable-manager"], }, LaunchOptionDefinition.Extras, ]; public override string MainBranch => "master"; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl, TorchIndex.Rocm, TorchIndex.Mps]; public override List GetExtraCommands() { var commands = new List(); if (Compat.IsWindows && SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() is true) { commands.Add( new ExtraPackageCommand { CommandName = "Install Triton and SageAttention", Command = InstallTritonAndSageAttention, } ); } if (!Compat.IsMacOS && SettingsManager.Settings.PreferredGpu?.ComputeCapabilityValue is >= 7.5m) { commands.Add( new ExtraPackageCommand { CommandName = "Install Nunchaku", Command = InstallNunchaku } ); } return commands; } public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var gfxArch = SettingsManager.Settings.PreferredGpu?.GetAmdGfxArch() ?? HardwareHelper.GetWindowsRocmSupportedGpu()?.GetAmdGfxArch(); // Special case for Windows ROCm Nightly builds if ( Compat.IsWindows && !string.IsNullOrWhiteSpace(gfxArch) && torchIndex is TorchIndex.Rocm && options.PythonOptions.PythonVersion >= PyVersion.Parse("3.11.0") ) { var config = new PipInstallConfig { RequirementsFilePaths = ["requirements.txt"], ExtraPipArgs = ["numpy<2"], SkipTorchInstall = true, PostInstallPipArgs = ["typing-extensions>=4.15.0"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); progress?.Report( new ProgressReport(-1f, "Installing ROCm nightly torch...", isIndeterminate: true) ); var indexUrl = gfxArch switch { "gfx1150" => "https://rocm.nightlies.amd.com/v2-staging/gfx1150", // Strix/Gorgon Point "gfx1151" => "https://rocm.nightlies.amd.com/v2/gfx1151", // Strix Halo _ when gfxArch.StartsWith("gfx110") => "https://rocm.nightlies.amd.com/v2/gfx110X-all", _ when gfxArch.StartsWith("gfx120") => "https://rocm.nightlies.amd.com/v2/gfx120X-all", _ => throw new ArgumentOutOfRangeException( nameof(gfxArch), $"Unsupported GFX Arch: {gfxArch}" ), }; var torchPipArgs = new PipInstallArgs() .AddArgs("--pre", "--upgrade") .WithTorch() .WithTorchVision() .WithTorchAudio() .AddArgs("--index-url", indexUrl); await venvRunner.PipInstall(torchPipArgs, onConsoleOutput).ConfigureAwait(false); } else // Standard installation path for all other cases { var isLegacyNvidia = torchIndex == TorchIndex.Cuda && ( SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu() ); var config = new PipInstallConfig { RequirementsFilePaths = ["requirements.txt"], ExtraPipArgs = ["numpy<2"], TorchaudioVersion = " ", // Request torchaudio without a specific version CudaIndex = isLegacyNvidia ? "cu126" : "cu130", RocmIndex = "rocm7.1", UpgradePackages = true, PostInstallPipArgs = ["typing-extensions>=4.15.0"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); } try { var sageVersion = await venvRunner.PipShow("sageattention").ConfigureAwait(false); var torchVersion = await venvRunner.PipShow("torch").ConfigureAwait(false); if (torchVersion is not null && sageVersion is not null) { var version = torchVersion.Version; var plusPos = version.IndexOf('+'); var index = plusPos >= 0 ? version[(plusPos + 1)..] : string.Empty; var versionWithoutIndex = plusPos >= 0 ? version[..plusPos] : version; if ( !sageVersion.Version.Contains(index) || !sageVersion.Version.Contains(versionWithoutIndex) ) { progress?.Report( new ProgressReport(-1f, "Updating SageAttention...", isIndeterminate: true) ); var step = new InstallSageAttentionStep( downloadService, prerequisiteHelper, pyInstallationManager ) { InstalledPackage = installedPackage, IsBlackwellGpu = SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu(), WorkingDirectory = installLocation, EnvironmentVariables = GetEnvVars(venvRunner.EnvironmentVariables), }; await step.ExecuteAsync(progress).ConfigureAwait(false); } } } catch (Exception e) { Logger.Error(e, "Failed to verify/update SageAttention after installation"); } // Install Comfy Manager (built-in to ComfyUI) try { var managerRequirementsFile = Path.Combine(installLocation, "manager_requirements.txt"); if (File.Exists(managerRequirementsFile)) { progress?.Report( new ProgressReport(-1f, "Installing Comfy Manager requirements...", isIndeterminate: true) ); var pipArgs = new PipInstallArgs().AddArg("-r").AddArg(managerRequirementsFile); await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report( new ProgressReport(-1f, "Comfy Manager installed successfully", isIndeterminate: true) ); } } catch (Exception e) { Logger.Error(e, "Failed to install Comfy Manager requirements"); } progress?.Report(new ProgressReport(1, "Install complete", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { // Use the same Python version that was used for installation await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.UpdateEnvironmentVariables(GetEnvVars); // Check for old NVIDIA driver version with cu130 installations var isNvidia = SettingsManager.Settings.PreferredGpu?.IsNvidia ?? HardwareHelper.HasNvidiaGpu(); var isLegacyNvidia = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); if (isNvidia && !isLegacyNvidia) { var driverVersion = HardwareHelper.GetNvidiaDriverVersion(); if (driverVersion is not null && driverVersion.Major < 580) { // Check if torch is installed with cu130 index var torchInfo = await VenvRunner.PipShow("torch").ConfigureAwait(false); if (torchInfo is not null) { var version = torchInfo.Version; var plusPos = version.IndexOf('+'); var torchIndex = plusPos >= 0 ? version[(plusPos + 1)..] : string.Empty; // Only warn if using cu130 (which requires driver 580+) if (torchIndex.Equals("cu130", StringComparison.OrdinalIgnoreCase)) { var warningMessage = $""" ============================================================ NVIDIA DRIVER WARNING ============================================================ Your NVIDIA driver version ({driverVersion}) is older than the minimum required version (580.x) for CUDA 13.0 (cu130). This may cause ComfyUI to fail to start or experience issues. Recommended actions: 1. Update your NVIDIA driver to version 580 or newer 2. Or manually downgrade your torch version to use an older torch index (e.g. cu128) ============================================================ """; Logger.Warn( "NVIDIA driver version {DriverVersion} is below 580.x minimum for cu130 (torch index: {TorchIndex})", driverVersion, torchIndex ); onConsoleOutput?.Invoke(ProcessOutput.FromStdErrLine(warningMessage)); return; } } } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); 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) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } public override TorchIndex GetRecommendedTorchVersion() { var preferRocm = (Compat.IsLinux && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferRocm())) || ( Compat.IsWindows && ( SettingsManager.Settings.PreferredGpu?.IsWindowsRocmSupportedGpu() ?? HardwareHelper.HasWindowsRocmSupportedGpu() ) ); if (AvailableTorchIndices.Contains(TorchIndex.Rocm) && preferRocm) { return TorchIndex.Rocm; } return base.GetRecommendedTorchVersion(); } public override IPackageExtensionManager ExtensionManager => new ComfyExtensionManager(this, settingsManager); private class ComfyExtensionManager(ComfyUI package, ISettingsManager settingsManager) : GitPackageExtensionManager(package.PrerequisiteHelper) { public override string RelativeInstallDirectory => "custom_nodes"; public override IEnumerable DefaultManifests => [ "https://cdn.jsdelivr.net/gh/ltdrdata/ComfyUI-Manager/custom-node-list.json", "https://cdn.jsdelivr.net/gh/LykosAI/ComfyUI-Extensions-Index/custom-node-list.json", ]; public override async Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ) { try { // Get json var content = await package .DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken) .ConfigureAwait(false); // Parse json var jsonManifest = JsonSerializer.Deserialize( content, ComfyExtensionManifestSerializerContext.Default.Options ); if (jsonManifest == null) return []; var extensions = jsonManifest.GetPackageExtensions().ToList(); return extensions; } catch (Exception e) { Logger.Error(e, "Failed to get package extensions"); return []; } } /// public override async Task UpdateExtensionAsync( InstalledPackageExtension installedExtension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { await base.UpdateExtensionAsync( installedExtension, installedPackage, version, progress, cancellationToken ) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var installedDirs = installedExtension.Paths.OfType().Where(dir => dir.Exists); await PostInstallAsync( installedPackage, installedDirs, installedExtension.Definition!, progress, cancellationToken ) .ConfigureAwait(false); } /// public override async Task InstallExtensionAsync( PackageExtension extension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { await base.InstallExtensionAsync( extension, installedPackage, version, progress, cancellationToken ) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var cloneRoot = new DirectoryPath(installedPackage.FullPath!, RelativeInstallDirectory); var installedDirs = extension .Files.Select(uri => uri.Segments.LastOrDefault()) .Where(path => !string.IsNullOrEmpty(path)) .Select(path => cloneRoot.JoinDir(path!)) .Where(dir => dir.Exists); await PostInstallAsync(installedPackage, installedDirs, extension, progress, cancellationToken) .ConfigureAwait(false); } /// /// Runs post install / update tasks (i.e. install.py, requirements.txt) /// private async Task PostInstallAsync( InstalledPackage installedPackage, IEnumerable installedDirs, PackageExtension extension, IProgress? progress = null, CancellationToken cancellationToken = default ) { // do pip installs if (extension.Pip != null) { await using var venvRunner = await package .SetupVenvPure( installedPackage.FullPath!, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); var pipArgs = new PipInstallArgs(); pipArgs = extension.Pip.Aggregate(pipArgs, (current, pip) => current.AddArg(pip)); await venvRunner .PipInstall(pipArgs, progress?.AsProcessOutputHandler()) .ConfigureAwait(false); } foreach (var installedDir in installedDirs) { cancellationToken.ThrowIfCancellationRequested(); // Install requirements.txt if found if (installedDir.JoinFile("requirements.txt") is { Exists: true } requirementsFile) { var requirementsContent = await requirementsFile .ReadAllTextAsync(cancellationToken) .ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(requirementsContent)) { progress?.Report( new ProgressReport( 0f, $"Installing requirements.txt for {installedDir.Name}", isIndeterminate: true ) ); await using var venvRunner = await package .SetupVenvPure( installedPackage.FullPath!, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); var pipArgs = new PipInstallArgs().WithParsedFromRequirementsTxt(requirementsContent); await venvRunner .PipInstall(pipArgs, progress.AsProcessOutputHandler()) .ConfigureAwait(false); progress?.Report( new ProgressReport(1f, $"Installed requirements.txt for {installedDir.Name}") ); } } cancellationToken.ThrowIfCancellationRequested(); // Run install.py if found if (installedDir.JoinFile("install.py") is { Exists: true } installScript) { progress?.Report( new ProgressReport( 0f, $"Running install.py for {installedDir.Name}", isIndeterminate: true ) ); await using var venvRunner = await package .SetupVenvPure( installedPackage.FullPath!, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); venvRunner.WorkingDirectory = installScript.Directory; venvRunner.UpdateEnvironmentVariables(env => { // set env vars for Impact Pack for Face Detailer env = env.SetItem("COMFYUI_PATH", installedPackage.FullPath!); var modelPath = installedPackage.PreferredSharedFolderMethod == SharedFolderMethod.None ? Path.Combine(installedPackage.FullPath!, "models") : settingsManager.ModelsDirectory; env = env.SetItem("COMFYUI_MODEL_PATH", modelPath); return env; }); venvRunner.RunDetached(["install.py"], progress.AsProcessOutputHandler()); await venvRunner.Process.WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false); await venvRunner.Process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (venvRunner.Process.HasExited && venvRunner.Process.ExitCode != 0) { throw new ProcessException( $"install.py for {installedDir.Name} exited with code {venvRunner.Process.ExitCode}" ); } progress?.Report(new ProgressReport(1f, $"Ran launch.py for {installedDir.Name}")); } } } } private async Task InstallTritonAndSageAttention(InstalledPackage? installedPackage) { if (installedPackage?.FullPath is null) return; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = "Triton and SageAttention installed successfully", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner .ExecuteSteps( [ new ActionPackageStep( async progress => { await using var venvRunner = await SetupVenvPure( installedPackage.FullPath, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); var gpuInfo = SettingsManager.Settings.PreferredGpu ?? HardwareHelper.IterGpuInfo().FirstOrDefault(x => x.IsNvidia); await PipWheelService .InstallTritonAsync(venvRunner, progress) .ConfigureAwait(false); await PipWheelService .InstallSageAttentionAsync(venvRunner, gpuInfo, progress) .ConfigureAwait(false); }, "Installing Triton and SageAttention" ), ] ) .ConfigureAwait(false); if (runner.Failed) return; await using var transaction = settingsManager.BeginTransaction(); var attentionOptions = transaction .Settings.InstalledPackages.First(x => x.Id == installedPackage.Id) .LaunchArgs?.Where(opt => opt.Name.Contains("attention")); if (attentionOptions is not null) { foreach (var option in attentionOptions) { option.OptionValue = false; } } var sageAttention = transaction .Settings.InstalledPackages.First(x => x.Id == installedPackage.Id) .LaunchArgs?.FirstOrDefault(opt => opt.Name.Contains("sage-attention")); if (sageAttention is not null) { sageAttention.OptionValue = true; } else { transaction .Settings.InstalledPackages.First(x => x.Id == installedPackage.Id) .LaunchArgs?.Add( new LaunchOption { Name = "--use-sage-attention", Type = LaunchOptionType.Bool, OptionValue = true, } ); } } private async Task InstallNunchaku(InstalledPackage? installedPackage) { if (installedPackage?.FullPath is null) return; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = "Nunchaku installed successfully", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner .ExecuteSteps( [ new ActionPackageStep( async progress => { await using var venvRunner = await SetupVenvPure( installedPackage.FullPath, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); var gpuInfo = SettingsManager.Settings.PreferredGpu ?? HardwareHelper.IterGpuInfo().FirstOrDefault(x => x.IsNvidia || x.IsAmd); await PipWheelService .InstallNunchakuAsync(venvRunner, gpuInfo, progress) .ConfigureAwait(false); }, "Installing Nunchaku" ), ] ) .ConfigureAwait(false); } private ImmutableDictionary GetEnvVars(ImmutableDictionary env) { // if we're not on windows or we don't have a windows rocm gpu, return original env var hasRocmGpu = SettingsManager.Settings.PreferredGpu?.IsWindowsRocmSupportedGpu() ?? HardwareHelper.HasWindowsRocmSupportedGpu(); if (!Compat.IsWindows || !hasRocmGpu) return env; // set some experimental speed improving env vars for Windows ROCm return env.SetItem("PYTORCH_TUNABLEOP_ENABLED", "1") .SetItem("MIOPEN_FIND_MODE", "2") .SetItem("TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL", "1") .SetItem("PYTORCH_ALLOC_CONF", "max_split_size_mb:6144,garbage_collection_threshold:0.8") // greatly helps prevent GPU OOM and instability/driver timeouts/OS hard locks and decreases dependency on Tiled VAE at standard res's .SetItem("COMFYUI_ENABLE_MIOPEN", "1"); // re-enables "cudnn" in ComfyUI as it's needed for MiOpen to function properly } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/ComfyZluda.cs ================================================ using System.Diagnostics; using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models; 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; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class ComfyZluda( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : ComfyUI( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private const string ZludaPatchDownloadUrl = "https://github.com/lshqqytiger/ZLUDA/releases/download/rel.5e717459179dc272b7d7d23391f0fad66c7459cf/ZLUDA-nightly-windows-rocm6-amd64.zip"; private const string HipSdkExtensionDownloadUrl = "https://cdn.lykos.ai/HIP-SDK-extension.7z"; private const string VenvDirectoryName = "venv"; private Process? zludaProcess; public override string Name => "ComfyUI-Zluda"; public override string DisplayName => "ComfyUI-Zluda"; public override string Author => "patientx"; public override string LicenseUrl => "https://github.com/patientx/ComfyUI-Zluda/blob/master/LICENSE"; public override string Blurb => "Windows-only version of ComfyUI which uses ZLUDA to get better performance with AMD GPUs."; public override string Disclaimer => "Prerequisite install may require admin privileges and a reboot. " + "Visual Studio Build Tools for C++ Desktop Development will be installed automatically. " + "AMD GPUs under the RX 6800 may require additional manual setup. "; public override string LaunchCommand => Path.Combine("zluda", "zluda.exe"); public override List LaunchOptions { get { var options = new List { new() { Name = "Cross Attention Method", Type = LaunchOptionType.Bool, InitialValue = "--use-quad-cross-attention", Options = [ "--use-split-cross-attention", "--use-quad-cross-attention", "--use-pytorch-cross-attention", "--use-sage-attention", ], }, new() { Name = "Disable Async Offload", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--disable-async-offload"], }, new() { Name = "Disable Pinned Memory", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--disable-pinned-memory"], }, new() { Name = "Disable Smart Memory", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--disable-smart-memory"], }, new() { Name = "Disable Model/Node Caching", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--cache-none"], }, }; options.AddRange(base.LaunchOptions.Where(x => x.Name != "Cross Attention Method")); return options; } } public override IEnumerable AvailableTorchIndices => [TorchIndex.Zluda]; public override TorchIndex GetRecommendedTorchVersion() => TorchIndex.Zluda; public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_11_13; public override bool IsCompatible => HardwareHelper.PreferDirectMLOrZluda(); public override bool ShouldIgnoreReleases => true; public override IEnumerable Prerequisites => base.Prerequisites.Concat([PackagePrerequisite.HipSdk, PackagePrerequisite.VcBuildTools]); public override bool InstallRequiresAdmin => true; public override string AdminRequiredReason => "HIP SDK and Visual Studio Build Tools installation, as well as (if applicable) ROCmLibs patching, require admin privileges for accessing files in the Program Files directory. This may take several minutes to complete."; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!PrerequisiteHelper.IsHipSdkInstalled) // for updates { progress?.Report(new ProgressReport(-1, "Installing HIP SDK 6.4", isIndeterminate: true)); await PrerequisiteHelper .InstallPackageRequirements(this, options.PythonOptions.PythonVersion, progress) .ConfigureAwait(false); } if (options.IsUpdate) { return; } progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var installNBatPath = new FilePath(installLocation, "install-n.bat"); var newInstallBatPath = new FilePath(installLocation, "install-sm.bat"); var installNText = await installNBatPath.ReadAllTextAsync(cancellationToken).ConfigureAwait(false); var installNLines = installNText.Split(Environment.NewLine); var cutoffIndex = Array.FindIndex(installNLines, line => line.Contains("Installation is completed")); IEnumerable filtered = installNLines; if (cutoffIndex >= 0) { filtered = installNLines.Take(cutoffIndex); } newInstallBatPath.Create(); await newInstallBatPath .WriteAllTextAsync(string.Join(Environment.NewLine, filtered), cancellationToken) .ConfigureAwait(false); var installProcess = ProcessRunner.StartAnsiProcess( newInstallBatPath, [], installLocation, onConsoleOutput, GetEnvVars(true) ); await installProcess.WaitForExitAsync(cancellationToken).ConfigureAwait(false); progress?.Report(new ProgressReport(1, "Installed Successfully", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!PrerequisiteHelper.IsHipSdkInstalled) { throw new MissingPrerequisiteException( "HIP SDK", "Your package has not yet been upgraded to use HIP SDK 6.4. To continue, please update this package or select \"Change Version\" from the 3-dots menu to have it upgraded automatically for you" ); } await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); var zludaPath = Path.Combine(installLocation, LaunchCommand); ProcessArgs args = ["--", VenvRunner.PythonPath.ToString(), "main.py", .. options.Arguments]; zludaProcess = ProcessRunner.StartAnsiProcess( zludaPath, args, installLocation, HandleConsoleOutput, GetEnvVars(false) ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); 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) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } public override async Task WaitForShutdown() { if (zludaProcess is { HasExited: false }) { zludaProcess.Kill(true); try { await zludaProcess .WaitForExitAsync(new CancellationTokenSource(5000).Token) .ConfigureAwait(false); } catch (OperationCanceledException e) { Console.WriteLine(e); } } zludaProcess = null; GC.SuppressFinalize(this); } private Dictionary GetEnvVars(bool isInstall) { var portableGitBin = new DirectoryPath(PrerequisiteHelper.GitBinPath); var hipPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "AMD", "ROCm", "6.4" ); var hipBinPath = Path.Combine(hipPath, "bin"); var envVars = new Dictionary { ["ZLUDA_COMGR_LOG_LEVEL"] = "1", ["HIP_PATH"] = hipPath, ["HIP_PATH_64"] = hipPath, ["GIT"] = portableGitBin.JoinFile("git.exe"), }; if (isInstall) { envVars["VIRTUAL_ENV"] = VenvDirectoryName; } if (envVars.TryGetValue("PATH", out var pathValue)) { envVars["PATH"] = Compat.GetEnvPathWithExtensions(hipBinPath, portableGitBin, pathValue); } else { envVars["PATH"] = Compat.GetEnvPathWithExtensions(hipBinPath, portableGitBin); } if (isInstall) return envVars; envVars["FLASH_ATTENTION_TRITON_AMD_ENABLE"] = "TRUE"; envVars["MIOPEN_FIND_MODE"] = "2"; envVars["MIOPEN_LOG_LEVEL"] = "3"; var gfxArch = PrerequisiteHelper.GetGfxArchFromAmdGpuName(); if (!string.IsNullOrWhiteSpace(gfxArch)) { envVars["TRITON_OVERRIDE_ARCH"] = gfxArch; } envVars.Update(settingsManager.Settings.EnvironmentVariables); return envVars; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/ConfigDefaultType.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Config; public enum ConfigDefaultType { /// /// Set as SharedFolderLayout.TargetRelativePaths. /// TargetRelativePaths, /// /// Clear the root key when defaulting. /// ClearRoot, } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/ConfigFileType.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Config; public enum ConfigFileType { Json, Yaml, Fds // Frenetic Data Syntax } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/ConfigSharingOptions.cs ================================================ using System.Text.Json; namespace StabilityMatrix.Core.Models.Packages.Config; // Options might need expansion later if format-specific settings are required public record ConfigSharingOptions { public static ConfigSharingOptions Default { get; } = new(); // For JSON: public JsonSerializerOptions JsonSerializerOptions { get; set; } = new() { WriteIndented = true }; // For JSON/YAML: Write single paths as arrays? public bool AlwaysWriteArray { get; set; } = false; // For YAML/FDS: Key under which to store SM paths (e.g., "stability_matrix") public string? RootKey { get; set; } // Do we want to clear the root key / set to relative paths when clearing? public ConfigDefaultType ConfigDefaultType { get; set; } = ConfigDefaultType.TargetRelativePaths; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/FdsConfigSharingStrategy.cs ================================================ using System.Diagnostics; using System.Text; using System.Text.Json.Nodes; using FreneticUtilities.FreneticDataSyntax; namespace StabilityMatrix.Core.Models.Packages.Config; public class FdsConfigSharingStrategy : IConfigSharingStrategy { public async Task UpdateAndWriteAsync( Stream configStream, SharedFolderLayout layout, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options, CancellationToken cancellationToken = default ) { FDSSection rootSection; var initialPosition = configStream.Position; var isEmpty = configStream.Length - initialPosition == 0; if (!isEmpty) { try { // FDSUtility reads from the current position using var reader = new StreamReader(configStream, leaveOpen: true); var fdsContent = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); rootSection = new FDSSection(fdsContent); } catch (Exception ex) // FDSUtility might throw various exceptions on parse errors { System.Diagnostics.Debug.WriteLine( $"Error deserializing FDS config: {ex.Message}. Treating as new." ); rootSection = new FDSSection(); isEmpty = true; } } else { rootSection = new FDSSection(); } // Debug.WriteLine($"-- Current Fds --\n\n{rootSection.SaveToString()}"); UpdateFdsConfig(layout, rootSection, pathsSelector, clearPaths, options); // Reset stream to original position before writing configStream.Seek(initialPosition, SeekOrigin.Begin); // Truncate the stream configStream.SetLength(initialPosition + 0); // Save using a StreamWriter to control encoding and leave stream open // Use BOM-less UTF-8 encoding !! (FSD not UTF-8 BOM compatible) var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); await using (var writer = new StreamWriter(configStream, encoding, leaveOpen: true)) { // Debug.WriteLine($"-- Saved Fds: --\n\n{rootSection.SaveToString()}"); await writer .WriteAsync(rootSection.SaveToString().AsMemory(), cancellationToken) .ConfigureAwait(false); await writer.FlushAsync(cancellationToken).ConfigureAwait(false); // Ensure content is written } await configStream.FlushAsync(cancellationToken).ConfigureAwait(false); // Flush the underlying stream } private static void UpdateFdsConfig( SharedFolderLayout layout, FDSSection rootSection, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options ) { var rulesByConfigPath = layout.GetRulesByConfigPath(); // SwarmUI typically stores paths under a "Paths" section // Get or create the Paths section var pathsSection = rootSection.GetSection("Paths"); if (pathsSection is null) { pathsSection = new FDSSection(); rootSection.Set("Paths", pathsSection); // Add Paths section to the root } // Keep track of keys managed by the layout to remove old ones var allRuleKeys = rulesByConfigPath.Keys.ToHashSet(); var currentKeysInPathsSection = pathsSection.GetRootKeys(); // Assuming FDS has a way to list keys foreach (var (configPath, rule) in rulesByConfigPath) { var paths = pathsSelector(rule).ToArray(); // Normalize paths for FDS - likely prefers native OS slashes or forward slashes var normalizedPaths = paths.Select(p => p.Replace('/', Path.DirectorySeparatorChar)).ToList(); if (normalizedPaths.Count > 0) { // FDS lists are separated by semicolon pathsSection.Set(configPath, string.Join(';', normalizedPaths)); // If FDS supports lists explicitly (e.g., via SetList), use that: // pathsSection.SetList(configPath, normalizedPaths); } else { // No paths for this rule, remove the key // pathsSection.Remove(configPath); // Assuming Remove method exists } } // Remove any keys in the Paths section that are no longer defined by any rule /*foreach (var existingKey in currentKeysInPathsSection) { if (!allRuleKeys.Contains(existingKey)) { pathsSection.Remove(existingKey); } }*/ /*// If the Paths section is not empty, add/update it in the root if (pathsSection.GetRootKeys().Any()) // Check if the section has content { rootSection.Set("Paths", pathsSection); // rootSection.SetSection("Paths", pathsSection); } else // Otherwise, remove the empty Paths section from the root { rootSection.Remove("Paths"); // Assuming Remove method exists for sections too }*/ } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/IConfigSharingStrategy.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Config; public interface IConfigSharingStrategy { /// /// Reads the config stream, updates paths based on the layout and selector, and writes back to the stream. /// Task UpdateAndWriteAsync( Stream configStream, SharedFolderLayout layout, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/JsonConfigSharingStrategy.cs ================================================ using System.Text.Json; using System.Text.Json.Nodes; namespace StabilityMatrix.Core.Models.Packages.Config; public class JsonConfigSharingStrategy : IConfigSharingStrategy { public async Task UpdateAndWriteAsync( Stream configStream, SharedFolderLayout layout, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options, CancellationToken cancellationToken = default ) { JsonObject jsonNode; var initialPosition = configStream.Position; var isEmpty = configStream.Length - initialPosition == 0; if (isEmpty) { jsonNode = new JsonObject(); } else { try { // Ensure we read from the current position, respecting potential BOMs etc. jsonNode = await JsonSerializer .DeserializeAsync( configStream, options.JsonSerializerOptions, cancellationToken ) .ConfigureAwait(false) ?? new JsonObject(); } catch (JsonException ex) { // Handle cases where the file might exist but be invalid JSON // Log the error, maybe throw a specific exception or return default // For now, we'll treat it as empty/new System.Diagnostics.Debug.WriteLine( $"Error deserializing JSON config: {ex.Message}. Treating as new." ); jsonNode = new JsonObject(); isEmpty = true; // Ensure we overwrite if deserialization failed } } UpdateJsonConfig(layout, jsonNode, pathsSelector, clearPaths, options); // Reset stream to original position (or beginning if new/failed) before writing configStream.Seek(initialPosition, SeekOrigin.Begin); // Truncate the stream in case the new content is shorter configStream.SetLength(initialPosition + 0); // Truncate from the original position onwards await JsonSerializer .SerializeAsync(configStream, jsonNode, options.JsonSerializerOptions, cancellationToken) .ConfigureAwait(false); await configStream.FlushAsync(cancellationToken).ConfigureAwait(false); } private static void UpdateJsonConfig( SharedFolderLayout layout, JsonObject rootNode, // Changed parameter name for clarity Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options ) { var rulesByConfigPath = layout.GetRulesByConfigPath(); var allRuleConfigPaths = rulesByConfigPath.Keys.ToHashSet(); // For cleanup foreach (var (configPath, rule) in rulesByConfigPath) { var paths = pathsSelector(rule).ToArray(); var normalizedPaths = paths.Select(p => p.Replace('\\', '/')).ToArray(); JsonNode? valueNode = null; if (normalizedPaths.Length > 1 || options.AlwaysWriteArray) { valueNode = new JsonArray( normalizedPaths.Select(p => JsonValue.Create(p)).OfType().ToArray() ); } else if (normalizedPaths.Length == 1) { valueNode = JsonValue.Create(normalizedPaths[0]); } SetJsonValue(rootNode, configPath, valueNode); // Use helper to set/remove value } // Optional: Cleanup - Remove keys defined in layout but now empty? // This might be complex if paths overlap. Current SetJsonValue(..., null) handles removal. // We might need a separate cleanup pass if strictly necessary. } private static void SetJsonValue(JsonObject root, string dottedPath, JsonNode? value) { var segments = dottedPath.Split('.'); JsonObject currentNode = root; // Traverse or create nodes up to the parent of the target for (int i = 0; i < segments.Length - 1; i++) { var segment = segments[i]; if ( !currentNode.TryGetPropertyValue(segment, out var nextNode) || nextNode is not JsonObject nextObj ) { // If node doesn't exist or isn't an object, create it (overwriting if necessary) nextObj = new JsonObject(); currentNode[segment] = nextObj; } currentNode = nextObj; } var finalSegment = segments[^1]; // Get the last segment (the key name) if (value != null) { // Set or replace the value currentNode[finalSegment] = value.DeepClone(); // Use DeepClone to avoid node reuse issues } else { // Remove the key if value is null currentNode.Remove(finalSegment); // Optional: Clean up empty parent nodes recursively if desired (more complex) } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Config/YamlConfigSharingStrategy.cs ================================================ using YamlDotNet.Core; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace StabilityMatrix.Core.Models.Packages.Config; public class YamlConfigSharingStrategy : IConfigSharingStrategy { public async Task UpdateAndWriteAsync( Stream configStream, SharedFolderLayout layout, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options, CancellationToken cancellationToken = default ) { YamlMappingNode rootNode; YamlStream yamlStream = []; var initialPosition = configStream.Position; var isEmpty = configStream.Length - initialPosition == 0; if (!isEmpty) { try { using var reader = new StreamReader(configStream, leaveOpen: true); yamlStream.Load(reader); // Load existing YAML if ( yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode mapping ) { rootNode = mapping; } else { // File exists but isn't a valid mapping node at the root, start fresh System.Diagnostics.Debug.WriteLine( $"YAML config exists but is not a mapping node. Treating as new." ); rootNode = []; yamlStream = new YamlStream(new YamlDocument(rootNode)); // Reset stream content isEmpty = true; } } catch (YamlException ex) { // Handle cases where the file might exist but be invalid YAML System.Diagnostics.Debug.WriteLine( $"Error deserializing YAML config: {ex.Message}. Treating as new." ); rootNode = []; yamlStream = new YamlStream(new YamlDocument(rootNode)); // Reset stream content isEmpty = true; } } else { // Stream is empty, create new structure rootNode = []; yamlStream.Add(new YamlDocument(rootNode)); } UpdateYamlConfig(layout, rootNode, pathsSelector, clearPaths, options); // Reset stream to original position (or beginning if new/failed) before writing configStream.Seek(initialPosition, SeekOrigin.Begin); // Truncate the stream in case the new content is shorter configStream.SetLength(initialPosition + 0); // Use StreamWriter to write back to the original stream // Use default encoding (UTF8 without BOM is common for YAML) await using (var writer = new StreamWriter(configStream, leaveOpen: true)) { // Configure serializer for better readability if desired var serializer = new SerializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) // Common for ComfyUI paths .WithDefaultScalarStyle(ScalarStyle.Literal) .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) // Optional: omit nulls/defaults .Build(); serializer.Serialize(writer, yamlStream.Documents[0].RootNode); // Serialize the modified root node await writer.FlushAsync(cancellationToken).ConfigureAwait(false); // Ensure content is written to stream } await configStream.FlushAsync(cancellationToken).ConfigureAwait(false); // Flush the underlying stream } private static void UpdateYamlConfig( SharedFolderLayout layout, YamlMappingNode rootNode, Func> pathsSelector, IEnumerable clearPaths, ConfigSharingOptions options ) { var rulesByConfigPath = layout.GetRulesByConfigPath(); YamlNode currentNode = rootNode; // Start at the actual root // Handle RootKey (like stability_matrix) if specified if (!string.IsNullOrEmpty(options.RootKey)) { var rootKeyNode = new YamlScalarNode(options.RootKey); if ( !rootNode.Children.TryGetValue(rootKeyNode, out var subNode) || subNode is not YamlMappingNode subMapping ) { if (subNode != null) rootNode.Children.Remove(rootKeyNode); // Remove if exists but wrong type subMapping = []; rootNode.Add(rootKeyNode, subMapping); } currentNode = subMapping; // Operate within the specified RootKey node } if (currentNode is not YamlMappingNode writableNode) { // This should not happen if RootKey logic is correct, but handle defensively System.Diagnostics.Debug.WriteLine($"Error: Target node for YAML updates is not a mapping node."); return; } foreach (var (configPath, rule) in rulesByConfigPath) { var paths = pathsSelector(rule).ToArray(); var normalizedPaths = paths.Select(p => p.Replace('\\', '/')).ToArray(); YamlNode? valueNode = null; if (normalizedPaths.Length > 0) { // Use Sequence for multiple paths /*valueNode = new YamlSequenceNode( normalizedPaths.Select(p => new YamlScalarNode(p)).Cast() );*/ // --- Multi-line literal scalar (ComfyUI default) --- var multiLinePath = string.Join("\n", normalizedPaths); valueNode = new YamlScalarNode(multiLinePath) { Style = ScalarStyle.Literal }; } SetYamlValue(writableNode, configPath, valueNode); // Use helper } // Clear specified paths foreach (var clearPath in clearPaths) { SetYamlValue(rootNode, clearPath, null); // Note we use root node here instead } // Optional: Cleanup empty nodes after setting values (could be complex) } /*private static void UpdateYamlConfig( SharedFolderLayout layout, YamlMappingNode rootNode, Func> pathsSelector, ConfigSharingOptions options ) { var rulesByConfigPath = layout.GetRulesByConfigPath(); var smKeyNode = new YamlScalarNode(options.RootKey); // Find or create the root key node (e.g., "stability_matrix:") if ( !rootNode.Children.TryGetValue(smKeyNode, out var smPathsNode) || smPathsNode is not YamlMappingNode smPathsMapping ) { // If it exists but isn't a mapping, remove it first (or handle error) if (smPathsNode != null) { rootNode.Children.Remove(smKeyNode); } smPathsMapping = new YamlMappingNode(); rootNode.Add(smKeyNode, smPathsMapping); } // Get all keys defined in the layout rules to manage removal later var allRuleKeys = rulesByConfigPath.Keys.Select(k => new YamlScalarNode(k)).ToHashSet(); var currentKeysInSmNode = smPathsMapping.Children.Keys.ToHashSet(); foreach (var (configPath, rule) in rulesByConfigPath) { var paths = pathsSelector(rule).ToArray(); var keyNode = new YamlScalarNode(configPath); if (paths.Length > 0) { // Represent multiple paths as a YAML sequence (list) for clarity // Normalize paths - YAML generally prefers forward slashes var normalizedPaths = paths .Select(p => new YamlScalarNode(p.Replace('\\', '/'))) .Cast() .ToList(); smPathsMapping.Children[keyNode] = new YamlSequenceNode(normalizedPaths); // --- Alternatively, represent as multi-line literal scalar (like ComfyUI default) --- // var multiLinePath = string.Join("\n", paths.Select(p => p.Replace('\\', '/'))); // var valueNode = new YamlScalarNode(multiLinePath) { Style = ScalarStyle.Literal }; // smPathsMapping.Children[keyNode] = valueNode; // --------------------------------------------------------------------------------- } else { // No paths for this rule, remove the key from the SM node smPathsMapping.Children.Remove(keyNode); } } // Remove any keys under the SM node that are no longer defined by any rule foreach (var existingKey in currentKeysInSmNode) { if (!allRuleKeys.Any(ruleKey => ruleKey.Value == existingKey.ToString())) { smPathsMapping.Children.Remove(existingKey); } } // If the SM node becomes empty, remove it entirely if (smPathsMapping.Children.Count == 0) { rootNode.Children.Remove(smKeyNode); } }*/ private static void SetYamlValue(YamlMappingNode rootMapping, string dottedPath, YamlNode? value) { var segments = dottedPath.Split('.'); var currentMapping = rootMapping; // Traverse or create nodes up to the parent of the target for (var i = 0; i < segments.Length - 1; i++) { var segmentNode = new YamlScalarNode(segments[i]); if ( !currentMapping.Children.TryGetValue(segmentNode, out var nextNode) || nextNode is not YamlMappingNode nextMapping ) { // If node doesn't exist or isn't a mapping, create it if (nextNode != null) currentMapping.Children.Remove(segmentNode); // Remove if wrong type nextMapping = []; currentMapping.Add(segmentNode, nextMapping); } currentMapping = nextMapping; } var finalSegmentNode = new YamlScalarNode(segments[^1]); if (value != null) { // Set or replace the value currentMapping.Children[finalSegmentNode] = value; } else { // Remove the key if value is null currentMapping.Children.Remove(finalSegmentNode); // Optional: Cleanup empty parent nodes recursively (more complex) } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/DankDiffusion.cs ================================================ 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; public class DankDiffusion : BaseGitPackage { public DankDiffusion( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : base( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { } public override string Name => "dank-diffusion"; public override string DisplayName { get; set; } = "Dank Diffusion"; public override string Author => "mohnjiles"; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE"; public override string Blurb => "A dank interface for diffusion"; public override string LaunchCommand => "test"; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override IReadOnlyDictionary ExtraLaunchCommands => new Dictionary { ["test-config"] = "test-config" }; public override Uri PreviewImageUri { get; } public override string OutputFolderName { get; } public override PackageDifficulty InstallerSortOrder { get; } public override Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } public override Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } public override Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } public override Task UpdateModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } public override Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } public override IEnumerable AvailableTorchIndices { get; } public override List LaunchOptions { get; } public override Dictionary>? SharedFolders { get; } public override Dictionary>? SharedOutputFolders { get; } public override string MainBranch { get; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/A1111ExtensionManifest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Packages.Extensions; public record A1111ExtensionManifest { public required IEnumerable Extensions { get; init; } public IEnumerable GetPackageExtensions() { return Extensions.Select( x => new PackageExtension { Author = x.FullName?.Split('/').FirstOrDefault() ?? "Unknown", Title = x.Name, Reference = x.Url, Files = [x.Url], Description = x.Description, InstallType = "git-clone" } ); } public record ManifestEntry { public string? FullName { get; init; } public required string Name { get; init; } public required Uri Url { get; init; } public string? Description { get; init; } } } [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] [JsonSerializable(typeof(A1111ExtensionManifest))] internal partial class A1111ExtensionManifestSerializerContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/ComfyExtensionManifest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Packages.Extensions; public record ComfyExtensionManifest { public required IEnumerable CustomNodes { get; init; } public IEnumerable GetPackageExtensions() { return CustomNodes.Select( x => new PackageExtension { Author = x.Author, Title = x.Title, Reference = x.Reference, Files = x.Files, Pip = x.Pip, Description = x.Description, InstallType = x.InstallType } ); } public record ManifestEntry { public required string Author { get; init; } public required string Title { get; init; } public required Uri Reference { get; init; } public required IEnumerable Files { get; init; } public IEnumerable? Pip { get; init; } public string? Description { get; init; } public string? InstallType { get; init; } } } [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] [JsonSerializable(typeof(ComfyExtensionManifest))] internal partial class ComfyExtensionManifestSerializerContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/ExtensionManifest.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public record ExtensionManifest(Uri Uri) { public static implicit operator ExtensionManifest(string uri) => new(new Uri(uri, UriKind.Absolute)); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/ExtensionPack.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public class ExtensionPack { public required string Name { get; set; } public required string PackageType { get; set; } public List Extensions { get; set; } = []; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/ExtensionSpecifier.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using JetBrains.Annotations; using Semver; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.Packages.Extensions; /// /// Extension specifier with optional version constraints. /// [PublicAPI] public partial class ExtensionSpecifier { public required string Name { get; init; } public string? Constraint { get; init; } public string? Version { get; init; } public string? VersionConstraint => Constraint is null || Version is null ? null : Constraint + Version; public bool TryGetSemVersionRange([NotNullWhen(true)] out SemVersionRange? semVersionRange) { if (!string.IsNullOrEmpty(VersionConstraint)) { return SemVersionRange.TryParse( VersionConstraint, SemVersionRangeOptions.Loose, out semVersionRange ); } semVersionRange = null; return false; } public static ExtensionSpecifier Parse(string value) { TryParse(value, true, out var packageSpecifier); return packageSpecifier!; } public static bool TryParse(string value, [NotNullWhen(true)] out ExtensionSpecifier? extensionSpecifier) { return TryParse(value, false, out extensionSpecifier); } private static bool TryParse( string value, bool throwOnFailure, [NotNullWhen(true)] out ExtensionSpecifier? packageSpecifier ) { var match = ExtensionSpecifierRegex().Match(value); if (!match.Success) { if (throwOnFailure) { throw new ArgumentException($"Invalid extension specifier: {value}"); } packageSpecifier = null; return false; } packageSpecifier = new ExtensionSpecifier { Name = match.Groups["extension_name"].Value, Constraint = match.Groups["version_constraint"].Value, Version = match.Groups["version"].Value }; return true; } /// public override string ToString() { return Name + VersionConstraint; } public Argument ToArgument() { if (VersionConstraint is not null) { // Use Name as key return new Argument(key: Name, value: ToString()); } return new Argument(ToString()); } public static implicit operator Argument(ExtensionSpecifier specifier) { return specifier.ToArgument(); } public static implicit operator ExtensionSpecifier(string specifier) { return Parse(specifier); } /// /// Regex to match a pip package specifier. /// [GeneratedRegex( @"(?\S+)\s*(?==|>=|<=|>|<|~=|!=)?\s*(?[a-zA-Z0-9_.]+)?", RegexOptions.CultureInvariant, 5000 )] private static partial Regex ExtensionSpecifierRegex(); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs ================================================ using System.Text.RegularExpressions; using KGySoft.CoreLibraries; using Microsoft.Extensions.Caching.Memory; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.Packages.Extensions; public abstract partial class GitPackageExtensionManager(IPrerequisiteHelper prerequisiteHelper) : IPackageExtensionManager { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); // Cache checks of installed extensions private readonly MemoryCache installedExtensionsCache = new(new MemoryCacheOptions()); public abstract string RelativeInstallDirectory { get; } public virtual IEnumerable DefaultManifests { get; } = Enumerable.Empty(); protected virtual IEnumerable IndexRelativeDirectories => [RelativeInstallDirectory]; public abstract Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ); /// Task> IPackageExtensionManager.GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken ) { return GetManifestExtensionsAsync(manifest, cancellationToken); } protected virtual IEnumerable GetManifests(InstalledPackage installedPackage) { if (installedPackage.ExtraExtensionManifestUrls is not { } customUrls) { return DefaultManifests; } var manifests = DefaultManifests.ToList(); foreach (var url in customUrls) { if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out var uri)) { manifests.Add(new ExtensionManifest(uri)); } } return manifests; } /// IEnumerable IPackageExtensionManager.GetManifests(InstalledPackage installedPackage) { return GetManifests(installedPackage); } /// public virtual async Task> GetInstalledExtensionsAsync( InstalledPackage installedPackage, CancellationToken cancellationToken = default ) { if (installedPackage.FullPath is not { } packagePath) { return Enumerable.Empty(); } var extensions = new List(); // Search for installed extensions in the package's index directories. foreach ( var indexDirectory in IndexRelativeDirectories.Select(path => new DirectoryPath( packagePath, path )) ) { cancellationToken.ThrowIfCancellationRequested(); // Skip directory if not exists if (!indexDirectory.Exists) { continue; } // Check subdirectories of the index directory foreach (var subDirectory in indexDirectory.EnumerateDirectories()) { cancellationToken.ThrowIfCancellationRequested(); // Skip if not valid git repository if (await prerequisiteHelper.CheckIsGitRepository(subDirectory).ConfigureAwait(false) != true) continue; // Get git version var version = await prerequisiteHelper .GetGitRepositoryVersion(subDirectory) .ConfigureAwait(false); // Get git remote var remoteUrlResult = await prerequisiteHelper .GetGitRepositoryRemoteOriginUrl(subDirectory) .ConfigureAwait(false); extensions.Add( new InstalledPackageExtension { Paths = [subDirectory], Version = new PackageExtensionVersion { Tag = version.Tag, Branch = version.Branch, CommitSha = version.CommitSha, }, GitRepositoryUrl = remoteUrlResult.IsSuccessExitCode ? remoteUrlResult.StandardOutput?.Trim() : null, } ); } } return extensions; } /// /// Like , but does not check git version and repository url. /// public virtual async Task> GetInstalledExtensionsLiteAsync( InstalledPackage installedPackage, CancellationToken cancellationToken = default ) { if (installedPackage.FullPath is not { } packagePath) { return Enumerable.Empty(); } var extensions = new List(); // Search for installed extensions in the package's index directories. foreach ( var indexDirectory in IndexRelativeDirectories.Select(path => new DirectoryPath( packagePath, path )) ) { cancellationToken.ThrowIfCancellationRequested(); // Skip directory if not exists if (!indexDirectory.Exists) { continue; } // Check subdirectories of the index directory foreach (var subDirectory in indexDirectory.EnumerateDirectories()) { cancellationToken.ThrowIfCancellationRequested(); // Skip if not valid git repository if (!subDirectory.JoinDir(".git").Exists) { continue; } // Get remote url with manual parsing string? remoteUrl = null; var gitConfigPath = subDirectory.JoinDir(".git").JoinFile("config"); if ( gitConfigPath.Exists && await gitConfigPath.ReadAllTextAsync(cancellationToken).ConfigureAwait(false) is { } gitConfigText ) { var pattern = GitConfigRemoteOriginUrlRegex(); var match = pattern.Match(gitConfigText); if (match.Success) { remoteUrl = match.Groups[1].Value; } } extensions.Add( new InstalledPackageExtension { Paths = [subDirectory], GitRepositoryUrl = remoteUrl } ); } } return extensions; } public virtual async Task GetInstalledExtensionInfoAsync( InstalledPackageExtension installedExtension ) { if (installedExtension.PrimaryPath is not DirectoryPath extensionDirectory) { return installedExtension; } // Get git version var version = await prerequisiteHelper .GetGitRepositoryVersion(extensionDirectory) .ConfigureAwait(false); return installedExtension with { Version = new PackageExtensionVersion { Tag = version.Tag, Branch = version.Branch, CommitSha = version.CommitSha, }, }; } /// public virtual async Task InstallExtensionAsync( PackageExtension extension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(installedPackage.FullPath); // Ensure type if (extension.InstallType?.ToLowerInvariant() != "git-clone") { throw new ArgumentException( $"Extension must have install type 'git-clone' but has '{extension.InstallType}'.", nameof(extension) ); } // Git clone all files var cloneRoot = new DirectoryPath(installedPackage.FullPath, RelativeInstallDirectory); foreach (var repositoryUri in extension.Files) { cancellationToken.ThrowIfCancellationRequested(); progress?.Report( new ProgressReport(0f, message: $"Cloning {repositoryUri}", isIndeterminate: true) ); try { await prerequisiteHelper .CloneGitRepository( cloneRoot, repositoryUri.ToString(), version, progress.AsProcessOutputHandler() ) .ConfigureAwait(false); } catch (ProcessException ex) { if (ex.Message.Contains("Git exited with code 128")) { progress?.Report( new ProgressReport( -1f, $"Unable to check out commit {version?.CommitSha} - continuing with latest commit from {version?.Branch}\n\n{ex.ProcessResult?.StandardError}\n", isIndeterminate: true ) ); } else { throw; } } progress?.Report(new ProgressReport(1f, message: $"Cloned {repositoryUri}")); } } /// public virtual async Task UpdateExtensionAsync( InstalledPackageExtension installedExtension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(installedPackage.FullPath); foreach (var repoPath in installedExtension.Paths.OfType()) { cancellationToken.ThrowIfCancellationRequested(); // Check git if (!await prerequisiteHelper.CheckIsGitRepository(repoPath.FullPath).ConfigureAwait(false)) continue; // Get remote url var remoteUrlResult = await prerequisiteHelper .GetGitRepositoryRemoteOriginUrl(repoPath.FullPath) .EnsureSuccessExitCode() .ConfigureAwait(false); progress?.Report( new ProgressReport( 0f, message: $"Updating git repository {repoPath.Name}", isIndeterminate: true ) ); // If version not provided, use current branch if (version is null) { ArgumentNullException.ThrowIfNull(installedExtension.Version?.Branch); version = new PackageExtensionVersion { Branch = installedExtension.Version?.Branch }; } await prerequisiteHelper .UpdateGitRepository( repoPath, remoteUrlResult.StandardOutput!.Trim(), version, progress.AsProcessOutputHandler() ) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, message: $"Updated git repository {repoPath.Name}")); } } /// public virtual async Task UninstallExtensionAsync( InstalledPackageExtension installedExtension, InstalledPackage installedPackage, IProgress? progress = null, CancellationToken cancellationToken = default ) { foreach (var path in installedExtension.Paths.Where(p => p.Exists)) { cancellationToken.ThrowIfCancellationRequested(); if (path is DirectoryPath directoryPath) { await directoryPath .DeleteVerboseAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); } else { await path.DeleteAsync().ConfigureAwait(false); } } progress?.Report(new ProgressReport(1f, message: "Uninstalled extension")); } [GeneratedRegex("""\[remote "origin"\][\s\S]*?url\s*=\s*(.+)""")] private static partial Regex GitConfigRemoteOriginUrlRegex(); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs ================================================ using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Models.Packages.Extensions; /// /// Interface for a package extension manager. /// public interface IPackageExtensionManager { /// /// Default manifests for this extension manager. /// IEnumerable DefaultManifests { get; } /// /// Get manifests given an installed package. /// By default returns . /// IEnumerable GetManifests(InstalledPackage installedPackage) { return DefaultManifests; } /// /// Get extensions from the provided manifest. /// Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ); /// /// Get extensions from all provided manifests. /// async Task> GetManifestExtensionsAsync( IEnumerable manifests, CancellationToken cancellationToken = default ) { var extensions = Enumerable.Empty(); foreach (var manifest in manifests) { cancellationToken.ThrowIfCancellationRequested(); extensions = extensions.Concat( await GetManifestExtensionsAsync(manifest, cancellationToken).ConfigureAwait(false) ); } return extensions; } /// /// Get unique extensions from all provided manifests. As a mapping of their reference. /// async Task> GetManifestExtensionsMapAsync( IEnumerable manifests, CancellationToken cancellationToken = default ) { var result = new Dictionary(); foreach ( var extension in await GetManifestExtensionsAsync(manifests, cancellationToken) .ConfigureAwait(false) ) { cancellationToken.ThrowIfCancellationRequested(); var key = extension.Reference.ToString(); if (!result.TryAdd(key, extension)) { // Replace result[key] = extension; } } return result; } /// /// Get all installed extensions for the provided package. /// Task> GetInstalledExtensionsAsync( InstalledPackage installedPackage, CancellationToken cancellationToken = default ); /// /// Like , but does not check version. /// Task> GetInstalledExtensionsLiteAsync( InstalledPackage installedPackage, CancellationToken cancellationToken = default ); /// /// Get updated info (version) for an installed extension. /// Task GetInstalledExtensionInfoAsync( InstalledPackageExtension installedExtension ); /// /// Install an extension to the provided package. /// Task InstallExtensionAsync( PackageExtension extension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ); /// /// Update an installed extension to the provided version. /// If no version is provided, the latest version will be used. /// Task UpdateExtensionAsync( InstalledPackageExtension installedExtension, InstalledPackage installedPackage, PackageExtensionVersion? version = null, IProgress? progress = null, CancellationToken cancellationToken = default ); /// /// Uninstall an installed extension. /// Task UninstallExtensionAsync( InstalledPackageExtension installedExtension, InstalledPackage installedPackage, IProgress? progress = null, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/InstalledPackageExtension.cs ================================================ using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Models.Packages.Extensions; public record InstalledPackageExtension { /// /// All folders or files of the extension. /// public required IEnumerable Paths { get; init; } /// /// Primary path of the extension. /// public IPathObject? PrimaryPath => Paths.FirstOrDefault(); /// /// The version of the extension. /// public PackageExtensionVersion? Version { get; init; } /// /// Remote git repository url, if the extension is a git repository. /// public string? GitRepositoryUrl { get; init; } /// /// The PackageExtension definition, if available. /// public PackageExtension? Definition { get; init; } public string Title { get { if (Definition?.Title is { } title) { return title; } if (Paths.FirstOrDefault()?.Name is { } pathName) { return pathName; } return ""; } } /// /// Path containing PrimaryPath and its parent. /// public string DisplayPath => PrimaryPath switch { null => "", DirectoryPath { Parent: { } parentDir } dir => $"{parentDir.Name}/{dir.Name}", _ => PrimaryPath.Name }; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/PackageExtension.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public record PackageExtension { public required string Author { get; init; } public required string Title { get; init; } public required Uri Reference { get; init; } public required IEnumerable Files { get; init; } public IEnumerable? Pip { get; init; } public string? Description { get; init; } public string? InstallType { get; init; } public bool IsInstalled { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/PackageExtensionVersion.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public record PackageExtensionVersion : GitVersion { public override string ToString() => base.ToString(); }; ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/SavedPackageExtension.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public class SavedPackageExtension { public required PackageExtension PackageExtension { get; set; } public PackageExtensionVersion? Version { get; set; } public bool AlwaysUseLatest { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Extensions/VladExtensionItem.cs ================================================ namespace StabilityMatrix.Core.Models.Packages.Extensions; public class VladExtensionItem { public required string Name { get; set; } public required Uri Url { get; set; } public string? Long { get; set; } public string? Description { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/FluxGym.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class FluxGym( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "FluxGym"; public override string DisplayName { get; set; } = "FluxGym"; public override string Author => "cocktailpeanut"; public override string Blurb => "Dead simple FLUX LoRA training UI with LOW VRAM support"; public override string LicenseType => "N/A"; public override string LicenseUrl => ""; public override string LaunchCommand => "app.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/fluxgym/fluxgym.webp"); public override List LaunchOptions => [LaunchOptionDefinition.Extras]; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; public override SharedFolderLayout SharedFolderLayout => new() { Rules = [ new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["models/clip"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.DiffusionModels], TargetRelativePaths = ["models/unet"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], }, ], }; public override Dictionary>? SharedOutputFolders => null; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cuda }; public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override string OutputFolderName => string.Empty; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override PackageType PackageType => PackageType.SdTraining; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Cloning / updating sd-scripts", isIndeterminate: true)); // check if sd-scripts is already installed - if so: pull, else: clone if (Directory.Exists(Path.Combine(installLocation, "sd-scripts"))) { await PrerequisiteHelper .RunGit(["pull"], onConsoleOutput, Path.Combine(installLocation, "sd-scripts")) .ConfigureAwait(false); } else { await PrerequisiteHelper .RunGit( ["clone", "-b", "sd3", "https://github.com/kohya-ss/sd-scripts"], onConsoleOutput, installLocation ) .ConfigureAwait(false); } progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var isLegacyNvidiaGpu = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var config = new PipInstallConfig { RequirementsFilePaths = ["sd-scripts/requirements.txt", "requirements.txt"], RequirementsExcludePattern = "(diffusers\\[torch\\]==0.32.1|torch|torchvision|torchaudio|xformers|bitsandbytes|-e\\s\\.)", TorchaudioVersion = " ", CudaIndex = isLegacyNvidiaGpu ? "cu126" : "cu128", ExtraPipArgs = ["bitsandbytes>=0.46.0"], PostInstallPipArgs = ["diffusers[torch]==0.32.1"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); await venvRunner.PipInstall(["-e", "./sd-scripts"], onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/FocusControlNet.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class FocusControlNet( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : Fooocus( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { 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 Disclaimer => "This package may no longer receive updates from its author."; 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.Impossible; public override bool OfferInOneClickInstaller => false; public override PackageType PackageType => PackageType.Legacy; public override SharedFolderLayout SharedFolderLayout => base.SharedFolderLayout with { RelativeConfigPath = "user_path_config.txt", }; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Fooocus.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Fooocus( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "Fooocus"; public override string DisplayName { get; set; } = "Fooocus"; public override string Author => "lllyasviel"; public override string Blurb => "Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs"; public override string LicenseType => "GPL-3.0"; public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"; public override string LaunchCommand => "launch.py"; public override PackageType PackageType => PackageType.Legacy; public override Uri PreviewImageUri => new( "https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png" ); public override List LaunchOptions => new() { new LaunchOptionDefinition { Name = "Preset", Type = LaunchOptionType.Bool, Options = { "--preset anime", "--preset realistic" }, }, new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, Description = "Sets the listen port", Options = { "--port" }, }, new LaunchOptionDefinition { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new LaunchOptionDefinition { Name = "Listen", Type = LaunchOptionType.String, Description = "Set the listen interface", Options = { "--listen" }, }, new LaunchOptionDefinition { Name = "Output Directory", Type = LaunchOptionType.String, Description = "Override the output directory", Options = { "--output-path" }, }, new LaunchOptionDefinition { Name = "Language", Type = LaunchOptionType.String, Description = "Change the language of the UI", Options = { "--language" }, }, new LaunchOptionDefinition { Name = "Disable Image Log", Type = LaunchOptionType.Bool, Options = { "--disable-image-log" }, }, new LaunchOptionDefinition { Name = "Disable Analytics", Type = LaunchOptionType.Bool, Options = { "--disable-analytics" }, }, new LaunchOptionDefinition { Name = "Disable Preset Model Downloads", Type = LaunchOptionType.Bool, Options = { "--disable-preset-download" }, }, new LaunchOptionDefinition { Name = "Always Download Newer Models", Type = LaunchOptionType.Bool, Options = { "--always-download-new-model" }, }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { MemoryLevel.Low => "--always-low-vram", MemoryLevel.Medium => "--always-normal-vram", _ => null, }, Options = { "--always-high-vram", "--always-normal-vram", "--always-low-vram", "--always-no-vram", }, }, new LaunchOptionDefinition { Name = "Use DirectML", Type = LaunchOptionType.Bool, Description = "Use pytorch with DirectML support", InitialValue = HardwareHelper.PreferDirectMLOrZluda(), Options = { "--directml" }, }, new LaunchOptionDefinition { Name = "Disable Xformers", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), Options = { "--disable-xformers" }, }, new LaunchOptionDefinition { Name = "Disable Offload from VRAM", Type = LaunchOptionType.Bool, InitialValue = Compat.IsMacOS, Options = { "--disable-offload-from-vram" }, }, LaunchOptionDefinition.Extras, }; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None }; public override SharedFolderLayout SharedFolderLayout => new() { RelativeConfigPath = "config.txt", ConfigFileType = ConfigFileType.Json, Rules = [ new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["models/checkpoints"], ConfigDocumentPaths = ["path_checkpoints"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Diffusers], TargetRelativePaths = ["models/diffusers"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["models/clip"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.GLIGEN], TargetRelativePaths = ["models/gligen"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ESRGAN], TargetRelativePaths = ["models/upscale_models"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Hypernetwork], TargetRelativePaths = ["models/hypernetworks"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Embeddings], TargetRelativePaths = ["models/embeddings"], ConfigDocumentPaths = ["path_embeddings"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], ConfigDocumentPaths = ["path_vae"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ApproxVAE], TargetRelativePaths = ["models/vae_approx"], ConfigDocumentPaths = ["path_vae_approx"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["models/loras"], ConfigDocumentPaths = ["path_loras"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ClipVision], TargetRelativePaths = ["models/clip_vision"], ConfigDocumentPaths = ["path_clip_vision"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ControlNet], TargetRelativePaths = ["models/controlnet"], ConfigDocumentPaths = ["path_controlnet"], }, new SharedFolderLayoutRule { TargetRelativePaths = ["models/inpaint"], ConfigDocumentPaths = ["path_inpaint"], }, new SharedFolderLayoutRule { TargetRelativePaths = ["models/prompt_expansion/fooocus_expansion"], ConfigDocumentPaths = ["path_fooocus_expansion"], }, new SharedFolderLayoutRule { TargetRelativePaths = [OutputFolderName], ConfigDocumentPaths = ["path_outputs"], }, ], }; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "outputs" } }; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl, TorchIndex.Rocm, TorchIndex.Mps }; public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override string OutputFolderName => "outputs"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var isBlackwell = torchIndex is TorchIndex.Cuda && (SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu()); var config = new PipInstallConfig { // Pip version 24.1 deprecated numpy requirement spec used by torchsde 0.2.5 PrePipInstallArgs = ["pip==23.3.2"], RequirementsFilePaths = ["requirements_versions.txt"], TorchVersion = isBlackwell ? "" : "==2.1.0", TorchvisionVersion = isBlackwell ? "" : "==0.16.0", CudaIndex = isBlackwell ? "cu128" : "cu121", RocmIndex = "rocm5.6", }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/FooocusMre.cs ================================================ using System.Diagnostics; using System.Text.RegularExpressions; using Injectio.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; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class FooocusMre( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "Fooocus-MRE"; public override string DisplayName { get; set; } = "Fooocus-MRE"; public override string Author => "MoonRide303"; public override string Blurb => "Fooocus-MRE is an image generating software, enhanced variant of the original Fooocus dedicated for a bit more advanced users"; public override string LicenseType => "GPL-3.0"; 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" ); public override string Disclaimer => "This package may no longer receive updates from its author."; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Impossible; public override bool OfferInOneClickInstaller => false; public override PackageType PackageType => PackageType.Legacy; public override List LaunchOptions => new() { new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, Description = "Sets the listen port", Options = { "--port" }, }, new LaunchOptionDefinition { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new LaunchOptionDefinition { Name = "Listen", Type = LaunchOptionType.String, Description = "Set the listen interface", Options = { "--listen" }, }, LaunchOptionDefinition.Extras, }; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, [SharedFolderType.Diffusers] = new[] { "models/diffusers" }, [SharedFolderType.Lora] = new[] { "models/loras" }, [SharedFolderType.TextEncoders] = new[] { "models/clip" }, [SharedFolderType.Embeddings] = new[] { "models/embeddings" }, [SharedFolderType.VAE] = new[] { "models/vae" }, [SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" }, [SharedFolderType.ControlNet] = new[] { "models/controlnet" }, [SharedFolderType.GLIGEN] = new[] { "models/gligen" }, [SharedFolderType.ESRGAN] = new[] { "models/upscale_models" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "outputs" } }; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.Rocm }; public override string MainBranch => "moonride-main"; public override string OutputFolderName => "outputs"; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var pipInstallArgs = new PipInstallArgs(); if (torchVersion == TorchIndex.DirectMl) { pipInstallArgs = pipInstallArgs.WithTorchDirectML(); } else { var extraIndex = torchVersion switch { TorchIndex.Cpu => "cpu", TorchIndex.Cuda => "cu118", TorchIndex.Rocm => "rocm5.4.2", _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null), }; pipInstallArgs = pipInstallArgs .WithTorch("==2.0.1") .WithTorchVision("==0.15.2") .WithTorchExtraIndex(extraIndex); } var requirements = new FilePath(installLocation, "requirements_versions.txt"); pipInstallArgs = pipInstallArgs.WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), excludePattern: "torch" ); if (installedPackage.PipOverrides != null) { pipInstallArgs = pipInstallArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipInstallArgs, onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Use the app with", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/ForgeAmdGpu.cs ================================================ using System.Collections.Immutable; using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class ForgeAmdGpu( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : SDWebForge( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "stable-diffusion-webui-amdgpu-forge"; public override string DisplayName => "Stable Diffusion WebUI AMDGPU Forge"; public override string Author => "lshqqytiger"; public override string RepositoryName => "stable-diffusion-webui-amdgpu-forge"; public override string Blurb => "A fork of Stable Diffusion WebUI Forge with support for AMD GPUs"; public override string LicenseUrl => "https://github.com/lshqqytiger/stable-diffusion-webui-amdgpu-forge/blob/main/LICENSE.txt"; public override string Disclaimer => "Prerequisite install may require admin privileges and a reboot. " + "AMD GPUs under the RX 6800 may require additional manual setup."; public override IEnumerable AvailableTorchIndices => [TorchIndex.Zluda]; public override TorchIndex GetRecommendedTorchVersion() => TorchIndex.Zluda; public override bool IsCompatible => HardwareHelper.PreferDirectMLOrZluda(); public override PackageType PackageType => PackageType.Legacy; public override IEnumerable Prerequisites => base.Prerequisites.Concat([PackagePrerequisite.HipSdk]); public override List LaunchOptions => base .LaunchOptions.Concat( [ new LaunchOptionDefinition { Name = "Use ZLUDA", Description = "Use ZLUDA for CUDA acceleration on AMD GPUs", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferDirectMLOrZluda(), Options = ["--use-zluda"], }, new LaunchOptionDefinition { Name = "Use DirectML", Description = "Use DirectML for DirectML acceleration on compatible GPUs", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--use-directml"], }, ] ) .ToList(); public override bool InstallRequiresAdmin => true; public override string AdminRequiredReason => "HIP SDK installation and (if applicable) ROCmLibs patching requires admin " + "privileges for accessing the HIP SDK files in the Program Files directory."; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!PrerequisiteHelper.IsHipSdkInstalled) // for updates { progress?.Report(new ProgressReport(-1, "Installing HIP SDK 6.4", isIndeterminate: true)); await PrerequisiteHelper .InstallPackageRequirements(this, options.PythonOptions.PythonVersion, progress) .ConfigureAwait(false); } progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1, "Install finished", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!PrerequisiteHelper.IsHipSdkInstalled) { throw new MissingPrerequisiteException( "HIP SDK", "Your package has not yet been upgraded to use HIP SDK 6.4. To continue, please update this package or select \"Change Version\" from the 3-dots menu to have it upgraded automatically for you" ); } await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); var portableGitBin = new DirectoryPath(PrerequisiteHelper.GitBinPath); var hipPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "AMD", "ROCm", "6.4" ); var hipBinPath = Path.Combine(hipPath, "bin"); var envVars = new Dictionary { ["ZLUDA_COMGR_LOG_LEVEL"] = "1", ["HIP_PATH"] = hipPath, ["HIP_PATH_64"] = hipPath, ["GIT"] = portableGitBin.JoinFile("git.exe"), }; envVars.Update(settingsManager.Settings.EnvironmentVariables); if (envVars.TryGetValue("PATH", out var pathValue)) { envVars["PATH"] = Compat.GetEnvPathWithExtensions(hipBinPath, portableGitBin, pathValue); } else { envVars["PATH"] = Compat.GetEnvPathWithExtensions(hipBinPath, portableGitBin); } VenvRunner.UpdateEnvironmentVariables(env => envVars.ToImmutableDictionary()); VenvRunner.RunDetached( [ Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments, .. ExtraLaunchArguments, ], HandleConsoleOutput, OnExit ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/ForgeClassic.cs ================================================ using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class ForgeClassic( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : SDWebForge( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private const string LegacyUpgradeAlert = "You are updating from an old version"; private const string ContinuePrompt = "Press Enter to Continue"; public override PyVersion? MinimumPythonVersion => Python.PyInstallationManager.Python_3_13_12; public override string Name => "forge-classic"; public override string Author => "Haoming02"; public override string RepositoryName => "sd-webui-forge-classic"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI Forge - Classic"; public override string MainBranch => "classic"; public override string Blurb => "This fork is focused exclusively on SD1 and SDXL checkpoints, having various optimizations implemented, with the main goal of being the lightest WebUI without any bloatwares."; public override string LicenseUrl => "https://github.com/Haoming02/sd-webui-forge-classic/blob/classic/LICENSE"; public override Uri PreviewImageUri => new("https://github.com/Haoming02/sd-webui-forge-classic/raw/classic/html/ui.webp"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.ReallyRecommended; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda]; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_13_12; public override PackageType PackageType => PackageType.Legacy; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Extras] = ["output/extras-images"], [SharedOutputType.Saved] = ["output/images"], [SharedOutputType.Img2Img] = ["output/img2img-images"], [SharedOutputType.Text2Img] = ["output/txt2img-images"], [SharedOutputType.Img2ImgGrids] = ["output/img2img-grids"], [SharedOutputType.Text2ImgGrids] = ["output/txt2img-grids"], [SharedOutputType.SVD] = ["output/videos"], }; public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new() { Name = "Xformers", Type = LaunchOptionType.Bool, Description = "Set whether to use xformers", Options = { "--xformers" }, }, new() { Name = "Use SageAttention", Type = LaunchOptionType.Bool, Description = "Set whether to use sage attention", Options = { "--sage" }, }, new() { Name = "Pin Shared Memory", Type = LaunchOptionType.Bool, Options = { "--pin-shared-memory" }, InitialValue = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? false, }, new() { Name = "CUDA Malloc", Type = LaunchOptionType.Bool, Options = { "--cuda-malloc" }, InitialValue = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? false, }, new() { Name = "CUDA Stream", Type = LaunchOptionType.Bool, Options = { "--cuda-stream" }, InitialValue = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? false, }, new() { Name = "Auto Launch", Type = LaunchOptionType.Bool, Description = "Set whether to auto launch the webui", Options = { "--autolaunch" }, }, new() { Name = "Skip Python Version Check", Type = LaunchOptionType.Bool, Description = "Set whether to skip python version check", Options = { "--skip-python-version-check" }, InitialValue = true, }, LaunchOptionDefinition.Extras, ]; public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = ["models/Stable-diffusion/sd"], [SharedFolderType.ESRGAN] = ["models/ESRGAN"], [SharedFolderType.Lora] = ["models/Lora"], [SharedFolderType.LyCORIS] = ["models/LyCORIS"], [SharedFolderType.ApproxVAE] = ["models/VAE-approx"], [SharedFolderType.VAE] = ["models/VAE"], [SharedFolderType.DeepDanbooru] = ["models/deepbooru"], [SharedFolderType.Embeddings] = ["models/embeddings"], [SharedFolderType.Hypernetwork] = ["models/hypernetworks"], [SharedFolderType.ControlNet] = ["models/controlnet/ControlNet"], [SharedFolderType.AfterDetailer] = ["models/adetailer"], [SharedFolderType.T2IAdapter] = ["models/controlnet/T2IAdapter"], [SharedFolderType.IpAdapter] = ["models/controlnet/IpAdapter"], [SharedFolderType.IpAdapters15] = ["models/controlnet/DiffusersIpAdapters"], [SharedFolderType.IpAdaptersXl] = ["models/controlnet/DiffusersIpAdaptersXL"], [SharedFolderType.TextEncoders] = ["models/text_encoder"], [SharedFolderType.DiffusionModels] = ["models/Stable-diffusion/unet"], }; public override List GetExtraCommands() { var commands = new List(); if (Compat.IsWindows && SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() is true) { commands.Add( new ExtraPackageCommand { CommandName = "Install Triton and SageAttention", Command = InstallTritonAndSageAttention, } ); } return commands; } public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var requestedPythonVersion = options.PythonOptions.PythonVersion ?? ( PyVersion.TryParse(installedPackage.PythonVersion, out var parsedVersion) ? parsedVersion : RecommendedPythonVersion ); var shouldUpgradePython = options.IsUpdate && requestedPythonVersion < MinimumPythonVersion; var targetPythonVersion = shouldUpgradePython ? MinimumPythonVersion!.Value : requestedPythonVersion; if (shouldUpgradePython) { onConsoleOutput?.Invoke( ProcessOutput.FromStdOutLine( $"Upgrading venv Python from {requestedPythonVersion} to {targetPythonVersion}" ) ); ResetVenvForPythonUpgrade(installLocation, onConsoleOutput); } progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, forceRecreate: shouldUpgradePython, pythonVersion: targetPythonVersion ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Running install script...", isIndeterminate: true)); // Build args for their launch.py - use --uv for fast installs, --exit to quit after setup var launchArgs = new List { "launch.py", "--uv", "--exit" }; // For Ampere or newer GPUs, enable sage attention, flash attention, and nunchaku var isAmpereOrNewer = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? HardwareHelper.IterGpuInfo().Any(x => x.IsNvidia && x.IsAmpereOrNewerGpu()); if (isAmpereOrNewer) { launchArgs.Add("--sage"); launchArgs.Add("--flash"); launchArgs.Add("--nunchaku"); } // Run their install script with our venv Python venvRunner.WorkingDirectory = new DirectoryPath(installLocation); var sawLegacyUpdatePrompt = false; var exitCode = await RunInstallScriptWithPromptHandling( venvRunner, launchArgs, onConsoleOutput, cancellationToken, onLegacyPromptDetected: () => sawLegacyUpdatePrompt = true ) .ConfigureAwait(false); // If legacy prompt was detected, back up old config files regardless of exit code. if (options.IsUpdate && sawLegacyUpdatePrompt) { BackupLegacyConfigFiles(installLocation, onConsoleOutput); // If it also failed, retry once after the backup. if (exitCode != 0) { onConsoleOutput?.Invoke( ProcessOutput.FromStdOutLine( "[ForgeClassic] Retrying install after backing up legacy config files..." ) ); exitCode = await RunInstallScriptWithPromptHandling( venvRunner, launchArgs, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); } } if (exitCode != 0) { throw new InvalidOperationException($"Install script failed with exit code {exitCode}"); } if ( !string.Equals( installedPackage.PythonVersion, targetPythonVersion.StringValue, StringComparison.Ordinal ) ) { installedPackage.PythonVersion = targetPythonVersion.StringValue; } progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); } private async Task RunInstallScriptWithPromptHandling( IPyVenvRunner venvRunner, IReadOnlyCollection launchArgs, Action? onConsoleOutput, CancellationToken cancellationToken, Action? onLegacyPromptDetected = null ) { var enterSent = false; void HandleInstallOutput(ProcessOutput output) { onConsoleOutput?.Invoke(output); var isLegacyPrompt = output.Text.Contains(LegacyUpgradeAlert, StringComparison.OrdinalIgnoreCase) || output.Text.Contains(ContinuePrompt, StringComparison.OrdinalIgnoreCase); if (!isLegacyPrompt) return; onLegacyPromptDetected?.Invoke(); if (enterSent || venvRunner.Process is null || venvRunner.Process.HasExited) return; try { venvRunner.Process.StandardInput.WriteLine(); enterSent = true; onConsoleOutput?.Invoke( ProcessOutput.FromStdOutLine( "[ForgeClassic] Detected legacy update prompt. Sent Enter automatically." ) ); } catch (Exception e) { Logger.Warn(e, "Failed to auto-submit Enter for Forge Classic update prompt"); } } venvRunner.RunDetached([.. launchArgs], HandleInstallOutput); var process = venvRunner.Process ?? throw new InvalidOperationException("Failed to start Forge Classic install process"); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); return process.ExitCode; } private void ResetVenvForPythonUpgrade(string installLocation, Action? onConsoleOutput) { var venvPath = Path.Combine(installLocation, "venv"); if (!Directory.Exists(venvPath)) return; try { Directory.Delete(venvPath, recursive: true); onConsoleOutput?.Invoke( ProcessOutput.FromStdOutLine("[ForgeClassic] Removed existing venv before Python upgrade.") ); } catch (Exception e) { Logger.Warn(e, "Failed to remove existing venv during Forge Classic Python upgrade"); throw new InvalidOperationException( "Failed to remove existing venv for Python upgrade. Ensure Forge is not running and retry.", e ); } } private void BackupLegacyConfigFiles(string installLocation, Action? onConsoleOutput) { BackupLegacyConfigFile(installLocation, "config.json", onConsoleOutput); BackupLegacyConfigFile(installLocation, "ui-config.json", onConsoleOutput); } private void BackupLegacyConfigFile( string installLocation, string fileName, Action? onConsoleOutput ) { var sourcePath = Path.Combine(installLocation, fileName); if (!File.Exists(sourcePath)) return; var backupPath = GetBackupPath(sourcePath); File.Move(sourcePath, backupPath); var message = $"[ForgeClassic] Backed up {fileName} to {Path.GetFileName(backupPath)}"; Logger.Info(message); onConsoleOutput?.Invoke(ProcessOutput.FromStdOutLine(message)); } private static string GetBackupPath(string sourcePath) { var nextPath = sourcePath + ".bak"; if (!File.Exists(nextPath)) return nextPath; var index = 1; while (true) { nextPath = sourcePath + $".bak.{index}"; if (!File.Exists(nextPath)) return nextPath; index++; } } private async Task InstallTritonAndSageAttention(InstalledPackage? installedPackage) { if (installedPackage?.FullPath is null) return; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = "Triton and SageAttention installed successfully", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner .ExecuteSteps( [ new ActionPackageStep( async progress => { await using var venvRunner = await SetupVenvPure( installedPackage.FullPath, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); var gpuInfo = SettingsManager.Settings.PreferredGpu ?? HardwareHelper.IterGpuInfo().FirstOrDefault(x => x.IsNvidia); var tritonVersion = Compat.IsWindows ? "3.5.1.post22" : "3.5.1"; await PipWheelService .InstallTritonAsync(venvRunner, progress, tritonVersion) .ConfigureAwait(false); await PipWheelService .InstallSageAttentionAsync(venvRunner, gpuInfo, progress, "2.2.0") .ConfigureAwait(false); }, "Installing Triton and SageAttention" ), ] ) .ConfigureAwait(false); if (runner.Failed) return; await using var transaction = settingsManager.BeginTransaction(); var packageInSettings = transaction.Settings.InstalledPackages.FirstOrDefault(x => x.Id == installedPackage.Id ); if (packageInSettings is null) return; var attentionOptions = packageInSettings.LaunchArgs?.Where(opt => opt.Name.Contains("attention", StringComparison.OrdinalIgnoreCase) ); if (attentionOptions is not null) { foreach (var option in attentionOptions) { option.OptionValue = false; } } var sageAttention = packageInSettings.LaunchArgs?.FirstOrDefault(opt => opt.Name.Contains("sage", StringComparison.OrdinalIgnoreCase) ); if (sageAttention is not null) { sageAttention.OptionValue = true; } else { packageInSettings.LaunchArgs?.Add( new LaunchOption { Name = "--sage", Type = LaunchOptionType.Bool, OptionValue = true, } ); } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/ForgeNeo.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class ForgeNeo( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : ForgeClassic( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "forge-neo"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI Forge - Neo"; public override string MainBranch => "neo"; public override PackageType PackageType => PackageType.SdInference; public override string Blurb => "Neo mainly serves as an continuation for the \"latest\" version of Forge. Additionally, this fork is focused on optimization and usability, with the main goal of being the lightest WebUI without any bloatwares."; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/FramePack.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class FramePack( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "framepack"; public override string DisplayName { get; set; } = "FramePack"; public override string Author => "lllyasviel"; public override string Blurb => "FramePack is a next-frame (next-frame-section) prediction neural network structure that generates videos progressively."; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/lllyasviel/FramePack/blob/main/LICENSE"; public override string LaunchCommand => "demo_gradio.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/framepack/framepack.png"); public override string OutputFolderName => "outputs"; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda]; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override List LaunchOptions => [ new() { Name = "Server", Type = LaunchOptionType.String, DefaultValue = "0.0.0.0", InitialValue = "127.0.0.1", Options = ["--server"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = ["--share"], }, new() { Name = "In Browser", Type = LaunchOptionType.Bool, Options = ["--inbrowser"], InitialValue = true, }, ]; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Img2Vid] = ["outputs"] }; public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.None]; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override IReadOnlyList ExtraLaunchArguments => settingsManager.IsLibraryDirSet ? ["--gradio-allowed-paths", settingsManager.ImagesDirectory] : []; public override IReadOnlyDictionary ExtraLaunchCommands => new Dictionary { ["FramePack F1"] = "demo_gradio_f1.py" }; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var isLegacyNvidia = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var isNewerNvidia = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? HardwareHelper.HasAmpereOrNewerGpu(); var extraArgs = new List(); if (isNewerNvidia) { extraArgs.Add(Compat.IsWindows ? "triton-windows" : "triton"); } var config = new PipInstallConfig { RequirementsFilePaths = ["requirements.txt"], TorchaudioVersion = " ", // Request torchaudio install XformersVersion = " ", // Request xformers install CudaIndex = isLegacyNvidia ? "cu126" : "cu128", UpgradePackages = true, ExtraPipArgs = extraArgs, PostInstallPipArgs = ["numpy==1.26.4"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); progress?.Report(new ProgressReport(1, "Install complete", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); // Path to the original demo_gradio.py file var originalDemoPath = Path.Combine(installLocation, options.Command ?? LaunchCommand); var modifiedDemoPath = Path.Combine(installLocation, "demo_gradio_modified.py"); // Read the original demo_gradio.py file var originalContent = await File.ReadAllTextAsync(originalDemoPath, cancellationToken) .ConfigureAwait(false); // Modify the content to add --gradio-allowed-paths support var modifiedContent = AddGradioAllowedPathsSupport(originalContent); // Write the modified content to a new file await File.WriteAllTextAsync(modifiedDemoPath, modifiedContent, cancellationToken) .ConfigureAwait(false); VenvRunner.RunDetached( [modifiedDemoPath, .. options.Arguments, .. ExtraLaunchArguments], HandleConsoleOutput, OnExit ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) WebUrl = match.Value; OnStartupComplete(WebUrl); } } public override List GetExtraCommands() { return Compat.IsWindows && SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() is true ? [ new ExtraPackageCommand { CommandName = "Install Triton and SageAttention", Command = async installedPackage => { if (installedPackage == null || string.IsNullOrEmpty(installedPackage.FullPath)) throw new InvalidOperationException( "Package not found or not installed correctly" ); await InstallTritonAndSageAttention(installedPackage).ConfigureAwait(false); }, }, ] : []; } private static string AddGradioAllowedPathsSupport(string originalContent) { // Add the --gradio-allowed-paths argument to the argument parser var parserPattern = @"(parser\.add_argument\(""--inbrowser"", action='store_true'\)\s*\n)(args = parser\.parse_args\(\))"; var parserReplacement = "$1parser.add_argument('--gradio-allowed-paths', nargs='*', default=[], help='Allowed paths for Gradio file access')\n$2"; var modifiedContent = Regex.Replace( originalContent, parserPattern, parserReplacement, RegexOptions.Multiline ); // Add the allowed_paths parameter to the block.launch() call var launchPattern = @"(block\.launch\(\s*\n\s*server_name=args\.server,\s*\n\s*server_port=args\.port,\s*\n\s*share=args\.share,\s*\n\s*inbrowser=args\.inbrowser,)\s*\n(\))"; var launchReplacement = "$1\n allowed_paths=args.gradio_allowed_paths,\n$2"; modifiedContent = Regex.Replace( modifiedContent, launchPattern, launchReplacement, RegexOptions.Multiline ); return modifiedContent; } private async Task InstallTritonAndSageAttention(InstalledPackage installedPackage) { if (installedPackage.FullPath is null) return; var installSageStep = new InstallSageAttentionStep( DownloadService, PrerequisiteHelper, PyInstallationManager ) { InstalledPackage = installedPackage, WorkingDirectory = new DirectoryPath(installedPackage.FullPath), EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables, IsBlackwellGpu = SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu(), }; var runner = new PackageModificationRunner { ShowDialogOnStart = true, ModificationCompleteMessage = "Triton and SageAttention installed successfully", }; EventManager.Instance.OnPackageInstallProgressAdded(runner); await runner.ExecuteSteps([installSageStep]).ConfigureAwait(false); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/FramePackStudio.cs ================================================ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class FramePackStudio( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : FramePack( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "framepack-studio"; public override string DisplayName { get; set; } = "FramePack Studio"; public override string Author => "colinurbs"; public override string RepositoryName => "FramePack-Studio"; public override string Blurb => "FramePack Studio is an AI video generation application based on FramePack that strives to provide everything you need to create high quality video projects."; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/colinurbs/FramePack-Studio/blob/main/LICENSE"; public override string LaunchCommand => "studio.py"; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.None, SharedFolderMethod.Configuration]; public override SharedFolderLayout? SharedFolderLayout => new() { ConfigFileType = ConfigFileType.Json, RelativeConfigPath = new FilePath(".framepack", "settings.json"), Rules = [ new SharedFolderLayoutRule { ConfigDocumentPaths = ["lora_dir"], TargetRelativePaths = ["loras"], SourceTypes = [SharedFolderType.Lora], }, ], }; public override IReadOnlyDictionary ExtraLaunchCommands => new Dictionary(); public override IReadOnlyList ExtraLaunchArguments => []; public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (installedPackage.PreferredSharedFolderMethod is SharedFolderMethod.Configuration) { var settingsPath = new FilePath(installLocation, ".framepack", "settings.json"); if (!settingsPath.Exists) { settingsPath.Create(); } // set the output_dir and metadata_dir var settingsText = await settingsPath.ReadAllTextAsync(cancellationToken).ConfigureAwait(false); var json = JsonSerializer.Deserialize(settingsText) ?? new JsonObject(); json["output_dir"] = SettingsManager .ImagesDirectory.JoinDir(nameof(SharedOutputType.Img2Vid)) .ToString(); json["metadata_dir"] = SettingsManager .ImagesDirectory.JoinDir(nameof(SharedOutputType.Img2Vid)) .ToString(); await settingsPath .WriteAllTextAsync( JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true }), cancellationToken ) .ConfigureAwait(false); } await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.RunDetached( [LaunchCommand, .. options.Arguments, .. ExtraLaunchArguments], HandleConsoleOutput, OnExit ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on local URL", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) WebUrl = match.Value; OnStartupComplete(WebUrl); } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/IArgParsable.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; /// /// Supports parsing launch options from a python script. /// public interface IArgParsable { /// /// Defines the relative path to the python script that defines the launch options. /// public string RelativeArgsDefinitionScriptPath { get; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/InvokeAI.cs ================================================ using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using Refit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.Api.Invoke; 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; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class InvokeAI( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private const string RelativeRootPath = "invokeai-root"; private readonly string relativeFrontendBuildPath = Path.Combine("invokeai", "frontend", "web", "dist"); public override string Name => "InvokeAI"; public override string DisplayName { get; set; } = "InvokeAI"; public override string Author => "invoke-ai"; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE"; public override string Blurb => "Professional Creative Tools for Stable Diffusion"; public override string LaunchCommand => "invokeai-web"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override Uri PreviewImageUri => new("https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png"); public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.None, SharedFolderMethod.Configuration]; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override string MainBranch => "main"; public override bool ShouldIgnoreBranches => true; public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = [Path.Combine(RelativeRootPath, "autoimport", "main")], [SharedFolderType.Lora] = [Path.Combine(RelativeRootPath, "autoimport", "lora")], [SharedFolderType.Embeddings] = [Path.Combine(RelativeRootPath, "autoimport", "embedding")], [SharedFolderType.ControlNet] = [Path.Combine(RelativeRootPath, "autoimport", "controlnet")], [SharedFolderType.IpAdapters15] = [ Path.Combine(RelativeRootPath, "models", "sd-1", "ip_adapter"), ], [SharedFolderType.IpAdaptersXl] = [ Path.Combine(RelativeRootPath, "models", "sdxl", "ip_adapter"), ], [SharedFolderType.ClipVision] = [Path.Combine(RelativeRootPath, "models", "any", "clip_vision")], [SharedFolderType.T2IAdapter] = [Path.Combine(RelativeRootPath, "autoimport", "t2i_adapter")], }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = [Path.Combine("invokeai-root", "outputs", "images")] }; public override string OutputFolderName => Path.Combine("invokeai-root", "outputs", "images"); // https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md public override List LaunchOptions => [ new() { Name = "Root Directory", Type = LaunchOptionType.String, Options = ["--root"], }, new() { Name = "Config File", Type = LaunchOptionType.String, Options = ["--config"], }, LaunchOptionDefinition.Extras, ]; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.Rocm, TorchIndex.Mps]; public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_12_10; public override TorchIndex GetRecommendedTorchVersion() { if (Compat.IsMacOS && Compat.IsArm) { return TorchIndex.Mps; } return base.GetRecommendedTorchVersion(); } public override IEnumerable Prerequisites => [PackagePrerequisite.Python310, PackagePrerequisite.VcRedist, PackagePrerequisite.Git]; public override Task DownloadPackage( string installLocation, DownloadPackageOptions options, IProgress? progress = null, CancellationToken cancellationToken = default ) { return Task.CompletedTask; } public override Task CheckForUpdates(InstalledPackage package) => package.PythonVersion == Python.PyInstallationManager.Python_3_10_11.ToString() ? Task.FromResult(false) : base.CheckForUpdates(package); public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { // Backup existing files/folders except for known directories try { var excludedNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "invokeai-root", "invoke.old", "venv", }; if (Directory.Exists(installLocation)) { var entriesToMove = Directory .EnumerateFileSystemEntries(installLocation) .Where(p => !excludedNames.Contains(Path.GetFileName(p))) .ToList(); if (entriesToMove.Count > 0) { var backupFolderName = "invoke.old"; var backupFolderPath = Path.Combine(installLocation, backupFolderName); if (Directory.Exists(backupFolderPath) || File.Exists(backupFolderPath)) { backupFolderPath = Path.Combine( installLocation, $"invoke.old.{DateTime.Now:yyyyMMddHHmmss}" ); } Directory.CreateDirectory(backupFolderPath); foreach (var entry in entriesToMove) { var destinationPath = Path.Combine(backupFolderPath, Path.GetFileName(entry)); // Ensure we do not overwrite existing files if names collide if (File.Exists(destinationPath) || Directory.Exists(destinationPath)) { var name = Path.GetFileNameWithoutExtension(entry); var ext = Path.GetExtension(entry); var uniqueName = $"{name}_{DateTime.Now:yyyyMMddHHmmss}{ext}"; destinationPath = Path.Combine(backupFolderPath, uniqueName); } if (Directory.Exists(entry)) { Directory.Move(entry, destinationPath); } else if (File.Exists(entry)) { File.Move(entry, destinationPath); } } Logger.Info($"Moved {entriesToMove.Count} item(s) to '{backupFolderPath}'."); } } } catch (Exception e) { Logger.Warn(e, "Failed to move existing files to 'invoke.old'. Continuing with installation."); } // Setup venv progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); venvRunner.UpdateEnvironmentVariables(env => GetEnvVars(env, installLocation)); progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true)); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var isLegacyNvidiaGpu = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var fallbackIndex = torchVersion switch { TorchIndex.Cpu when Compat.IsLinux => "https://download.pytorch.org/whl/cpu", TorchIndex.Cuda when isLegacyNvidiaGpu => "https://download.pytorch.org/whl/cu126", TorchIndex.Cuda => "https://download.pytorch.org/whl/cu128", TorchIndex.Rocm => "https://download.pytorch.org/whl/rocm6.3", _ => string.Empty, }; var invokeInstallArgs = new PipInstallArgs($"invokeai=={options.VersionOptions.VersionTag}"); var contentStream = await DownloadService .GetContentAsync( $"https://raw.githubusercontent.com/invoke-ai/InvokeAI/refs/tags/{options.VersionOptions.VersionTag}/pins.json", cancellationToken ) .ConfigureAwait(false); // read to json, just deserialize as JObject or whtaever it is in System.Text>json using var reader = new StreamReader(contentStream); var json = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var pins = JsonNode.Parse(json); var platform = Compat.IsWindows ? "win32" : Compat.IsMacOS ? "darwin" : "linux"; var index = pins?["torchIndexUrl"]?[platform]?[ torchVersion.ToString().ToLowerInvariant() ]?.GetValue(); if (!string.IsNullOrWhiteSpace(index) && !isLegacyNvidiaGpu) { invokeInstallArgs = invokeInstallArgs.AddArg("--index").AddArg(index); } else if (!string.IsNullOrWhiteSpace(fallbackIndex)) { invokeInstallArgs = invokeInstallArgs.AddArg("--index").AddArg(fallbackIndex); } invokeInstallArgs = invokeInstallArgs.AddArg("--force-reinstall"); await venvRunner.PipInstall(invokeInstallArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false)); } public override Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) => RunInvokeCommand( installLocation, options.Command ?? LaunchCommand, options.Arguments, true, installedPackage, onConsoleOutput ); private async Task RunInvokeCommand( string installedPackagePath, string command, string arguments, bool runDetached, InstalledPackage installedPackage, Action? onConsoleOutput, bool spam3 = false ) { if (spam3 && !runDetached) { throw new InvalidOperationException("Cannot spam 3 if not running detached"); } await SetupVenv(installedPackagePath, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.UpdateEnvironmentVariables(env => GetEnvVars(env, installedPackagePath)); // Check for legacy Python 3.10.11 installations if (installedPackage.PythonVersion == Python.PyInstallationManager.Python_3_10_11.ToString()) { var warningMessage = """ ============================================================ LEGACY INVOKEAI INSTALLATION ============================================================ This InvokeAI installation is using Python 3.10.11, which is no longer supported by InvokeAI. Automatic updates have been disabled for this installation to prevent compatibility issues. Your current installation will continue to work, but will not receive updates. Recommended actions: 1. Install a new InvokeAI instance with Python 3.12+ 2. Copy your settings and data from this installation to the new one 3. Once verified, you can delete this old installation Note: You can run both installations side-by-side during the migration. ============================================================ """; Logger.Warn( "InvokeAI installation using legacy Python {PythonVersion} - updates disabled", installedPackage.PythonVersion ); onConsoleOutput?.Invoke(ProcessOutput.FromStdErrLine(warningMessage)); } // Launch command is for a console entry point, and not a direct script var entryPoint = await VenvRunner.GetEntryPoint(command).ConfigureAwait(false); // Split at ':' to get package and function var split = entryPoint?.Split(':'); // Console message because Invoke takes forever to start sometimes with no output of what its doing onConsoleOutput?.Invoke(new ProcessOutput { Text = "Starting InvokeAI...\n" }); if (split is not { Length: > 1 }) { throw new Exception($"Could not find entry point for InvokeAI: {entryPoint.ToRepr()}"); } // Compile a startup command according to // https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts // For invokeai, also patch the shutil.get_terminal_size function to return a fixed value // above the minimum in invokeai.frontend.install.widgets var code = $""" import sys from {split[0]} import {split[1]} sys.exit({split[1]}()) """; if (runDetached) { async void HandleConsoleOutput(ProcessOutput s) { if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; if (installedPackage.PreferredSharedFolderMethod == SharedFolderMethod.Configuration) { try { // returns true if we printed the url already cuz it took too long if ( await SetupInvokeModelSharingConfig(onConsoleOutput, match, s) .ConfigureAwait(false) ) return; } catch (Exception e) { Logger.Error(e, "Failed to setup InvokeAI model sharing config"); } } onConsoleOutput?.Invoke(s); WebUrl = match.Value; OnStartupComplete(WebUrl); } else { onConsoleOutput?.Invoke(s); if ( spam3 && s.Text.Contains("[3] Accept the best guess;", StringComparison.OrdinalIgnoreCase) ) { VenvRunner.Process?.StandardInput.WriteLine("3"); } } } VenvRunner.RunDetached($"-c \"{code}\" {arguments}".TrimEnd(), HandleConsoleOutput, OnExit); } else { var result = await VenvRunner.Run($"-c \"{code}\" {arguments}".TrimEnd()).ConfigureAwait(false); onConsoleOutput?.Invoke(new ProcessOutput { Text = result.StandardOutput }); } } public override async Task Update( string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await InstallPackage( installLocation, installedPackage, options.AsInstallOptions(), progress, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(options.VersionOptions.VersionTag)) { return new InstalledPackageVersion { InstalledReleaseVersion = options.VersionOptions.VersionTag, IsPrerelease = options.VersionOptions.IsPrerelease, }; } return new InstalledPackageVersion { InstalledBranch = options.VersionOptions.BranchName, InstalledCommitSha = options.VersionOptions.CommitHash, IsPrerelease = options.VersionOptions.IsPrerelease, }; } // Invoke doing shared folders on startup instead public override Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) => Task.CompletedTask; public override Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) => Task.CompletedTask; private async Task SetupInvokeModelSharingConfig( Action? onConsoleOutput, Match match, ProcessOutput s ) { var invokeAiUrl = match.Value; if (invokeAiUrl.Contains("0.0.0.0")) { invokeAiUrl = invokeAiUrl.Replace("0.0.0.0", "127.0.0.1"); } var invokeAiApi = RestService.For( invokeAiUrl, new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer( new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } ), } ); var result = await invokeAiApi.ScanFolder(SettingsManager.ModelsDirectory).ConfigureAwait(false); var modelsToScan = result.Where(x => !x.IsInstalled).ToList(); if (modelsToScan.Count <= 0) return false; foreach (var model in modelsToScan) { Logger.Info($"Installing model {model.Path}"); await invokeAiApi .InstallModel( new InstallModelRequest { Name = Path.GetFileNameWithoutExtension(model.Path), Description = Path.GetFileName(model.Path), }, source: model.Path, inplace: true ) .ConfigureAwait(false); } var installStatus = await invokeAiApi.GetModelInstallStatus().ConfigureAwait(false); var installCheckCount = 0; while ( !installStatus.All(x => (x.Status != null && x.Status.Equals("completed", StringComparison.OrdinalIgnoreCase)) || (x.Status != null && x.Status.Equals("error", StringComparison.OrdinalIgnoreCase)) ) ) { installCheckCount++; if (installCheckCount > 5) { onConsoleOutput?.Invoke( new ProcessOutput { Text = "This may take awhile, feel free to use the web interface while the rest of your models are imported.\n", } ); onConsoleOutput?.Invoke(s); WebUrl = match.Value; OnStartupComplete(WebUrl); break; } onConsoleOutput?.Invoke( new ProcessOutput { Text = $"\nWaiting for model import... ({installStatus.Count(x => (x.Status != null && !x.Status.Equals("completed", StringComparison.OrdinalIgnoreCase)) && !x.Status.Equals("error", StringComparison.OrdinalIgnoreCase))} remaining)\n", } ); await Task.Delay(5000).ConfigureAwait(false); try { installStatus = await invokeAiApi.GetModelInstallStatus().ConfigureAwait(false); } catch (Exception e) { Logger.Error(e, "Failed to get model install status"); } } return installCheckCount > 5; } private ImmutableDictionary GetEnvVars( ImmutableDictionary env, DirectoryPath installPath ) { // Set additional required environment variables // Need to make subdirectory because they store config in the // directory *above* the root directory var root = installPath.JoinDir(RelativeRootPath); root.Create(); env = env.SetItem("INVOKEAI_ROOT", root); var path = env.GetValueOrDefault("PATH", string.Empty); if (string.IsNullOrEmpty(path)) { path += $"{Compat.PathDelimiter}{Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs")}"; } else { path += Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs"); } path += $"{Compat.PathDelimiter}{Path.Combine(installPath, "node_modules", ".bin")}"; if (Compat.IsMacOS || Compat.IsLinux) { path += $"{Compat.PathDelimiter}{Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs", "bin")}"; } if (Compat.IsWindows) { path += $"{Compat.PathDelimiter}{Environment.GetFolderPath(Environment.SpecialFolder.System)}"; } return env.SetItem("PATH", path); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/KohyaSs.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class KohyaSs( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyRunner runner, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "kohya_ss"; public override string DisplayName { get; set; } = "kohya_ss"; public override string Author => "bmaltais"; public override string Blurb => "A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers"; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md"; public override string LaunchCommand => "kohya_gui.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/kohyass/preview.webp"); public override string OutputFolderName => string.Empty; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override TorchIndex GetRecommendedTorchVersion() => TorchIndex.Cuda; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.UltraNightmare; public override PackageType PackageType => PackageType.SdTraining; public override bool OfferInOneClickInstaller => false; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda]; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.None]; public override IEnumerable Prerequisites => base.Prerequisites.Concat([PackagePrerequisite.Tkinter]); public override List LaunchOptions => [ new LaunchOptionDefinition { Name = "Listen Address", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", Options = ["--listen"], }, new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, Options = ["--port"], }, new LaunchOptionDefinition { Name = "Skip Requirements Verification", Type = LaunchOptionType.Bool, Options = ["--noverify"], InitialValue = true, }, new LaunchOptionDefinition { Name = "Username", Type = LaunchOptionType.String, Options = ["--username"], }, new LaunchOptionDefinition { Name = "Password", Type = LaunchOptionType.String, Options = ["--password"], }, new LaunchOptionDefinition { Name = "Auto-Launch Browser", Type = LaunchOptionType.Bool, Options = ["--inbrowser"], }, new LaunchOptionDefinition { Name = "Share", Type = LaunchOptionType.Bool, Options = ["--share"], }, new LaunchOptionDefinition { Name = "Headless", Type = LaunchOptionType.Bool, Options = ["--headless"], }, new LaunchOptionDefinition { Name = "Language", Type = LaunchOptionType.String, Options = ["--language"], }, LaunchOptionDefinition.Extras, ]; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Updating submodules", isIndeterminate: true)); await PrerequisiteHelper .RunGit( ["submodule", "update", "--init", "--recursive", "--quiet"], onConsoleOutput, installLocation ) .ConfigureAwait(false); if (Compat.IsWindows) { await PrerequisiteHelper.FixGitLongPaths().ConfigureAwait(false); } progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); // --- Platform-Specific Installation --- if (Compat.IsLinux) { // Linux path is too custom for the orchestrator, so it remains as is. await venvRunner .PipInstall("rich packaging setuptools uv", onConsoleOutput) .ConfigureAwait(false); await venvRunner .CustomInstall( [ "setup/setup_linux.py", "--platform-requirements-file=requirements_linux.txt", "--no_run_accelerate", ], onConsoleOutput ) .ConfigureAwait(false); } else if (Compat.IsWindows) { // Windows path is a perfect fit for the orchestrator. var isLegacyNvidia = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var config = new PipInstallConfig { PrePipInstallArgs = ["rich", "packaging", "setuptools", "uv"], RequirementsFilePaths = ["requirements_windows.txt"], // Exclude torch ecosystem (default) AND the specific bitsandbytes version RequirementsExcludePattern = "(torch|torchvision|torchaudio|xformers|bitsandbytes==0\\.44\\.0)", TorchaudioVersion = " ", XformersVersion = " ", CudaIndex = isLegacyNvidia ? "cu126" : "cu128", // Add back the generic bitsandbytes and the specific numpy version ExtraPipArgs = ["bitsandbytes"], PostInstallPipArgs = ["numpy==1.26.4"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); } } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } public override Dictionary>? SharedFolders { get; } public override Dictionary>? SharedOutputFolders { get; } public override string MainBranch => "master"; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Mashb1tFooocus.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Mashb1tFooocus( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : Fooocus( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "mashb1t-fooocus"; public override string Author => "mashb1t"; public override string RepositoryName => "Fooocus"; public override string DisplayName { get; set; } = "Fooocus - mashb1t's 1-Up Edition"; public override string Blurb => "The purpose of this fork is to add new features / fix bugs and contribute back to Fooocus."; public override string LicenseUrl => "https://github.com/mashb1t/Fooocus/blob/main/LICENSE"; public override bool ShouldIgnoreReleases => false; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/OneTrainer.cs ================================================ using System.Diagnostics; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class OneTrainer( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "OneTrainer"; public override string DisplayName { get; set; } = "OneTrainer"; public override string Author => "Nerogar"; public override string Blurb => "OneTrainer is a one-stop solution for all your stable diffusion training needs"; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/Nerogar/OneTrainer/blob/master/LICENSE.txt"; public override string LaunchCommand => "scripts/train_ui.py"; public override Uri PreviewImageUri => new("https://github.com/Nerogar/OneTrainer/blob/master/resources/icons/icon.png?raw=true"); public override string OutputFolderName => string.Empty; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda, TorchIndex.Rocm]; public override PackageType PackageType => PackageType.SdTraining; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.None }; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Nightmare; public override bool OfferInOneClickInstaller => false; public override bool ShouldIgnoreReleases => true; public override IEnumerable Prerequisites => base.Prerequisites.Concat([PackagePrerequisite.Tkinter]); public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing requirements", isIndeterminate: true)); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var requirementsFileName = torchVersion switch { TorchIndex.Cuda => "requirements-cuda.txt", TorchIndex.Rocm => "requirements-rocm.txt", _ => "requirements-default.txt", }; await venvRunner.PipInstall(["-r", requirementsFileName], onConsoleOutput).ConfigureAwait(false); var requirementsGlobal = new FilePath(installLocation, "requirements-global.txt"); var pipArgs = new PipInstallArgs().WithParsedFromRequirementsTxt( (await requirementsGlobal.ReadAllTextAsync(cancellationToken).ConfigureAwait(false)).Replace( "-e ", "" ), "scipy==1.15.1; sys_platform != 'win32'" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], onConsoleOutput, OnExit ); } public override List LaunchOptions => [LaunchOptionDefinition.Extras]; public override Dictionary>? SharedFolders { get; } public override Dictionary>? SharedOutputFolders { get; } public override string MainBranch => "master"; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Options/DownloadPackageOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; public class DownloadPackageOptions { public required DownloadPackageVersionOptions VersionOptions { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Options/InstallPackageOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; public class InstallPackageOptions { public DownloadPackageVersionOptions VersionOptions { get; init; } = new(); public PythonPackageOptions PythonOptions { get; init; } = new(); public SharedFolderMethod SharedFolderMethod { get; init; } = SharedFolderMethod.None; public bool IsUpdate { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Options/PythonPackageOptions.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.Packages; public class PythonPackageOptions { [JsonConverter(typeof(JsonStringEnumConverter))] public TorchIndex? TorchIndex { get; set; } public string? TorchVersion { get; set; } public PyVersion? PythonVersion { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Options/RunPackageOptions.cs ================================================ using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.Packages; public class RunPackageOptions { public string? Command { get; set; } public ProcessArgs Arguments { get; set; } = []; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Options/UpdatePackageOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; public class UpdatePackageOptions { public DownloadPackageVersionOptions VersionOptions { get; init; } = new(); public PythonPackageOptions PythonOptions { get; init; } = new(); public InstallPackageOptions AsInstallOptions() { return new InstallPackageOptions { VersionOptions = VersionOptions, PythonOptions = PythonOptions, IsUpdate = true }; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; public class PackageVersionOptions { public IEnumerable? AvailableVersions { get; set; } = Enumerable.Empty(); public IEnumerable? AvailableBranches { get; set; } = Enumerable.Empty(); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/PackageVulnerability.cs ================================================ using System; namespace StabilityMatrix.Core.Models.Packages; /// /// Represents a security vulnerability in a package /// public class PackageVulnerability { /// /// Unique identifier for the vulnerability (e.g. CVE number) /// public string Id { get; set; } = string.Empty; /// /// Short title describing the vulnerability /// public string Title { get; set; } = string.Empty; /// /// Detailed description of the vulnerability /// public string Description { get; set; } = string.Empty; /// /// URL with more information about the vulnerability /// public Uri? InfoUrl { get; set; } /// /// Severity level of the vulnerability /// public VulnerabilitySeverity Severity { get; set; } /// /// When this vulnerability was discovered/published /// public DateTimeOffset PublishedDate { get; set; } /// /// Version ranges affected by this vulnerability /// public string[] AffectedVersions { get; set; } = Array.Empty(); /// /// Version that fixes this vulnerability, if available /// public string? FixedInVersion { get; set; } } /// /// Severity levels for package vulnerabilities /// public enum VulnerabilitySeverity { Low, Medium, High, Critical } ================================================ FILE: StabilityMatrix.Core/Models/Packages/PipInstallConfig.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; /// /// Configuration for the standard pip installation process. /// public record PipInstallConfig { public IEnumerable RequirementsFilePaths { get; init; } = []; public string RequirementsExcludePattern { get; init; } = "(torch|torchvision|torchaudio|xformers)"; public IEnumerable PrePipInstallArgs { get; init; } = []; public IEnumerable ExtraPipArgs { get; init; } = []; public IEnumerable PostInstallPipArgs { get; init; } = []; public string TorchVersion { get; init; } = ""; public string TorchvisionVersion { get; init; } = ""; public string TorchaudioVersion { get; init; } = ""; public string XformersVersion { get; init; } = ""; public string CudaIndex { get; init; } = "cu130"; public string RocmIndex { get; init; } = "rocm6.4"; public bool ForceReinstallTorch { get; init; } = true; public bool UpgradePackages { get; init; } = false; public bool SkipTorchInstall { get; init; } = false; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Reforge.cs ================================================ using System.Collections.Immutable; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Reforge( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : SDWebForge( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "reforge"; public override string Author => "Panchovix"; public override string RepositoryName => "stable-diffusion-webui-reForge"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI reForge"; public override string Blurb => "Stable Diffusion WebUI reForge is a platform on top of Stable Diffusion WebUI (based on Gradio) to make development easier, optimize resource management, speed up inference, and study experimental features."; public override string LicenseUrl => "https://github.com/Panchovix/stable-diffusion-webui-reForge/blob/main/LICENSE.txt"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/reforge/preview.webp"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Recommended; public override bool OfferInOneClickInstaller => true; public override PackageType PackageType => PackageType.SdInference; protected override ImmutableDictionary GetEnvVars( ImmutableDictionary env ) => env; } ================================================ FILE: StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class RuinedFooocus( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : Fooocus( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { 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 Uri PreviewImageUri => new("https://raw.githubusercontent.com/runew0lf/pmmconfigs/main/RuinedFooocus_ss.png"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.Symlink, SharedFolderMethod.None]; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override List LaunchOptions => [ new() { Name = "Port", Type = LaunchOptionType.String, Description = "Sets the listen port", Options = { "--port" }, }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new() { Name = "Listen", Type = LaunchOptionType.String, Description = "Set the listen interface", Options = { "--listen" }, }, new() { Name = "Auth", Type = LaunchOptionType.String, Description = "Set credentials username/password", Options = { "--auth" }, }, new() { Name = "No Browser", Type = LaunchOptionType.Bool, Description = "Do not launch in browser", Options = { "--nobrowser" }, }, LaunchOptionDefinition.Extras, ]; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); if (torchVersion == TorchIndex.Cuda) { await using var venvRunner = await SetupVenvPure( installLocation, forceRecreate: true, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); var isLegacyNvidia = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var torchExtraIndex = isLegacyNvidia ? "cu126" : "cu128"; var requirements = new FilePath(installLocation, "requirements_versions.txt"); var pipArgs = new PipInstallArgs() .WithTorchExtraIndex(torchExtraIndex) .WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), "--extra-index-url.*|--index-url.*" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } else { await base.InstallPackage( installLocation, installedPackage, options, progress, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); } // Create output folder since it's not created by default var outputFolder = new DirectoryPath(installLocation, OutputFolderName); outputFolder.Create(); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/SDWebForge.cs ================================================ using System.Text; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class SDWebForge( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : A3WebUI( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "stable-diffusion-webui-forge"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI Forge"; public override string Author => "lllyasviel"; public override string Blurb => "Stable Diffusion WebUI Forge is a platform on top of Stable Diffusion WebUI (based on Gradio) to make development easier, optimize resource management, and speed up inference."; public override string LicenseUrl => "https://github.com/lllyasviel/stable-diffusion-webui-forge/blob/main/LICENSE.txt"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/sdwebforge/preview.webp"); public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override IPackageExtensionManager ExtensionManager => null; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override PackageType PackageType => PackageType.Legacy; public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new() { Name = "Pin Shared Memory", Type = LaunchOptionType.Bool, Options = { "--pin-shared-memory" }, InitialValue = HardwareHelper.HasNvidiaGpu() && ( SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() is false || !HardwareHelper.HasLegacyNvidiaGpu() ), }, new() { Name = "CUDA Malloc", Type = LaunchOptionType.Bool, Options = { "--cuda-malloc" }, InitialValue = HardwareHelper.HasNvidiaGpu() && ( SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() is false || !HardwareHelper.HasLegacyNvidiaGpu() ), }, new() { Name = "CUDA Stream", Type = LaunchOptionType.Bool, Options = { "--cuda-stream" }, InitialValue = HardwareHelper.HasNvidiaGpu() && ( SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() is false || !HardwareHelper.HasLegacyNvidiaGpu() ), }, new() { Name = "Skip Install", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--skip-install"], }, new() { Name = "Always Offload from VRAM", Type = LaunchOptionType.Bool, Options = ["--always-offload-from-vram"], }, new() { Name = "Always GPU", Type = LaunchOptionType.Bool, Options = ["--always-gpu"], }, new() { Name = "Always CPU", Type = LaunchOptionType.Bool, Options = ["--always-cpu"], }, new() { Name = "Skip Torch CUDA Test", Type = LaunchOptionType.Bool, InitialValue = Compat.IsMacOS, Options = ["--skip-torch-cuda-test"], }, new() { Name = "No half-precision VAE", Type = LaunchOptionType.Bool, InitialValue = Compat.IsMacOS, Options = ["--no-half-vae"], }, LaunchOptionDefinition.Extras, ]; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.Rocm, TorchIndex.Mps]; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); // Dynamically discover all requirements files var requirementsPaths = new List { "requirements_versions.txt" }; var extensionsBuiltinDir = new DirectoryPath(installLocation, "extensions-builtin"); if (extensionsBuiltinDir.Exists) { requirementsPaths.AddRange( extensionsBuiltinDir .EnumerateFiles("requirements.txt", EnumerationOptionConstants.AllDirectories) .Select(f => Path.GetRelativePath(installLocation, f.ToString())) ); } var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var isBlackwell = torchIndex is TorchIndex.Cuda && (SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu()); var config = new PipInstallConfig { PrePipInstallArgs = ["joblib"], RequirementsFilePaths = requirementsPaths, TorchVersion = "", TorchvisionVersion = "", CudaIndex = isBlackwell ? "cu128" : "cu126", RocmIndex = "rocm7.1", ExtraPipArgs = [ "https://github.com/openai/CLIP/archive/d50d76daa670286dd6cacf3bcd80b5e4823fc8e1.zip", ], PostInstallPipArgs = ["numpy==1.26.4"], }; await StandardPipInstallProcessAsync( venvRunner, options, installedPackage, config, onConsoleOutput, progress, cancellationToken ) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Sdfx.cs ================================================ using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Sdfx( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "sdfx"; public override string DisplayName { get; set; } = "SDFX"; public override string Author => "sdfxai"; public override string Blurb => "The ultimate no-code platform to build and share AI apps with beautiful UI."; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/sdfxai/sdfx/blob/main/LICENSE"; public override string LaunchCommand => "setup.py"; public override Uri PreviewImageUri => new("https://github.com/sdfxai/sdfx/raw/main/docs/static/screen-sdfx.png"); public override string OutputFolderName => Path.Combine("data", "media", "output"); public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl, TorchIndex.Rocm, TorchIndex.Mps]; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Impossible; public override bool OfferInOneClickInstaller => false; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override List LaunchOptions => [LaunchOptionDefinition.Extras]; public override string Disclaimer => "This package may no longer receive updates from its author."; public override PackageType PackageType => PackageType.Legacy; public override SharedFolderLayout SharedFolderLayout => new() { RelativeConfigPath = "sdfx.config.json", ConfigFileType = ConfigFileType.Json, Rules = [ // Assuming JSON keys are top-level, adjust ConfigDocumentPaths if nested (e.g., "paths.models.checkpoints") new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["data/models/checkpoints"], ConfigDocumentPaths = ["path.models.checkpoints"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Diffusers], TargetRelativePaths = ["data/models/diffusers"], ConfigDocumentPaths = ["path.models.diffusers"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["data/models/vae"], ConfigDocumentPaths = ["path.models.vae"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["data/models/loras"], ConfigDocumentPaths = ["path.models.loras"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Embeddings], TargetRelativePaths = ["data/models/embeddings"], ConfigDocumentPaths = ["path.models.embeddings"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Hypernetwork], TargetRelativePaths = ["data/models/hypernetworks"], ConfigDocumentPaths = ["path.models.hypernetworks"], }, new SharedFolderLayoutRule { SourceTypes = [ SharedFolderType.ESRGAN, SharedFolderType.RealESRGAN, SharedFolderType.SwinIR, ], TargetRelativePaths = ["data/models/upscale_models"], ConfigDocumentPaths = ["path.models.upscale_models"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["data/models/clip"], ConfigDocumentPaths = ["path.models.clip"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ClipVision], TargetRelativePaths = ["data/models/clip_vision"], ConfigDocumentPaths = ["path.models.clip_vision"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ControlNet, SharedFolderType.T2IAdapter], TargetRelativePaths = ["data/models/controlnet"], ConfigDocumentPaths = ["path.models.controlnet"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.GLIGEN], TargetRelativePaths = ["data/models/gligen"], ConfigDocumentPaths = ["path.models.gligen"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ApproxVAE], TargetRelativePaths = ["data/models/vae_approx"], ConfigDocumentPaths = ["path.models.vae_approx"], }, new SharedFolderLayoutRule { SourceTypes = [ SharedFolderType.IpAdapter, SharedFolderType.IpAdapters15, SharedFolderType.IpAdaptersXl, ], TargetRelativePaths = ["data/models/ipadapter"], ConfigDocumentPaths = ["path.models.ipadapter"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.PromptExpansion], TargetRelativePaths = ["data/models/prompt_expansion"], ConfigDocumentPaths = ["path.models.prompt_expansion"], }, ], }; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "data/media/output" } }; public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override IEnumerable Prerequisites => [ PackagePrerequisite.Python310, PackagePrerequisite.VcRedist, PackagePrerequisite.Git, PackagePrerequisite.Node, ]; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); // Setup venv await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); venvRunner.UpdateEnvironmentVariables(GetEnvVars); progress?.Report( new ProgressReport(-1f, "Installing Package Requirements...", isIndeterminate: true) ); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var gpuArg = torchVersion switch { TorchIndex.Cuda => "--nvidia", TorchIndex.Rocm => "--amd", TorchIndex.DirectMl => "--directml", TorchIndex.Cpu => "--cpu", TorchIndex.Mps => "--mac", _ => throw new NotSupportedException($"Torch version {torchVersion} is not supported."), }; await venvRunner .CustomInstall(["setup.py", "--install", gpuArg], onConsoleOutput) .ConfigureAwait(false); if (installedPackage.PipOverrides != null) { var pipArgs = new PipInstallArgs().WithUserOverrides(installedPackage.PipOverrides); await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } progress?.Report(new ProgressReport(1, "Installed Package Requirements", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var venvRunner = await SetupVenv( installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); venvRunner.UpdateEnvironmentVariables(GetEnvVars); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); } venvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), "--run", .. options.Arguments], HandleConsoleOutput, OnExit ); // Cuz node was getting detached on process exit if (Compat.IsWindows) { ProcessTracker.AttachExitHandlerJobToProcess(venvRunner.Process); } } private ImmutableDictionary GetEnvVars(ImmutableDictionary env) { var pathBuilder = new EnvPathBuilder(); if (env.TryGetValue("PATH", out var value)) { pathBuilder.AddPath(value); } pathBuilder.AddPath( Compat.IsWindows ? Environment.GetFolderPath(Environment.SpecialFolder.System) : Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs-18", "bin") ); pathBuilder.AddPath(Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs-18")); return env.SetItem("PATH", pathBuilder.ToString()); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/SharedFolderLayout.cs ================================================ using System.Collections.Immutable; using System.ComponentModel; using StabilityMatrix.Core.Models.Packages.Config; namespace StabilityMatrix.Core.Models.Packages; [Localizable(false)] public record SharedFolderLayout { /// /// Optional config file path, relative from package installation directory /// public string? RelativeConfigPath { get; set; } public ConfigFileType? ConfigFileType { get; set; } public ConfigSharingOptions ConfigSharingOptions { get; set; } = ConfigSharingOptions.Default; public IImmutableList Rules { get; set; } = []; public Dictionary GetRulesByConfigPath() { // Dictionary of config path to rule var configPathToRule = new Dictionary(); foreach (var rule in Rules) { // Ignore rules without config paths if (rule.ConfigDocumentPaths is not { Length: > 0 } configPaths) { continue; } foreach (var configPath in configPaths) { // Get or create rule var existingRule = configPathToRule.GetValueOrDefault( configPath, new SharedFolderLayoutRule() ); // Add unique configPathToRule[configPath] = existingRule.Union(rule); } } return configPathToRule; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/SharedFolderLayoutRule.cs ================================================ namespace StabilityMatrix.Core.Models.Packages; public readonly record struct SharedFolderLayoutRule() { public SharedFolderType[] SourceTypes { get; init; } = []; public string[] TargetRelativePaths { get; init; } = []; public string[] ConfigDocumentPaths { get; init; } = []; /// /// For rules that use the root models folder instead of a specific SharedFolderType /// public bool IsRoot { get; init; } /// /// Optional sub-path from all source types to the target path. /// public string? SourceSubPath { get; init; } public SharedFolderLayoutRule Union(SharedFolderLayoutRule other) { return this with { SourceTypes = SourceTypes.Union(other.SourceTypes).ToArray(), TargetRelativePaths = TargetRelativePaths.Union(other.TargetRelativePaths).ToArray(), ConfigDocumentPaths = ConfigDocumentPaths.Union(other.ConfigDocumentPaths).ToArray(), IsRoot = IsRoot || other.IsRoot, SourceSubPath = SourceSubPath ?? other.SourceSubPath }; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/SimpleSDXL.cs ================================================ using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class SimpleSDXL( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : Fooocus( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "SimpleSDXL"; public override string DisplayName { get; set; } = "SimpleSDXL"; public override string Author => "metercai"; public override string Blurb => "Enhanced version of Fooocus for SDXL, more suitable for Chinese and Cloud. Supports Flux."; public override string LicenseUrl => "https://github.com/metercai/SimpleSDXL/blob/SimpleSDXL/LICENSE"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/simplesdxl/preview.webp"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.Configuration, SharedFolderMethod.Symlink, SharedFolderMethod.None]; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override string MainBranch => "SimpleSDXL"; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda]; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu(); public override IReadOnlyList KnownVulnerabilities => [ new() { Id = "GHSA-qq8j-phpf-c63j", Title = "Undisclosed Data Collection and Remote Access in simpleai_base Dependency", Description = "SimpleSDXL depends on simpleai_base which contains compiled Rust code with:\n" + "- Undisclosed remote access functionality using rathole\n" + "- Hidden system information gathering via concealed executable calls\n" + "- Covert data upload to tokentm.net (blockchain-associated domain)\n" + "- Undisclosed VPN functionality pointing to servers blocked by Chinese authorities\n\n" + "This poses significant security and privacy risks as system information is uploaded without consent " + "and the compiled nature of the code means the full extent of the remote access capabilities cannot be verified.", Severity = VulnerabilitySeverity.Critical, PublishedDate = DateTimeOffset.Parse("2025-01-11"), InfoUrl = new Uri("https://github.com/metercai/SimpleSDXL/issues/97"), AffectedVersions = ["*"], // Affects all versions }, ]; public override List LaunchOptions => [ new() { Name = "Preset", Description = "Apply specified UI preset.", Type = LaunchOptionType.Bool, Options = { "--preset anime", "--preset realistic", "--preset Flux", "--preset Kolors", "--preset pony_v6", }, }, new() { Name = "Language", Type = LaunchOptionType.String, Description = "Translate UI using json files in [language] folder.", InitialValue = "en", Options = { "--language" }, }, new() { Name = "Port", Type = LaunchOptionType.String, Description = "Sets the listen port", Options = { "--port" }, }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = { "--share" }, }, new() { Name = "Listen", Type = LaunchOptionType.String, Description = "Set the listen interface", Options = { "--listen" }, }, new() { Name = "Disable preset download", Description = "Disables downloading models for presets", DefaultValue = false, Type = LaunchOptionType.Bool, Options = { "--disable-preset-download" }, }, new() { Name = "Theme", Description = "Launches the UI with light or dark theme", Type = LaunchOptionType.String, DefaultValue = "dark", Options = { "--theme" }, }, new() { Name = "Disable offload from VRAM", Description = "Force loading models to vram when the unload can be avoided. Some Mac users may need this.", Type = LaunchOptionType.Bool, InitialValue = Compat.IsMacOS, Options = { "--disable-offload-from-vram" }, }, new() { Name = "Disable image log", Description = "Prevent writing images and logs to the outputs folder.", Type = LaunchOptionType.Bool, Options = { "--disable-image-log" }, }, new() { Name = "Disable metadata", Description = "Disables saving metadata to images.", Type = LaunchOptionType.Bool, Options = { "--disable-metadata" }, }, new() { Name = "Disable enhance output sorting", Description = "Disables enhance output sorting for final image gallery.", Type = LaunchOptionType.Bool, Options = { "--disable-enhance-output-sorting" }, }, new() { Name = "Enable auto describe image", Description = "Enables automatic description of uov and enhance image when prompt is empty", DefaultValue = true, Type = LaunchOptionType.Bool, Options = { "--enable-auto-describe-image" }, }, new() { Name = "Always download new models", Description = "Always download newer models.", DefaultValue = false, Type = LaunchOptionType.Bool, Options = { "--always-download-new-model" }, }, new() { Name = "Disable comfyd", Description = "Disable auto start comfyd server at launch", Type = LaunchOptionType.Bool, Options = { "--disable-comfyd" }, }, LaunchOptionDefinition.Extras, ]; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await using var venvRunner = await SetupVenvPure( installLocation, forceRecreate: true, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); // Get necessary dependencies await venvRunner.PipInstall("--upgrade pip", onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("nvidia-pyindex pygit2", onConsoleOutput).ConfigureAwait(false); await venvRunner.PipInstall("facexlib cpm_kernels", onConsoleOutput).ConfigureAwait(false); if (Compat.IsWindows) { // Download and Install pre-built insightface const string wheelUrl = "https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp310-cp310-win_amd64.whl"; var wheelPath = new FilePath(installLocation, "insightface-0.7.3-cp310-cp310-win_amd64.whl"); await DownloadService .DownloadToFileAsync(wheelUrl, wheelPath, cancellationToken: cancellationToken) .ConfigureAwait(false); await venvRunner.PipInstall($"{wheelPath}", onConsoleOutput).ConfigureAwait(false); await wheelPath.DeleteAsync(cancellationToken).ConfigureAwait(false); } var requirements = new FilePath(installLocation, "requirements_versions.txt"); var pipArgs = new PipInstallArgs() .WithTorch("==2.3.1") .WithTorchVision("==0.18.1") .WithTorchAudio("==2.3.1") .WithTorchExtraIndex("cu121") .WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), "torch" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); // Create output folder since it's not created by default var outputFolder = new DirectoryPath(installLocation, OutputFolderName); outputFolder.Create(); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs ================================================ using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class StableDiffusionDirectMl( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : A3WebUI( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "stable-diffusion-webui-directml"; public override string DisplayName { get; set; } = "Stable Diffusion Web UI"; public override string Author => "lshqqytiger"; 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 LaunchCommand => "launch.py"; public override Uri PreviewImageUri => new("https://github.com/lshqqytiger/stable-diffusion-webui-directml/raw/master/screenshot.png"); public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override TorchIndex GetRecommendedTorchVersion() => HardwareHelper.PreferDirectMLOrZluda() ? TorchIndex.DirectMl : base.GetRecommendedTorchVersion(); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override List LaunchOptions { get { var baseLaunchOptions = base.LaunchOptions; baseLaunchOptions.Insert( 0, new LaunchOptionDefinition { Name = "Use DirectML", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferDirectMLOrZluda(), Options = ["--use-directml"], } ); return baseLaunchOptions; } } public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.DirectMl }; public override bool ShouldIgnoreReleases => true; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); // Setup venv await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var pipArgs = new PipInstallArgs() .WithTorch("==2.3.1") .WithTorchVision("==0.18.1") .AddArg("httpx==0.24.1"); if (torchVersion == TorchIndex.DirectMl) { pipArgs = pipArgs.WithTorchDirectML(); } // Install requirements file progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)); Logger.Info("Installing requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt"); pipArgs = pipArgs.WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), excludePattern: "torch" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class StableDiffusionUx( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "stable-diffusion-webui-ux"; public override string DisplayName { get; set; } = "Stable Diffusion Web UI-UX"; public override string Author => "anapnoe"; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt"; public override string Blurb => "A pixel perfect design, mobile friendly, customizable interface that adds accessibility, " + "ease of use and extended functionallity to the stable diffusion web ui."; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => new("https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png"); public override string Disclaimer => "This package may no longer receive updates from its author."; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Impossible; public override IPackageExtensionManager? ExtensionManager => new A3WebUiExtensionManager(this); public override PackageType PackageType => PackageType.Legacy; public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" }, [SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" }, [SharedFolderType.RealESRGAN] = new[] { "models/RealESRGAN" }, [SharedFolderType.SwinIR] = new[] { "models/SwinIR" }, [SharedFolderType.Lora] = new[] { "models/Lora" }, [SharedFolderType.LyCORIS] = new[] { "models/LyCORIS" }, [SharedFolderType.ApproxVAE] = new[] { "models/VAE-approx" }, [SharedFolderType.VAE] = new[] { "models/VAE" }, [SharedFolderType.DeepDanbooru] = new[] { "models/deepbooru" }, [SharedFolderType.Karlo] = new[] { "models/karlo" }, [SharedFolderType.Embeddings] = new[] { "embeddings" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, [SharedFolderType.ControlNet] = new[] { "models/ControlNet" }, [SharedFolderType.Codeformer] = new[] { "models/Codeformer" }, [SharedFolderType.LDSR] = new[] { "models/LDSR" }, [SharedFolderType.AfterDetailer] = new[] { "models/adetailer" }, }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Extras] = new[] { "outputs/extras-images" }, [SharedOutputType.Saved] = new[] { "log/images" }, [SharedOutputType.Img2Img] = new[] { "outputs/img2img-images" }, [SharedOutputType.Text2Img] = new[] { "outputs/txt2img-images" }, [SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" }, [SharedOutputType.Text2ImgGrids] = new[] { "outputs/txt2img-grids" }, }; [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { MemoryLevel.Low => "--lowvram", MemoryLevel.Medium => "--medvram", _ => null, }, Options = ["--lowvram", "--medvram", "--medvram-sdxl"], }, new() { Name = "Xformers", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), Options = ["--xformers"], }, new() { Name = "API", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--api"], }, new() { Name = "Auto Launch Web UI", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--autolaunch"], }, new() { Name = "Skip Torch CUDA Check", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), Options = ["--skip-torch-cuda-test"], }, new() { Name = "Skip Python Version Check", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--skip-python-version-check"], }, new() { Name = "No Half", Type = LaunchOptionType.Bool, Description = "Do not switch the model to 16-bit floats", InitialValue = HardwareHelper.PreferRocm() || HardwareHelper.PreferDirectMLOrZluda() || Compat.IsMacOS, Options = ["--no-half"], }, new() { Name = "Skip SD Model Download", Type = LaunchOptionType.Bool, InitialValue = false, Options = ["--no-download-sd-model"], }, new() { Name = "Skip Install", Type = LaunchOptionType.Bool, Options = ["--skip-install"], }, LaunchOptionDefinition.Extras, ]; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.Rocm, TorchIndex.Mps }; public override string MainBranch => "master"; public override bool ShouldIgnoreReleases => true; public override string OutputFolderName => "outputs"; public override IReadOnlyList ExtraLaunchArguments => settingsManager.IsLibraryDirSet ? ["--gradio-allowed-path", settingsManager.ImagesDirectory] : []; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); var pipArgs = new PipInstallArgs(); switch (torchVersion) { case TorchIndex.Cpu: pipArgs = pipArgs.WithTorch("==2.1.2").WithTorchVision("==0.16.2"); break; case TorchIndex.Cuda: pipArgs = pipArgs .WithTorch("==2.1.2") .WithTorchVision("==0.16.2") .WithXFormers("==0.0.23post1") .WithTorchExtraIndex("cu121"); break; case TorchIndex.Rocm: pipArgs = pipArgs .WithTorch("==2.0.1") .WithTorchVision("==0.15.2") .WithTorchExtraIndex("rocm5.4.2"); break; case TorchIndex.Mps: pipArgs = pipArgs.WithTorch("==2.1.2").WithTorchVision("==0.16.2").WithTorchExtraIndex("cpu"); break; } // Install requirements file progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)); Logger.Info("Installing requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt"); pipArgs = pipArgs.WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), excludePattern: "torch" ); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); } VenvRunner.RunDetached( [ Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments, .. ExtraLaunchArguments, ], HandleConsoleOutput, OnExit ); } private class A3WebUiExtensionManager(StableDiffusionUx package) : GitPackageExtensionManager(package.PrerequisiteHelper) { public override string RelativeInstallDirectory => "extensions"; public override IEnumerable DefaultManifests => [ new ExtensionManifest( new Uri( "https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json" ) ), ]; public override async Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ) { try { // Get json var content = await package .DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken) .ConfigureAwait(false); // Parse json var jsonManifest = JsonSerializer.Deserialize( content, A1111ExtensionManifestSerializerContext.Default.Options ); return jsonManifest?.GetPackageExtensions() ?? Enumerable.Empty(); } catch (Exception e) { Logger.Error(e, "Failed to get extensions from manifest"); return Enumerable.Empty(); } } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/StableSwarm.cs ================================================ using System.Diagnostics; using System.Text.RegularExpressions; using FreneticUtilities.FreneticDataSyntax; using Injectio.Attributes; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.FDS; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class StableSwarm( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private Process? dotnetProcess; public override string Name => "StableSwarmUI"; public override string RepositoryName => "SwarmUI"; public override string DisplayName { get; set; } = "SwarmUI"; public override string Author => "mcmonkeyprojects"; public override string Blurb => "A Modular Stable Diffusion Web-User-Interface, with an emphasis on making powertools easily accessible, high performance, and extensibility."; public override string LicenseType => "MIT"; public override string LicenseUrl => "https://github.com/mcmonkeyprojects/SwarmUI/blob/master/LICENSE.txt"; public override string LaunchCommand => string.Empty; public override Uri PreviewImageUri => new("https://github.com/mcmonkeyprojects/SwarmUI/raw/master/.github/images/swarmui.jpg"); public override string OutputFolderName => "Output"; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None]; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; public override bool OfferInOneClickInstaller => false; public override bool UsesVenv => false; public override List GetExtraCommands() => [ new() { CommandName = "Rebuild .NET Project", Command = async installedPackage => { if (installedPackage == null || string.IsNullOrEmpty(installedPackage.FullPath)) { throw new InvalidOperationException("Package not found or not installed correctly"); } var srcFolder = Path.Combine(installedPackage.FullPath, "src"); var csprojName = "StableSwarmUI.csproj"; if (File.Exists(Path.Combine(srcFolder, "SwarmUI.csproj"))) { csprojName = "SwarmUI.csproj"; } await RebuildDotnetProject(installedPackage.FullPath, csprojName, null) .ConfigureAwait(false); }, }, ]; public override List LaunchOptions => [ new LaunchOptionDefinition { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", Options = ["--host"], }, new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7801", Options = ["--port"], }, new LaunchOptionDefinition { Name = "Ngrok Path", Type = LaunchOptionType.String, Options = ["--ngrok-path"], }, new LaunchOptionDefinition { Name = "Ngrok Basic Auth", Type = LaunchOptionType.String, Options = ["--ngrok-basic-auth"], }, new LaunchOptionDefinition { Name = "Cloudflared Path", Type = LaunchOptionType.String, Options = ["--cloudflared-path"], }, new LaunchOptionDefinition { Name = "Proxy Region", Type = LaunchOptionType.String, Options = ["--proxy-region"], }, new LaunchOptionDefinition { Name = "Launch Mode", Type = LaunchOptionType.Bool, Options = ["--launch-mode web", "--launch-mode webinstall"], }, LaunchOptionDefinition.Extras, ]; public override SharedFolderLayout SharedFolderLayout => new() { RelativeConfigPath = Path.Combine("Data/Settings.fds"), ConfigFileType = ConfigFileType.Fds, Rules = [ new SharedFolderLayoutRule { IsRoot = true, ConfigDocumentPaths = ["ModelRoot"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["Models/Stable-Diffusion"], ConfigDocumentPaths = ["SDModelFolder"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["Models/Lora"], ConfigDocumentPaths = ["SDLoraFolder"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["Models/VAE"], ConfigDocumentPaths = ["SDVAEFolder"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Embeddings], TargetRelativePaths = ["Models/Embeddings"], ConfigDocumentPaths = ["SDEmbeddingFolder"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ControlNet, SharedFolderType.T2IAdapter], TargetRelativePaths = ["Models/controlnet"], ConfigDocumentPaths = ["SDControlNetsFolder"], }, // Assuming Swarm maps T2I to ControlNet folder new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ClipVision], TargetRelativePaths = ["Models/clip_vision"], ConfigDocumentPaths = ["SDClipVisionFolder"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["Models/clip"], ConfigDocumentPaths = ["SDClipFolder"], }, ], }; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Text2Img] = [OutputFolderName] }; public override string MainBranch => "master"; public override bool ShouldIgnoreReleases => true; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl, TorchIndex.Rocm, TorchIndex.Mps]; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; public override IEnumerable Prerequisites => [ PackagePrerequisite.Git, PackagePrerequisite.Dotnet, PackagePrerequisite.Python310, PackagePrerequisite.VcRedist, ]; private FilePath GetSettingsPath(string installLocation) => Path.Combine(installLocation, "Data", "Settings.fds"); private FilePath GetBackendsPath(string installLocation) => Path.Combine(installLocation, "Data", "Backends.fds"); public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Installing SwarmUI...", isIndeterminate: true)); var comfy = settingsManager.Settings.InstalledPackages.FirstOrDefault(x => x.PackageName is nameof(ComfyUI) or "ComfyUI-Zluda" ); if (comfy == null) { throw new InvalidOperationException("ComfyUI must be installed to use SwarmUI"); } try { await prerequisiteHelper .RunDotnet( [ "nuget", "add", "source", "https://api.nuget.org/v3/index.json", "--name", "\"NuGet official package source\"", ], workingDirectory: installLocation, onProcessOutput: onConsoleOutput ) .ConfigureAwait(false); } catch (ProcessException e) { // ignore, probably means the source is already there } var srcFolder = Path.Combine(installLocation, "src"); var csprojName = "StableSwarmUI.csproj"; if (File.Exists(Path.Combine(srcFolder, "SwarmUI.csproj"))) { csprojName = "SwarmUI.csproj"; } await prerequisiteHelper .RunDotnet( ["build", $"src/{csprojName}", "--configuration", "Release", "-o", "src/bin/live_release"], workingDirectory: installLocation, onProcessOutput: onConsoleOutput ) .ConfigureAwait(false); if (!options.IsUpdate) { // set default settings var settings = new StableSwarmSettings { IsInstalled = true }; if (options.SharedFolderMethod is SharedFolderMethod.Configuration) { settings.Paths = new StableSwarmSettings.PathsData { ModelRoot = settingsManager.ModelsDirectory, SDModelFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.StableDiffusion.ToString() ), SDLoraFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.Lora.ToString() ), SDVAEFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString() ), SDEmbeddingFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.Embeddings.ToString() ), SDControlNetsFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.ControlNet.ToString() ), SDClipVisionFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.ClipVision.ToString() ), }; } settings.Save(true).SaveToFile(GetSettingsPath(installLocation)); var backendsFile = new FDSSection(); var dataSection = new FDSSection(); dataSection.Set("type", "comfyui_selfstart"); dataSection.Set("title", "StabilityMatrix ComfyUI Self-Start"); dataSection.Set("enabled", true); var launchArgs = comfy.LaunchArgs ?? []; var comfyArgs = string.Join( ' ', launchArgs .Select(arg => arg.ToArgString()?.TrimEnd()) .Where(arg => !string.IsNullOrWhiteSpace(arg)) ); if (comfy.PackageName == "ComfyUI-Zluda") { var fullComfyZludaPath = Path.Combine(SettingsManager.LibraryDir, comfy.LibraryPath); var zludaPath = Path.Combine(fullComfyZludaPath, "zluda", "zluda.exe"); var comfyVenvPath = Path.Combine( fullComfyZludaPath, "venv", Compat.Switch( (PlatformKind.Windows, Path.Combine("Scripts", "python.exe")), (PlatformKind.Unix, Path.Combine("bin", "python3")) ) ); ProcessArgs args = ["--", comfyVenvPath, "main.py", comfyArgs]; // Create a wrapper batch file that runs zluda.exe var wrapperScriptPath = Path.Combine(installLocation, "Data", "zluda_wrapper.bat"); var scriptContent = $""" @echo off "{zludaPath}" {args} """; // Ensure the Data directory exists Directory.CreateDirectory(Path.Combine(installLocation, "Data")); // Write the batch file await File.WriteAllTextAsync(wrapperScriptPath, scriptContent, cancellationToken) .ConfigureAwait(false); dataSection.Set( "settings", new ComfyUiSelfStartSettings { StartScript = wrapperScriptPath, DisableInternalArgs = false, AutoUpdate = false, UpdateManagedNodes = "true", ExtraArgs = string.Empty, // Arguments are already in the batch file }.Save(true) ); } else { dataSection.Set( "settings", new ComfyUiSelfStartSettings { StartScript = $"../{comfy.DisplayName}/main.py", DisableInternalArgs = false, AutoUpdate = false, UpdateManagedNodes = "true", ExtraArgs = comfyArgs, }.Save(true) ); } backendsFile.Set("0", dataSection); backendsFile.SaveToFile(GetBackendsPath(installLocation)); } } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var portableGitBin = new DirectoryPath(PrerequisiteHelper.GitBinPath); var dotnetDir = PrerequisiteHelper.DotnetDir; var aspEnvVars = new Dictionary { ["ASPNETCORE_ENVIRONMENT"] = "Production", ["ASPNETCORE_URLS"] = "http://*:7801", ["GIT"] = portableGitBin.JoinFile("git.exe"), ["DOTNET_ROOT"] = dotnetDir.FullPath, }; if (aspEnvVars.TryGetValue("PATH", out var pathValue)) { aspEnvVars["PATH"] = Compat.GetEnvPathWithExtensions( dotnetDir.FullPath, portableGitBin, pathValue ); } else { aspEnvVars["PATH"] = Compat.GetEnvPathWithExtensions(dotnetDir.FullPath, portableGitBin); } aspEnvVars.Update(settingsManager.Settings.EnvironmentVariables); void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Starting webserver", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; } OnStartupComplete(WebUrl); } } var sharedDiffusionModelsPath = new DirectoryPath( settingsManager.ModelsDirectory, nameof(SharedFolderType.DiffusionModels) ); var swarmDiffusionModelsPath = new DirectoryPath(settingsManager.ModelsDirectory, "diffusion_models"); try { swarmDiffusionModelsPath.Create(); await Helper .SharedFolders.CreateOrUpdateLink(sharedDiffusionModelsPath, swarmDiffusionModelsPath) .ConfigureAwait(false); } catch (Exception e) { onConsoleOutput?.Invoke( new ProcessOutput { Text = $"Failed to create symlink for {nameof(SharedFolderType.DiffusionModels)}: {e.Message}.", } ); } var launchScriptPath = Path.Combine( installLocation, Compat.IsWindows ? "launch-windows.bat" : Compat.IsMacOS ? "launch-macos.sh" : "launch-linux.sh" ); dotnetProcess = ProcessRunner.StartAnsiProcess( launchScriptPath, options.Arguments, installLocation, HandleConsoleOutput, aspEnvVars ); } public override async Task CheckForUpdates(InstalledPackage package) { var needsMigrate = false; try { var output = await prerequisiteHelper .GetGitOutput(["remote", "get-url", "origin"], package.FullPath) .ConfigureAwait(false); if ( output.StandardOutput != null && output.StandardOutput.Contains("Stability", StringComparison.OrdinalIgnoreCase) ) { needsMigrate = true; } } catch (Exception) { needsMigrate = true; } if (needsMigrate) { await prerequisiteHelper .RunGit(["remote", "set-url", "origin", GithubUrl], workingDirectory: package.FullPath) .ConfigureAwait(false); } return await base.CheckForUpdates(package).ConfigureAwait(false); } /*public override Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) => sharedFolderMethod switch { SharedFolderMethod.Symlink => base.SetupModelFolders(installDirectory, SharedFolderMethod.Symlink), SharedFolderMethod.Configuration => SetupModelFoldersConfig(installDirectory), _ => Task.CompletedTask }; public override Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) => sharedFolderMethod switch { SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), SharedFolderMethod.Configuration => RemoveModelFoldersConfig(installDirectory), _ => Task.CompletedTask };*/ public override async Task WaitForShutdown() { if (dotnetProcess is { HasExited: false }) { dotnetProcess.Kill(true); try { await dotnetProcess .WaitForExitAsync(new CancellationTokenSource(5000).Token) .ConfigureAwait(false); } catch (OperationCanceledException e) { Console.WriteLine(e); } } dotnetProcess = null; GC.SuppressFinalize(this); } public async Task RebuildDotnetProject( string installLocation, string csprojName, Action? onConsoleOutput ) { await prerequisiteHelper .RunDotnet( [ "build", $"src/{csprojName}", "--no-incremental", "--configuration", "Release", "-o", "src/bin/live_release", ], workingDirectory: installLocation, onProcessOutput: onConsoleOutput ) .ConfigureAwait(false); } private Task SetupModelFoldersConfig(DirectoryPath installDirectory) { var settingsPath = GetSettingsPath(installDirectory); var existingSettings = new StableSwarmSettings(); var settingsExists = File.Exists(settingsPath); if (settingsExists) { var section = FDSUtility.ReadFile(settingsPath); var paths = section.GetSection("Paths"); paths.Set("ModelRoot", settingsManager.ModelsDirectory); paths.Set( "SDModelFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.StableDiffusion.ToString()) ); paths.Set( "SDLoraFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Lora.ToString()) ); paths.Set( "SDVAEFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString()) ); paths.Set( "SDEmbeddingFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Embeddings.ToString()) ); paths.Set( "SDControlNetsFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.ControlNet.ToString()) ); paths.Set( "SDClipVisionFolder", Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.ClipVision.ToString()) ); section.Set("Paths", paths); section.SaveToFile(settingsPath); return Task.CompletedTask; } existingSettings.Paths = new StableSwarmSettings.PathsData { ModelRoot = settingsManager.ModelsDirectory, SDModelFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.StableDiffusion.ToString() ), SDLoraFolder = Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Lora.ToString()), SDVAEFolder = Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString()), SDEmbeddingFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.Embeddings.ToString() ), SDControlNetsFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.ControlNet.ToString() ), SDClipVisionFolder = Path.Combine( settingsManager.ModelsDirectory, SharedFolderType.ClipVision.ToString() ), }; existingSettings.Save(true).SaveToFile(settingsPath); return Task.CompletedTask; } private Task RemoveModelFoldersConfig(DirectoryPath installDirectory) { var settingsPath = GetSettingsPath(installDirectory); var existingSettings = new StableSwarmSettings(); var settingsExists = File.Exists(settingsPath); if (settingsExists) { var section = FDSUtility.ReadFile(settingsPath); existingSettings.Load(section); } existingSettings.Paths = new StableSwarmSettings.PathsData(); existingSettings.Save(true).SaveToFile(settingsPath); return Task.CompletedTask; } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/UnknownPackage.cs ================================================ using Octokit; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; public class UnknownPackage(ISettingsManager settingsManager) : BasePackage(settingsManager) { public static string Key => "unknown-package"; public override string Name => Key; public override string DisplayName { get; set; } = "Unknown Package"; public override string Author => ""; public override string GithubUrl => ""; public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/LykosAI/StabilityMatrix/blob/main/LICENSE"; public override string Blurb => "A dank interface for diffusion"; public override string LaunchCommand => "test"; public override Uri PreviewImageUri => new(""); public override IReadOnlyDictionary ExtraLaunchCommands => new Dictionary(); public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override string OutputFolderName { get; } public override PackageDifficulty InstallerSortOrder { get; } public override Task DownloadPackage( string installLocation, DownloadPackageOptions options, IProgress? progress = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } /// public override Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } public override Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } /// public override Task SetupModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } /// public override Task UpdateModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } /// public override Task RemoveModelFolderLinks( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { throw new NotImplementedException(); } public override Task SetupOutputFolderLinks(DirectoryPath installDirectory) { throw new NotImplementedException(); } public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory) { throw new NotImplementedException(); } public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cuda, TorchIndex.Cpu, TorchIndex.Rocm, TorchIndex.DirectMl }; /// public override void Shutdown() { throw new NotImplementedException(); } /// public override Task WaitForShutdown() { throw new NotImplementedException(); } /// public override Task CheckForUpdates(InstalledPackage package) { throw new NotImplementedException(); } /// public override Task Update( string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { throw new NotImplementedException(); } /// public override Task> GetReleaseTags() => Task.FromResult(Enumerable.Empty()); public override List LaunchOptions => new(); public override Dictionary>? SharedFolders { get; } public override Dictionary>? SharedOutputFolders { get; } public override Task GetLatestVersion(bool includePrerelease = false) { throw new NotImplementedException(); } public override string MainBranch { get; } public override Task GetUpdate(InstalledPackage installedPackage) { return Task.FromResult(new DownloadPackageVersionOptions { IsLatest = true, VersionTag = "1.8.0" }); } public override Task GetAllVersionOptions() => Task.FromResult(new PackageVersionOptions()); /// public override Task?> GetAllCommits( string branch, int page = 1, int perPage = 10 ) => Task.FromResult?>(null); } ================================================ FILE: StabilityMatrix.Core/Models/Packages/VladAutomatic.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Config; using StabilityMatrix.Core.Models.Packages.Extensions; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class VladAutomatic( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "automatic"; public override string DisplayName { get; set; } = "SD.Next"; public override string Author => "vladmandic"; public override string LicenseType => "Apache License 2.0"; public override string LicenseUrl => "https://github.com/vladmandic/sdnext/blob/master/LICENSE.txt"; public override string Blurb => "SD.Next: All-in-one WebUI for AI generative image and video creation"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/vladautomatic/preview.webp"); public override bool ShouldIgnoreReleases => true; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override PyVersion RecommendedPythonVersion => Python.PyInstallationManager.Python_3_12_10; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl, TorchIndex.Ipex, TorchIndex.Rocm, TorchIndex.Zluda, }; // https://github.com/vladmandic/automatic/blob/master/modules/shared.py#L324 public override SharedFolderLayout SharedFolderLayout => new() { RelativeConfigPath = "config.json", ConfigFileType = ConfigFileType.Json, Rules = [ new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["models/Stable-diffusion"], ConfigDocumentPaths = ["ckpt_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Diffusers], TargetRelativePaths = ["models/Diffusers"], ConfigDocumentPaths = ["diffusers_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/VAE"], ConfigDocumentPaths = ["vae_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Embeddings], TargetRelativePaths = ["models/embeddings"], ConfigDocumentPaths = ["embeddings_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Hypernetwork], TargetRelativePaths = ["models/hypernetworks"], ConfigDocumentPaths = ["hypernetwork_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Codeformer], TargetRelativePaths = ["models/Codeformer"], ConfigDocumentPaths = ["codeformer_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.GFPGAN], TargetRelativePaths = ["models/GFPGAN"], ConfigDocumentPaths = ["gfpgan_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.BSRGAN], TargetRelativePaths = ["models/BSRGAN"], ConfigDocumentPaths = ["bsrgan_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ESRGAN], TargetRelativePaths = ["models/ESRGAN"], ConfigDocumentPaths = ["esrgan_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.RealESRGAN], TargetRelativePaths = ["models/RealESRGAN"], ConfigDocumentPaths = ["realesrgan_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ScuNET], TargetRelativePaths = ["models/ScuNET"], ConfigDocumentPaths = ["scunet_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.SwinIR], TargetRelativePaths = ["models/SwinIR"], ConfigDocumentPaths = ["swinir_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.LDSR], TargetRelativePaths = ["models/LDSR"], ConfigDocumentPaths = ["ldsr_models_path"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], TargetRelativePaths = ["models/CLIP"], ConfigDocumentPaths = ["clip_models_path"], }, // CLIP new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora], TargetRelativePaths = ["models/Lora"], ConfigDocumentPaths = ["lora_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.LyCORIS], TargetRelativePaths = ["models/LyCORIS"], ConfigDocumentPaths = ["lyco_dir"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.ControlNet, SharedFolderType.T2IAdapter], TargetRelativePaths = ["models/ControlNet"], ConfigDocumentPaths = ["control_net_models_path"], }, // Combined ControlNet/T2I new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.DiffusionModels], TargetRelativePaths = ["models/UNET"], ConfigDocumentPaths = ["unet_dir"], }, ], }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "outputs/text" }, [SharedOutputType.Img2Img] = new[] { "outputs/image" }, [SharedOutputType.Extras] = new[] { "outputs/extras" }, [SharedOutputType.Img2ImgGrids] = new[] { "outputs/grids" }, [SharedOutputType.Text2ImgGrids] = new[] { "outputs/grids" }, [SharedOutputType.Saved] = new[] { "outputs/save" }, }; public override string OutputFolderName => "outputs"; public override IPackageExtensionManager ExtensionManager => new VladExtensionManager(this); [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--port"], }, new() { Name = "Use uv", Type = LaunchOptionType.Bool, InitialValue = true, Options = ["--uv"], }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { MemoryLevel.Low => "--lowvram", MemoryLevel.Medium => "--medvram", _ => null, }, Options = ["--lowvram", "--medvram"], }, new() { Name = "Auto-Launch Web UI", Type = LaunchOptionType.Bool, Options = ["--autolaunch"], }, new() { Name = "Use DirectML if no compatible GPU is detected", Type = LaunchOptionType.Bool, Options = ["--use-directml"], }, new() { Name = "Force use of Nvidia CUDA backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), Options = ["--use-cuda"], }, new() { Name = "Force use of Intel OneAPI XPU backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasIntelGpu(), Options = ["--use-ipex"], }, new() { Name = "Force use of AMD ROCm backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferRocm(), Options = ["--use-rocm"], }, new() { Name = "Force use of ZLUDA backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferDirectMLOrZluda(), Options = ["--use-zluda"], }, new() { Name = "CUDA Device ID", Type = LaunchOptionType.String, Options = ["--device-id"], }, new() { Name = "API", Type = LaunchOptionType.Bool, Options = ["--api"], }, new() { Name = "Debug Logging", Type = LaunchOptionType.Bool, Options = ["--debug"], }, LaunchOptionDefinition.Extras, ]; public override string MainBranch => "master"; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true)); // Setup venv await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); await venvRunner.PipInstall(["setuptools", "rich", "uv"]).ConfigureAwait(false); if (options.PythonOptions.PythonVersion is { Minor: < 12 }) { venvRunner.UpdateEnvironmentVariables(env => env.SetItem("SETUPTOOLS_USE_DISTUTILS", "setuptools") ); } if (installedPackage.PipOverrides is { Count: > 0 }) { var pipArgs = new PipInstallArgs().WithUserOverrides(installedPackage.PipOverrides); await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); switch (torchVersion) { // Run initial install case TorchIndex.Cuda: await venvRunner .CustomInstall("launch.py --use-cuda --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; case TorchIndex.Rocm: await venvRunner .CustomInstall("launch.py --use-rocm --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; case TorchIndex.DirectMl: await venvRunner .CustomInstall("launch.py --use-directml --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; case TorchIndex.Zluda: await venvRunner .CustomInstall("launch.py --use-zluda --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; case TorchIndex.Ipex: await venvRunner .CustomInstall("launch.py --use-ipex --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; default: // CPU await venvRunner .CustomInstall("launch.py --debug --test --uv", onConsoleOutput) .ConfigureAwait(false); break; } progress?.Report(new ProgressReport(1f, isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); if (PyVersion.Parse(installedPackage.PythonVersion) is { Minor: < 12 }) { VenvRunner.UpdateEnvironmentVariables(env => env.SetItem("SETUPTOOLS_USE_DISTUTILS", "setuptools") ); } void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("Local URL", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; OnStartupComplete(WebUrl); } } } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } public override async Task Update( string installLocation, InstalledPackage installedPackage, UpdatePackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var baseUpdateResult = await base.Update( installLocation, installedPackage, options, progress, onConsoleOutput, cancellationToken ) .ConfigureAwait(false); await using var venvRunner = await SetupVenvPure( installedPackage.FullPath!.Unwrap(), pythonVersion: PyVersion.Parse(installedPackage.PythonVersion) ) .ConfigureAwait(false); await venvRunner.CustomInstall("launch.py --upgrade --test", onConsoleOutput).ConfigureAwait(false); try { var result = await PrerequisiteHelper .GetGitOutput(["rev-parse", "HEAD"], installedPackage.FullPath) .EnsureSuccessExitCode() .ConfigureAwait(false); return new InstalledPackageVersion { InstalledBranch = options.VersionOptions.BranchName, InstalledCommitSha = result .StandardOutput?.Replace(Environment.NewLine, "") .Replace("\n", ""), IsPrerelease = false, }; } catch (Exception e) { Logger.Warn(e, "Could not get current git hash, continuing with update"); } finally { progress?.Report( new ProgressReport( 1f, message: "Update Complete", isIndeterminate: false, type: ProgressType.Update ) ); } return baseUpdateResult; } private class VladExtensionManager(VladAutomatic package) : GitPackageExtensionManager(package.PrerequisiteHelper) { public override string RelativeInstallDirectory => "extensions"; public override IEnumerable DefaultManifests => [new ExtensionManifest(new Uri("https://vladmandic.github.io/sd-data/pages/extensions.json"))]; public override async Task> GetManifestExtensionsAsync( ExtensionManifest manifest, CancellationToken cancellationToken = default ) { try { // Get json var content = await package .DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken) .ConfigureAwait(false); // Parse json var jsonManifest = JsonSerializer.Deserialize>( content, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } ); return jsonManifest?.Select(entry => new PackageExtension { Title = entry.Name, Author = entry.Long?.Split('/').FirstOrDefault() ?? "Unknown", Reference = entry.Url, Files = [entry.Url], Description = entry.Description, InstallType = "git-clone", }) ?? Enumerable.Empty(); } catch (Exception e) { Logger.Error(e, "Failed to get extensions from manifest"); return Enumerable.Empty(); } } } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/VoltaML.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using Python.Runtime; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class VoltaML( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { 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 Blurb => "Fast Stable Diffusion with support for AITemplate"; public override string LaunchCommand => "main.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/voltaml/preview.webp"); public override string Disclaimer => "This package may no longer receive updates from its author."; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Impossible; public override bool OfferInOneClickInstaller => false; public override PackageType PackageType => PackageType.Legacy; // There are releases but the manager just downloads the latest commit anyways, // so we'll just limit to commit mode to be more consistent public override bool ShouldIgnoreReleases => true; // https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L86 public override Dictionary> SharedFolders => new() { [SharedFolderType.StableDiffusion] = new[] { "data/models" }, [SharedFolderType.Lora] = new[] { "data/lora" }, [SharedFolderType.Embeddings] = new[] { "data/textual-inversion" }, }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "data/outputs/txt2img" }, [SharedOutputType.Extras] = new[] { "data/outputs/extra" }, [SharedOutputType.Img2Img] = new[] { "data/outputs/img2img" }, }; public override string OutputFolderName => "data/outputs"; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override IEnumerable AvailableTorchIndices => new[] { TorchIndex.Cpu, TorchIndex.Cuda, TorchIndex.DirectMl }; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; // https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L45 public override List LaunchOptions => new List { new() { Name = "Log Level", Type = LaunchOptionType.Bool, DefaultValue = "--log-level INFO", Options = { "--log-level DEBUG", "--log-level INFO", "--log-level WARNING", "--log-level ERROR", "--log-level CRITICAL", }, }, new() { Name = "Use ngrok to expose the API", Type = LaunchOptionType.Bool, Options = { "--ngrok" }, }, new() { Name = "Expose the API to the network", Type = LaunchOptionType.Bool, Options = { "--host" }, }, new() { Name = "Skip virtualenv check", Type = LaunchOptionType.Bool, InitialValue = true, Options = { "--in-container" }, }, new() { Name = "Force VoltaML to use a specific type of PyTorch distribution", Type = LaunchOptionType.Bool, Options = { "--pytorch-type cpu", "--pytorch-type cuda", "--pytorch-type rocm", "--pytorch-type directml", "--pytorch-type intel", "--pytorch-type vulkan", }, }, new() { Name = "Run in tandem with the Discord bot", Type = LaunchOptionType.Bool, Options = { "--bot" }, }, new() { Name = "Enable Cloudflare R2 bucket upload support", Type = LaunchOptionType.Bool, Options = { "--enable-r2" }, }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "5003", Options = { "--port" }, }, new() { Name = "Only install requirements and exit", Type = LaunchOptionType.Bool, Options = { "--install-only" }, }, LaunchOptionDefinition.Extras, }; public override string MainBranch => "main"; public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { // Setup venv progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); // Install requirements progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)); var pipArgs = new PipInstallArgs(["rich", "packaging", "python-dotenv"]); if (installedPackage.PipOverrides != null) { pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); } await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); var foundIndicator = false; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (s.Text.Contains("running on", StringComparison.OrdinalIgnoreCase)) { // Next line will have the Web UI URL, so set a flag & wait for that foundIndicator = true; return; } if (!foundIndicator) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (!match.Success) return; WebUrl = match.Value; OnStartupComplete(WebUrl); foundIndicator = false; } VenvRunner.RunDetached( [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); } } ================================================ FILE: StabilityMatrix.Core/Models/Packages/Wan2GP.cs ================================================ using System.Collections.Immutable; using System.Text.RegularExpressions; using Injectio.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; /// /// Package for Wan2GP - Super Optimized Gradio UI for AI video creation. /// Supports Wan 2.1/2.2, Qwen, Hunyuan Video, LTX Video and Flux. /// https://github.com/deepbeepmeep/Wan2GP /// /// /// Model Sharing: This package does not support Stability Matrix shared folder configuration. /// Wan2GP manages model paths through its own wgp_config.json file, which is created and managed /// by the Gradio UI on first launch. Users should configure model paths via the Settings tab /// in the Wan2GP UI. /// [RegisterSingleton(Duplicate = DuplicateStrategy.Append)] public class Wan2GP( IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper, IPyInstallationManager pyInstallationManager, IPipWheelService pipWheelService ) : BaseGitPackage( githubApi, settingsManager, downloadService, prerequisiteHelper, pyInstallationManager, pipWheelService ) { public override string Name => "Wan2GP"; public override string DisplayName { get; set; } = "Wan2GP"; public override string Author => "deepbeepmeep"; public override string Blurb => "Super Optimized Gradio UI for AI video creation for GPU poor machines (6GB+ VRAM). " + "Supports Wan 2.1/2.2, Qwen, Hunyuan Video, LTX Video and Flux."; public override string LicenseType => "Apache-2.0"; public override string LicenseUrl => "https://github.com/deepbeepmeep/Wan2GP/blob/main/LICENSE.txt"; public override string LaunchCommand => "wgp.py"; public override Uri PreviewImageUri => new("https://cdn.lykos.ai/sm/packages/wan2gp/wan2gp.webp"); public override string OutputFolderName => "outputs"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None; public override IEnumerable AvailableSharedFolderMethods => [SharedFolderMethod.None]; public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda, TorchIndex.Rocm]; public override bool IsCompatible => HardwareHelper.HasNvidiaGpu() || (Compat.IsWindows ? HardwareHelper.HasWindowsRocmSupportedGpu() : HardwareHelper.HasAmdGpu()); public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; public override Dictionary> SharedOutputFolders => new() { [SharedOutputType.Img2Vid] = ["outputs"] }; // AMD ROCm requires Python 3.11, NVIDIA uses 3.10 public override PyVersion RecommendedPythonVersion => IsAmdRocm ? Python.PyInstallationManager.Python_3_11_13 : Python.PyInstallationManager.Python_3_10_17; public override string Disclaimer => IsAmdRocm && Compat.IsWindows ? "AMD GPU support on Windows requires RX 7000 series or newer GPU" : string.Empty; /// /// Helper property to check if we're using AMD ROCm /// private bool IsAmdRocm => GetRecommendedTorchVersion() == TorchIndex.Rocm; /// /// Python wrapper script that patches logging to also print to stdout/stderr, so /// StabilityMatrix can capture the output. Wan2GP logs through Gradio UI notifications /// (gr.Info/Warning/Error) and callback-driven UI updates that never reach the console. /// This script: /// 1. Configures Python's logging module to output to stderr (captures library logging) /// 2. Prevents transformers from suppressing its own logging (wgp.py calls set_verbosity_error) /// 3. Monkey-patches gr.Info/Warning/Error to also print to stdout/stderr /// 4. Runs the target script (wgp.py) via runpy /// private const string GradioLogPatchScript = """ # StabilityMatrix: Patch logging to print to console for capture. import sys import logging def _apply_logging_patch(): # Configure Python's root logger to output to stderr at INFO level. # Many libraries (torch, diffusers, transformers, etc.) use the logging # module but output may be suppressed without a handler configured. root = logging.getLogger() if not any(isinstance(h, logging.StreamHandler) for h in root.handlers): handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("[%(name)s] %(levelname)s: %(message)s")) root.addHandler(handler) if root.level > logging.INFO: root.setLevel(logging.INFO) # Prevent transformers from suppressing its own logging. # wgp.py calls transformers.utils.logging.set_verbosity_error() which # silences all non-error messages. We neutralize those calls so model # loading and download messages remain visible. try: import transformers.utils.logging as tf_logging tf_logging.set_verbosity_error = lambda: None tf_logging.set_verbosity_warning = lambda: None tf_logging.set_verbosity(logging.INFO) except Exception as e: print(f"[StabilityMatrix] Failed to patch transformers logging: {e}", file=sys.stderr, flush=True) # Monkey-patch Gradio's UI notification functions to also print to console. # These only fire for validation/error messages, not generation progress. try: import gradio as gr _orig_info = getattr(gr, 'Info', None) _orig_warning = getattr(gr, 'Warning', None) _orig_error = getattr(gr, 'Error', None) if _orig_info is not None: def patched_info(message, *args, **kwargs): print(f"[Gradio] {message}", flush=True) return _orig_info(message, *args, **kwargs) gr.Info = patched_info if _orig_warning is not None: def patched_warning(message, *args, **kwargs): print(f"[Gradio] WARNING: {message}", flush=True) return _orig_warning(message, *args, **kwargs) gr.Warning = patched_warning if _orig_error is not None: def patched_error(message, *args, **kwargs): print(f"[Gradio] ERROR: {message}", file=sys.stderr, flush=True) return _orig_error(message, *args, **kwargs) gr.Error = patched_error except Exception as e: print(f"[StabilityMatrix] Failed to patch Gradio logging: {e}", file=sys.stderr, flush=True) if __name__ == "__main__": _apply_logging_patch() target_script = sys.argv[1] sys.argv = sys.argv[1:] import runpy runpy.run_path(target_script, run_name="__main__") """; public override List LaunchOptions => [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", Options = ["--server-name"], }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", Options = ["--server-port"], }, new() { Name = "Share", Type = LaunchOptionType.Bool, Description = "Set whether to share on Gradio", Options = ["--share"], }, new() { Name = "Listen", Type = LaunchOptionType.Bool, Description = "Make server accessible on network", Options = ["--listen"], }, new() { Name = "Multiple Images", Type = LaunchOptionType.Bool, Description = "Enable multiple images mode", InitialValue = true, Options = ["--multiple-images"], }, new() { Name = "Compile", Type = LaunchOptionType.Bool, Description = "Enable model compilation for faster inference (may not work on all systems)", Options = ["--compile"], }, LaunchOptionDefinition.Extras, ]; public override TorchIndex GetRecommendedTorchVersion() { // Check for AMD ROCm support (Windows or Linux) var preferRocm = ( Compat.IsWindows && ( SettingsManager.Settings.PreferredGpu?.IsWindowsRocmSupportedGpu() ?? HardwareHelper.HasWindowsRocmSupportedGpu() ) ) || ( Compat.IsLinux && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferRocm()) ); if (preferRocm) { return TorchIndex.Rocm; } // NVIDIA CUDA if (SettingsManager.Settings.PreferredGpu?.IsNvidia ?? HardwareHelper.HasNvidiaGpu()) { return TorchIndex.Cuda; } return base.GetRecommendedTorchVersion(); } public override async Task InstallPackage( string installLocation, InstalledPackage installedPackage, InstallPackageOptions options, IProgress? progress = null, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); await using var venvRunner = await SetupVenvPure( installLocation, pythonVersion: options.PythonOptions.PythonVersion ) .ConfigureAwait(false); if (torchIndex == TorchIndex.Rocm) { await InstallAmdRocmAsync(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); } else { await InstallNvidiaAsync(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); } progress?.Report(new ProgressReport(1, "Install complete", isIndeterminate: false)); } private async Task InstallNvidiaAsync( IPyVenvRunner venvRunner, IProgress? progress, Action? onConsoleOutput ) { var isLegacyNvidia = SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); var isNewerNvidia = SettingsManager.Settings.PreferredGpu?.IsAmpereOrNewerGpu() ?? HardwareHelper.HasAmpereOrNewerGpu(); // Platform-specific versions from pinokio torch.js // Windows: torch 2.7.1, Linux: torch 2.7.0 (to match prebuilt attention wheel requirements) var torchVersion = Compat.IsWindows ? "2.7.1" : "2.7.0"; var torchvisionVersion = Compat.IsWindows ? "0.22.1" : "0.22.0"; var torchaudioVersion = Compat.IsWindows ? "2.7.1" : "2.7.0"; var cudaIndex = isLegacyNvidia ? "cu126" : "cu128"; progress?.Report(new ProgressReport(-1f, "Upgrading pip...", isIndeterminate: true)); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); // Install requirements directly using -r flag (handles @ URL syntax properly) progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); await venvRunner.PipInstall("-r requirements.txt", onConsoleOutput).ConfigureAwait(false); // Install torch with specific versions and CUDA index (force reinstall to ensure correct version) progress?.Report(new ProgressReport(-1f, "Installing PyTorch...", isIndeterminate: true)); var torchArgs = new PipInstallArgs() .WithTorch($"=={torchVersion}") .WithTorchVision($"=={torchvisionVersion}") .WithTorchAudio($"=={torchaudioVersion}") .WithXFormers("==0.0.30") .WithTorchExtraIndex(cudaIndex) .AddArg("--force-reinstall") .AddArg("--no-deps"); await venvRunner.PipInstall(torchArgs, onConsoleOutput).ConfigureAwait(false); // Install hf-xet and pin setuptools to avoid distutils compatibility issues with Python 3.10 await venvRunner.PipInstall("hf-xet \"setuptools<70.0.0\"", onConsoleOutput).ConfigureAwait(false); if (!isNewerNvidia) return; // Install triton n stuff for newer NVIDIA GPUs if (Compat.IsWindows) { progress?.Report(new ProgressReport(-1f, "Installing triton-windows...", isIndeterminate: true)); await venvRunner .PipInstall("triton-windows==3.3.1.post19", onConsoleOutput) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing SageAttention...", isIndeterminate: true)); await venvRunner .PipInstall( "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows/sageattention-2.2.0+cu128torch2.7.1-cp310-cp310-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing Flash Attention...", isIndeterminate: true)); await venvRunner .PipInstall( "https://huggingface.co/lldacing/flash-attention-windows-wheel/resolve/main/flash_attn-2.7.4.post1%2Bcu128torch2.7.0cxx11abiFALSE-cp310-cp310-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); } else if (Compat.IsLinux) { progress?.Report(new ProgressReport(-1f, "Installing SageAttention...", isIndeterminate: true)); await venvRunner .PipInstall( "https://huggingface.co/MonsterMMORPG/SECourses_Premium_Flash_Attention/resolve/main/sageattention-2.1.1-cp310-cp310-linux_x86_64.whl", onConsoleOutput ) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing Flash Attention...", isIndeterminate: true)); await venvRunner .PipInstall( "https://huggingface.co/cocktailpeanut/wheels/resolve/main/flash_attn-2.8.3%2Bcu128torch2.7-cp310-cp310-linux_x86_64.whl", onConsoleOutput ) .ConfigureAwait(false); await venvRunner.PipInstall("numpy==2.1.2", onConsoleOutput).ConfigureAwait(false); } } private async Task InstallAmdRocmAsync( IPyVenvRunner venvRunner, IProgress? progress, Action? onConsoleOutput ) { progress?.Report(new ProgressReport(-1f, "Upgrading pip...", isIndeterminate: true)); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); if (Compat.IsWindows) { // Windows AMD ROCm - special TheRock wheels progress?.Report( new ProgressReport(-1f, "Installing PyTorch ROCm wheels...", isIndeterminate: true) ); // Set environment variable for wheel filename check bypass venvRunner.UpdateEnvironmentVariables(env => env.SetItem("UV_SKIP_WHEEL_FILENAME_CHECK", "1")); // Install PyTorch ROCm wheels from TheRock releases (Python 3.11) await venvRunner .PipInstall( "https://github.com/scottt/rocm-TheRock/releases/download/v6.5.0rc-pytorch-gfx110x/torch-2.7.0a0+rocm_git3f903c3-cp311-cp311-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); await venvRunner .PipInstall( "https://github.com/scottt/rocm-TheRock/releases/download/v6.5.0rc-pytorch-gfx110x/torchaudio-2.7.0a0+52638ef-cp311-cp311-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); await venvRunner .PipInstall( "https://github.com/scottt/rocm-TheRock/releases/download/v6.5.0rc-pytorch-gfx110x/torchvision-0.22.0+9eb57cd-cp311-cp311-win_amd64.whl", onConsoleOutput ) .ConfigureAwait(false); // Install requirements directly using -r flag (handles @ URL syntax properly) progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); await venvRunner.PipInstall("-r requirements.txt", onConsoleOutput).ConfigureAwait(false); } else { // Linux AMD ROCm - standard PyTorch ROCm // Install requirements directly using -r flag (handles @ URL syntax properly) progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); await venvRunner.PipInstall("-r requirements.txt", onConsoleOutput).ConfigureAwait(false); // Install torch with ROCm index (force reinstall to ensure correct version) progress?.Report(new ProgressReport(-1f, "Installing PyTorch ROCm...", isIndeterminate: true)); var torchArgs = new PipInstallArgs() .WithTorch("==2.7.0") .WithTorchVision("==0.22.0") .WithTorchAudio("==2.7.0") .WithTorchExtraIndex("rocm6.3") .AddArg("--force-reinstall") .AddArg("--no-deps"); await venvRunner.PipInstall(torchArgs, onConsoleOutput).ConfigureAwait(false); } // Install additional packages await venvRunner.PipInstall("hf-xet setuptools numpy==1.26.4", onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( string installLocation, InstalledPackage installedPackage, RunPackageOptions options, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage.PythonVersion)) .ConfigureAwait(false); // Fix for distutils compatibility issue with Python 3.10 and setuptools VenvRunner.UpdateEnvironmentVariables(env => env.SetItem("SETUPTOOLS_USE_DISTUTILS", "stdlib")); // Write the Gradio logging patch wrapper script so gr.Info/Warning/Error // messages are also printed to stdout/stderr for console capture var patchScriptPath = Path.Combine(installLocation, "_sm_gradio_log_patch.py"); await File.WriteAllTextAsync(patchScriptPath, GradioLogPatchScript, cancellationToken) .ConfigureAwait(false); var targetScript = Path.Combine(installLocation, options.Command ?? LaunchCommand); // Notify user that the package is starting (loading can take a while) onConsoleOutput?.Invoke( new ProcessOutput { Text = "Launching Wan2GP, please wait while the UI initializes...\n" } ); // Launch via the patch wrapper, which monkey-patches Gradio then runs wgp.py VenvRunner.RunDetached( [patchScriptPath, targetScript, .. options.Arguments], HandleConsoleOutput, OnExit ); return; void HandleConsoleOutput(ProcessOutput s) { onConsoleOutput?.Invoke(s); if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase)) return; var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); var match = regex.Match(s.Text); if (match.Success) { WebUrl = match.Value; OnStartupComplete(WebUrl); } } } } ================================================ FILE: StabilityMatrix.Core/Models/Progress/ProgressItem.cs ================================================ namespace StabilityMatrix.Core.Models.Progress; public record ProgressItem( Guid ProgressId, string Name, ProgressReport Progress, bool Failed = false ); ================================================ FILE: StabilityMatrix.Core/Models/Progress/ProgressReport.cs ================================================ using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Models.Progress; public readonly record struct ProgressReport { /// /// Progress value as percentage between 0 and 1. /// public double? Progress { get; init; } = 0; /// /// Current progress count. /// public ulong? Current { get; init; } = 0; /// /// Total progress count. /// public ulong? Total { get; init; } = 0; public string? Title { get; init; } public string? Message { get; init; } public ProcessOutput? ProcessOutput { get; init; } public bool IsIndeterminate { get; init; } = false; public float Percentage => (float)Math.Ceiling(Math.Clamp(Progress ?? 0, 0, 1) * 100); public ProgressType Type { get; init; } = ProgressType.Generic; public bool PrintToConsole { get; init; } = true; public double SpeedInMBps { get; init; } = 0f; public static ProgressReport ForProcessOutput(ProcessOutput output) => new(-1f, isIndeterminate: true) { ProcessOutput = output }; public ProgressReport( double progress, string? title = null, string? message = null, bool isIndeterminate = false, bool printToConsole = true, double speedInMBps = 0, ProgressType type = ProgressType.Generic ) { Progress = progress; Title = title; Message = message; IsIndeterminate = isIndeterminate; Type = type; PrintToConsole = printToConsole; SpeedInMBps = speedInMBps; } public ProgressReport( ulong current, ulong total, string? title = null, string? message = null, bool isIndeterminate = false, bool printToConsole = true, double speedInMBps = 0, ProgressType type = ProgressType.Generic ) { Current = current; Total = total; Progress = (double)current / total; Title = title; Message = message; IsIndeterminate = isIndeterminate; Type = type; PrintToConsole = printToConsole; SpeedInMBps = speedInMBps; } public ProgressReport( int current, int total, string? title = null, string? message = null, bool isIndeterminate = false, bool printToConsole = true, double speedInMBps = 0, ProgressType type = ProgressType.Generic ) { if (current < 0) throw new ArgumentOutOfRangeException(nameof(current), "Current progress cannot negative."); if (total < 0) throw new ArgumentOutOfRangeException(nameof(total), "Total progress cannot be negative."); Current = (ulong)current; Total = (ulong)total; Progress = (double)current / total; Title = title; Message = message; IsIndeterminate = isIndeterminate; Type = type; PrintToConsole = printToConsole; SpeedInMBps = speedInMBps; } public ProgressReport( ulong current, string? title = null, string? message = null, ProgressType type = ProgressType.Generic ) { Current = current; Title = title; Message = message; IsIndeterminate = true; Type = type; } // Implicit conversion from action } ================================================ FILE: StabilityMatrix.Core/Models/Progress/ProgressState.cs ================================================ namespace StabilityMatrix.Core.Models.Progress; public enum ProgressState { Inactive, Paused, Pending, Working, Success, Failed, Cancelled } ================================================ FILE: StabilityMatrix.Core/Models/Progress/ProgressType.cs ================================================ namespace StabilityMatrix.Core.Models.Progress; public enum ProgressType { Generic, Download, Extract, Update, Hashing, } ================================================ FILE: StabilityMatrix.Core/Models/PromptSyntax/PromptNode.cs ================================================ using System.Diagnostics; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.PromptSyntax; /// /// Interface for nodes that can have children. /// public interface IHasChildren { IEnumerable Children { get; } } [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] public abstract class PromptNode { public PromptNode? Parent { get; set; } public int StartIndex { get; set; } public int EndIndex { get => StartIndex + Length; set => Length = value - StartIndex; } public int Length { get; set; } public required TextSpan Span { get => new(StartIndex, Length); set => (StartIndex, Length) = (value.Start, value.Length); } /// /// Gets a list of ancestor nodes /// public IEnumerable Ancestors() { return Parent?.AncestorsAndSelf() ?? []; } /// /// Gets a list of ancestor nodes (including this node) /// public IEnumerable AncestorsAndSelf() { for (var node = this; node != null; node = Parent) { yield return node; } } /// /// Gets a list of descendant nodes. /// /// Determines if the search descends into a node's children. public IEnumerable DescendantNodes(Func? descendIntoChildren = null) { if (this is not IHasChildren hasChildren) yield break; foreach (var child in hasChildren.Children) { yield return child; if (descendIntoChildren == null || descendIntoChildren(child)) { foreach (var descendant in child.DescendantNodes(descendIntoChildren)) { yield return descendant; } } } } /// /// Gets a list of descendant nodes. /// /// The span the node's full span must intersect. /// Determines if the search descends into a node's children. public IEnumerable DescendantNodes( TextSpan span, Func? descendIntoChildren = null ) { if (this is not IHasChildren hasChildren) yield break; foreach (var child in hasChildren.Children) { // Stop if exceeded if (child.StartIndex > span.End) break; // Check span if (!child.Span.IntersectsWith(span)) continue; yield return child; // Check if we should descend into children if (descendIntoChildren != null && !descendIntoChildren(child)) continue; foreach (var descendant in child.DescendantNodes(span, descendIntoChildren)) { yield return descendant; } } } /// /// Finds the descendant node with the smallest span that completely contains the provided target span. /// Returns null if this node does not contain the target span. /// public PromptNode FindSmallestContainingDescendant(TextSpan span) { // Ensure the current node contains the target span if (!Span.Contains(span)) { throw new ArgumentOutOfRangeException( nameof(span), $"Node span {Span} does not contain the target span {span}" ); } var bestMatch = this; // Iterate through all descendant nodes foreach (var descendant in DescendantNodes()) { // Check if descendant fully contains the target span if (descendant.StartIndex <= span.Start && descendant.EndIndex >= span.End) { // Select this descendant if its span is smaller than the current best match if (descendant.Length < bestMatch.Length) { bestMatch = descendant; } } } return bestMatch; } public override string ToString() { return $"{GetType().Name} {Span}"; } private string GetDebuggerDisplay() { return ToString(); } } public class DocumentNode : PromptNode, IHasChildren { public List Content { get; init; } = []; IEnumerable IHasChildren.Children => Content; } public class IdentifierNode : PromptNode { public required string Name { get; set; } } public class LiteralNode : PromptNode { public required string Raw { get; init; } public required T Value { get; init; } } public class TextNode : PromptNode { public required string Text { get; set; } } public class SeperatorNode : TextNode; public class NumberNode : LiteralNode; public class ParenthesizedNode : PromptNode, IHasChildren { public List Content { get; } = []; public NumberNode? Weight { get; set; } IEnumerable IHasChildren.Children => Content.AppendIfNotNull(Weight); } public class ArrayNode : PromptNode, IHasChildren { public List Elements { get; } = []; IEnumerable IHasChildren.Children => Elements; } public class NetworkNode : PromptNode, IHasChildren { public required IdentifierNode NetworkType { get; init; } public required TextNode ModelName { get; init; } public NumberNode? ModelWeight { get; set; } public NumberNode? ClipWeight { get; set; } IEnumerable IHasChildren.Children => new List { NetworkType, ModelName } .AppendIfNotNull(ModelWeight) .AppendIfNotNull(ClipWeight); } public class WildcardNode : PromptNode, IHasChildren { public List Options { get; } = []; IEnumerable IHasChildren.Children => Options; } public class CommentNode : PromptNode { public string? Text { get; set; } } public class KeywordNode : PromptNode // AND, BREAK { public required string Keyword { get; set; } } // Add other node types as needed (e.g., SeparatorNode, EscapeNode, etc.) ================================================ FILE: StabilityMatrix.Core/Models/PromptSyntax/PromptSyntaxBuilder.cs ================================================ using System.Globalization; using TextMateSharp.Grammars; namespace StabilityMatrix.Core.Models.PromptSyntax; public class PromptSyntaxBuilder(ITokenizeLineResult tokenizeResult, string sourceText) { private int currentTokenIndex; public PromptSyntaxTree BuildAST() { var nodes = new List(); while (MoreTokens()) { nodes.Add(ParseNode()); } // Set parents foreach (var node in nodes) { SetParents(node); } var startIndex = nodes.FirstOrDefault()?.Span.Start ?? 0; // Ensure we don't exceed source text var endIndex = Math.Min(nodes.LastOrDefault()?.Span.End ?? sourceText.Length, sourceText.Length); var rootNode = new DocumentNode { Span = new TextSpan(startIndex, endIndex - startIndex), Content = nodes, }; return new PromptSyntaxTree(sourceText, rootNode, tokenizeResult.Tokens.ToList()); } private static void SetParents(PromptNode node) { if (node is not IHasChildren hasChildren) return; foreach (var child in hasChildren.Children) { child.Parent = node; SetParents(child); } } private string GetTextSubstring(IToken token) { // IMPORTANT TextMate notes: // 1. IToken.EndIndex is exclusive // 2. Last token may exceed the length of the string, (length 10 string has EndIndex = 11) var length = token.EndIndex > sourceText.Length ? sourceText.Length - token.StartIndex : token.EndIndex - token.StartIndex; return sourceText.Substring(token.StartIndex, length); } private PromptNode ParseNode() { var token = PeekToken(); // Look at the next token without consuming it. if (token is null) { throw new InvalidOperationException("Unexpected end of input."); } if (token.Scopes.Contains("comment.line.number-sign.prompt")) { return ParseComment(); } else if ( token.Scopes.Contains("meta.structure.wildcard.prompt") && token.Scopes.Contains("punctuation.definition.wildcard.begin.prompt") ) { return ParseWildcard(); } else if ( token.Scopes.Contains("meta.structure.array.prompt") && token.Scopes.Contains("punctuation.definition.array.begin.prompt") ) { return ParseParenthesized(); } else if ( token.Scopes.Contains("meta.structure.array.prompt") && token.Scopes.Contains("punctuation.definition.array.begin.prompt") ) { return ParseArray(); } else if ( token.Scopes.Contains("meta.structure.network.prompt") && token.Scopes.Contains("punctuation.definition.network.begin.prompt") ) { return ParseNetwork(); } else if (token.Scopes.Contains("keyword.control")) { return ParseKeyword(); } else if (token.Scopes.Contains("meta.embedded")) { return ParseText(); } else { // Handle other token types (separator, escape, etc.) // Or throw an exception for unexpected tokens. return ParseText(); } } private CommentNode ParseComment() { var token = ConsumeToken(); var text = GetTextSubstring(token); return new CommentNode { Span = new TextSpan(token.StartIndex, token.Length), Text = text }; } private IdentifierNode ParseIdentifier() { var token = ConsumeToken(); var text = GetTextSubstring(token); return new IdentifierNode { Span = new TextSpan(token.StartIndex, token.Length), Name = text }; } private TextNode ParseText() { var token = ConsumeToken(); // Consume the text token. var text = GetTextSubstring(token); // Check if it's a separator if ( token.Scopes.Contains("meta.structure.array.prompt") && token.Scopes.Contains("punctuation.separator.variable.prompt") ) { return new SeperatorNode { Span = new TextSpan(token.StartIndex, token.Length), Text = text }; } return new TextNode { Span = new TextSpan(token.StartIndex, token.Length), Text = text }; } private KeywordNode ParseKeyword() { var token = ConsumeToken(); var keyword = GetTextSubstring(token); return new KeywordNode { Span = new TextSpan(token.StartIndex, token.Length), Keyword = keyword }; } private NumberNode ParseNumber() { var token = ConsumeToken(); var number = GetTextSubstring(token); return new NumberNode { Raw = number, Span = new TextSpan(token.StartIndex, token.Length), Value = decimal.Parse(number, CultureInfo.InvariantCulture), }; } private ParenthesizedNode ParseParenthesized() { var openParenToken = ConsumeToken(); // Consume the '(' if ( openParenToken is null || !openParenToken.Scopes.Contains("punctuation.definition.array.begin.prompt") ) throw new InvalidOperationException("Expected opening parenthesis."); // Set start index var node = new ParenthesizedNode { Span = new TextSpan(openParenToken.StartIndex, 0) }; while (MoreTokens()) { // Check if no more tokens to consume. if (PeekToken() is not { } nextToken) { // Ensure we have length set if (node.Span.Length == 0) throw new InvalidOperationException("Unexpected end of input."); break; } if (nextToken.Scopes.Contains("punctuation.separator.weight.prompt")) { // Parse the weight. ConsumeToken(); // Consume the ':' // Check the weight value token. var weightToken = PeekToken(); if (weightToken is null || !weightToken.Scopes.Contains("constant.numeric")) { throw new InvalidOperationException("Expected numeric weight value."); } // Consume the weight token. node.Weight = ParseNumber(); } // We're supposed to check `punctuation.definition.array.end.prompt` here, textmate is not parsing it // separately always with current tmLanguage grammar, so ALSO use `meta.structure.weight.prompt` for now // We check this AFTER `punctuation.separator.weight.prompt` to avoid consuming the ':' else if ( nextToken.Scopes.Contains("punctuation.definition.array.end.prompt") || nextToken.Scopes.Contains("meta.structure.weight.prompt") ) { // Verify contents if (GetTextSubstring(nextToken) != ")") throw new InvalidOperationException("Expected closing parenthesis."); ConsumeToken(); // Consume the ')' node.EndIndex = nextToken.EndIndex; // Set end index break; } else { // It's part of the content. node.Content.Add(ParseNode()); // Recursively parse nested nodes. } } return node; } private NetworkNode ParseNetwork() { var beginNetworkToken = ConsumeToken(); if ( beginNetworkToken is null || !beginNetworkToken.Scopes.Contains("punctuation.definition.network.begin.prompt") ) throw new InvalidOperationException("Expected opening bracket."); // type var typeToken = PeekToken(); if (typeToken is null || !typeToken.Scopes.Contains("meta.embedded.network.type.prompt")) throw new InvalidOperationException("Expected network type."); var type = ParseIdentifier(); // colon var colonToken = ConsumeToken(); if (colonToken is null || !colonToken.Scopes.Contains("punctuation.separator.variable.prompt")) throw new InvalidOperationException("Expected colon."); // name var nameToken = PeekToken(); if (nameToken is null || !nameToken.Scopes.Contains("meta.embedded.network.model.prompt")) throw new InvalidOperationException("Expected network name."); var name = ParseText(); // model weight, clip weight NumberNode? modelWeight = null; NumberNode? clipWeight = null; // colon var nextToken = PeekToken(); if (nextToken is not null && nextToken.Scopes.Contains("punctuation.separator.variable.prompt")) { ConsumeToken(); // consume colon // Parse the model weight. var modelWeightToken = ConsumeToken(); if (modelWeightToken is null || !modelWeightToken.Scopes.Contains("constant.numeric")) throw new InvalidOperationException("Expected network weight."); modelWeight = ParseNumber(); // colon nextToken = PeekToken(); if (nextToken is not null && nextToken.Scopes.Contains("punctuation.separator.variable.prompt")) { ConsumeToken(); // consume colon // Parse the clip weight. var clipWeightToken = ConsumeToken(); if (clipWeightToken is null || !clipWeightToken.Scopes.Contains("constant.numeric")) throw new InvalidOperationException("Expected network weight."); clipWeight = ParseNumber(); } } var endNetworkToken = ConsumeToken(); if ( endNetworkToken is null || !endNetworkToken.Scopes.Contains("punctuation.definition.network.end.prompt") ) throw new InvalidOperationException("Expected closing bracket."); return new NetworkNode { Span = TextSpan.FromBounds(beginNetworkToken.StartIndex, endNetworkToken.EndIndex), NetworkType = type, ModelName = name, ModelWeight = modelWeight, ClipWeight = clipWeight, }; } private ArrayNode ParseArray() { var openBracket = ConsumeToken(); if (openBracket is null || !openBracket.Scopes.Contains("punctuation.definition.array.begin.prompt")) throw new InvalidOperationException("Expected opening bracket."); var node = new ArrayNode { Span = new TextSpan(openBracket.StartIndex, 0), // Set start index }; while (MoreTokens()) { var nextToken = PeekToken(); if (nextToken is null) break; if (nextToken.Scopes.Contains("punctuation.definition.array.end.prompt")) { ConsumeToken(); // Consume the ']' node.EndIndex = nextToken.EndIndex; //Set end index break; } else { node.Elements.Add(ParseNode()); // Recursively parse nested nodes. } } return node; } private WildcardNode ParseWildcard() { var openBraceToken = ConsumeToken(); // Consume the '{' if ( openBraceToken is null || !openBraceToken.Scopes.Contains("punctuation.definition.wildcard.begin.prompt") ) throw new InvalidOperationException("Expected opening brace."); var node = new WildcardNode { Span = new TextSpan(openBraceToken.StartIndex, 0), // Set start index }; while (MoreTokens()) { var nextToken = PeekToken(); if (nextToken is null) break; if (nextToken.Scopes.Contains("punctuation.definition.wildcard.end.prompt")) { ConsumeToken(); // Consume the '}' node.EndIndex = nextToken.EndIndex; break; } else if (nextToken.Scopes.Contains("keyword.operator.choice.prompt")) { ConsumeToken(); // Consume the '|' } else { node.Options.Add(ParseNode()); // Recursively parse nested nodes. } } return node; } private IToken? PeekToken() { if (currentTokenIndex < tokenizeResult.Tokens.Length) { var result = tokenizeResult.Tokens[currentTokenIndex]; // If this is the last token, ensure it doesn't exceed the source text length if (currentTokenIndex == tokenizeResult.Tokens.Length - 1) { if (result.EndIndex > sourceText.Length) { result = new Token { StartIndex = result.StartIndex, EndIndex = sourceText.Length, Length = sourceText.Length - result.StartIndex, Scopes = result.Scopes, }; } } return result; } return null; } private IToken ConsumeToken() { if (!MoreTokens()) throw new InvalidOperationException("No more tokens to consume."); var result = tokenizeResult.Tokens[currentTokenIndex++]; // If this is the last token, ensure it doesn't exceed the source text length if (currentTokenIndex == tokenizeResult.Tokens.Length - 1) { if (result.EndIndex > sourceText.Length) { result = new Token { StartIndex = result.StartIndex, EndIndex = sourceText.Length, Length = sourceText.Length - result.StartIndex, Scopes = result.Scopes, }; } } return result; } private bool MoreTokens() { return currentTokenIndex < tokenizeResult.Tokens.Length; } private class Token : IToken { public int StartIndex { get; set; } public int EndIndex { get; init; } public int Length { get; init; } public List Scopes { get; init; } = []; } } ================================================ FILE: StabilityMatrix.Core/Models/PromptSyntax/PromptSyntaxTree.cs ================================================ using System.Text; using TextMateSharp.Grammars; namespace StabilityMatrix.Core.Models.PromptSyntax; public class PromptSyntaxTree(string sourceText, DocumentNode rootNode, IReadOnlyList tokens) { public DocumentNode RootNode { get; } = rootNode; public IReadOnlyList Tokens { get; } = tokens; public string SourceText { get; } = sourceText; // Get source text for a specific node public string GetSourceText(PromptNode node) { return GetSourceText(node.Span); } // Get source text for a specific node public string GetSourceText(TextSpan span) { if (span.Start < 0) throw new ArgumentOutOfRangeException(nameof(span), "Node indices are out of range."); // Trim length if it exceeds the source text length var length = span.End > SourceText.Length ? SourceText.Length - span.Start : span.Length; return SourceText.Substring(span.Start, length); } public string ToDebugString() { var sb = new StringBuilder(); foreach (var node in RootNode.Content) { AppendNode(node, sb, 0); } return sb.ToString(); } private void AppendNode(PromptNode node, StringBuilder sb, int indentLevel) { sb.Append(' ', indentLevel * 4); // 4 spaces per indent level sb.Append("- "); switch (node) { case TextNode textNode: sb.AppendLine( $"TextNode: \"{textNode.Text.Replace("\n", "\\n")}\" ({textNode.StartIndex}-{textNode.EndIndex})" ); // Escape newlines break; case ParenthesizedNode parenNode: sb.AppendLine($"ParenthesizedNode: ({parenNode.StartIndex}-{parenNode.EndIndex})"); foreach (var child in parenNode.Content) { AppendNode(child, sb, indentLevel + 1); } if (parenNode.Weight != null) { AppendNode(parenNode.Weight, sb, indentLevel + 1); } break; case NetworkNode networkNode: sb.AppendLine( $"NetworkNode: Type={networkNode.NetworkType}, Model={networkNode.ModelName}, Weight={networkNode.ModelWeight}, ClipWeight={networkNode.ClipWeight} ({networkNode.StartIndex}-{networkNode.EndIndex})" ); break; case WildcardNode wildcardNode: sb.AppendLine($"WildcardNode: ({node.StartIndex}-{node.EndIndex})"); foreach (var option in wildcardNode.Options) { AppendNode(option, sb, indentLevel + 1); } break; case CommentNode commentNode: sb.AppendLine($"CommentNode: \"{commentNode.Text}\" ({node.StartIndex}-{node.EndIndex})"); break; case NumberNode numberNode: sb.AppendLine($"NumberNode: \"{numberNode.Value}\" ({node.StartIndex}-{node.EndIndex})"); break; case KeywordNode keywordNode: sb.AppendLine($"KeywordNode: \"{keywordNode.Keyword}\" ({node.StartIndex}-{node.EndIndex})"); break; case ArrayNode arrayNode: sb.AppendLine($"ArrayNode: ({node.StartIndex}-{node.EndIndex})"); foreach (var child in arrayNode.Elements) { AppendNode(child, sb, indentLevel + 1); } break; // Add cases for other node types... default: sb.AppendLine( $"Unknown Node Type: {node.GetType().Name} ({node.StartIndex}-{node.EndIndex})" ); break; } } } ================================================ FILE: StabilityMatrix.Core/Models/PromptSyntax/TextSpan.cs ================================================ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Runtime.Serialization; using TextMateSharp.Grammars; namespace StabilityMatrix.Core.Models.PromptSyntax; /// /// Immutable abstract representation of a span of text. For example, in an error diagnostic that reports a /// location, it could come from a parsed string, text from a tool editor buffer, etc. /// [DataContract] public readonly struct TextSpan : IEquatable, IComparable { /// /// Creates a TextSpan instance beginning with the position Start and having the Length /// specified with . /// public TextSpan(int start, int length) { if (start < 0) { throw new ArgumentOutOfRangeException(nameof(start)); } if (start + length < start) { throw new ArgumentOutOfRangeException(nameof(length)); } Start = start; Length = length; } /// /// Start point of the span. /// [DataMember(Order = 0)] public int Start { get; } /// /// End of the span. /// public int End => Start + Length; /// /// Length of the span. /// [DataMember(Order = 1)] public int Length { get; } /// /// Determines whether or not the span is empty. /// public bool IsEmpty => Length == 0; /// /// Determines whether the position lies within the span. /// /// /// The position to check. /// /// /// true if the position is greater than or equal to Start and strictly less /// than End, otherwise false. /// public bool Contains(int position) { return unchecked((uint)(position - Start) < (uint)Length); } /// /// Determines whether falls completely within this span. /// /// /// The span to check. /// /// /// true if the specified span falls completely within this span, otherwise false. /// public bool Contains(TextSpan span) { return span.Start >= Start && span.End <= End; } /// /// Determines whether overlaps this span. Two spans are considered to overlap /// if they have positions in common and neither is empty. Empty spans do not overlap with any /// other span. /// /// /// The span to check. /// /// /// true if the spans overlap, otherwise false. /// public bool OverlapsWith(TextSpan span) { var overlapStart = Math.Max(Start, span.Start); var overlapEnd = Math.Min(End, span.End); return overlapStart < overlapEnd; } /// /// Returns the overlap with the given span, or null if there is no overlap. /// /// /// The span to check. /// /// /// The overlap of the spans, or null if the overlap is empty. /// public TextSpan? Overlap(TextSpan span) { var overlapStart = Math.Max(Start, span.Start); var overlapEnd = Math.Min(End, span.End); return overlapStart < overlapEnd ? FromBounds(overlapStart, overlapEnd) : null; } /// /// Determines whether intersects this span. Two spans are considered to /// intersect if they have positions in common or the end of one span /// coincides with the start of the other span. /// /// /// The span to check. /// /// /// true if the spans intersect, otherwise false. /// public bool IntersectsWith(TextSpan span) { return span.Start <= End && span.End >= Start; } /// /// Determines whether intersects this span. /// A position is considered to intersect if it is between the start and /// end positions (inclusive) of this span. /// /// /// The position to check. /// /// /// true if the position intersects, otherwise false. /// public bool IntersectsWith(int position) { return unchecked((uint)(position - Start) <= (uint)Length); } /// /// Returns the intersection with the given span, or null if there is no intersection. /// /// /// The span to check. /// /// /// The intersection of the spans, or null if the intersection is empty. /// public TextSpan? Intersection(TextSpan span) { var intersectStart = Math.Max(Start, span.Start); var intersectEnd = Math.Min(End, span.End); return intersectStart <= intersectEnd ? FromBounds(intersectStart, intersectEnd) : null; } /// /// Creates a new from and positions as opposed to a position and length. /// /// The returned TextSpan contains the range with inclusive, /// and exclusive. /// public static TextSpan FromBounds(int start, int end) { ArgumentOutOfRangeException.ThrowIfNegative(start, nameof(start)); ArgumentOutOfRangeException.ThrowIfLessThan(end, start, nameof(end)); return new TextSpan(start, end - start); } /// /// Determines if two instances of are the same. /// public static bool operator ==(TextSpan left, TextSpan right) { return left.Equals(right); } /// /// Determines if two instances of are different. /// public static bool operator !=(TextSpan left, TextSpan right) { return !left.Equals(right); } /// /// Determines if current instance of is equal to another. /// public bool Equals(TextSpan other) { return Start == other.Start && Length == other.Length; } /// /// Determines if current instance of is equal to another. /// public override bool Equals(object? obj) => obj is TextSpan span && Equals(span); /// /// Produces a hash code for . /// public override int GetHashCode() { return HashCode.Combine(Start, Length); } /// /// Provides a string representation for . /// This representation uses "half-open interval" notation, indicating the endpoint character is not included. /// Example: [10..20), indicating the text starts at position 10 and ends at position 20 not included. /// public override string ToString() { return $"[{Start}..{End})"; } /// /// Compares current instance of with another. /// public int CompareTo(TextSpan other) { var diff = Start - other.Start; if (diff != 0) { return diff; } return Length - other.Length; } } ================================================ FILE: StabilityMatrix.Core/Models/RelayPropertyChangedEventArgs.cs ================================================ using System.ComponentModel; namespace StabilityMatrix.Core.Models; public class RelayPropertyChangedEventArgs : PropertyChangedEventArgs { public bool IsRelay { get; } /// public RelayPropertyChangedEventArgs(string? propertyName, bool isRelay = false) : base(propertyName) { IsRelay = isRelay; } } ================================================ FILE: StabilityMatrix.Core/Models/RemoteResource.cs ================================================ namespace StabilityMatrix.Core.Models; /// /// Defines a remote downloadable resource. /// public readonly record struct RemoteResource { public required Uri Url { get; init; } public Uri[]? FallbackUrls { get; init; } public string? FileNameOverride { get; init; } public string FileName => FileNameOverride ?? Path.GetFileName(Url.ToString()); /// /// Optional relative subdirectory to download the file to. /// public string? RelativeDirectory { get; init; } /// /// Relative path to download the file to. /// This is combined with if is not null. /// Otherwise, it is just . /// public string RelativePath => !string.IsNullOrEmpty(RelativeDirectory) ? Path.Combine(RelativeDirectory, FileName) : FileName; public string? HashSha256 { get; init; } /// /// Type info, for remote models this is of the model. /// public object? ContextType { get; init; } public Uri? InfoUrl { get; init; } public string? LicenseType { get; init; } public Uri? LicenseUrl { get; init; } public string? Author { get; init; } /// /// Whether to auto-extract the archive after download /// public bool AutoExtractArchive { get; init; } /// /// Optional relative path to extract the archive to, if AutoExtractArchive is true /// public string? ExtractRelativePath { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/SafetensorMetadata.cs ================================================ using System.Buffers; using System.Buffers.Binary; using System.Text.Json; namespace StabilityMatrix.Core.Models; public record SafetensorMetadata { // public string? NetworkModule { get; init; } // public string? ModelSpecArchitecture { get; init; } public List? TagFrequency { get; init; } public required List OtherMetadata { get; init; } /// /// Tries to parse the metadata from a SafeTensor file. /// /// Path to the SafeTensor file. /// The parsed metadata. Can be if the file does not contain metadata. public static async Task ParseAsync(string safetensorPath) { using var stream = new FileStream(safetensorPath, FileMode.Open, FileAccess.Read, FileShare.Read); return await ParseAsync(stream); } /// /// Tries to parse the metadata from a SafeTensor file. /// /// Stream to the SafeTensor file. /// The parsed metadata. Can be if the file does not contain metadata. public static async Task ParseAsync(Stream safetensorStream) { // 8 bytes unsigned little-endian 64-bit integer // 1 byte start of JSON object '{' Memory buffer = new byte[9]; await safetensorStream.ReadExactlyAsync(buffer).ConfigureAwait(false); var span = buffer.Span; const ulong MAX_ALLOWED_JSON_LENGTH = 100 * 1024 * 1024; // 100 MB var jsonLength = BinaryPrimitives.ReadUInt64LittleEndian(span); if (jsonLength > MAX_ALLOWED_JSON_LENGTH) { throw new InvalidDataException("JSON length exceeds the maximum allowed size."); } if (span[8] != '{') { throw new InvalidDataException("JSON does not start with '{'."); } // Unfornately Utf8JsonReader does not support reading from a stream directly. // Usually the size of the entire JSON object is less than 500KB, // using a pooled buffer should reduce the number of large allocations. var jsonBytes = ArrayPool.Shared.Rent((int)jsonLength); try { // Important: the length of the rented buffer can be larger than jsonLength // and there can be additional junk data at the end. // we already read {, so start from index 1 jsonBytes[0] = (byte)'{'; await safetensorStream .ReadExactlyAsync(jsonBytes, 1, (int)(jsonLength - 1)) .ConfigureAwait(false); // read the JSON with Utf8JsonReader, then only deserialize what we need // saves us from allocating a bunch of strings then throwing them away var reader = new Utf8JsonReader(jsonBytes.AsSpan(0, (int)jsonLength)); reader.Read(); if (reader.TokenType != JsonTokenType.StartObject) { // expecting a JSON object throw new InvalidDataException("JSON does not start with '{'."); } while (reader.Read()) { // for each property in the object if (reader.TokenType == JsonTokenType.EndObject) { // end of the object, no "__metadata__" found // return true to indicate that we successfully read the JSON // but it does not contain metadata return null; } if (reader.TokenType != JsonTokenType.PropertyName) { // expecting a property name throw new InvalidDataException( $"Invalid metadata JSON, expected property name but got {reader.TokenType}." ); } if (reader.ValueTextEquals("__metadata__")) { if (JsonSerializer.Deserialize>(ref reader) is { } dict) { return FromDictionary(dict); } // got null from Deserialize throw new InvalidDataException("Failed to deserialize metadata."); } else { // skip the property value reader.Skip(); } } // should not reach here, json is malformed throw new InvalidDataException("Invalid metadata JSON."); } finally { ArrayPool.Shared.Return(jsonBytes); } } private static readonly HashSet MetadataKeys = [ // "ss_network_module", // "modelspec.architecture", "ss_tag_frequency", ]; internal static SafetensorMetadata FromDictionary(Dictionary metadataDict) { // equivalent to the following code, rewitten manually for performance // otherMetadata = metadataDict // .Where(kv => !MetadataKeys.Contains(kv.Key)) // .Select(kv => new Metadata(kv.Key, kv.Value)) // .OrderBy(x => x.Name) // .ToList(); var otherMetadata = new List(metadataDict.Count); foreach (var kv in metadataDict) { if (MetadataKeys.Contains(kv.Key)) { continue; } otherMetadata.Add(new Metadata(kv.Key, kv.Value)); } otherMetadata.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); var metadata = new SafetensorMetadata { // NetworkModule = metadataDict.GetValueOrDefault("ss_network_module"), // ModelSpecArchitecture = metadataDict.GetValueOrDefault("modelspec.architecture"), OtherMetadata = otherMetadata }; if (metadataDict.TryGetValue("ss_tag_frequency", out var tagFrequencyJson)) { try { // ss_tag_frequency example: // { "some_name": {"tag1": 5, "tag2": 10}, "another_name": {"tag1": 3, "tag3": 1} } // we flatten the dictionary of dictionaries into a single dictionary var tagFrequencyDict = new Dictionary(); var doc = JsonDocument.Parse(tagFrequencyJson); var root = doc.RootElement; if (root.ValueKind == JsonValueKind.Object) { foreach (var property in root.EnumerateObject()) { var tags = property.Value; if (tags.ValueKind != JsonValueKind.Object) { continue; } foreach (var tagProperty in tags.EnumerateObject()) { var tagName = tagProperty.Name; if ( string.IsNullOrEmpty(tagName) || tagProperty.Value.ValueKind != JsonValueKind.Number ) { continue; } var count = tagProperty.Value.GetInt32(); if (!tagFrequencyDict.TryAdd(tagName, count)) { // tag already exists, increment the count tagFrequencyDict[tagName] += count; } } } } // equivalent to the following code, rewitten manually for performance // tagFrequency = tagFrequencyDict // .Select(kv => new Tag(kv.Key, kv.Value)) // .OrderByDescending(x => x.Frequency) // .ToList(); var tagFrequency = new List(tagFrequencyDict.Count); foreach (var kv in tagFrequencyDict) { tagFrequency.Add(new Tag(kv.Key, kv.Value)); } tagFrequency.Sort((x, y) => y.Frequency.CompareTo(x.Frequency)); metadata = metadata with { TagFrequency = tagFrequency }; } catch (Exception) { // ignore } } return metadata; } public readonly record struct Tag(string Name, int Frequency); public readonly record struct Metadata(string Name, string Value); } ================================================ FILE: StabilityMatrix.Core/Models/Secrets.cs ================================================ using StabilityMatrix.Core.Models.Api.CivitTRPC; using StabilityMatrix.Core.Models.Api.Lykos; namespace StabilityMatrix.Core.Models; public readonly record struct Secrets { [Obsolete("Use LykosAccountV2 instead")] public LykosAccountV1Tokens? LykosAccount { get; init; } public CivitApiTokens? CivitApi { get; init; } public LykosAccountV2Tokens? LykosAccountV2 { get; init; } public string? HuggingFaceToken { get; init; } } public static class SecretsExtensions { public static bool HasLegacyLykosAccount(this Secrets secrets) { #pragma warning disable CS0618 // Type or member is obsolete return secrets.LykosAccount is not null; #pragma warning restore CS0618 // Type or member is obsolete } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/AnalyticsSettings.cs ================================================ using System.Text.Json.Serialization; using Semver; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Settings; public class AnalyticsSettings { [JsonIgnore] public static TimeSpan DefaultLaunchDataSendInterval { get; set; } = TimeSpan.FromDays(1); [JsonConverter(typeof(SemVersionJsonConverter))] public SemVersion? LastSeenConsentVersion { get; set; } public bool? LastSeenConsentAccepted { get; set; } public bool IsUsageDataEnabled { get; set; } public DateTimeOffset? LaunchDataLastSentAt { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/GlobalSettings.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public record GlobalSettings { public bool EulaAccepted { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/HolidayMode.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Settings; [JsonConverter(typeof(JsonStringEnumConverter))] public enum HolidayMode { Automatic, Enabled, Disabled } ================================================ FILE: StabilityMatrix.Core/Models/Settings/LastDownloadLocationInfo.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public class LastDownloadLocationInfo { public string? SelectedInstallLocation { get; set; } public string? CustomInstallLocation { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/LibrarySettings.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public class LibrarySettings { public string? LibraryPath { get; set; } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/ModelSearchOptions.cs ================================================ using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Settings; public record ModelSearchOptions(CivitPeriod SelectedPeriod, CivitSortMode SortMode, CivitModelType SelectedModelType, string SelectedBaseModelType); ================================================ FILE: StabilityMatrix.Core/Models/Settings/NotificationKey.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Models.Settings; /// /// Notification Names /// [SuppressMessage("ReSharper", "InconsistentNaming")] [JsonConverter(typeof(ParsableStringValueJsonConverter))] public record NotificationKey(string Value) : StringValue(Value), IParsable { public NotificationOption DefaultOption { get; init; } public NotificationLevel Level { get; init; } public string? DisplayName { get; init; } public static NotificationKey Inference_PromptCompleted => new("Inference_PromptCompleted") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Success, DisplayName = "Inference Prompt Completed" }; public static NotificationKey Download_Completed => new("Download_Completed") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Success, DisplayName = "Download Completed" }; public static NotificationKey Download_Failed => new("Download_Failed") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Error, DisplayName = "Download Failed" }; public static NotificationKey Download_Canceled => new("Download_Canceled") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Warning, DisplayName = "Download Canceled" }; public static NotificationKey Package_Install_Completed => new("Package_Install_Completed") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Success, DisplayName = "Package Install Completed" }; public static NotificationKey Package_Install_Failed => new("Package_Install_Failed") { DefaultOption = Compat.IsLinux ? NotificationOption.AppToast : NotificationOption.NativePush, Level = NotificationLevel.Error, DisplayName = "Package Install Failed" }; public static Dictionary All { get; } = GetValues(); /// public override string ToString() => base.ToString(); /// public static NotificationKey Parse(string s, IFormatProvider? provider) { return All[s]; } /// public static bool TryParse( string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out NotificationKey result ) { return All.TryGetValue(s ?? "", out result); } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/NotificationLevel.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public enum NotificationLevel { Information, Success, Warning, Error } ================================================ FILE: StabilityMatrix.Core/Models/Settings/NotificationOption.cs ================================================ using System.ComponentModel.DataAnnotations; namespace StabilityMatrix.Core.Models.Settings; public enum NotificationOption { [Display(Name = "None", Description = "No notification")] None, [Display(Name = "In-App", Description = "Show a toast in the app")] AppToast, [Display(Name = "Desktop", Description = "Native desktop push notification")] NativePush } ================================================ FILE: StabilityMatrix.Core/Models/Settings/NumberFormatMode.cs ================================================ using System.ComponentModel.DataAnnotations; namespace StabilityMatrix.Core.Models.Settings; public enum NumberFormatMode { /// /// Use the default number format /// [Display(Name = "Default")] Default, /// /// Use the number format from the current culture /// [Display(Name = "Locale Specific")] CurrentCulture, /// /// Use the number format from the invariant culture /// [Display(Name = "Invariant")] InvariantCulture } ================================================ FILE: StabilityMatrix.Core/Models/Settings/Settings.cs ================================================ using System.ComponentModel; using System.Globalization; using System.Text.Json.Serialization; using Semver; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Core.Models.Settings; public class Settings { public int? Version { get; set; } = 1; public bool FirstLaunchSetupComplete { get; set; } public string? Theme { get; set; } = "Dark"; public string? Language { get; set; } = GetDefaultCulture().Name; public NumberFormatMode NumberFormatMode { get; set; } = NumberFormatMode.CurrentCulture; public List InstalledPackages { get; set; } = new(); [JsonPropertyName("ActiveInstalledPackage")] public Guid? ActiveInstalledPackageId { get; set; } /// /// The first installed package matching the /// or null if no matching package /// [JsonIgnore] public InstalledPackage? ActiveInstalledPackage { get => ActiveInstalledPackageId == null ? null : InstalledPackages.FirstOrDefault(x => x.Id == ActiveInstalledPackageId); set => ActiveInstalledPackageId = value?.Id; } [JsonPropertyName("PreferredWorkflowPackage")] public Guid? PreferredWorkflowPackageId { get; set; } [JsonIgnore] public InstalledPackage? PreferredWorkflowPackage { get => PreferredWorkflowPackageId == null ? null : InstalledPackages.FirstOrDefault(x => x.Id == PreferredWorkflowPackageId); set => PreferredWorkflowPackageId = value?.Id; } public bool HasSeenWelcomeNotification { get; set; } public List? PathExtensions { get; set; } public string? WebApiHost { get; set; } public string? WebApiPort { get; set; } /// /// Preferred update channel /// public UpdateChannel PreferredUpdateChannel { get; set; } = UpdateChannel.Stable; /// /// Whether to check for updates /// public bool CheckForUpdates { get; set; } = true; /// /// The last auto-update version that had a notification dismissed by the user /// [JsonConverter(typeof(SemVersionJsonConverter))] public SemVersion? LastSeenUpdateVersion { get; set; } /// /// Set to the version the user is updating from when updating /// [JsonConverter(typeof(SemVersionJsonConverter))] public SemVersion? UpdatingFromVersion { get; set; } // UI states public bool ModelBrowserNsfwEnabled { get; set; } public bool IsNavExpanded { get; set; } public bool IsImportAsConnected { get; set; } public bool ShowConnectedModelImages { get; set; } public WindowSettings? WindowSettings { get; set; } public ModelSearchOptions? ModelSearchOptions { get; set; } /// /// Whether prompt auto completion is enabled /// public bool IsPromptCompletionEnabled { get; set; } = true; /// /// Relative path to the tag completion CSV file from 'LibraryDir/Tags' /// public string? TagCompletionCsv { get; set; } /// /// Whether to remove underscores from completions /// public bool IsCompletionRemoveUnderscoresEnabled { get; set; } = true; /// /// Format for Inference output image file names /// public string? InferenceOutputImageFileNameFormat { get; set; } /// /// Whether the Inference Image Viewer shows pixel grids at high zoom levels /// public bool IsImageViewerPixelGridEnabled { get; set; } = true; /// /// Whether Inference Image Browser delete action uses recycle bin if available /// public bool IsInferenceImageBrowserUseRecycleBinForDelete { get; set; } = true; public bool RemoveFolderLinksOnShutdown { get; set; } public bool IsDiscordRichPresenceEnabled { get; set; } public HashSet DisabledBaseModelTypes { get; set; } = []; public HashSet SavedInferenceDimensions { get; set; } = [ "1024 x 1024", "1152 x 896", "1216 x 832", "1280 x 720", "1344 x 768", "1536 x 640", "768 x 768", "512 x 512", "640 x 1536", "768 x 1344", "720 x 1280", "832 x 1216", "896 x 1152", ]; [JsonIgnore] public Dictionary DefaultEnvironmentVariables { get; } = new() { // Fixes potential setuptools error on Portable Windows Python // ["SETUPTOOLS_USE_DISTUTILS"] = "stdlib", // Suppresses 'A new release of pip is available' messages ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1", }; [JsonPropertyName("EnvironmentVariables")] public Dictionary? UserEnvironmentVariables { get; set; } [JsonIgnore] public IReadOnlyDictionary EnvironmentVariables { get { // add here when can use GlobalConfig DefaultEnvironmentVariables["UV_CACHE_DIR"] = Path.Combine( GlobalConfig.LibraryDir, "Assets", "uv", "cache" ); if (UserEnvironmentVariables is null || UserEnvironmentVariables.Count == 0) { return DefaultEnvironmentVariables; } return DefaultEnvironmentVariables .Concat(UserEnvironmentVariables) .GroupBy(pair => pair.Key) // User variables override default variables with the same key .ToDictionary(grouping => grouping.Key, grouping => grouping.Last().Value); } } public float AnimationScale { get; set; } = 1.0f; public bool AutoScrollLaunchConsoleToEnd { get; set; } = true; public int ConsoleLogHistorySize { get; set; } = 9001; public HashSet FavoriteModels { get; set; } = new(); public HashSet SeenTeachingTips { get; set; } = new(); public Dictionary NotificationOptions { get; set; } = new(); public List SelectedBaseModels { get; set; } = []; public List SelectedCivitBaseModels { get; set; } = []; public Size InferenceImageSize { get; set; } = new(150, 190); [Obsolete("Use OutputsPageResizeFactor instead")] public Size OutputsImageSize { get; set; } = new(300, 300); public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic; public bool IsWorkflowInfiniteScrollEnabled { get; set; } = true; public bool IsOutputsTreeViewEnabled { get; set; } = true; public CheckpointSortMode CheckpointSortMode { get; set; } = CheckpointSortMode.SharedFolderType; public ListSortDirection CheckpointSortDirection { get; set; } = ListSortDirection.Descending; public bool ShowModelsInSubfolders { get; set; } = true; public bool SortConnectedModelsFirst { get; set; } = true; public int ConsoleFontSize { get; set; } = 14; public bool AutoLoadCivitModels { get; set; } = true; /// /// When false, will copy files when drag/drop import happens /// Otherwise, it will move, as it states /// public bool MoveFilesOnImport { get; set; } = true; public bool DragMovesAllSelected { get; set; } = true; public bool HideEmptyRootCategories { get; set; } public bool HideInstalledModelsInModelBrowser { get; set; } public bool ShowNsfwInCheckpointsPage { get; set; } // public bool OptedInToInstallTelemetry { get; set; } public AnalyticsSettings Analytics { get; set; } = new(); public double CheckpointsPageResizeFactor { get; set; } = 1.0d; public double OutputsPageResizeFactor { get; set; } = 1.0d; public double CivitBrowserResizeFactor { get; set; } = 1.0d; public bool HideEarlyAccessModels { get; set; } public bool CivitUseDiscoveryApi { get; set; } public string? ModelDirectoryOverride { get; set; } = null; public GpuInfo? PreferredGpu { get; set; } public int MaxConcurrentDownloads { get; set; } public bool FilterExtraNetworksByBaseModel { get; set; } = true; public bool ShowAllAvailablePythonVersions { get; set; } public bool IsMainWindowSidebarOpen { get; set; } public Dictionary ModelTypeDownloadPreferences { get; set; } = new(); public bool ShowTrainingDataInModelBrowser { get; set; } public string? CivitModelBrowserFileNamePattern { get; set; } public int InferenceDimensionStepChange { get; set; } = 128; [JsonIgnore] public bool IsHolidayModeActive => HolidayModeSetting == HolidayMode.Automatic ? DateTimeOffset.Now.Month == 12 : HolidayModeSetting == HolidayMode.Enabled; public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package) { RemoveInstalledPackageAndUpdateActive(package.Id); } public void RemoveInstalledPackageAndUpdateActive(Guid id) { InstalledPackages.RemoveAll(x => x.Id == id); UpdateActiveInstalledPackage(); } /// /// Update ActiveInstalledPackage if not valid /// uses first package or null if no packages /// public void UpdateActiveInstalledPackage() { // Empty packages - set to null if (InstalledPackages.Count == 0) { ActiveInstalledPackageId = null; } // Active package is not in package - set to first package else if (InstalledPackages.All(x => x.Id != ActiveInstalledPackageId)) { ActiveInstalledPackageId = InstalledPackages[0].Id; } } public void SetUpdateCheckDisabledForPackage(InstalledPackage package, bool disabled) { var installedPackage = InstalledPackages.FirstOrDefault(p => p.Id == package.Id); if (installedPackage != null) { installedPackage.DontCheckForUpdates = disabled; } } /// /// Return either the system default culture, if supported, or en-US /// /// public static CultureInfo GetDefaultCulture() { var supportedCultures = new[] { "en-US", "ja-JP", "zh-Hans", "zh-Hant" }; var systemCulture = CultureInfo.InstalledUICulture; if (systemCulture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase)) { return new CultureInfo("zh-Hans"); } if (systemCulture.Name.StartsWith("zh-Hant")) { return new CultureInfo("zh-Hant"); } return supportedCultures.Contains(systemCulture.Name) ? systemCulture : new CultureInfo("en-US"); } } [JsonSourceGenerationOptions( WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull )] [JsonSerializable(typeof(Settings))] [JsonSerializable(typeof(bool))] [JsonSerializable(typeof(int))] [JsonSerializable(typeof(string))] [JsonSerializable(typeof(LastDownloadLocationInfo))] [JsonSerializable(typeof(Dictionary))] internal partial class SettingsSerializerContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Models/Settings/SettingsTransaction.cs ================================================ using AsyncAwaitBestPractices; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Settings; /// /// Transaction object which saves settings manager changes when disposed. /// public class SettingsTransaction(ISettingsManager settingsManager, Action onCommit, Func onCommitAsync) : IDisposable, IAsyncDisposable { public Settings Settings => settingsManager.Settings; public void Dispose() { onCommit(); GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { await onCommitAsync().ConfigureAwait(false); GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/Size.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public record struct Size(double Width, double Height) { public static Size operator +(Size current, Size other) => new(current.Width + other.Width, current.Height + other.Height); public static Size operator -(Size current, Size other) => new(current.Width - other.Width, current.Height - other.Height); } ================================================ FILE: StabilityMatrix.Core/Models/Settings/TeachingTip.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Settings; /// /// Teaching tip names /// [JsonConverter(typeof(StringJsonConverter))] public record TeachingTip(string Value) : StringValue(Value) { public static TeachingTip AccountsCredentialsStorageNotice => new("AccountsCredentialsStorageNotice"); public static TeachingTip CheckpointCategoriesTip => new("CheckpointCategoriesTip"); public static TeachingTip PackageExtensionsInstallNotice => new("PackageExtensionsInstallNotice"); public static TeachingTip DownloadsTip => new("DownloadsTip"); public static TeachingTip WebUiButtonMovedTip => new("WebUiButtonMovedTip"); public static TeachingTip InferencePromptHelpButtonTip => new("InferencePromptHelpButtonTip"); public static TeachingTip LykosAccountMigrateTip => new("LykosAccountMigrateTip"); public static TeachingTip SharedFolderMigrationTip => new("SharedFolderMigrationTip"); public static TeachingTip FolderMapTip => new("FolderMapTip"); public static TeachingTip InferencePromptAmplifyTip => new("InferencePromptAmplifyTip"); public static TeachingTip PromptAmplifyDisclaimer => new("PromptAmplifyDisclaimer"); /// public override string ToString() { return base.ToString(); } } ================================================ FILE: StabilityMatrix.Core/Models/Settings/WindowSettings.cs ================================================ namespace StabilityMatrix.Core.Models.Settings; public record WindowSettings(double Width, double Height, int X, int Y, bool IsMaximized); ================================================ FILE: StabilityMatrix.Core/Models/SharedFolderMethod.cs ================================================ namespace StabilityMatrix.Core.Models; public enum SharedFolderMethod { Symlink, Configuration, None } ================================================ FILE: StabilityMatrix.Core/Models/SharedFolderType.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models; [SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "IdentifierTypo")] [Flags] public enum SharedFolderType : ulong { Unknown = 0, [Extensions.Description("Checkpoints")] StableDiffusion = 1 << 0, Lora = 1 << 1, LyCORIS = 1 << 2, [Extensions.Description("Upscalers (ESRGAN)")] ESRGAN = 1 << 3, GFPGAN = 1 << 4, BSRGAN = 1 << 5, Codeformer = 1 << 6, Diffusers = 1 << 7, RealESRGAN = 1 << 8, SwinIR = 1 << 9, VAE = 1 << 10, ApproxVAE = 1 << 11, Karlo = 1 << 12, DeepDanbooru = 1 << 13, Embeddings = 1 << 14, Hypernetwork = 1 << 15, ControlNet = 1 << 16, LDSR = 1 << 17, TextEncoders = 1 << 18, ScuNET = 1 << 19, GLIGEN = 1 << 20, AfterDetailer = 1 << 21, IpAdapter = 1 << 22, T2IAdapter = 1 << 23, IpAdapters15 = 1 << 24, IpAdaptersXl = 1 << 25, ClipVision = 1 << 26, SVD = 1 << 27, Ultralytics = 1 << 28, Sams = 1 << 29, PromptExpansion = 1 << 30, [Extensions.Description("Diffusion Models (UNet-only)")] DiffusionModels = 1ul << 31, } ================================================ FILE: StabilityMatrix.Core/Models/SharedOutputType.cs ================================================ namespace StabilityMatrix.Core.Models; public enum SharedOutputType { All, Text2Img, Text2Vid, Img2Img, Img2Vid, Extras, Text2ImgGrids, Img2ImgGrids, SVD, Saved, Consolidated, } ================================================ FILE: StabilityMatrix.Core/Models/StringValue.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.Serialization; namespace StabilityMatrix.Core.Models; public abstract record StringValue(string Value) : IFormattable { /// public override string ToString() { return Value; } /// public string ToString(string? format, IFormatProvider? formatProvider) { return Value; } /// /// Get all values of type as a dictionary. /// Includes all public static properties. /// protected static Dictionary GetValues< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T >() where T : StringValue { var values = new Dictionary(); foreach (var field in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Static)) { if (field.GetValue(null) is T value) { // Exclude if IgnoreDataMember if (field.GetCustomAttribute() is not null) continue; values.Add(value.Value, value); } } return values; } } ================================================ FILE: StabilityMatrix.Core/Models/TaskResult.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models; public readonly record struct TaskResult { public readonly T? Result; public readonly Exception? Exception; [MemberNotNullWhen(true, nameof(Result))] public bool IsSuccessful => Exception is null && Result != null; public TaskResult(T? result, Exception? exception) { Result = result; Exception = exception; } public TaskResult(T result) { Result = result; } public static TaskResult FromException(Exception exception) => new(default, exception); public void Deconstruct(out T? result, out Exception? exception) { result = Result; exception = Exception; } } ================================================ FILE: StabilityMatrix.Core/Models/Tokens/PromptExtraNetwork.cs ================================================ namespace StabilityMatrix.Core.Models.Tokens; /// /// Represents an extra network token in a prompt. /// In format /// public record PromptExtraNetwork { public required PromptExtraNetworkType Type { get; init; } public required string Name { get; init; } public double? ModelWeight { get; init; } public double? ClipWeight { get; init; } } ================================================ FILE: StabilityMatrix.Core/Models/Tokens/PromptExtraNetworkType.cs ================================================ using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.Tokens; [Flags] public enum PromptExtraNetworkType { [ConvertTo(SharedFolderType.Lora)] Lora = 1 << 0, [ConvertTo(SharedFolderType.LyCORIS)] LyCORIS = 1 << 1, [ConvertTo(SharedFolderType.Embeddings)] Embedding = 1 << 2 } ================================================ FILE: StabilityMatrix.Core/Models/TorchIndex.cs ================================================ namespace StabilityMatrix.Core.Models; public enum TorchIndex { Cuda, Rocm, DirectMl, Cpu, Mps, Zluda, Ipex, None } ================================================ FILE: StabilityMatrix.Core/Models/TrackedDownload.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using AsyncAwaitBestPractices; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models; public class TrackedDownload { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [JsonIgnore] private IDownloadService? downloadService; [JsonIgnore] private Task? downloadTask; [JsonIgnore] private CancellationTokenSource? downloadCancellationTokenSource; [JsonIgnore] private CancellationTokenSource? downloadPauseTokenSource; [JsonIgnore] private CancellationTokenSource AggregateCancellationTokenSource => CancellationTokenSource.CreateLinkedTokenSource( downloadCancellationTokenSource?.Token ?? CancellationToken.None, downloadPauseTokenSource?.Token ?? CancellationToken.None ); public required Guid Id { get; init; } public required Uri SourceUrl { get; init; } public Uri? RedirectedUrl { get; init; } public required DirectoryPath DownloadDirectory { get; init; } public required string FileName { get; init; } public required string TempFileName { get; init; } public string? ExpectedHashSha256 { get; set; } /// /// Whether to auto-extract the archive after download /// public bool AutoExtractArchive { get; set; } /// /// Optional relative path to extract the archive to, if AutoExtractArchive is true /// public string? ExtractRelativePath { get; set; } [JsonIgnore] [MemberNotNullWhen(true, nameof(ExpectedHashSha256))] public bool ValidateHash => ExpectedHashSha256 is not null; [JsonConverter(typeof(JsonStringEnumConverter))] public ProgressState ProgressState { get; set; } = ProgressState.Inactive; public List ExtraCleanupFileNames { get; init; } = new(); // Used for restoring progress on load public long DownloadedBytes { get; set; } public long TotalBytes { get; set; } /// /// Optional context action to be invoked on completion /// public IContextAction? ContextAction { get; set; } [JsonIgnore] public Exception? Exception { get; private set; } private int attempts; #region Events public event EventHandler? ProgressUpdate; private void OnProgressUpdate(ProgressReport e) { // Update downloaded and total bytes DownloadedBytes = Convert.ToInt64(e.Current); TotalBytes = Convert.ToInt64(e.Total); ProgressUpdate?.Invoke(this, e); } public event EventHandler? ProgressStateChanging; private void OnProgressStateChanging(ProgressState e) { Logger.Debug("Download {Download}: State changing to {State}", FileName, e); ProgressStateChanging?.Invoke(this, e); } public event EventHandler? ProgressStateChanged; private void OnProgressStateChanged(ProgressState e) { Logger.Debug("Download {Download}: State changed to {State}", FileName, e); ProgressStateChanged?.Invoke(this, e); } #endregion [MemberNotNull(nameof(downloadService))] private void EnsureDownloadService() { if (downloadService == null) { throw new InvalidOperationException("Download service is not set."); } } private async Task StartDownloadTask(long resumeFromByte, CancellationToken cancellationToken) { var progress = new Progress(OnProgressUpdate); DownloadDirectory.Create(); await downloadService! .ResumeDownloadToFileAsync( SourceUrl.ToString(), DownloadDirectory.JoinFile(TempFileName), resumeFromByte, progress, cancellationToken: cancellationToken ) .ConfigureAwait(false); // If hash validation is enabled, validate the hash if (ValidateHash) { OnProgressUpdate(new ProgressReport(0, isIndeterminate: true, type: ProgressType.Hashing)); var hash = await FileHash .GetSha256Async(DownloadDirectory.JoinFile(TempFileName), progress) .ConfigureAwait(false); if (hash != ExpectedHashSha256?.ToLowerInvariant()) { throw new Exception( $"Hash validation for {FileName} failed, expected {ExpectedHashSha256} but got {hash}" ); } } // Rename the temp file to the final file var tempFile = DownloadDirectory.JoinFile(TempFileName); var finalFile = tempFile.Rename(FileName); // If auto-extract is enabled, extract the archive if (AutoExtractArchive) { OnProgressUpdate(new ProgressReport(0, isIndeterminate: true, type: ProgressType.Extract)); var extractDirectory = string.IsNullOrWhiteSpace(ExtractRelativePath) ? DownloadDirectory : DownloadDirectory.JoinDir(ExtractRelativePath); extractDirectory.Create(); await ArchiveHelper .Extract7Z(finalFile, extractDirectory, new Progress(OnProgressUpdate)) .ConfigureAwait(false); } } /// /// This is only intended for use by the download service. /// Please use .TryStartDownload instead. /// /// internal void Start() { if (ProgressState != ProgressState.Inactive && ProgressState != ProgressState.Pending) { throw new InvalidOperationException( $"Download state must be inactive or pending to start, not {ProgressState}" ); } Logger.Debug("Starting download {Download}", FileName); EnsureDownloadService(); downloadCancellationTokenSource = new CancellationTokenSource(); downloadPauseTokenSource = new CancellationTokenSource(); downloadTask = StartDownloadTask(0, AggregateCancellationTokenSource.Token) .ContinueWith(OnDownloadTaskCompleted); OnProgressStateChanging(ProgressState.Working); ProgressState = ProgressState.Working; OnProgressStateChanged(ProgressState); } internal void Resume() { if (ProgressState != ProgressState.Inactive && ProgressState != ProgressState.Paused) { Logger.Warn( "Attempted to resume download {Download} but it is not paused ({State})", FileName, ProgressState ); } Logger.Debug("Resuming download {Download}", FileName); // Read the temp file to get the current size var tempSize = 0L; var tempFile = DownloadDirectory.JoinFile(TempFileName); if (tempFile.Exists) { tempSize = tempFile.Info.Length; } EnsureDownloadService(); downloadCancellationTokenSource = new CancellationTokenSource(); downloadPauseTokenSource = new CancellationTokenSource(); downloadTask = StartDownloadTask(tempSize, AggregateCancellationTokenSource.Token) .ContinueWith(OnDownloadTaskCompleted); OnProgressStateChanging(ProgressState.Working); ProgressState = ProgressState.Working; OnProgressStateChanged(ProgressState); } public void Pause() { if (ProgressState != ProgressState.Working) { Logger.Warn( "Attempted to pause download {Download} but it is not in progress ({State})", FileName, ProgressState ); return; } Logger.Debug("Pausing download {Download}", FileName); downloadPauseTokenSource?.Cancel(); OnProgressStateChanging(ProgressState.Paused); ProgressState = ProgressState.Paused; OnProgressStateChanged(ProgressState); } public void Cancel() { if (ProgressState is not (ProgressState.Working or ProgressState.Inactive)) { Logger.Warn( "Attempted to cancel download {Download} but it is not in progress ({State})", FileName, ProgressState ); return; } Logger.Debug("Cancelling download {Download}", FileName); // Cancel token if it exists if (downloadCancellationTokenSource is { } token) { token.Cancel(); } // Otherwise handle it manually else { DoCleanup(); OnProgressStateChanging(ProgressState.Cancelled); ProgressState = ProgressState.Cancelled; OnProgressStateChanged(ProgressState); } } public void SetPending() { OnProgressStateChanging(ProgressState.Pending); ProgressState = ProgressState.Pending; OnProgressStateChanged(ProgressState); } /// /// Deletes the temp file and any extra cleanup files /// private void DoCleanup() { try { DownloadDirectory.JoinFile(TempFileName).Delete(); } catch (IOException) { Logger.Warn("Failed to delete temp file {TempFile}", TempFileName); } foreach (var extraFile in ExtraCleanupFileNames) { try { DownloadDirectory.JoinFile(extraFile).Delete(); } catch (IOException) { Logger.Warn("Failed to delete extra cleanup file {ExtraFile}", extraFile); } } } /// /// Invoked by the task's completion callback /// private void OnDownloadTaskCompleted(Task task) { // For cancelled, check if it was actually cancelled or paused if (task.IsCanceled) { // If the task was cancelled, set the state to cancelled if (downloadCancellationTokenSource?.IsCancellationRequested == true) { OnProgressStateChanging(ProgressState.Cancelled); ProgressState = ProgressState.Cancelled; } // If the task was not cancelled, set the state to paused else if (downloadPauseTokenSource?.IsCancellationRequested == true) { OnProgressStateChanging(ProgressState.Inactive); ProgressState = ProgressState.Inactive; } else { throw new InvalidOperationException( "Download task was cancelled but neither cancellation token was cancelled." ); } } // For faulted else if (task.IsFaulted) { // Set the exception Exception = task.Exception; if ((Exception is IOException || Exception?.InnerException is IOException) && attempts < 3) { attempts++; Logger.Warn( "Download {Download} failed with {Exception}, retrying ({Attempt})", FileName, Exception, attempts ); OnProgressStateChanging(ProgressState.Inactive); ProgressState = ProgressState.Inactive; Resume(); return; } Logger.Warn(Exception, "Download {Download} failed", FileName); OnProgressStateChanging(ProgressState.Failed); ProgressState = ProgressState.Failed; } // Otherwise success else { OnProgressStateChanging(ProgressState.Success); ProgressState = ProgressState.Success; } // For failed or cancelled, delete the temp files if (ProgressState is ProgressState.Failed or ProgressState.Cancelled) { DoCleanup(); } // For pause, just do nothing OnProgressStateChanged(ProgressState); // Dispose of the task and cancellation token downloadTask = null; downloadCancellationTokenSource = null; downloadPauseTokenSource = null; } public void SetDownloadService(IDownloadService service) { downloadService = service; } } ================================================ FILE: StabilityMatrix.Core/Models/UnknownInstalledPackage.cs ================================================ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models; public class UnknownInstalledPackage : InstalledPackage { public static UnknownInstalledPackage FromDirectoryName(string name) { return new UnknownInstalledPackage { Id = Guid.NewGuid(), PackageName = UnknownPackage.Key, DisplayName = name, PythonVersion = PyInstallationManager.Python_3_10_17.StringValue, LibraryPath = $"Packages{System.IO.Path.DirectorySeparatorChar}{name}", }; } } ================================================ FILE: StabilityMatrix.Core/Models/Update/UpdateChannel.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Update; [JsonConverter(typeof(DefaultUnknownEnumConverter))] public enum UpdateChannel { Unknown, Stable, Preview, Development } ================================================ FILE: StabilityMatrix.Core/Models/Update/UpdateInfo.cs ================================================ using System.Globalization; using System.Text.Json.Serialization; using Semver; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Models.Update; public record UpdateInfo { [JsonConverter(typeof(SemVersionJsonConverter))] public required SemVersion Version { get; init; } public required DateTimeOffset ReleaseDate { get; init; } public UpdateChannel Channel { get; init; } public UpdateType Type { get; init; } public required Uri Url { get; init; } public required Uri Changelog { get; init; } /// /// Blake3 hash of the file /// public required string HashBlake3 { get; init; } /// /// ED25519 signature of the semicolon seperated string: /// "version + releaseDate + channel + type + url + changelog + hash_blake3" /// verifiable using our stored public key /// public required string Signature { get; init; } /// /// Data for use in signature verification. /// Semicolon separated string of fields: /// "version, releaseDate, channel, type, url, changelog, hashBlake3" /// public string GetSignedData() { var channel = Channel.GetStringValue().ToLowerInvariant(); var date = FormatDateTimeOffsetInvariant(ReleaseDate); return $"{Version};{date};{channel};" + $"{(int)Type};{Url};{Changelog};" + $"{HashBlake3}"; } /// /// Format a DatetimeOffset to a culture invariant string for use in signature verification. /// private static string FormatDateTimeOffsetInvariant(DateTimeOffset dateTimeOffset) { return dateTimeOffset.ToString( @"yyyy-MM-ddTHH\:mm\:ss.ffffffzzz", CultureInfo.InvariantCulture ); } } ================================================ FILE: StabilityMatrix.Core/Models/Update/UpdateManifest.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Models.Update; [JsonSerializable(typeof(UpdateManifest))] public record UpdateManifest { public required Dictionary Updates { get; init; } } // TODO: Bugged in .NET 7 but we can use in 8 https://github.com/dotnet/runtime/pull/79828 /*[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(UpdateManifest))] public partial class UpdateManifestContext : JsonSerializerContext { }*/ ================================================ FILE: StabilityMatrix.Core/Models/Update/UpdatePlatforms.cs ================================================ using System.Text.Json.Serialization; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Models.Update; public record UpdatePlatforms { [JsonPropertyName("win-x64")] public UpdateInfo? WindowsX64 { get; init; } [JsonPropertyName("linux-x64")] public UpdateInfo? LinuxX64 { get; init; } [JsonPropertyName("macos-arm64")] public UpdateInfo? MacOsArm64 { get; init; } public UpdateInfo? GetInfoForCurrentPlatform() { if (Compat.IsWindows) { return WindowsX64; } if (Compat.IsLinux) { return LinuxX64; } if (Compat.IsMacOS && Compat.IsArm) { return MacOsArm64; } return null; } } ================================================ FILE: StabilityMatrix.Core/Models/Update/UpdateType.cs ================================================ namespace StabilityMatrix.Core.Models.Update; [Flags] public enum UpdateType { Normal = 1 << 0, Critical = 1 << 1, Mandatory = 1 << 2, } ================================================ FILE: StabilityMatrix.Core/Processes/AnsiCommand.cs ================================================ namespace StabilityMatrix.Core.Processes; [Flags] public enum AnsiCommand { /// /// Default value /// None = 0, // Erase commands /// /// Erase from cursor to end of line /// ESC[K or ESC[0K /// EraseToEndOfLine = 1 << 0, /// /// Erase from start of line to cursor /// ESC[1K /// EraseFromStartOfLine = 1 << 1, /// /// Erase entire line /// ESC[2K /// EraseLine = 1 << 2, } ================================================ FILE: StabilityMatrix.Core/Processes/AnsiParser.cs ================================================ using System.Text.RegularExpressions; namespace StabilityMatrix.Core.Processes; public static partial class AnsiParser { /// /// From https://github.com/chalk/ansi-regex /// /// [GeneratedRegex(@"[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))")] public static partial Regex AnsiEscapeSequenceRegex(); } ================================================ FILE: StabilityMatrix.Core/Processes/AnsiProcess.cs ================================================ using System.Diagnostics; using System.Text; namespace StabilityMatrix.Core.Processes; /// /// Process supporting parsing of ANSI escape sequences /// public class AnsiProcess : Process { private AsyncStreamReader? stdoutReader; private AsyncStreamReader? stderrReader; public AnsiProcess(ProcessStartInfo startInfo) { StartInfo = startInfo; EnableRaisingEvents = false; StartInfo.UseShellExecute = false; StartInfo.CreateNoWindow = true; StartInfo.RedirectStandardOutput = true; StartInfo.RedirectStandardInput = true; StartInfo.RedirectStandardError = true; // Need this to parse ANSI escape sequences correctly StartInfo.StandardOutputEncoding = new UTF8Encoding(false); StartInfo.StandardErrorEncoding = new UTF8Encoding(false); StartInfo.StandardInputEncoding = new UTF8Encoding(false); } /// /// Start asynchronous reading of stdout and stderr /// /// Called on each new line public void BeginAnsiRead(Action callback) { var stdoutStream = StandardOutput.BaseStream; stdoutReader = new AsyncStreamReader( stdoutStream, s => { if (s == null) return; callback(ProcessOutput.FromStdOutLine(s)); }, StandardOutput.CurrentEncoding ); var stderrStream = StandardError.BaseStream; stderrReader = new AsyncStreamReader( stderrStream, s => { if (s == null) return; callback(ProcessOutput.FromStdErrLine(s)); }, StandardError.CurrentEncoding ); stdoutReader.BeginReadLine(); stderrReader.BeginReadLine(); } /// /// Waits for output readers to finish /// public async Task WaitUntilOutputEOF(CancellationToken ct = default) { if (stdoutReader is not null) { await stdoutReader.EOF.WaitAsync(ct).ConfigureAwait(false); } if (stderrReader is not null) { await stderrReader.EOF.WaitAsync(ct).ConfigureAwait(false); } } /// /// Cancels stream readers, no effect if already cancelled /// public void CancelStreamReaders() { stdoutReader?.CancelOperation(); stderrReader?.CancelOperation(); } protected override void Dispose(bool disposing) { CancelStreamReaders(); stdoutReader?.Dispose(); stdoutReader = null; stderrReader?.Dispose(); stderrReader = null; base.Dispose(disposing); } } ================================================ FILE: StabilityMatrix.Core/Processes/ApcMessage.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using NLog; namespace StabilityMatrix.Core.Processes; /// /// Defines a custom APC message, embeddable in a subprocess output stream. /// Format is as such: /// "{APC}{CustomPrefix}(JsonSerialized ApcMessage){StChar}" /// "\u009f[SM;{"type":"input","data":"hello"}\u009c" /// public readonly record struct ApcMessage { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public const char ApcChar = (char) 0x9F; public const char StChar = (char) 0x9C; public const string CustomPrefix = "[SM;"; [JsonPropertyName("type")] public required ApcType Type { get; init; } [JsonPropertyName("data")] public required string Data { get; init; } /// /// Attempts to extract an APC message from the given text /// /// ApcMessage struct public static bool TryParse(string value, [NotNullWhen(true)] out ApcMessage? message) { message = null; var startIndex = value.IndexOf(ApcChar); if (startIndex == -1) return false; // Check the IdPrefix follows the ApcEscape var idIndex = value.IndexOf(CustomPrefix, startIndex + 1, StringComparison.Ordinal); if (idIndex == -1) return false; // Get the end index (ST escape) var stIndex = value.IndexOf(StChar, idIndex + CustomPrefix.Length); if (stIndex == -1) return false; // Extract the json string (between idIndex and stIndex) var json = value.Substring(idIndex + CustomPrefix.Length, stIndex - idIndex - CustomPrefix.Length); try { message = JsonSerializer.Deserialize(json); return true; } catch (Exception e) { Logger.Warn($"Failed to deserialize APC message: {e.Message}"); return false; } } } ================================================ FILE: StabilityMatrix.Core/Processes/ApcParser.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace StabilityMatrix.Core.Processes; /// /// Parse escaped messages from subprocess /// The message standard: /// - Message events are prefixed with char 'APC' (9F) /// - Followed by '[SM;' /// - Json dict string of 2 strings, 'type' and 'data' /// - Ends with char 'ST' (9C) /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] internal static class ApcParser { public const char ApcEscape = (char) 0x9F; public const string IdPrefix = "[SM;"; public const char StEscape = (char) 0x9C; /// /// Attempts to extract an APC message from the given text /// /// ApcMessage struct public static bool TryParse(string text, out ApcMessage? message) { message = null; var startIndex = text.IndexOf(ApcEscape); if (startIndex == -1) return false; // Check the IdPrefix follows the ApcEscape var idIndex = text.IndexOf(IdPrefix, startIndex + 1, StringComparison.Ordinal); if (idIndex == -1) return false; // Get the end index (ST escape) var stIndex = text.IndexOf(StEscape, idIndex + IdPrefix.Length); if (stIndex == -1) return false; // Extract the json string (between idIndex and stIndex) var json = text.Substring(idIndex + IdPrefix.Length, stIndex - idIndex - IdPrefix.Length); try { message = JsonSerializer.Deserialize(json); return true; } catch (Exception e) { Debug.WriteLine($"Failed to parse APC message: {e.Message}"); return false; } } } ================================================ FILE: StabilityMatrix.Core/Processes/ApcType.cs ================================================ using System.Runtime.Serialization; using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Processes; [JsonConverter(typeof(JsonStringEnumConverter))] public enum ApcType { [EnumMember(Value = "input")] Input = 1, } ================================================ FILE: StabilityMatrix.Core/Processes/Argument.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Processes; /// /// Represents a command line argument. /// public readonly record struct Argument { /// /// The value of the argument. /// public string Value { get; init; } = string.Empty; /// /// Optional key for the argument. /// public string? Key { get; init; } /// /// Whether the property is set and not empty. /// [MemberNotNullWhen(true, nameof(Key))] public bool HasKey => !string.IsNullOrEmpty(Key); /// /// Whether the argument value is already quoted for command line usage. /// public bool IsQuoted { get; init; } /// /// Gets the value with quoting if necessary. /// Is equal to if is . /// /// public string GetQuotedValue() => IsQuoted ? Value : ProcessRunner.Quote(Value); /// /// Create a new argument with the given pre-quoted value. /// public static Argument Quoted(string value) => new(value) { IsQuoted = true }; /// /// Create a new keyed argument with the given pre-quoted value. /// public static Argument Quoted(string key, string value) => new(key, value) { IsQuoted = true }; public Argument() { } public Argument(string value) { Value = value; } public Argument(string key, string value) { Value = value; Key = key; } // Implicit (string -> Argument) public static implicit operator Argument(string _) => new(_); // Explicit (Argument -> string) public static explicit operator string(Argument _) => _.Value; // Implicit ((string, string) -> Argument) public static implicit operator Argument((string Key, string Value) _) => new(_.Key, _.Value); } ================================================ FILE: StabilityMatrix.Core/Processes/AsyncStreamReader.cs ================================================ // Based on System.Diagnostics.AsyncStreamReader // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Processes; /// /// Modified from System.Diagnostics.AsyncStreamReader to support terminal processing. /// /// Currently has these modifications: /// - Carriage returns do not count as newlines '\r'. /// - APC messages are sent immediately without needing a newline. /// /// /// [SuppressMessage("ReSharper", "InconsistentNaming")] internal sealed class AsyncStreamReader : IDisposable { private const int DefaultBufferSize = 1024; // Byte buffer size private readonly Stream _stream; private readonly Decoder _decoder; private readonly byte[] _byteBuffer; private readonly char[] _charBuffer; // Delegate to call user function. private readonly Action _userCallBack; private readonly CancellationTokenSource _cts; private Task? _readToBufferTask; private readonly Queue _messageQueue; private StringBuilder? _sb; private bool _bLastCarriageReturn; private bool _cancelOperation; // Cache the last position scanned in sb when searching for lines. private int _currentLinePos; // (new) Flag to send next buffer immediately private bool _sendNextBufferImmediately; // Creates a new AsyncStreamReader for the given stream. The // character encoding is set by encoding and the buffer size, // in number of 16-bit characters, is set by bufferSize. internal AsyncStreamReader(Stream stream, Action callback, Encoding encoding) { Debug.Assert(stream != null && encoding != null && callback != null, "Invalid arguments!"); Debug.Assert(stream.CanRead, "Stream must be readable!"); _stream = stream; _userCallBack = callback; _decoder = encoding.GetDecoder(); _byteBuffer = new byte[DefaultBufferSize]; // This is the maximum number of chars we can get from one iteration in loop inside ReadBuffer. // Used so ReadBuffer can tell when to copy data into a user's char[] directly, instead of our internal char[]. var maxCharsPerBuffer = encoding.GetMaxCharCount(DefaultBufferSize); _charBuffer = new char[maxCharsPerBuffer]; _cts = new CancellationTokenSource(); _messageQueue = new Queue(); } // User calls BeginRead to start the asynchronous read internal void BeginReadLine() { _cancelOperation = false; if (_sb == null) { _sb = new StringBuilder(DefaultBufferSize); _readToBufferTask = Task.Run((Func)ReadBufferAsync); } else { FlushMessageQueue(rethrowInNewThread: false); } } internal void CancelOperation() { _cancelOperation = true; } // This is the async callback function. Only one thread could/should call this. private async Task ReadBufferAsync() { while (true) { try { var bytesRead = await _stream .ReadAsync(new Memory(_byteBuffer), _cts.Token) .ConfigureAwait(false); if (bytesRead == 0) break; var charLen = _decoder.GetChars(_byteBuffer, 0, bytesRead, _charBuffer, 0); Debug.WriteLine( $"AsyncStreamReader - Read {charLen} chars: " + $"{new string(_charBuffer, 0, charLen).ToRepr()}" ); _sb!.Append(_charBuffer, 0, charLen); MoveLinesFromStringBuilderToMessageQueue(); } catch (IOException) { // We should ideally consume errors from operations getting cancelled // so that we don't crash the unsuspecting parent with an unhandled exc. // This seems to come in 2 forms of exceptions (depending on platform and scenario), // namely OperationCanceledException and IOException (for errorcode that we don't // map explicitly). break; // Treat this as EOF } catch (OperationCanceledException) { // We should consume any OperationCanceledException from child read here // so that we don't crash the parent with an unhandled exc break; // Treat this as EOF } // If user's delegate throws exception we treat this as EOF and // completing without processing current buffer content if (FlushMessageQueue(rethrowInNewThread: true)) { return; } } // We're at EOF, process current buffer content and flush message queue. lock (_messageQueue) { if (_sb!.Length != 0) { var remaining = _sb.ToString(); _messageQueue.Enqueue(remaining); _sb.Length = 0; Debug.WriteLine( $"AsyncStreamReader - Reached EOF, sent remaining buffer: {remaining}" ); } _messageQueue.Enqueue(null); } FlushMessageQueue(rethrowInNewThread: true); } // Send remaining buffer [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SendRemainingBuffer() { lock (_messageQueue) { if (_sb!.Length == 0) return; _messageQueue.Enqueue(_sb.ToString()); _sb.Length = 0; } } // Send remaining buffer from index [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SendRemainingBuffer(int startIndex) { lock (_messageQueue) { if (_sb!.Length == 0) return; _messageQueue.Enqueue(_sb.ToString(startIndex, _sb.Length - startIndex)); _sb.Length = 0; } } // Sends a message to the queue if not null or empty private void SendToQueue(string? message) { if (string.IsNullOrEmpty(message)) return; lock (_messageQueue) { _messageQueue.Enqueue(message); } } // Read lines stored in StringBuilder and the buffer we just read into. // A line is defined as a sequence of characters followed by // a carriage return ('\r'), a line feed ('\n'), or a carriage return // immediately followed by a line feed. The resulting string does not // contain the terminating carriage return and/or line feed. The returned // value is null if the end of the input stream has been reached. private void MoveLinesFromStringBuilderToMessageQueue() { var currentIndex = _currentLinePos; var lineStart = 0; var len = _sb!.Length; // skip a beginning '\n' character of new block if last block ended // with '\r' if (_bLastCarriageReturn && len > 0 && _sb[0] == '\n') { currentIndex = 1; lineStart = 1; _bLastCarriageReturn = false; } while (currentIndex < len) { var ch = _sb[currentIndex]; // Note the following common line feed chars: // \n - UNIX \r\n - DOS switch (ch) { case '\n': { // Include the '\n' as part of line. var line = _sb.ToString(lineStart, currentIndex - lineStart + 1); lineStart = currentIndex + 1; lock (_messageQueue) { _messageQueue.Enqueue(line); } break; } // \r\n - Windows // \r alone is parsed as carriage return case '\r': { // when next char is \n, we found \r\n - linebreak if (currentIndex + 1 < len && _sb[currentIndex + 1] == '\n') { // Include the '\r\n' as part of line. var line = _sb.ToString(lineStart, currentIndex - lineStart + 2); lock (_messageQueue) { _messageQueue.Enqueue(line); } // Advance 2 chars for \r\n lineStart = currentIndex + 2; // Increment one extra plus the end of the loop increment currentIndex++; } else { // Send buffer up to this point, not including \r // But skip if there's no content if (currentIndex == lineStart) break; var line = _sb.ToString(lineStart, currentIndex - lineStart); lock (_messageQueue) { _messageQueue.Enqueue(line); } // Set line start to current index lineStart = currentIndex; } break; } // Additional handling for Apc escape messages case ApcParser.ApcEscape: { // Unconditionally consume until StEscape // Look for index of StEscape var searchIndex = currentIndex; while (searchIndex < len && _sb[searchIndex] != ApcParser.StEscape) { searchIndex++; } // If we found StEscape, we have a complete APC message if (searchIndex < len) { // Include the StEscape as part of line. var line = _sb.ToString(lineStart, searchIndex - lineStart + 1); lock (_messageQueue) { _messageQueue.Enqueue(line); } Debug.WriteLine($"AsyncStreamReader - Sent Apc: '{line}'"); // Flag to send the next buffer immediately _sendNextBufferImmediately = true; // Advance currentIndex and lineStart to StEscape // lineStart = searchIndex + 1; currentIndex = searchIndex; var remainingStart = currentIndex + 1; var remainingStr = _sb.ToString( remainingStart, _sb.Length - remainingStart ); Debug.WriteLine( $"AsyncStreamReader - Sending remaining buffer: '{remainingStr}'" ); // Send the rest of the buffer immediately SendRemainingBuffer(currentIndex + 1); return; } // Otherwise continue without any other changes break; } // If we receive an Ansi escape, send the existing buffer immediately // Kind of behaves like newlines case '\u001b': { // Unlike '\n', this char is not included in the line var line = _sb.ToString(lineStart, currentIndex - lineStart); SendToQueue(line); // Set line start to current index lineStart = currentIndex; // Look ahead and match the escape sequence var remaining = _sb.ToString(currentIndex, len - currentIndex); var result = AnsiParser.AnsiEscapeSequenceRegex().Match(remaining); // If we found a match, send the escape sequence match, and move forward if (result.Success) { var escapeSequence = result.Value; SendToQueue(escapeSequence); Debug.WriteLine( $"AsyncStreamReader - Sent Ansi escape sequence: {escapeSequence.ToRepr()}" ); // Advance currentIndex and lineStart to end of escape sequence // minus 1 since we will increment currentIndex at the end of the loop lineStart = currentIndex + escapeSequence.Length; currentIndex = lineStart - 1; } else { Debug.WriteLine( $"AsyncStreamReader - No match for Ansi escape sequence: {remaining.ToRepr()}" ); } break; } } currentIndex++; } if (len > 0 && _sb[len - 1] == '\r') { _bLastCarriageReturn = true; } // If flagged, send remaining buffer immediately if (_sendNextBufferImmediately) { SendRemainingBuffer(); _sendNextBufferImmediately = false; return; } // Keep the rest characters which can't form a new line in string builder. if (lineStart < len) { if (lineStart == 0) { // we found no linebreaks, in this case we cache the position // so next time we don't have to restart from the beginning _currentLinePos = currentIndex; } else { _sb.Remove(0, lineStart); _currentLinePos = 0; } } else { _sb.Length = 0; _currentLinePos = 0; } } // If everything runs without exception, returns false. // If an exception occurs and rethrowInNewThread is true, returns true. // If an exception occurs and rethrowInNewThread is false, the exception propagates. private bool FlushMessageQueue(bool rethrowInNewThread) { try { // Keep going until we're out of data to process. while (true) { // Get the next line (if there isn't one, we're done) and // invoke the user's callback with it. string? line; lock (_messageQueue) { if (_messageQueue.Count == 0) { break; } line = _messageQueue.Dequeue(); } if (!_cancelOperation) { _userCallBack(line); // invoked outside of the lock } } return false; } catch (Exception e) { // If rethrowInNewThread is true, we can't let the exception propagate synchronously on this thread, // so propagate it in a thread pool thread and return true to indicate to the caller that this failed. // Otherwise, let the exception propagate. if (rethrowInNewThread) { ThreadPool.QueueUserWorkItem( edi => ((ExceptionDispatchInfo)edi!).Throw(), ExceptionDispatchInfo.Capture(e) ); return true; } throw; } } internal Task EOF => _readToBufferTask ?? Task.CompletedTask; public void Dispose() { _cts.Cancel(); } } ================================================ FILE: StabilityMatrix.Core/Processes/ProcessArgs.cs ================================================ using System.Collections; using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using OneOf; namespace StabilityMatrix.Core.Processes; /// /// Parameter type for command line arguments /// Implicitly converts between string and string[], /// with no parsing if the input and output types are the same. /// [Localizable(false)] [CollectionBuilder(typeof(ProcessArgsCollectionBuilder), "Create")] public partial class ProcessArgs : OneOfBase>, IEnumerable { public static ProcessArgs Empty { get; } = new(ImmutableArray.Empty); /// /// Create a new from pre-quoted argument parts, /// which may contain spaces or multiple arguments. /// /// Quoted string arguments /// A new instance public static ProcessArgs FromQuoted(IEnumerable inputs) { var args = inputs.Select(Argument.Quoted).ToImmutableArray(); return new ProcessArgs(args); } /*public ProcessArgs(string arguments) : base(arguments) { } public ProcessArgs(IEnumerable arguments) : base(arguments.ToImmutableArray()) { }*/ public ProcessArgs(OneOf> input) : base(input) { } /// /// Whether the argument string contains the given substring, /// or any of the given arguments if the input is an array. /// public bool Contains(string argument) => Match( str => str.Contains(argument), arr => arr.Any(arg => arg.Value == argument || arg.Key == argument) ); [Pure] public ProcessArgs Concat(ProcessArgs other) => Match( str => new ProcessArgs(string.Join(' ', str, other.ToString())), argsArray => new ProcessArgs(argsArray.AddRange(other.ToArgumentArray())) ); [Pure] public ProcessArgs Prepend(ProcessArgs other) => Match( str => new ProcessArgs(string.Join(' ', other.ToString(), str)), argsArray => new ProcessArgs(other.ToArgumentArray().AddRange(argsArray)) ); /// /// Gets a process string representation for command line execution. /// [Pure] public override string ToString() { return Match( str => str, argsArray => string.Join(' ', argsArray.Select(arg => arg.GetQuotedValue())) ); } /// /// Gets an immutable array of instances. /// [Pure] public ImmutableArray ToArgumentArray() => Match(str => [..ParseArguments(str)], argsArray => argsArray); IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public IEnumerator GetEnumerator() { return ToArgumentArray().AsEnumerable().GetEnumerator(); } /// /// Parses the input string into instances. /// private static IEnumerable ParseArguments(string input) => ArgumentsRegex().Matches(input).Select(match => new Argument(match.Value.Trim('"'))); [GeneratedRegex("""[\"].+?[\"]|[^ ]+""", RegexOptions.IgnoreCase)] private static partial Regex ArgumentsRegex(); // Implicit (string -> ProcessArgs) public static implicit operator ProcessArgs(string input) => new(input); // Implicit (string[] -> Argument[] -> ProcessArgs) public static implicit operator ProcessArgs(string[] input) => new(input.Select(x => new Argument(x)).ToImmutableArray()); // Implicit (Argument[] -> ProcessArgs) public static implicit operator ProcessArgs(Argument[] input) => new(input.ToImmutableArray()); // Implicit (ProcessArgs -> string) public static implicit operator string(ProcessArgs input) => input.ToString(); } [Localizable(false)] public static class ProcessArgsCollectionBuilder { public static ProcessArgs Create(ReadOnlySpan values) => new(values.ToImmutableArray()); } ================================================ FILE: StabilityMatrix.Core/Processes/ProcessArgsBuilder.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.Contracts; namespace StabilityMatrix.Core.Processes; /// /// Builder for . /// public record ProcessArgsBuilder { public ImmutableList Arguments { get; init; } = ImmutableList.Empty; public ProcessArgsBuilder(params Argument[] arguments) { Arguments = arguments.ToImmutableList(); } /// public override string ToString() { return ToProcessArgs().ToString(); } public ProcessArgs ToProcessArgs() { return new ProcessArgs(Arguments.ToImmutableArray()); } public static implicit operator ProcessArgs(ProcessArgsBuilder builder) => builder.ToProcessArgs(); } public static class ProcessArgBuilderExtensions { [Pure] public static T AddArg(this T builder, Argument argument) where T : ProcessArgsBuilder { return builder with { Arguments = builder.Arguments.Add(argument) }; } [Pure] public static T AddArgs(this T builder, params Argument[] argument) where T : ProcessArgsBuilder { return builder with { Arguments = builder.Arguments.AddRange(argument) }; } /// /// Add arguments from strings using the given key. /// [Pure] public static T AddKeyedArgs(this T builder, string key, IEnumerable arguments) where T : ProcessArgsBuilder { return builder with { Arguments = builder.Arguments.AddRange(arguments.Select(arg => new Argument(key, arg))) }; } [Pure] public static T UpdateArg(this T builder, string key, Argument argument) where T : ProcessArgsBuilder { foreach (var arg in builder.Arguments) { if ((arg.Key ?? arg.Value) == key) { return builder with { Arguments = builder.Arguments.Replace(arg, argument) }; } } // No match, add the new argument return builder.AddArg(argument); } [Pure] public static T RemoveArgKey(this T builder, string argumentKey) where T : ProcessArgsBuilder { return builder with { Arguments = builder .Arguments.Where(arg => (arg.Key ?? arg.Value) != argumentKey) .ToImmutableList() }; } } ================================================ FILE: StabilityMatrix.Core/Processes/ProcessOutput.cs ================================================ using System.Diagnostics; using System.Text.RegularExpressions; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Processes; public readonly record struct ProcessOutput { /// /// Parsed text with escape sequences and line endings removed /// public required string Text { get; init; } /// /// Optional Raw output, /// mainly for debug and logging. /// public string? RawText { get; init; } /// /// True if output from stderr, false for stdout. /// public bool IsStdErr { get; init; } /// /// Count of newlines to append to the output. /// (Currently not used) /// public int NewLines { get; init; } /// /// Instruction to clear last n lines /// From carriage return '\r' /// public int CarriageReturn { get; init; } /// /// Instruction to move write cursor up n lines /// From Ansi sequence ESC[#A where # is count of lines /// public int CursorUp { get; init; } /// /// Flag-type Ansi commands /// public AnsiCommand AnsiCommand { get; init; } /// /// Apc message sent from the subprocess /// public ApcMessage? ApcMessage { get; init; } public static ProcessOutput FromStdOutLine(string text) { return FromLine(text, false); } public static ProcessOutput FromStdErrLine(string text) { return FromLine(text, true); } private static ProcessOutput FromLine(string text, bool isStdErr) { // Parse APC message if (ApcParser.TryParse(text, out var message)) { // Override and return return new ProcessOutput { RawText = text, Text = text, IsStdErr = isStdErr, ApcMessage = message }; } // Normal parsing var originalText = text; // Remove \r from the beginning of the line, and add them to count var clearLines = 0; // Strip leading carriage return until newline while (!text.StartsWith(Environment.NewLine) && text.StartsWith('\r')) { text = text[1..]; clearLines++; } // Detect ansi escape sequences var ansiCommands = AnsiCommand.None; var cursorUp = 0; if (text.Contains('\u001b')) { // Cursor up sequence - ESC[#A // Where # is count of lines to move up, if not specified, default to 1 if (Regex.Match(text, @"\x1B\[(\d+)?A") is {Success: true} match) { // Default to 1 if no count cursorUp = int.TryParse(match.Groups[1].Value, out var n) ? n : 1; // Remove the sequence from the text text = text[..match.Index] + text[(match.Index + match.Length)..]; } // Erase line sequence - ESC[#K // (For erasing we don't move the cursor) // Omitted - defaults to 0 // 0 - clear from cursor to end of line // 1 - clear from start of line to cursor // 2 - clear entire line if (Regex.Match(text, @"\x1B\[(0|1|2)?K") is {Success: true} match2) { // Default to 0 if no count var eraseLineMode = int.TryParse(match2.Groups[1].Value, out var n) ? n : 0; ansiCommands |= eraseLineMode switch { 0 => AnsiCommand.EraseToEndOfLine, 1 => AnsiCommand.EraseFromStartOfLine, 2 => AnsiCommand.EraseLine, _ => AnsiCommand.None }; // Remove the sequence from the text text = text[..match2.Index] + text[(match2.Index + match2.Length)..]; } // Private modes, all of these can be safely ignored if (Regex.Match(text, @"\x1B\[?(25l|25h|47l|47h|1049h|1049l)") is {Success: true} match3) { // Remove the sequence from the text text = text[..match3.Index] + text[(match3.Index + match3.Length)..]; } } // If text still contains escape sequences, remove them if (text.Contains('\u001b')) { Debug.WriteLine($"Removing unhandled escape sequences: {text.ToRepr()}"); text = AnsiParser.AnsiEscapeSequenceRegex().Replace(text, ""); } var output = new ProcessOutput { RawText = originalText, Text = text, IsStdErr = isStdErr, CarriageReturn = clearLines, CursorUp = cursorUp, AnsiCommand = ansiCommands, }; return output; } } ================================================ FILE: StabilityMatrix.Core/Processes/ProcessResult.cs ================================================ using StabilityMatrix.Core.Exceptions; namespace StabilityMatrix.Core.Processes; public readonly record struct ProcessResult { public required int ExitCode { get; init; } public string? StandardOutput { get; init; } public string? StandardError { get; init; } public string? ProcessName { get; init; } public TimeSpan Elapsed { get; init; } public bool IsSuccessExitCode => ExitCode == 0; public void EnsureSuccessExitCode() { if (!IsSuccessExitCode) { throw new ProcessException(this); } } } public static class ProcessResultTaskExtensions { public static async Task EnsureSuccessExitCode(this Task task) { var result = await task.ConfigureAwait(false); result.EnsureSuccessExitCode(); return result; } } ================================================ FILE: StabilityMatrix.Core/Processes/ProcessRunner.cs ================================================ using System.Diagnostics; using System.Text; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Processes; public static class ProcessRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Opens the given URL in the default browser. /// /// URL as string public static void OpenUrl(string url) { Logger.Debug($"Opening URL '{url}'"); Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } /// /// Opens the given URL in the default browser. /// /// URI, using AbsoluteUri component public static void OpenUrl(Uri url) { OpenUrl(url.AbsoluteUri); } /// /// Start an executable or .app on macOS. /// public static Process StartApp(string path, ProcessArgs args) { var startInfo = new ProcessStartInfo(); if (Compat.IsMacOS) { startInfo.FileName = "open"; startInfo.Arguments = args.Prepend([path, "--args"]).ToString(); startInfo.UseShellExecute = true; } else { startInfo.FileName = path; startInfo.Arguments = args; } return Process.Start(startInfo) ?? throw new NullReferenceException("Process.Start returned null"); } /// /// Opens the given folder in the system file explorer. /// public static async Task OpenFolderBrowser(string directoryPath) { if (Compat.IsWindows) { // Ensure path ends in DirectorySeparatorChar to unambiguously point to a directory if (!directoryPath.EndsWith(Path.DirectorySeparatorChar)) { directoryPath += Path.DirectorySeparatorChar; } using var process = new Process(); process.StartInfo.FileName = Quote(directoryPath); process.StartInfo.UseShellExecute = true; process.StartInfo.Verb = "open"; process.Start(); // Apparently using verb open detaches the process object, so we can't wait for process exit here } else if (Compat.IsLinux) { using var process = new Process(); process.StartInfo.FileName = directoryPath; process.StartInfo.UseShellExecute = true; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsMacOS) { using var process = new Process(); process.StartInfo.FileName = "open"; process.StartInfo.Arguments = Quote(directoryPath); process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else { throw new PlatformNotSupportedException(); } } /// /// Opens the given file within its folder in the system file explorer. /// public static async Task OpenFileBrowser(string filePath) { if (Compat.IsWindows) { using var process = new Process(); process.StartInfo.FileName = "explorer.exe"; process.StartInfo.Arguments = $"/select, {Quote(filePath)}"; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsLinux) { using var process = new Process(); process.StartInfo.FileName = "dbus-send"; process.StartInfo.Arguments = "--print-reply --dest=org.freedesktop.FileManager1 " + "/org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems " + $"array:string:\"file://{filePath}\" string:\"\""; process.StartInfo.UseShellExecute = true; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsMacOS) { using var process = new Process(); process.StartInfo.FileName = "open"; process.StartInfo.Arguments = $"-R {Quote(filePath)}"; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else { throw new PlatformNotSupportedException(); } } /// /// Starts and tracks a process. /// private static Process StartTrackedProcess(Process process) { process.Start(); // Currently only supported on Windows if (Compat.IsWindows) { ProcessTracker.AddProcess(process); } return process; } public static async Task GetProcessOutputAsync( string fileName, string arguments, string? workingDirectory = null, Dictionary? environmentVariables = null ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } using var process = new Process(); process.StartInfo = info; StartTrackedProcess(process); var output = await process.StandardOutput.ReadToEndAsync(); await process.WaitForExitAsync(); return output; } public static async Task GetProcessResultAsync( string fileName, ProcessArgs arguments, string? workingDirectory = null, IReadOnlyDictionary? environmentVariables = null, bool useUtf8Encoding = false ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; if (useUtf8Encoding) { info.StandardOutputEncoding = Encoding.UTF8; info.StandardErrorEncoding = Encoding.UTF8; } if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } using var process = new Process(); process.StartInfo = info; StartTrackedProcess(process); var stdout = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false); var stderr = await process.StandardError.ReadToEndAsync().ConfigureAwait(false); await process.WaitForExitAsync().ConfigureAwait(false); string? processName = null; TimeSpan elapsed = default; // Accessing these properties may throw an exception if the process has already exited try { processName = process.ProcessName; } catch (SystemException) { } try { elapsed = process.ExitTime - process.StartTime; } catch (SystemException) { } return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout, StandardError = stderr, ProcessName = processName, Elapsed = elapsed, }; } /// /// Starts a process, captures its output in real-time via a callback, and returns the final process result. /// /// The name of the file to execute. /// The command-line arguments to pass to the executable. /// The working directory for the process. /// Callback that receives process output in real-time. /// Environment variables to set for the process. /// Cancellation token to cancel waiting for process exit and close the process. /// A ProcessResult containing the exit code and combined output. public static async Task GetAnsiProcessResultAsync( string fileName, ProcessArgs arguments, string? workingDirectory = null, Action? outputDataReceived = null, IReadOnlyDictionary? environmentVariables = null, CancellationToken cancellationToken = default ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } var stdoutBuilder = new StringBuilder(); var stderrBuilder = new StringBuilder(); using var process = new AnsiProcess(info); StartTrackedProcess(process); try { if (outputDataReceived != null) { process.BeginAnsiRead(output => { // Call the user's callback outputDataReceived(output); // Also capture the output for the final result if (output.IsStdErr) { stderrBuilder.AppendLine(output.Text); } else { stdoutBuilder.AppendLine(output.Text); } }); } await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); // Ensure we've processed all output if (outputDataReceived != null) { await process.WaitUntilOutputEOF(cancellationToken).ConfigureAwait(false); } string? processName = null; var elapsed = TimeSpan.Zero; // Accessing these properties may throw an exception if the process has already exited try { processName = process.ProcessName; } catch (SystemException) { } try { elapsed = process.ExitTime - process.StartTime; } catch (SystemException) { } return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdoutBuilder.ToString(), StandardError = stderrBuilder.ToString(), ProcessName = processName, Elapsed = elapsed, }; } catch (OperationCanceledException e) { // Handle cancellation Logger.Info($"Process '{fileName}' was cancelled. Killing the process."); process.CancelStreamReaders(); process.Kill(true); var result = new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdoutBuilder.ToString(), StandardError = stderrBuilder.ToString(), }; // Accessing these properties may throw an exception if the process has already exited try { result = result with { ProcessName = process.ProcessName }; } catch (SystemException) { } try { result = result with { Elapsed = process.ExitTime - process.StartTime }; } catch (SystemException) { } throw new OperationCanceledException(e.Message, new ProcessException(result), cancellationToken); } } public static Process StartProcess( string fileName, string arguments, string? workingDirectory = null, Action? outputDataReceived = null, IReadOnlyDictionary? environmentVariables = null ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } var process = new Process { StartInfo = info }; StartTrackedProcess(process); if (outputDataReceived == null) return process; process.OutputDataReceived += (sender, args) => outputDataReceived(args.Data); process.ErrorDataReceived += (sender, args) => outputDataReceived(args.Data); process.BeginOutputReadLine(); process.BeginErrorReadLine(); return process; } public static AnsiProcess StartAnsiProcess( string fileName, string arguments, string? workingDirectory = null, Action? outputDataReceived = null, IReadOnlyDictionary? environmentVariables = null ) { Logger.Debug( $"Starting process '{fileName}' with arguments '{arguments}' in working directory '{workingDirectory}'" ); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } var process = new AnsiProcess(info); StartTrackedProcess(process); if (outputDataReceived != null) { process.BeginAnsiRead(outputDataReceived); } return process; } public static AnsiProcess StartAnsiProcess( string fileName, IEnumerable arguments, string? workingDirectory = null, Action? outputDataReceived = null, Dictionary? environmentVariables = null ) { // Quote arguments containing spaces var args = string.Join(" ", arguments.Where(s => !string.IsNullOrEmpty(s)).Select(Quote)); return StartAnsiProcess(fileName, args, workingDirectory, outputDataReceived, environmentVariables); } public static async Task RunBashCommand( string command, string workingDirectory = "", IReadOnlyDictionary? environmentVariables = null ) { // Escape any single quotes in the command var escapedCommand = command.Replace("\"", "\\\""); var arguments = $"-c \"{escapedCommand}\""; Logger.Info($"Running bash command [bash {arguments}]"); var processInfo = new ProcessStartInfo("bash", arguments) { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = workingDirectory, }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { processInfo.EnvironmentVariables[key] = value; } } using var process = new Process(); process.StartInfo = processInfo; var stdout = new StringBuilder(); var stderr = new StringBuilder(); process.OutputDataReceived += (_, args) => stdout.Append(args.Data); process.ErrorDataReceived += (_, args) => stderr.Append(args.Data); StartTrackedProcess(process); process.BeginOutputReadLine(); process.BeginErrorReadLine(); await process.WaitForExitAsync().ConfigureAwait(false); return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout.ToString(), StandardError = stderr.ToString(), }; } public static Task RunBashCommand( IEnumerable commands, string workingDirectory = "", IReadOnlyDictionary? environmentVariables = null ) { // Quote arguments containing spaces var args = string.Join(" ", commands.Select(Quote)); return RunBashCommand(args, workingDirectory, environmentVariables); } /// /// Quotes argument with double quotes if it contains spaces, /// and does not already start and end with double quotes. /// public static string Quote(string argument) { var inner = argument.Trim('"'); return inner.Contains(' ') ? $"\"{inner}\"" : argument; } /// /// Waits for process to exit, then validates exit code. /// /// Process to check. /// Expected exit code. /// Cancellation token. /// Thrown if exit code does not match expected value. public static async Task WaitForExitConditionAsync( Process process, int expectedExitCode = 0, CancellationToken cancelToken = default ) { if (!process.HasExited) { await process.WaitForExitAsync(cancelToken).ConfigureAwait(false); } if (process.ExitCode == expectedExitCode) { return; } // Accessing ProcessName may error on some platforms string? processName = null; try { processName = process.ProcessName; } catch (SystemException) { } throw new ProcessException( "Process " + (processName == null ? "" : processName + " ") + $"failed with exit-code {process.ExitCode}." ); } } ================================================ FILE: StabilityMatrix.Core/Python/ArgParser.cs ================================================ using NLog; using Python.Runtime; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Python; /// /// Extracts command arguments from Python source file. /// public class ArgParser { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IPyRunner pyRunner; private string rootPath; private string moduleName; public ArgParser(IPyRunner pyRunner, string rootPath, string moduleName) { this.pyRunner = pyRunner; this.rootPath = rootPath; this.moduleName = moduleName; } /// /// Convert a PyObject to a value that can be used as a LaunchOption value. /// public static object PyObjectToOptionValue(PyObject obj) { var type = obj.GetPythonType().Name; return type switch { "bool" => obj.As(), "int" => obj.As(), "str" => obj.As(), _ => throw new ArgumentException($"Unknown option type {type}") }; } /// /// Convert a PyObject to a LaunchOptionType enum. /// public static LaunchOptionType? PyObjectToOptionType(PyObject typeObj) { var typeName = typeObj.GetAttr("__name__").As(); return typeName switch { "bool" => LaunchOptionType.Bool, "int" => LaunchOptionType.Int, "str" => LaunchOptionType.String, _ => null }; } public async Task> GetArgsAsync() { await pyRunner.Initialize(); return await pyRunner.RunInThreadWithLock(() => { using var scope = Py.CreateScope(); dynamic sys = scope.Import("sys"); dynamic argparse = scope.Import("argparse"); // Add root path to sys.path sys.path.insert(0, rootPath); // Import module var argsModule = scope.Import(moduleName); var argsDict = argsModule.GetAttr("__dict__").As(); // Find ArgumentParser object in module dynamic? argParser = null; var argParserType = argparse.ArgumentParser; foreach (var obj in argsDict.Values()) { if (obj.IsInstance(argParserType)) { argParser = obj; break; } } if (argParser == null) { throw new ArgumentException($"Could not find ArgumentParser object in module '{moduleName}'"); } // Loop through arguments var definitions = new List(); foreach (var action in argParser._actions) { var name = (action.dest as PyObject)?.As(); if (name == null) { throw new Exception("Argument option did not have a `dest` value"); } var optionStrings = ((PyObject) action.option_strings).As(); var dest = (action.dest as PyObject)?.As(); // var nArgs = (action.nargs as PyObject)?.As(); var isConst = (action.@const as PyObject)?.IsTrue() ?? false; var isRequired = (action.required as PyObject)?.IsTrue() ?? false; var type = action.type as PyObject; // Bool types will have a type of None (null) var optionType = type == null ? LaunchOptionType.Bool : PyObjectToOptionType(type); if (optionType == null) { Logger.Warn("Skipping option {Dest} with type {Name}", dest, type); continue; } // Parse default var @default = action.@default as PyObject; var defaultValue = @default != null ? PyObjectToOptionValue(@default) : null; var help = (action.help as PyObject)?.As(); definitions.Add(new LaunchOptionDefinition { Name = help ?? name, Description = help, Options = new List { optionStrings[0] }, // ReSharper disable once ConstantNullCoalescingCondition Type = optionType ?? LaunchOptionType.Bool, DefaultValue = defaultValue, MinSelectedOptions = isRequired ? 1 : 0, }); } return definitions; }); } } ================================================ FILE: StabilityMatrix.Core/Python/IPyInstallationManager.cs ================================================ namespace StabilityMatrix.Core.Python; /// /// Interface for managing Python installations /// public interface IPyInstallationManager { /// /// Gets all discoverable Python installations (legacy and UV-managed). /// This is now an async method. /// Task> GetAllInstallationsAsync(); /// /// Gets an installation for a specific version. /// If not found, and UV is configured, it may attempt to install it using UV. /// This is now an async method. /// Task GetInstallationAsync(PyVersion version); /// /// Gets the default installation. /// This is now an async method. /// Task GetDefaultInstallationAsync(); Task> GetAllAvailablePythonsAsync(); } ================================================ FILE: StabilityMatrix.Core/Python/IPyRunner.cs ================================================ using Python.Runtime; using StabilityMatrix.Core.Python.Interop; namespace StabilityMatrix.Core.Python; public interface IPyRunner { PyIOStream? StdOutStream { get; } PyIOStream? StdErrStream { get; } /// /// Initializes the Python runtime using the embedded dll. /// Task Initialize(); /// /// Switch to a specific Python installation /// Task SwitchToInstallation(PyVersion version); /// /// One-time setup for get-pip /// Task SetupPip(PyVersion? version = null); /// /// Install a Python package with pip /// Task InstallPackage(string package, PyVersion? version = null); /// /// Run a Function with PyRunning lock as a Task with GIL. /// Task RunInThreadWithLock( Func func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default ); /// /// Run an Action with PyRunning lock as a Task with GIL. /// Task RunInThreadWithLock( Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default ); /// /// Evaluate Python expression and return its value as a string /// Task Eval(string expression); /// /// Evaluate Python expression and return its value /// Task Eval(string expression); /// /// Execute Python code without returning a value /// Task Exec(string code); /// /// Return the Python version as a PyVersionInfo struct /// Task GetVersionInfo(); /// /// Get Python directory name for the given version /// string GetPythonDirName(PyVersion? version = null); /// /// Get Python directory for the given version /// string GetPythonDir(PyVersion? version = null); /// /// Get Python DLL path for the given version /// string GetPythonDllPath(PyVersion? version = null); /// /// Get Python executable path for the given version /// string GetPythonExePath(PyVersion? version = null); /// /// Get Pip executable path for the given version /// string GetPipExePath(PyVersion? version = null); } ================================================ FILE: StabilityMatrix.Core/Python/IPyVenvRunner.cs ================================================ using System.Collections.Immutable; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; public interface IPyVenvRunner { PyBaseInstall BaseInstall { get; } /// /// The process running the python executable. /// AnsiProcess? Process { get; } /// /// The path to the venv root directory. /// DirectoryPath RootPath { get; } /// /// Optional working directory for the python process. /// DirectoryPath? WorkingDirectory { get; set; } /// /// Optional environment variables for the python process. /// ImmutableDictionary EnvironmentVariables { get; set; } /// /// The full path to the python executable. /// FilePath PythonPath { get; } /// /// The full path to the pip executable. /// FilePath PipPath { get; } /// /// The Python version of this venv /// PyVersion Version { get; } /// /// List of substrings to suppress from the output. /// When a line contains any of these substrings, it will not be forwarded to callbacks. /// A corresponding Info log will be written instead. /// List SuppressOutput { get; } void UpdateEnvironmentVariables( Func, ImmutableDictionary> env ); /// True if the venv has a Scripts\python.exe file bool Exists(); /// /// Creates a venv at the configured path. /// Task Setup( bool existsOk = false, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); /// /// Run a pip install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// Task PipInstall(ProcessArgs args, Action? outputDataReceived = null); /// /// Run a pip uninstall command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// Task PipUninstall(ProcessArgs args, Action? outputDataReceived = null); /// /// Run a pip list command, return results as PipPackageInfo objects. /// Task> PipList(); /// /// Run a pip show command, return results as PipPackageInfo objects. /// Task PipShow(string packageName); /// /// Run a pip index command, return result as PipIndexResult. /// Task PipIndex(string packageName, string? indexUrl = null); /// /// Run a custom install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// Task CustomInstall(ProcessArgs args, Action? outputDataReceived = null); /// /// Run a command using the venv Python executable and return the result. /// /// Arguments to pass to the Python executable. Task Run(ProcessArgs arguments); void RunDetached( ProcessArgs args, Action? outputDataReceived, Action? onExit = null, bool unbuffered = true ); /// /// Get entry points for a package. /// https://packaging.python.org/en/latest/specifications/entry-points/#entry-points /// Task GetEntryPoint(string entryPointName); /// /// Kills the running process and cancels stream readers, does not wait for exit. /// void Dispose(); /// /// Kills the running process, waits for exit. /// ValueTask DisposeAsync(); } ================================================ FILE: StabilityMatrix.Core/Python/IUvManager.cs ================================================ using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; public interface IUvManager { Task IsUvAvailableAsync(CancellationToken cancellationToken = default); /// /// Lists Python distributions known to UV. /// /// If true, only lists Pythons UV reports as installed. /// Optional callback for console output. /// Cancellation token. /// A list of UvPythonInfo objects. Task> ListAvailablePythonsAsync( bool installedOnly = false, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); /// /// Gets information about a specific installed Python version managed by UV. /// Task GetInstalledPythonAsync( PyVersion version, CancellationToken cancellationToken = default ); /// /// Installs a specific Python version using UV. /// /// Python version to install (e.g., "3.10" or "3.10.13"). /// Optional. If provided, UV_PYTHON_INSTALL_DIR will be set for the uv process. /// Optional callback for console output. /// Cancellation token. /// UvPythonInfo for the installed Python, or null if installation failed or info couldn't be retrieved. Task InstallPythonVersionAsync( PyVersion version, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ); } ================================================ FILE: StabilityMatrix.Core/Python/Interop/PyIOStream.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Text; namespace StabilityMatrix.Core.Python.Interop; /// /// Implement the interface of the sys.stdout redirection /// [SuppressMessage("ReSharper", "InconsistentNaming")] public class PyIOStream { private readonly StringBuilder TextBuilder; private readonly StringWriter TextWriter; public PyIOStream(StringBuilder? builder = null) { TextBuilder = builder ?? new StringBuilder(); TextWriter = new StringWriter(TextBuilder); } public event EventHandler? OnWriteUpdate; public void ClearBuffer() { TextBuilder.Clear(); } public string GetBuffer() { return TextBuilder.ToString(); } [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public void write(string str) { TextWriter.Write(str); OnWriteUpdate?.Invoke(this, str); } [SuppressMessage("ReSharper", "UnusedMember.Global")] public void writelines(IEnumerable str) { foreach (var line in str) { write(line); } } [SuppressMessage("ReSharper", "UnusedMember.Global")] public void flush() { TextWriter.Flush(); } [SuppressMessage("ReSharper", "UnusedMember.Global")] public void close() { TextWriter?.Close(); } } ================================================ FILE: StabilityMatrix.Core/Python/MajorMinorVersion.cs ================================================ namespace StabilityMatrix.Core.Python; public readonly record struct MajorMinorVersion(int Major, int Minor); ================================================ FILE: StabilityMatrix.Core/Python/PipIndexResult.cs ================================================ using System.Collections.Immutable; using System.Text.RegularExpressions; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Python; public partial record PipIndexResult { public required IReadOnlyList AvailableVersions { get; init; } public static PipIndexResult Parse(string output) { var match = AvailableVersionsRegex().Matches(output); var versions = output .SplitLines() .Select(line => AvailableVersionsRegex().Match(line)) .First(m => m.Success) .Groups["versions"].Value .Split( new[] { ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ) .ToImmutableArray(); return new PipIndexResult { AvailableVersions = versions }; } // Regex, capture the line starting with "Available versions:" [GeneratedRegex(@"^Available versions:\s*(?.*)$")] private static partial Regex AvailableVersionsRegex(); } ================================================ FILE: StabilityMatrix.Core/Python/PipInstallArgs.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Text.RegularExpressions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; [SuppressMessage("ReSharper", "StringLiteralTypo")] public partial record PipInstallArgs : ProcessArgsBuilder { public PipInstallArgs(params Argument[] arguments) : base(arguments) { } public PipInstallArgs WithTorch(string version = "") => this.AddArg(new Argument("torch", $"torch{version}")); public PipInstallArgs WithTorchDirectML(string version = "") => this.AddArg(new Argument("torch-directml", $"torch-directml{version}")); public PipInstallArgs WithTorchVision(string version = "") => this.AddArg(new Argument("torchvision", $"torchvision{version}")); public PipInstallArgs WithTorchAudio(string version = "") => this.AddArg(new Argument("torchaudio", $"torchaudio{version}")); public PipInstallArgs WithXFormers(string version = "") => this.AddArg(new Argument("xformers", $"xformers{version}")); public PipInstallArgs WithExtraIndex(string indexUrl) => this.AddKeyedArgs("--extra-index-url", ["--extra-index-url", indexUrl]); public PipInstallArgs WithTorchExtraIndex(string index) => WithExtraIndex($"https://download.pytorch.org/whl/{index}"); public PipInstallArgs WithParsedFromRequirementsTxt( string requirements, [StringSyntax(StringSyntaxAttribute.Regex)] string? excludePattern = null ) { var requirementsEntries = requirements .SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .Where(s => !s.StartsWith('#')) .Select(s => s.Contains('#') ? s.Substring(0, s.IndexOf('#')) : s) .Select(s => s.Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(NormalizePackageSpecifier); if (excludePattern is not null) { var excludeRegex = new Regex($"^{excludePattern}$"); requirementsEntries = requirementsEntries.Where(s => !excludeRegex.IsMatch(s)); } return this.AddArgs(requirementsEntries.Select(Argument.Quoted).ToArray()); } /// /// Normalizes a package specifier by removing spaces around version constraint operators. /// /// The package specifier to normalize. /// The normalized package specifier. private static string NormalizePackageSpecifier(string specifier) { // Skip normalization for special pip commands that start with a hyphen if (specifier.StartsWith('-')) return specifier; // Regex to match common version constraint patterns with spaces // Matches: package >= 1.0.0, package <= 1.0.0, package == 1.0.0, etc. var versionConstraintPattern = PackageSpecifierRegex(); var match = versionConstraintPattern.Match(specifier); if (match.Success) { var packageName = match.Groups[1].Value; var versionOperator = match.Groups[2].Value; var version = match.Groups[3].Value; return $"{packageName}{versionOperator}{version}"; } return specifier; } public PipInstallArgs WithUserOverrides(List overrides) { var newArgs = this; foreach (var pipOverride in overrides) { if (string.IsNullOrWhiteSpace(pipOverride.Name)) continue; if (pipOverride.Name is "--extra-index-url" or "--index-url") { pipOverride.Constraint = "="; } var pipOverrideArg = pipOverride.ToArgument(); if (pipOverride.Action is PipPackageSpecifierOverrideAction.Update) { newArgs = newArgs.RemovePipArgKey(pipOverrideArg.Key ?? pipOverrideArg.Value); newArgs = newArgs.AddArg(pipOverrideArg); } else if (pipOverride.Action is PipPackageSpecifierOverrideAction.Remove) { newArgs = newArgs.RemovePipArgKey(pipOverrideArg.Key ?? pipOverrideArg.Value); } } return newArgs; } [Pure] public PipInstallArgs RemovePipArgKey(string argumentKey) { return this with { Arguments = Arguments .Where( arg => arg.HasKey ? (arg.Key != argumentKey) : (arg.Value != argumentKey && !arg.Value.Contains($"{argumentKey}==")) ) .ToImmutableList() }; } /// public override string ToString() { return base.ToString(); } [GeneratedRegex(@"^([a-zA-Z0-9\-_.]+)\s*(>=|<=|==|>|<|!=|~=)\s*(.+)$")] private static partial Regex PackageSpecifierRegex(); } ================================================ FILE: StabilityMatrix.Core/Python/PipPackageInfo.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Python; public readonly record struct PipPackageInfo( string Name, string Version, string? EditableProjectLocation = null ); [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)] [JsonSerializable(typeof(PipPackageInfo))] [JsonSerializable(typeof(List))] internal partial class PipPackageInfoSerializerContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Python/PipPackageSpecifier.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; public partial record PipPackageSpecifier { [JsonIgnore] public static IReadOnlyList ConstraintOptions => ["", "==", "~=", ">=", "<=", ">", "<"]; public string? Name { get; set; } public string? Constraint { get; set; } public string? Version { get; set; } public string? VersionConstraint => Constraint is null || Version is null ? null : Constraint + Version; public static PipPackageSpecifier Parse(string value) { var result = TryParse(value, true, out var packageSpecifier); Debug.Assert(result); return packageSpecifier!; } public static bool TryParse(string value, [NotNullWhen(true)] out PipPackageSpecifier? packageSpecifier) { return TryParse(value, false, out packageSpecifier); } private static bool TryParse( string value, bool throwOnFailure, [NotNullWhen(true)] out PipPackageSpecifier? packageSpecifier ) { var match = PackageSpecifierRegex().Match(value); if (!match.Success) { if (throwOnFailure) { throw new ArgumentException($"Invalid package specifier: {value}"); } packageSpecifier = null; return false; } packageSpecifier = new PipPackageSpecifier { Name = match.Groups["package_name"].Value, Constraint = match.Groups["version_constraint"].Value, Version = match.Groups["version"].Value }; return true; } /// public override string ToString() { return Name + VersionConstraint; } public Argument ToArgument() { if (Name is null) { return new Argument(""); } // Normal package specifier with version constraint if (VersionConstraint is not null) { // Use Name as key return new Argument(key: Name, value: ToString()); } // Possible multi arg (e.g. '--extra-index-url ...') if (Name.Trim().StartsWith('-')) { var parts = Name.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 1) { var key = parts[0]; var quotedParts = string.Join(' ', parts.Select(ProcessRunner.Quote)); return Argument.Quoted(key, quotedParts); } } return new Argument(ToString()); } public static implicit operator Argument(PipPackageSpecifier specifier) { return specifier.ToArgument(); } public static implicit operator PipPackageSpecifier(string specifier) { return Parse(specifier); } /// /// Regex to match a pip package specifier. /// [GeneratedRegex( "(?[a-zA-Z0-9_]+)(?(?==|>=|<=|>|<|~=|!=)([a-zA-Z0-9_.]+))?", RegexOptions.CultureInvariant, 1000 )] private static partial Regex PackageSpecifierRegex(); } ================================================ FILE: StabilityMatrix.Core/Python/PipPackageSpecifierOverride.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Python; public record PipPackageSpecifierOverride : PipPackageSpecifier { public PipPackageSpecifierOverrideAction Action { get; init; } = PipPackageSpecifierOverrideAction.Update; [JsonIgnore] public bool IsUpdate => Action is PipPackageSpecifierOverrideAction.Update; /// public override string ToString() { return base.ToString(); } } ================================================ FILE: StabilityMatrix.Core/Python/PipPackageSpecifierOverrideAction.cs ================================================ namespace StabilityMatrix.Core.Python; public enum PipPackageSpecifierOverrideAction { None, Update, Remove } ================================================ FILE: StabilityMatrix.Core/Python/PipShowResult.cs ================================================ using System.Diagnostics; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Python; public record PipShowResult { public required string Name { get; init; } public required string Version { get; init; } public string? Summary { get; init; } public string? HomePage { get; init; } public string? Author { get; init; } public string? AuthorEmail { get; init; } public string? License { get; init; } public string? Location { get; init; } public List? Requires { get; init; } public List? RequiredBy { get; init; } public static PipShowResult Parse(string output) { // Decode each line by splitting on first ":" to key and value var lines = output .SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) // Filter warning lines .Where(line => !line.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase)) .ToList(); var indexOfLicense = GetIndexBySubstring(lines, "License:"); var indexOfLocation = GetIndexBySubstring(lines, "Location:"); var licenseText = indexOfLicense == -1 ? null : string.Join('\n', lines[indexOfLicense..indexOfLocation]); if (indexOfLicense != -1) { lines.RemoveRange(indexOfLicense, indexOfLocation - indexOfLicense); } var linesDict = new Dictionary(); foreach (var line in lines) { var split = line.Split(':', 2); if (split.Length != 2) continue; var key = split[0].Trim(); var value = split[1].Trim(); if (key == "Name" && linesDict.ContainsKey("Name")) { // We've hit a new package, so stop parsing break; } linesDict.TryAdd(key, value); } if (!linesDict.TryGetValue("Name", out var name)) { throw new FormatException("The 'Name' key was not found in the pip show output."); } if (!linesDict.TryGetValue("Version", out var version)) { throw new FormatException("The 'Version' key was not found in the pip show output."); } return new PipShowResult { Name = name, Version = version, Summary = linesDict.GetValueOrDefault("Summary"), HomePage = linesDict.GetValueOrDefault("Home-page"), Author = linesDict.GetValueOrDefault("Author"), AuthorEmail = linesDict.GetValueOrDefault("Author-email"), License = licenseText, Location = linesDict.GetValueOrDefault("Location"), Requires = linesDict .GetValueOrDefault("Requires") ?.Split(',', StringSplitOptions.TrimEntries) .ToList(), RequiredBy = linesDict .GetValueOrDefault("Required-by") ?.Split(',', StringSplitOptions.TrimEntries) .ToList(), }; } private static int GetIndexBySubstring(List lines, string searchString) { var index = -1; for (var i = 0; i < lines.Count; i++) { if (!lines[i].StartsWith(searchString, StringComparison.OrdinalIgnoreCase)) continue; index = i; break; } return index; } } ================================================ FILE: StabilityMatrix.Core/Python/PyBaseInstall.cs ================================================ using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Python; /// /// Represents a base Python installation that can be used by PyVenvRunner /// public class PyBaseInstall(PyInstallation installation) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Gets a PyBaseInstall instance for the default Python installation. /// This uses the default Python 3.10.16 installation. /// public static PyBaseInstall Default => new(new PyInstallation(PyInstallationManager.DefaultVersion)); /// /// The Python installation /// public PyInstallation Installation { get; } = installation; /// /// Root path of the Python installation /// public string RootPath => Installation.InstallPath; /// /// Python executable path /// public string PythonExePath => Installation.PythonExePath; /// /// Pip executable path /// public string PipExePath => Installation.PipExePath; /// /// Version of the Python installation /// public PyVersion Version => Installation.Version; public bool UsesUv => Installation.UsesUv; /// /// Create a virtual environment with this Python installation as the base and /// configure it with the specified parameters. /// /// Path where the virtual environment will be created /// Optional working directory for the Python process /// Optional environment variables for the Python process /// Whether to set up the default Tkinter environment variables (Windows) /// Whether to query and set up Tkinter environment variables (Unix) /// A configured PyVenvRunner instance public IPyVenvRunner CreateVenvRunner( DirectoryPath venvPath, DirectoryPath? workingDirectory = null, IReadOnlyDictionary? environmentVariables = null, bool withDefaultTclTkEnv = false, bool withQueriedTclTkEnv = false ) { IPyVenvRunner venvRunner = new UvVenvRunner(this, venvPath); // Set working directory if provided if (workingDirectory != null) { venvRunner.WorkingDirectory = workingDirectory; } // Set environment variables if provided if (environmentVariables != null) { var envVarDict = venvRunner.EnvironmentVariables; foreach (var (key, value) in environmentVariables) { envVarDict = envVarDict.SetItem(key, value); } if ( Version == PyInstallationManager.Python_3_10_11 && !envVarDict.ContainsKey("SETUPTOOLS_USE_DISTUTILS") ) { // Fixes potential setuptools error on Portable Windows Python envVarDict = envVarDict.SetItem("SETUPTOOLS_USE_DISTUTILS", "stdlib"); } venvRunner.EnvironmentVariables = envVarDict; } // Configure Tkinter environment variables if requested if (withDefaultTclTkEnv && Compat.IsWindows) { // Set up default TCL/TK environment variables for Windows var envVarDict = venvRunner.EnvironmentVariables; envVarDict = envVarDict.SetItem("TCL_LIBRARY", Path.Combine(RootPath, "tcl", "tcl8.6")); envVarDict = envVarDict.SetItem("TK_LIBRARY", Path.Combine(RootPath, "tcl", "tk8.6")); venvRunner.EnvironmentVariables = envVarDict; } else if (withQueriedTclTkEnv && Compat.IsUnix) { // For Unix, we might need to query the system for TCL/TK locations try { // Implementation would depend on how your system detects TCL/TK on Unix Logger.Debug("Setting up TCL/TK environment for Unix"); // This would be implemented based on your system's requirements } catch (Exception ex) { Logger.Warn(ex, "Failed to set up TCL/TK environment for Unix"); } } return venvRunner; } /// /// Asynchronously create a virtual environment with this Python installation as the base and /// configure it with the specified parameters. /// /// Path where the virtual environment will be created /// Optional working directory for the Python process /// Optional environment variables for the Python process /// Whether to set up the default Tkinter environment variables (Windows) /// Whether to query and set up Tkinter environment variables (Unix) /// A configured PyVenvRunner instance public async Task CreateVenvRunnerAsync( string venvPath, string? workingDirectory = null, IReadOnlyDictionary? environmentVariables = null, bool withDefaultTclTkEnv = false, bool withQueriedTclTkEnv = false ) { var dirPath = new DirectoryPath(venvPath); var workingDir = workingDirectory != null ? new DirectoryPath(workingDirectory) : null; // Use the synchronous version and just return with a completed task var venvRunner = CreateVenvRunner( dirPath, workingDir, environmentVariables, withDefaultTclTkEnv, withQueriedTclTkEnv ); return venvRunner; } } ================================================ FILE: StabilityMatrix.Core/Python/PyInstallation.cs ================================================ using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Python; /// /// Represents a specific Python installation /// public class PyInstallation { /// /// The version of this Python installation /// public PyVersion Version { get; } /// /// The root directory of this Python installation. /// This is the primary source of truth for the installation's location. /// public DirectoryPath RootDir { get; } /// /// Path to the Python installation directory. /// Derived from RootDir. /// public string InstallPath => RootDir.FullPath; /// /// The name of the Python directory (e.g., "Python310", "Python31011") /// This is more of a convention for legacy paths or naming. /// If RootDir is arbitrary (e.g., from UV default), this might just be RootDir.Name. /// public string DirectoryName { get { // If the RootDir seems to follow our old convention, use the old logic. // Otherwise, just use the directory name from RootDir. var expectedLegacyDirName = GetDirectoryNameForVersion(Version); if (Version == PyInstallationManager.Python_3_10_11) // Special case from original { expectedLegacyDirName = "Python310"; } if ( RootDir.Name.Equals(expectedLegacyDirName, StringComparison.OrdinalIgnoreCase) || ( Version == PyInstallationManager.Python_3_10_11 && RootDir.Name.Equals("Python310", StringComparison.OrdinalIgnoreCase) ) ) { return RootDir.Name; // It matches a known pattern or is the direct name } // If InstallPath was calculated by the old logic, RootDir.Name would be the DirectoryName. // If InstallPath was provided directly (e.g. UV default path), then RootDir.Name is just the last segment of that path. return RootDir.Name; } } /// /// Path to the Python linked library relative from the Python directory /// public string RelativePythonDllPath => Compat.Switch( (PlatformKind.Windows, $"python{Version.Major}{Version.Minor}.dll"), (PlatformKind.Linux, Path.Combine("lib", $"libpython{Version.Major}.{Version.Minor}.so")), (PlatformKind.MacOS, Path.Combine("lib", $"libpython{Version.Major}.{Version.Minor}.dylib")) ); /// /// Full path to the Python linked library /// public string PythonDllPath => Path.Combine(InstallPath, RelativePythonDllPath); /// /// Path to the Python executable /// public string PythonExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(InstallPath, "python.exe")), (PlatformKind.Linux, Path.Combine(InstallPath, "bin", "python3")), // Could also be 'python' if uv installs it that way or it's a system python (PlatformKind.MacOS, Path.Combine(InstallPath, "bin", "python3")) // Same as Linux ); /// /// Path to the pip executable /// public string PipExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(InstallPath, "Scripts", "pip.exe")), (PlatformKind.Linux, Path.Combine(InstallPath, "bin", "pip3")), (PlatformKind.MacOS, Path.Combine(InstallPath, "bin", "pip3")) ); // These might become less relevant if UV handles venv creation and pip directly for venvs // but the base Python installation will still have them. /// /// Path to the get-pip script (less relevant with UV) /// public string GetPipPath => Path.Combine(InstallPath, "get-pip.pyc"); // This path is specific, might not exist in UV installs /// /// Path to the virtualenv executable (less relevant with UV) /// public string VenvPath => Path.Combine(InstallPath, "Scripts", "virtualenv" + Compat.ExeExtension); /// /// Check if pip is installed in this base Python. /// public bool PipInstalled => File.Exists(PipExePath); /// /// Check if virtualenv is installed (less relevant with UV). /// public bool VenvInstalled => File.Exists(VenvPath); public bool UsesUv => Version != PyInstallationManager.Python_3_10_11; /// /// Primary constructor for when the installation path is known. /// This should be used by PyInstallationManager when it discovers an installation (legacy or UV-managed). /// /// The Python version. /// The full path to the root of the Python installation. public PyInstallation(PyVersion version, string installPath) { Version = version; RootDir = new DirectoryPath(installPath); // Set RootDir directly // Basic validation: ensure the path is not empty. More checks could be added. if (string.IsNullOrWhiteSpace(installPath)) { throw new ArgumentException("Installation path cannot be null or empty.", nameof(installPath)); } } /// /// Constructor for legacy/default Python installations where the path is derived. /// This calculates InstallPath based on GlobalConfig and version. /// /// The Python version. public PyInstallation(PyVersion version) : this(version, CalculateDefaultInstallPath(version)) // Delegate to the primary constructor { } /// /// Constructor for legacy/default Python installations with explicit major, minor, micro. /// public PyInstallation(int major, int minor, int micro = 0) : this(new PyVersion(major, minor, micro)) { } /// /// Calculates the default installation path based on the version. /// Used by the legacy constructor. /// private static string CalculateDefaultInstallPath(PyVersion version) { return Path.Combine(GlobalConfig.LibraryDir, "Assets", GetDirectoryNameForVersion(version)); } /// /// Gets the conventional directory name for a given Python version. /// This is mainly for deriving legacy paths or for when UV is instructed /// to install into a directory with this naming scheme. /// /// The Python version. /// How precise the directory name should be (Major.Minor or Major.Minor.Patch). /// The directory name string. public static string GetDirectoryNameForVersion( PyVersion version, VersionEqualityPrecision precision = VersionEqualityPrecision.MajorMinorPatch ) { // Handle the special case for 3.10.11 which was previously just "Python310" if (version is { Major: 3, Minor: 10, Micro: 11 } && precision != VersionEqualityPrecision.MajorMinor) { // If we're checking against the specific 3.10.11 from PyInstallationManager, and precision allows for micro if (version == PyInstallationManager.Python_3_10_11) return "Python310"; } return precision switch { VersionEqualityPrecision.MajorMinor => $"Python{version.Major}{version.Minor}", _ => $"Python{version.Major}{version.Minor}{version.Micro}", }; } public enum VersionEqualityPrecision { MajorMinor, MajorMinorPatch, } /// /// Check if this Python installation appears to be valid by checking for essential files. /// (e.g., Python DLL or executable). /// public bool Exists() { if (!Directory.Exists(InstallPath)) return false; // A more robust check might be needed. PythonExePath and PythonDllPath depend on OS. // For now, let's check for the DLL on Windows and Exe on others as a primary indicator. // Or just check PythonExePath as it should always exist. return File.Exists(PythonExePath) || File.Exists(PythonDllPath); } /// /// Creates a unique identifier for this Python installation /// public override string ToString() => $"Python {Version} (at {InstallPath})"; public override bool Equals(object? obj) { if (obj is PyInstallation other) { // Consider installations equal if version and path are the same. return Version.Equals(other.Version) && StringComparer.OrdinalIgnoreCase.Equals(InstallPath, other.InstallPath); } return false; } public override int GetHashCode() { return HashCode.Combine(Version, InstallPath.ToLowerInvariant()); } } ================================================ FILE: StabilityMatrix.Core/Python/PyInstallationManager.cs ================================================ using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Python; /// /// Manages multiple Python installations, potentially leveraging UV. /// [RegisterSingleton] public class PyInstallationManager(IUvManager uvManager, ISettingsManager settingsManager) : IPyInstallationManager { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); // Default Python versions - these are TARGET versions SM knows about public static readonly PyVersion Python_3_10_11 = new(3, 10, 11); public static readonly PyVersion Python_3_10_17 = new(3, 10, 17); public static readonly PyVersion Python_3_11_13 = new(3, 11, 13); public static readonly PyVersion Python_3_12_10 = new(3, 12, 10); public static readonly PyVersion Python_3_13_12 = new(3, 13, 12); /// /// List of preferred/target Python versions StabilityMatrix officially supports. /// UV can be used to fetch these if not present. /// public static readonly IReadOnlyList OldVersions = new List { Python_3_10_11, }.AsReadOnly(); /// /// The default Python version to use if none is specified. /// public static readonly PyVersion DefaultVersion = Python_3_10_11; /// /// Gets all discoverable Python installations (legacy and UV-managed). /// This is now an async method. /// public async Task> GetAllInstallationsAsync() { var allInstallations = new List(); var discoveredInstallPaths = new HashSet(StringComparer.OrdinalIgnoreCase); // To avoid duplicates by path // 1. Legacy/Bundled Installations (based on TargetVersions and expected paths) Logger.Debug("Discovering legacy/bundled Python installations..."); foreach (var version in OldVersions) { // The PyInstallation constructor (PyVersion version) now calculates the default path. var legacyPyInstall = new PyInstallation(version); if (legacyPyInstall.Exists() && discoveredInstallPaths.Add(legacyPyInstall.InstallPath)) { allInstallations.Add(legacyPyInstall); Logger.Debug($"Found legacy Python: {legacyPyInstall}"); } } // 2. UV-Managed Installations if (await uvManager.IsUvAvailableAsync().ConfigureAwait(false)) { Logger.Debug("Discovering UV-managed Python installations..."); try { var uvPythons = await uvManager .ListAvailablePythonsAsync(installedOnly: true) .ConfigureAwait(false); foreach (var uvPythonInfo in uvPythons) { if (string.IsNullOrWhiteSpace(uvPythonInfo.InstallPath)) continue; if (discoveredInstallPaths.Add(uvPythonInfo.InstallPath)) // Check if we haven't already added this path (e.g., UV installed to a legacy spot) { var uvPyInstall = new PyInstallation(uvPythonInfo.Version, uvPythonInfo.InstallPath); if (uvPyInstall.Exists()) // Double check, UV said it's installed { allInstallations.Add(uvPyInstall); Logger.Debug($"Found UV-managed Python: {uvPyInstall}"); } else { Logger.Warn( $"UV listed Python at {uvPythonInfo.InstallPath} as installed, but PyInstallation.Exists() check failed." ); } } } } catch (Exception ex) { Logger.Error(ex, "Failed to list UV-managed Python installations."); } } else { Logger.Debug("UV management of base Pythons is enabled, but UV is not available/detected."); } // Return distinct by version, prioritizing (if necessary, though path check helps) // For now, just distinct by the PyInstallation object itself (which considers version and path) return allInstallations.Distinct().OrderBy(p => p.Version).ToList(); } public async Task> GetAllAvailablePythonsAsync() { var allPythons = await uvManager.ListAvailablePythonsAsync().ConfigureAwait(false); Func isSupportedVersion = settingsManager.Settings.ShowAllAvailablePythonVersions ? p => p is { Source: "cpython", Version.Minor: >= 10 } : p => p is { Source: "cpython", Version.Minor: >= 10 and <= 13, Variant: not "freethreaded" }; var filteredPythons = allPythons .Where(isSupportedVersion) .GroupBy(p => p.Key) .Select(g => g.OrderByDescending(p => p.IsInstalled).First()) .OrderBy(p => p.Version) .ToList(); var legacyPythonPath = Path.Combine(settingsManager.LibraryDir, "Assets", "Python310"); if ( filteredPythons.Any(x => x.Version == Python_3_10_11 && x.InstallPath == legacyPythonPath) is false ) { var legacyPythonKey = Compat.IsWindows ? "python-3.10.11-embed-amd64" : Compat.IsMacOS ? "cpython-3.10.11-macos-arm64" : "cpython-3.10.11-x86_64-unknown-linux-gnu"; filteredPythons.Insert( 0, new UvPythonInfo( Python_3_10_11, legacyPythonPath, true, "cpython", null, null, legacyPythonKey, null, null ) ); } return filteredPythons; } /// /// Gets an installation for a specific version. /// If not found, and UV is configured, it may attempt to install it using UV. /// This is now an async method. /// public async Task GetInstallationAsync(PyVersion version) { // 1. Try to find an already existing installation (legacy or UV-managed) var existingInstallations = await GetAllInstallationsAsync().ConfigureAwait(false); // Try exact match first var exactMatch = existingInstallations.FirstOrDefault(p => p.Version == version); if (exactMatch != null) { Logger.Debug($"Found existing exact match for Python {version}: {exactMatch.InstallPath}"); return exactMatch; } // 2. If not found, and UV is allowed to install missing base Pythons, try to install it with UV if (await uvManager.IsUvAvailableAsync().ConfigureAwait(false)) { Logger.Info($"Python {version} not found. Attempting to install with UV."); try { var installedUvPython = await uvManager .InstallPythonVersionAsync(version) .ConfigureAwait(false); if ( installedUvPython.HasValue && !string.IsNullOrWhiteSpace(installedUvPython.Value.InstallPath) ) { var newPyInstall = new PyInstallation( installedUvPython.Value.Version, installedUvPython.Value.InstallPath ); if (newPyInstall.Exists()) { Logger.Info( $"Successfully installed Python {installedUvPython.Value.Version} with UV at {newPyInstall.InstallPath}" ); return newPyInstall; } Logger.Error( $"UV reported successful install of Python {installedUvPython.Value.Version} at {newPyInstall.InstallPath}, but PyInstallation.Exists() check failed." ); } else { Logger.Warn( $"UV failed to install Python {version}. Result from UV manager was null or had no path." ); } } catch (Exception ex) { Logger.Error(ex, $"Error attempting to install Python {version} with UV."); } } // 3. Fallback: Return a PyInstallation object representing the *expected* legacy path. // The caller can then check .Exists() on it. // This maintains compatibility with code that might expect a PyInstallation object even if the files aren't there. Logger.Warn( $"Python {version} not found and UV installation was not attempted or failed. Returning prospective legacy PyInstallation object." ); return new PyInstallation(version); // This constructor uses the default/legacy path. } public async Task GetDefaultInstallationAsync() { return await GetInstallationAsync(DefaultVersion).ConfigureAwait(false); } } ================================================ FILE: StabilityMatrix.Core/Python/PyRunner.cs ================================================ using System.Diagnostics.CodeAnalysis; using Injectio.Attributes; using NLog; using Python.Runtime; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python.Interop; namespace StabilityMatrix.Core.Python; [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Global")] public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial); [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [RegisterSingleton] public class PyRunner : IPyRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); // Set by ISettingsManager.TryFindLibrary() public static DirectoryPath HomeDir { get; set; } = string.Empty; // The installation manager for handling different Python versions private readonly IPyInstallationManager installationManager; // The current Python installation being used private PyInstallation? currentInstallation; /// /// Get the Python directory name for the given version, or the default version if none specified /// public string GetPythonDirName(PyVersion? version = null) => version != null ? $"Python{version.Value.Major}{version.Value.Minor}{version.Value.Micro}" : "Python310"; // Default to 3.10.11 for compatibility /// /// Get the Python directory for the given version, or the default version if none specified /// public string GetPythonDir(PyVersion? version = null) => Path.Combine(GlobalConfig.LibraryDir, "Assets", GetPythonDirName(version)); /// /// Get the Python DLL path for the given version, or the default version if none specified /// public string GetPythonDllPath(PyVersion? version = null) { var pythonDir = GetPythonDir(version); var relativePath = version != null ? Compat.Switch( (PlatformKind.Windows, $"python{version.Value.Major}{version.Value.Minor}.dll"), ( PlatformKind.Linux, Path.Combine("lib", $"libpython{version.Value.Major}.{version.Value.Minor}.so") ), ( PlatformKind.MacOS, Path.Combine("lib", $"libpython{version.Value.Major}.{version.Value.Minor}.dylib") ) ) : RelativePythonDllPath; return Path.Combine(pythonDir, relativePath); } /// /// Get the Python executable path for the given version, or the default version if none specified /// public string GetPythonExePath(PyVersion? version = null) { var pythonDir = GetPythonDir(version); return Compat.Switch( (PlatformKind.Windows, Path.Combine(pythonDir, "python.exe")), (PlatformKind.Linux, Path.Combine(pythonDir, "bin", "python3")), (PlatformKind.MacOS, Path.Combine(pythonDir, "bin", "python3")) ); } /// /// Get the pip executable path for the given version, or the default version if none specified /// public string GetPipExePath(PyVersion? version = null) { var pythonDir = GetPythonDir(version); return Compat.Switch( (PlatformKind.Windows, Path.Combine(pythonDir, "Scripts", "pip.exe")), (PlatformKind.Linux, Path.Combine(pythonDir, "bin", "pip3")), (PlatformKind.MacOS, Path.Combine(pythonDir, "bin", "pip3")) ); } // Legacy properties for compatibility - these use the default Python version public const string PythonDirName = "Python310"; public static string PythonDir => Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName); /// /// Path to the Python Linked library relative from the Python directory. /// public static string RelativePythonDllPath => Compat.Switch( (PlatformKind.Windows, "python310.dll"), (PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so")), (PlatformKind.MacOS, Path.Combine("lib", "libpython3.10.dylib")) ); public static string PythonDllPath => Path.Combine(PythonDir, RelativePythonDllPath); public static string PythonExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "python.exe")), (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3")), (PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "python3")) ); public static string PipExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "Scripts", "pip.exe")), (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3")), (PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "pip3")) ); public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv" + Compat.ExeExtension); public static bool PipInstalled => File.Exists(PipExePath); public static bool VenvInstalled => File.Exists(VenvPath); private static readonly SemaphoreSlim PyRunning = new(1, 1); public PyIOStream? StdOutStream { get; private set; } public PyIOStream? StdErrStream { get; private set; } public PyRunner(IPyInstallationManager installationManager) { this.installationManager = installationManager; } /// /// Switch to a specific Python installation /// public async Task SwitchToInstallation(PyVersion version) { // If Python is already initialized with a different version, we need to shutdown first if (PythonEngine.IsInitialized && currentInstallation?.Version != version) { // hacky stuff until Python.NET stops using BinaryFormatter AppContext.SetSwitch( "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true ); Logger.Info("Shutting down previous Python runtime for version switch"); PythonEngine.Shutdown(); AppContext.SetSwitch( "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", false ); } // If not initialized or we had to shutdown, initialize with the new version if (!PythonEngine.IsInitialized) { // Get the installation for this version var installation = await installationManager.GetInstallationAsync(version).ConfigureAwait(false); if (!installation.Exists()) { throw new FileNotFoundException( $"Python {version} installation not found at {installation.InstallPath}" ); } currentInstallation = installation; // Initialize with this installation await InitializeWithInstallation(installation).ConfigureAwait(false); } } /// /// Initialize Python runtime with a specific installation /// private async Task InitializeWithInstallation(PyInstallation installation) { if (PythonEngine.IsInitialized) return; Logger.Info("Setting PYTHONHOME={PythonDir}", installation.InstallPath.ToRepr()); // Append Python path to PATH var newEnvPath = Compat.GetEnvPathWithExtensions(installation.InstallPath); Logger.Debug("Setting PATH={NewEnvPath}", newEnvPath.ToRepr()); Environment.SetEnvironmentVariable("PATH", newEnvPath, EnvironmentVariableTarget.Process); Logger.Info("Initializing Python runtime with DLL: {DllPath}", installation.PythonDllPath); // Check PythonDLL exists if (!File.Exists(installation.PythonDllPath)) { throw new FileNotFoundException("Python linked library not found", installation.PythonDllPath); } Runtime.PythonDLL = installation.PythonDllPath; PythonEngine.PythonHome = installation.InstallPath; PythonEngine.Initialize(); PythonEngine.BeginAllowThreads(); // Redirect stdout and stderr StdOutStream = new PyIOStream(); StdErrStream = new PyIOStream(); await RunInThreadWithLock(() => { var sys = Py.Import("sys") as PyModule ?? throw new NullReferenceException("sys module not found"); sys.Set("stdout", StdOutStream); sys.Set("stderr", StdErrStream); }) .ConfigureAwait(false); } /// $ /// Initializes the Python runtime using the embedded dll. /// Can be called with no effect after initialization. /// /// Thrown if Python DLL not found. public async Task Initialize() { if (PythonEngine.IsInitialized) return; // Get the default installation var defaultInstallation = await installationManager .GetDefaultInstallationAsync() .ConfigureAwait(false); if (!defaultInstallation.Exists()) { throw new FileNotFoundException( $"Default Python installation not found at {defaultInstallation.InstallPath}" ); } currentInstallation = defaultInstallation; await InitializeWithInstallation(defaultInstallation).ConfigureAwait(false); } /// /// One-time setup for get-pip /// public async Task SetupPip(PyVersion? version = null) { // Use either the specified version or the current installation var installation = version != null ? await installationManager.GetInstallationAsync(version.Value).ConfigureAwait(false) : currentInstallation ?? await installationManager.GetDefaultInstallationAsync().ConfigureAwait(false); var getPipPath = Path.Combine(installation.InstallPath, "get-pip.pyc"); if (!File.Exists(getPipPath)) { throw new FileNotFoundException("get-pip not found", getPipPath); } await ProcessRunner .GetProcessResultAsync(installation.PythonExePath, ["-m", "get-pip"]) .EnsureSuccessExitCode() .ConfigureAwait(false); // Pip version 24.1 deprecated numpy star requirement spec used by some packages // So make the base pip less than that for compatibility, venvs can upgrade themselves if needed await ProcessRunner .GetProcessResultAsync( installation.PythonExePath, ["-m", "pip", "install", "pip==23.3.2", "setuptools==69.5.1"] ) .EnsureSuccessExitCode() .ConfigureAwait(false); } /// /// Install a Python package with pip /// public async Task InstallPackage(string package, PyVersion? version = null) { // Use either the specified version or the current installation var installation = version != null ? await installationManager.GetInstallationAsync(version.Value).ConfigureAwait(false) : currentInstallation ?? await installationManager.GetDefaultInstallationAsync().ConfigureAwait(false); if (!File.Exists(installation.PipExePath)) { throw new FileNotFoundException("pip not found", installation.PipExePath); } var result = await ProcessRunner .GetProcessResultAsync(installation.PythonExePath, $"-m pip install {package}") .ConfigureAwait(false); result.EnsureSuccessExitCode(); } /// /// Run a Function with PyRunning lock as a Task with GIL. /// /// Function to run. /// Time limit for waiting on PyRunning lock. /// Cancellation token. /// cancelToken was canceled, or waitTimeout expired. public async Task RunInThreadWithLock( Func func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default ) { // Wait to acquire PyRunning lock await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); try { return await Task.Run( () => { using (Py.GIL()) { return func(); } }, cancelToken ) .ConfigureAwait(false); } finally { PyRunning.Release(); } } /// /// Run an Action with PyRunning lock as a Task with GIL. /// /// Action to run. /// Time limit for waiting on PyRunning lock. /// Cancellation token. /// cancelToken was canceled, or waitTimeout expired. public async Task RunInThreadWithLock( Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default ) { // Wait to acquire PyRunning lock await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); try { await Task.Run( () => { using (Py.GIL()) { action(); } }, cancelToken ) .ConfigureAwait(false); } finally { PyRunning.Release(); } } /// /// Evaluate Python expression and return its value as a string /// /// public async Task Eval(string expression) { return await Eval(expression); } /// /// Evaluate Python expression and return its value /// /// public Task Eval(string expression) { return RunInThreadWithLock(() => { using var scope = Py.CreateScope(); var result = scope.Eval(expression); // For string, cast with __str__() if (typeof(T) == typeof(string)) { return result.GetAttr("__str__").Invoke().As(); } return result.As(); }); } /// /// Execute Python code without returning a value /// /// public Task Exec(string code) { return RunInThreadWithLock(() => { using var scope = Py.CreateScope(); scope.Exec(code); }); } /// /// Return the Python version as a PyVersionInfo struct /// public async Task GetVersionInfo() { var info = await Eval("tuple(__import__('sys').version_info)"); return new PyVersionInfo( info[0].As(), info[1].As(), info[2].As(), info[3].As(), info[4].As() ); } } ================================================ FILE: StabilityMatrix.Core/Python/PyVenvRunner.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using NLog; using Salaros.Configuration; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; /// /// Python runner using a subprocess, mainly for venv support. /// public class PyVenvRunner : IDisposable, IAsyncDisposable, IPyVenvRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private string? lastSetPyvenvCfgPath; /// /// Relative path to the site-packages folder from the venv root. /// This is platform specific. /// public static string GetRelativeSitePackagesPath(PyVersion? version = null) { var minorVersion = version?.Minor ?? 10; return Compat.Switch( (PlatformKind.Windows, "Lib/site-packages"), (PlatformKind.Unix, $"lib/python3.{minorVersion}/site-packages") ); } /// /// Legacy path for compatibility /// public static string RelativeSitePackagesPath => Compat.Switch( (PlatformKind.Windows, "Lib/site-packages"), (PlatformKind.Unix, "lib/python3.10/site-packages") ); public PyBaseInstall BaseInstall { get; } /// /// The process running the python executable. /// public AnsiProcess? Process { get; private set; } /// /// The path to the venv root directory. /// public DirectoryPath RootPath { get; } /// /// Optional working directory for the python process. /// public DirectoryPath? WorkingDirectory { get; set; } /// /// Optional environment variables for the python process. /// public ImmutableDictionary EnvironmentVariables { get; set; } = ImmutableDictionary.Empty; /// /// Name of the python binary folder. /// 'Scripts' on Windows, 'bin' on Unix. /// public static string RelativeBinPath => Compat.Switch((PlatformKind.Windows, "Scripts"), (PlatformKind.Unix, "bin")); /// /// The relative path to the python executable. /// public static string RelativePythonPath => Compat.Switch( (PlatformKind.Windows, Path.Combine("Scripts", "python.exe")), (PlatformKind.Unix, Path.Combine("bin", "python3")) ); /// /// The full path to the python executable. /// public FilePath PythonPath => RootPath.JoinFile(RelativePythonPath); /// /// The relative path to the pip executable. /// public static string RelativePipPath => Compat.Switch( (PlatformKind.Windows, Path.Combine("Scripts", "pip.exe")), (PlatformKind.Unix, Path.Combine("bin", "pip3")) ); /// /// The full path to the pip executable. /// public FilePath PipPath => RootPath.JoinFile(RelativePipPath); /// /// The Python version of this venv /// public PyVersion Version => BaseInstall.Version; /// /// List of substrings to suppress from the output. /// When a line contains any of these substrings, it will not be forwarded to callbacks. /// A corresponding Info log will be written instead. /// public List SuppressOutput { get; } = new() { "fatal: not a git repository" }; internal PyVenvRunner(PyBaseInstall baseInstall, DirectoryPath rootPath) { BaseInstall = baseInstall; RootPath = rootPath; EnvironmentVariables = EnvironmentVariables.SetItem("VIRTUAL_ENV", rootPath.FullPath); } public void UpdateEnvironmentVariables( Func, ImmutableDictionary> env ) { EnvironmentVariables = env(EnvironmentVariables); } /// True if the venv has a Scripts\python.exe file public bool Exists() => PythonPath.Exists; /// /// Creates a venv at the configured path. /// public async Task Setup( bool existsOk = false, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!existsOk && Exists()) { throw new InvalidOperationException("Venv already exists"); } // Create RootPath if it doesn't exist RootPath.Create(); // Create venv (copy mode if windows) var args = new string[] { "-m", "virtualenv", Compat.IsWindows ? "--always-copy" : "", RootPath }; var venvProc = ProcessRunner.StartAnsiProcess( BaseInstall.PythonExePath, args, WorkingDirectory?.FullPath, onConsoleOutput ); try { await venvProc.WaitForExitAsync(cancellationToken).ConfigureAwait(false); // Check return code if (venvProc.ExitCode != 0) { throw new ProcessException($"Venv creation failed with code {venvProc.ExitCode}"); } } catch (OperationCanceledException) { venvProc.CancelStreamReaders(); } finally { venvProc.Kill(); venvProc.Dispose(); } } /// /// Set current python path to pyvenv.cfg /// This should be called before using the venv, in case user moves the venv directory. /// private void SetPyvenvCfg(string pythonDirectory, bool force = false) { // Skip if we are not created yet if (!Exists()) return; // Skip if already set to same value if (lastSetPyvenvCfgPath == pythonDirectory && !force) return; // Path to pyvenv.cfg var cfgPath = Path.Combine(RootPath, "pyvenv.cfg"); if (!File.Exists(cfgPath)) { throw new FileNotFoundException("pyvenv.cfg not found", cfgPath); } Logger.Info("Updating pyvenv.cfg with embedded Python directory {PyDir}", pythonDirectory); // Insert a top section var topSection = "[top]" + Environment.NewLine; var cfg = new ConfigParser(topSection + File.ReadAllText(cfgPath)); // Need to set all path keys - home, base-prefix, base-exec-prefix, base-executable cfg.SetValue("top", "home", pythonDirectory); cfg.SetValue("top", "base-prefix", pythonDirectory); cfg.SetValue("top", "base-exec-prefix", pythonDirectory); cfg.SetValue( "top", "base-executable", Path.Combine(pythonDirectory, Compat.IsWindows ? "python.exe" : RelativePythonPath) ); // Convert to string for writing, strip the top section var cfgString = cfg.ToString()!.Replace(topSection, ""); File.WriteAllText(cfgPath, cfgString); // Update last set path lastSetPyvenvCfgPath = pythonDirectory; } /// /// Run a pip install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task PipInstall(ProcessArgs args, Action? outputDataReceived = null) { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { Logger.Debug($"Pip output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived?.Invoke(s); }); RunDetached(args.Prepend("-m pip install").Concat("--exists-action s"), outputAction); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a pip uninstall command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task PipUninstall(ProcessArgs args, Action? outputDataReceived = null) { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { Logger.Debug($"Pip output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived?.Invoke(s); }); RunDetached($"-m pip uninstall -y {args}", outputAction); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a pip list command, return results as PipPackageInfo objects. /// public async Task> PipList() { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } SetPyvenvCfg(BaseInstall.RootPath); var result = await ProcessRunner .GetProcessResultAsync( PythonPath, "-m pip list --format=json", WorkingDirectory?.FullPath, EnvironmentVariables ) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip list failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } // There may be warning lines before the Json line, or update messages after // Filter to find the first line that starts with [ var jsonLine = result .StandardOutput?.SplitLines( StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ) .Select(line => line.Trim()) .FirstOrDefault( line => line.StartsWith("[", StringComparison.OrdinalIgnoreCase) && line.EndsWith("]", StringComparison.OrdinalIgnoreCase) ); if (jsonLine is null) { return []; } return JsonSerializer.Deserialize>( jsonLine, PipPackageInfoSerializerContext.Default.Options ) ?? []; } /// /// Run a pip show command, return results as PipPackageInfo objects. /// public async Task PipShow(string packageName) { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } SetPyvenvCfg(BaseInstall.RootPath); var result = await ProcessRunner .GetProcessResultAsync( PythonPath, new[] { "-m", "pip", "show", packageName }, WorkingDirectory?.FullPath, EnvironmentVariables ) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip show failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } if (result.StandardOutput!.StartsWith("WARNING: Package(s) not found:")) { return null; } return PipShowResult.Parse(result.StandardOutput); } /// /// Run a pip index command, return result as PipIndexResult. /// public async Task PipIndex(string packageName, string? indexUrl = null) { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } SetPyvenvCfg(BaseInstall.RootPath); var args = new ProcessArgsBuilder( "-m", "pip", "index", "versions", packageName, "--no-color", "--disable-pip-version-check" ); if (indexUrl is not null) { args = args.AddKeyedArgs("--index-url", ["--index-url", indexUrl]); } var result = await ProcessRunner .GetProcessResultAsync(PythonPath, args, WorkingDirectory?.FullPath, EnvironmentVariables) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip index failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } if ( string.IsNullOrEmpty(result.StandardOutput) || result .StandardOutput!.SplitLines() .Any(l => l.StartsWith("ERROR: No matching distribution found")) ) { return null; } return PipIndexResult.Parse(result.StandardOutput); } /// /// Run a custom install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task CustomInstall(ProcessArgs args, Action? outputDataReceived = null) { // Record output for errors var output = new StringBuilder(); var outputAction = outputDataReceived == null ? null : new Action(s => { Logger.Debug($"Install output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived(s); }); RunDetached(args, outputAction); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"install script failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a command using the venv Python executable and return the result. /// /// Arguments to pass to the Python executable. public async Task Run(ProcessArgs arguments) { // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { if (s == null) return; Logger.Debug("Pip output: {Text}", s); output.Append(s); }); SetPyvenvCfg(BaseInstall.RootPath); using var process = ProcessRunner.StartProcess( PythonPath, arguments, WorkingDirectory?.FullPath, outputAction, EnvironmentVariables ); await process.WaitForExitAsync().ConfigureAwait(false); return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = output.ToString() }; } [MemberNotNull(nameof(Process))] public void RunDetached( ProcessArgs args, Action? outputDataReceived, Action? onExit = null, bool unbuffered = true ) { var arguments = args.ToString(); if (!PythonPath.Exists) { throw new FileNotFoundException("Venv python not found", PythonPath); } SetPyvenvCfg(BaseInstall.RootPath); Logger.Info( "Launching venv process [{PythonPath}] " + "in working directory [{WorkingDirectory}] with args {Arguments}", PythonPath, WorkingDirectory?.ToString(), arguments ); var filteredOutput = outputDataReceived == null ? null : new Action(s => { if (SuppressOutput.Any(s.Text.Contains)) { Logger.Info("Filtered output: {S}", s); return; } outputDataReceived.Invoke(s); }); var env = EnvironmentVariables; // Disable pip caching - uses significant memory for large packages like torch // env["PIP_NO_CACHE_DIR"] = "true"; // On windows, add portable git to PATH and binary as GIT if (Compat.IsWindows) { var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var venvBin = RootPath.JoinDir(RelativeBinPath); if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem( "PATH", Compat.GetEnvPathWithExtensions(portableGitBin, venvBin, pathValue) ); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(portableGitBin, venvBin)); } env = env.SetItem("GIT", portableGitBin.JoinFile("git.exe")); } else { if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(pathValue)); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions()); } } if (unbuffered) { env = env.SetItem("PYTHONUNBUFFERED", "1"); // If arguments starts with -, it's a flag, insert `u` after it for unbuffered mode if (arguments.StartsWith('-')) { arguments = arguments.Insert(1, "u"); } // Otherwise insert -u at the beginning else { arguments = "-u " + arguments; } } Logger.Info("PATH: {Path}", env["PATH"]); Process = ProcessRunner.StartAnsiProcess( PythonPath, arguments, workingDirectory: WorkingDirectory?.FullPath, outputDataReceived: filteredOutput, environmentVariables: env ); if (onExit != null) { Process.EnableRaisingEvents = true; Process.Exited += (sender, _) => { onExit((sender as AnsiProcess)?.ExitCode ?? -1); }; } } /// /// Get entry points for a package. /// https://packaging.python.org/en/latest/specifications/entry-points/#entry-points /// public async Task GetEntryPoint(string entryPointName) { // ReSharper disable once StringLiteralTypo var code = $""" from importlib.metadata import entry_points results = entry_points(group='console_scripts', name='{entryPointName}') print(tuple(results)[0].value, end='') """; var result = await Run($"-c \"{code}\"").ConfigureAwait(false); if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StandardOutput)) { return result.StandardOutput; } return null; } /// /// Kills the running process and cancels stream readers, does not wait for exit. /// public void Dispose() { if (Process is not null) { Process.CancelStreamReaders(); Process.Kill(true); Process.Dispose(); } Process = null; GC.SuppressFinalize(this); } /// /// Kills the running process, waits for exit. /// public async ValueTask DisposeAsync() { if (Process is { HasExited: false }) { Process.Kill(true); try { await Process.WaitForExitAsync(new CancellationTokenSource(5000).Token).ConfigureAwait(false); } catch (OperationCanceledException e) { Logger.Warn(e, "Venv Process did not exit in time in DisposeAsync"); Process.CancelStreamReaders(); } } Process = null; GC.SuppressFinalize(this); } ~PyVenvRunner() { Dispose(); } } ================================================ FILE: StabilityMatrix.Core/Python/PyVersion.cs ================================================ using System; using System.Text.RegularExpressions; namespace StabilityMatrix.Core.Python; /// /// Represents a Python version /// public readonly struct PyVersion : IEquatable, IComparable { /// /// Major version number /// public int Major { get; } /// /// Minor version number /// public int Minor { get; } /// /// Micro/patch version number /// public int Micro { get; } /// /// Creates a new PyVersion /// public PyVersion(int major, int minor, int micro) { Major = major; Minor = minor; Micro = micro; } /// /// Parses a version string in the format "major.minor.micro" /// public static PyVersion Parse(string versionString) { var parts = versionString.Split('.'); if (parts.Length is < 2 or > 3) { throw new ArgumentException($"Invalid version format: {versionString}", nameof(versionString)); } if (!int.TryParse(parts[0], out var major) || !int.TryParse(parts[1], out var minor)) { throw new ArgumentException($"Invalid version format: {versionString}", nameof(versionString)); } var micro = 0; if (parts.Length <= 2) return new PyVersion(major, minor, micro); if (!int.TryParse(parts[2], out micro)) { throw new ArgumentException($"Invalid version format: {versionString}", nameof(versionString)); } return new PyVersion(major, minor, micro); } /// /// Tries to parse a version string /// public static bool TryParse(string versionString, out PyVersion version) { try { version = Parse(versionString); return true; } catch { version = default; return false; } } // Inside PyVersion.cs (or a new PyVersionParser.cs utility class) public static bool TryParseFromComplexString(string versionString, out PyVersion version) { version = default; if (string.IsNullOrWhiteSpace(versionString)) return false; // Regex to capture major.minor.micro and optional pre-release (e.g., a6, rc1) // It tries to be greedy on the numeric part. var match = Regex.Match( versionString, @"^(?\d+)(?:\.(?\d+))?(?:\.(?\d+))?(?:[a-zA-Z]+\d*)?$" ); if (!match.Success) return false; if (!int.TryParse(match.Groups["major"].Value, out var major)) return false; var minor = 0; if (match.Groups["minor"].Success && !string.IsNullOrEmpty(match.Groups["minor"].Value)) { if (!int.TryParse(match.Groups["minor"].Value, out minor)) return false; } var micro = 0; if (match.Groups["micro"].Success && !string.IsNullOrEmpty(match.Groups["micro"].Value)) { if (!int.TryParse(match.Groups["micro"].Value, out micro)) return false; } version = new PyVersion(major, minor, micro); return true; } /// /// Returns the version as a string in the format "major.minor.micro" /// public override string ToString() => $"{Major}.{Minor}.{Micro}"; /// /// Checks if this version equals another version /// public bool Equals(PyVersion other) => Major == other.Major && Minor == other.Minor && Micro == other.Micro; /// /// Compares this version to another version /// public int CompareTo(PyVersion other) { var majorComparison = Major.CompareTo(other.Major); if (majorComparison != 0) return majorComparison; var minorComparison = Minor.CompareTo(other.Minor); if (minorComparison != 0) return minorComparison; return Micro.CompareTo(other.Micro); } /// /// Checks if this version equals another object /// public override bool Equals(object? obj) => obj is PyVersion other && Equals(other); /// /// Gets a hash code for this version /// public override int GetHashCode() => HashCode.Combine(Major, Minor, Micro); public static bool operator ==(PyVersion left, PyVersion right) => left.Equals(right); public static bool operator !=(PyVersion left, PyVersion right) => !left.Equals(right); public static bool operator <(PyVersion left, PyVersion right) => left.CompareTo(right) < 0; public static bool operator <=(PyVersion left, PyVersion right) => left.CompareTo(right) <= 0; public static bool operator >(PyVersion left, PyVersion right) => left.CompareTo(right) > 0; public static bool operator >=(PyVersion left, PyVersion right) => left.CompareTo(right) >= 0; public string StringValue => $"{Major}.{Minor}.{Micro}"; } ================================================ FILE: StabilityMatrix.Core/Python/QueryTclTkLibraryResult.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Python; public record QueryTclTkLibraryResult(string? TclLibrary, string? TkLibrary); [JsonSerializable(typeof(QueryTclTkLibraryResult))] internal partial class QueryTclTkLibraryResultJsonContext : JsonSerializerContext; ================================================ FILE: StabilityMatrix.Core/Python/UvInstallArgs.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Text.RegularExpressions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; /// /// Builds arguments for 'uv pip install' commands. /// [SuppressMessage("ReSharper", "StringLiteralTypo")] public record UvInstallArgs : ProcessArgsBuilder { public UvInstallArgs(params Argument[] arguments) : base(arguments) { } /// /// Adds the Torch package. /// /// Optional version specifier (e.g., "==2.1.0+cu118", ">=2.0"). public UvInstallArgs WithTorch(string versionSpecifier = "") => this.AddArg(UvPackageSpecifier.Parse($"torch{versionSpecifier}")); /// /// Adds the Torch-DirectML package. /// /// Optional version specifier. public UvInstallArgs WithTorchDirectML(string versionSpecifier = "") => this.AddArg(UvPackageSpecifier.Parse($"torch-directml{versionSpecifier}")); /// /// Adds the TorchVision package. /// /// Optional version specifier. public UvInstallArgs WithTorchVision(string versionSpecifier = "") => this.AddArg(UvPackageSpecifier.Parse($"torchvision{versionSpecifier}")); /// /// Adds the TorchAudio package. /// /// Optional version specifier. public UvInstallArgs WithTorchAudio(string versionSpecifier = "") => this.AddArg(UvPackageSpecifier.Parse($"torchaudio{versionSpecifier}")); /// /// Adds the xFormers package. /// /// Optional version specifier. public UvInstallArgs WithXFormers(string versionSpecifier = "") => this.AddArg(UvPackageSpecifier.Parse($"xformers{versionSpecifier}")); /// /// Adds an extra index URL. /// uv equivalent: --extra-index-url <URL> /// /// The URL of the extra index. public UvInstallArgs WithExtraIndex(string indexUrl) => this.AddKeyedArgs("--extra-index-url", ["--extra-index-url", indexUrl]); /// /// Adds the PyTorch specific extra index URL. /// /// The PyTorch index variant (e.g., "cu118", "cu121", "cpu"). public UvInstallArgs WithTorchExtraIndex(string torchIndexVariant) => WithExtraIndex($"https://download.pytorch.org/whl/{torchIndexVariant}"); /// /// Parses package specifiers from a requirements.txt-formatted string. /// Lines starting with '#' are ignored. Inline comments are removed. /// /// The string content of a requirements.txt file. /// Optional regex pattern to exclude packages by name. public UvInstallArgs WithParsedFromRequirementsTxt( string requirements, [StringSyntax(StringSyntaxAttribute.Regex)] string? excludePattern = null ) { var lines = requirements .SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .Where(s => !s.StartsWith('#')) .Select(s => s.Contains('#') ? s.Substring(0, s.IndexOf('#')).Trim() : s.Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)); var argumentsToAdd = new List(); Regex? excludeRegex = null; if (excludePattern is not null) { excludeRegex = new Regex($"^{excludePattern}$", RegexOptions.Compiled); } foreach (var line in lines) { try { var specifier = UvPackageSpecifier.Parse(line); if ( excludeRegex is not null && specifier.Name is not null && excludeRegex.IsMatch(specifier.Name) ) { continue; } argumentsToAdd.Add(specifier); // Implicit conversion to Argument } catch (ArgumentException ex) { // Line is not a valid UvPackageSpecifier according to UvPackageSpecifier.Parse. // This could be a pip command/option (e.g., flags like --no-cache-dir, -r other.txt, -e path). // If the line starts with a hyphen, treat it as a command-line option directly. var trimmedLine = line.Trim(); if (trimmedLine.StartsWith("-")) { // Add as a raw argument. ProcessArgsBuilder usually handles splitting if it's like "--key value". // Or it could be a simple flag like "--no-deps". argumentsToAdd.Add(new Argument(trimmedLine)); } else { // Log or handle other unparseable lines if necessary. For now, skipping non-flag unparseable lines. // Logger.Warn($"Skipping unparseable line in requirements: {line}. Exception: {ex.Message}"); } } } return this.AddArgs(argumentsToAdd.ToArray()); } /// /// Applies user-defined overrides to the package specifiers. /// /// A list of package specifier overrides. public UvInstallArgs WithUserOverrides(List overrides) { var newArgs = this; foreach (var uvOverride in overrides) { if (string.IsNullOrWhiteSpace(uvOverride.Name)) continue; // Special handling for index URLs, ensuring constraint is treated as assignment if (uvOverride.Name is "--extra-index-url" or "--index-url") { uvOverride.Constraint = "="; // Or ensure ToArgument() for these produces correct format. } var uvOverrideArg = uvOverride.ToArgument(); if (uvOverride.Action is UvPackageSpecifierOverrideAction.Update) { newArgs = newArgs.RemoveUvArgKey(uvOverrideArg.Key ?? uvOverrideArg.Value); newArgs = newArgs.AddArg(uvOverrideArg); } else if (uvOverride.Action is UvPackageSpecifierOverrideAction.Remove) { newArgs = newArgs.RemoveUvArgKey(uvOverrideArg.Key ?? uvOverrideArg.Value); } } return newArgs; } public UvInstallArgs WithUserOverrides(List overrides) { var newArgs = this; foreach (var uvOverride in overrides) { if (string.IsNullOrWhiteSpace(uvOverride.Name)) continue; // Special handling for index URLs, ensuring constraint is treated as assignment if (uvOverride.Name is "--extra-index-url" or "--index-url") { uvOverride.Constraint = "="; } var uvOverrideArg = uvOverride.ToArgument(); if (uvOverride.Action is PipPackageSpecifierOverrideAction.Update) { newArgs = newArgs.RemoveUvArgKey(uvOverrideArg.Key ?? uvOverrideArg.Value); newArgs = newArgs.AddArg(uvOverrideArg); } else if (uvOverride.Action is PipPackageSpecifierOverrideAction.Remove) { newArgs = newArgs.RemoveUvArgKey(uvOverrideArg.Key ?? uvOverrideArg.Value); } } return newArgs; } /// /// Removes an argument or package specifier by its key. /// For packages, the key is typically the package name. /// [Pure] public UvInstallArgs RemoveUvArgKey(string argumentKey) { return this with { Arguments = Arguments .Where( arg => arg.HasKey ? (arg.Key != argumentKey) : ( arg.Value != argumentKey && !( arg.Value.StartsWith($"{argumentKey}==") || arg.Value.StartsWith($"{argumentKey}~=") || arg.Value.StartsWith($"{argumentKey}>=") || arg.Value.StartsWith($"{argumentKey}<=") || arg.Value.StartsWith($"{argumentKey}!=") || arg.Value.StartsWith($"{argumentKey}>") || arg.Value.StartsWith($"{argumentKey}<") ) ) ) .ToImmutableList() }; } /// public override string ToString() { // Prepends "pip install" to the arguments for clarity if used directly as a command string. // However, UvManager will call "uv" with "pip install" and then these arguments. // So, the base.ToString() which just joins arguments is usually what's needed by UvManager. return base.ToString(); } } ================================================ FILE: StabilityMatrix.Core/Python/UvManager.cs ================================================ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Text.Unicode; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Python; [RegisterSingleton] public partial class UvManager : IUvManager { private readonly ISettingsManager settingsManager; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly JsonSerializerOptions JsonSettings = new() { Converters = { new JsonStringEnumConverter() }, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), }; private string? uvExecutablePath; private DirectoryPath? uvPythonInstallPath; // Regex to parse lines from 'uv python list' // Example lines: // cpython@3.10.13 (installed at /home/user/.local/share/uv/python/cpython-3.10.13-x86_64-unknown-linux-gnu) // cpython@3.11.7 // pypy@3.9.18 // More complex if it includes source/arch/os: // cpython@3.12.2 x86_64-unknown-linux-gnu (installed at /path) // We need a flexible regex. Let's assume a structure like: // [optional_arch] [optional_os] [(installed at )] // Or simpler from newer uv versions: // 3.10.13 cpython x86_64-unknown-linux-gnu (installed at /path) // 3.11.7 cpython x86_64-unknown-linux-gnu private static readonly Regex UvPythonListRegex = UvListRegex(); public UvManager(ISettingsManager settingsManager) { this.settingsManager = settingsManager; if (!settingsManager.IsLibraryDirSet) return; uvPythonInstallPath = new DirectoryPath(settingsManager.LibraryDir, "Assets", "Python"); uvExecutablePath = Path.Combine( settingsManager.LibraryDir, "Assets", "uv", Compat.IsWindows ? "uv.exe" : "uv" ); Logger.Debug($"UvManager initialized with uv executable path: {uvExecutablePath}"); } public async Task IsUvAvailableAsync(CancellationToken cancellationToken = default) { try { uvExecutablePath ??= Path.Combine( settingsManager.LibraryDir, "Assets", "uv", Compat.IsWindows ? "uv.exe" : "uv" ); var result = await ProcessRunner .GetAnsiProcessResultAsync( uvExecutablePath, ["--version"], cancellationToken: cancellationToken ) .ConfigureAwait(false); return result.IsSuccessExitCode; } catch (Exception ex) { Logger.Warn( ex, $"UV availability check failed for path '{uvExecutablePath}'. UV might not be installed or accessible." ); return false; } } /// /// Lists Python distributions known to UV. /// /// If true, only lists Pythons UV reports as installed. /// Optional callback for console output. /// Cancellation token. /// A list of UvPythonInfo objects. public async Task> ListAvailablePythonsAsync( bool installedOnly = false, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { uvPythonInstallPath ??= new DirectoryPath(settingsManager.LibraryDir, "Assets", "Python"); uvExecutablePath ??= Path.Combine( settingsManager.LibraryDir, "Assets", "uv", Compat.IsWindows ? "uv.exe" : "uv" ); var args = new ProcessArgsBuilder("python", "list", "--managed-python", "--output-format", "json"); if (settingsManager.Settings.ShowAllAvailablePythonVersions) { args = args.AddArg("--all-versions"); } var envVars = new Dictionary { // Always use the centrally configured path ["UV_PYTHON_INSTALL_DIR"] = uvPythonInstallPath, }; var uvDirectory = Path.GetDirectoryName(uvExecutablePath); var result = await ProcessRunner .GetProcessResultAsync(uvExecutablePath, args, uvDirectory, envVars, useUtf8Encoding: true) .ConfigureAwait(false); if (!result.IsSuccessExitCode) { Logger.Error( $"Failed to list UV Python versions. Exit Code: {result.ExitCode}. Error: {result.StandardError}" ); return []; } var pythons = new List(); var json = result.StandardOutput; if (string.IsNullOrWhiteSpace(json)) { Logger.Warn("UV Python list output is empty or null."); return pythons.AsReadOnly(); } var uvPythonListEntries = JsonSerializer.Deserialize>(json, JsonSettings); if (uvPythonListEntries == null) { Logger.Warn("Failed to deserialize UV Python list output."); return pythons.AsReadOnly(); } var filteredPythons = uvPythonListEntries .Where(e => e.Path == null || e.Path.StartsWith(uvPythonInstallPath)) .Where(e => settingsManager.Settings.ShowAllAvailablePythonVersions || (!e.Version.Contains("a") && !e.Version.Contains("b")) ) .Select(e => new UvPythonInfo { InstallPath = Path.GetDirectoryName(e.Path) ?? string.Empty, Version = e.VersionParts, Architecture = e.Arch, IsInstalled = e.Path != null, Key = e.Key, Os = e.Os.ToLowerInvariant(), Source = e.Implementation.ToLowerInvariant(), Libc = e.Libc, Variant = e.Variant, }); pythons.AddRange(filteredPythons); return pythons.AsReadOnly(); } /// /// Gets information about a specific installed Python version managed by UV. /// public async Task GetInstalledPythonAsync( PyVersion version, CancellationToken cancellationToken = default ) { var installedPythons = await ListAvailablePythonsAsync( installedOnly: true, cancellationToken: cancellationToken ) .ConfigureAwait(false); // Find best match (exact or major.minor with highest patch) var exactMatch = installedPythons.FirstOrDefault(p => p.IsInstalled && p.Version == version); if (exactMatch is { IsInstalled: true }) return exactMatch; // Struct default is not null return installedPythons .Where(p => p.IsInstalled && p.Version.Major == version.Major && p.Version.Minor == version.Minor) .OrderByDescending(p => p.Version.Micro) .FirstOrDefault(); } /// /// Installs a specific Python version using UV. /// /// Python version to install (e.g., "3.10" or "3.10.13"). /// Optional callback for console output. /// Cancellation token. /// UvPythonInfo for the installed Python, or null if installation failed or info couldn't be retrieved. public async Task InstallPythonVersionAsync( PyVersion version, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { uvPythonInstallPath ??= new DirectoryPath(settingsManager.LibraryDir, "Assets", "Python"); uvExecutablePath ??= Path.Combine( settingsManager.LibraryDir, "Assets", "uv", Compat.IsWindows ? "uv.exe" : "uv" ); var versionString = $"{version.Major}.{version.Minor}.{version.Micro}"; if (version.Micro == 0) { versionString = $"{version.Major}.{version.Minor}"; } var args = new ProcessArgsBuilder("python", "install", versionString); var envVars = new Dictionary { // Always use the centrally configured path ["UV_PYTHON_INSTALL_DIR"] = uvPythonInstallPath, }; Logger.Debug( $"Setting UV_PYTHON_INSTALL_DIR to central path '{uvPythonInstallPath}' for Python {versionString} installation." ); Directory.CreateDirectory(uvPythonInstallPath); var processResult = await ProcessRunner .GetAnsiProcessResultAsync( uvExecutablePath, args, environmentVariables: envVars, outputDataReceived: onConsoleOutput, cancellationToken: cancellationToken ) .ConfigureAwait(false); if (!processResult.IsSuccessExitCode) { /* Log error */ return null; } Logger.Info($"UV install command completed for Python {versionString}. Verifying..."); // Verification Strategy 1: Use GetInstalledPythonAsync var installedPythonInfo = await GetInstalledPythonAsync(version, cancellationToken) .ConfigureAwait(false); if ( installedPythonInfo is { IsInstalled: true } && !string.IsNullOrWhiteSpace(installedPythonInfo.Value.InstallPath) ) { var verifiedInstall = new PyInstallation( installedPythonInfo.Value.Version, installedPythonInfo.Value.InstallPath ); if (verifiedInstall.Exists()) { Logger.Info( $"Verified install via GetInstalledPythonAsync: {installedPythonInfo.Value.Version} at {installedPythonInfo.Value.InstallPath}" ); return installedPythonInfo.Value; } Logger.Warn( $"GetInstalledPythonAsync found path {installedPythonInfo.Value.InstallPath} but PyInstallation.Exists() failed." ); } else { Logger.Warn( $"Could not find Python {version} via GetInstalledPythonAsync after install command." ); } // Verification Strategy 2 (Fallback): Look inside the known parent directory Logger.Debug($"Attempting fallback path discovery in central directory: {uvPythonInstallPath}"); try { var subdirectories = Directory.GetDirectories(uvPythonInstallPath); var potentialDirs = subdirectories .Select(dir => new { Path = dir, DirInfo = new DirectoryInfo(dir) }) .Where(x => x.DirInfo.Name.StartsWith("cpython-", StringComparison.OrdinalIgnoreCase) || x.DirInfo.Name.StartsWith("pypy-", StringComparison.OrdinalIgnoreCase) ) .Where(x => x.DirInfo.Name.Contains($"{version.Major}.{version.Minor}")) .OrderByDescending(x => x.DirInfo.CreationTimeUtc) .ToList(); foreach (var potentialDir in potentialDirs) { var actualInstallPath = potentialDir.Path; var pyInstallCheck = new PyInstallation(version, actualInstallPath); if (!pyInstallCheck.Exists()) continue; Logger.Info($"Fallback discovery found likely installation at: {actualInstallPath}"); var inferredKey = Path.GetFileName(actualInstallPath); var inferredSource = inferredKey.Split('-')[0]; return new UvPythonInfo( version, actualInstallPath, true, inferredSource, null, null, inferredKey, null, null ); } } catch (Exception ex) { Logger.Error(ex, $"Error during fallback path discovery in {uvPythonInstallPath}"); } Logger.Error($"Failed to verify and locate Python {version} after UV install command."); return null; } [GeneratedRegex( @"^\s*(?[a-zA-Z0-9_.-]+(?:[\+\-][a-zA-Z0-9_.-]+)?)\s+(?.+)\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US" )] private static partial Regex UvListRegex(); } ================================================ FILE: StabilityMatrix.Core/Python/UvPackageSpecifier.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; public partial record UvPackageSpecifier { [JsonIgnore] public static IReadOnlyList ConstraintOptions => ["", "==", "~=", ">=", "<=", ">", "<"]; public string? Name { get; set; } public string? Constraint { get; set; } public string? Version { get; set; } public string? VersionConstraint => Constraint is null || Version is null ? null : Constraint + Version; public static UvPackageSpecifier Parse(string value) { var result = TryParse(value, true, out var packageSpecifier); Debug.Assert(result); return packageSpecifier!; } public static bool TryParse(string value, [NotNullWhen(true)] out UvPackageSpecifier? packageSpecifier) { return TryParse(value, false, out packageSpecifier); } private static bool TryParse( string value, bool throwOnFailure, [NotNullWhen(true)] out UvPackageSpecifier? packageSpecifier ) { // uv allows for more complex specifiers, including URLs and path specifiers directly. // For now, this regex focuses on PyPI-style name and version specifiers. // Enhancements could be made here to support git URLs, local paths, etc. if needed. var match = PackageSpecifierRegex().Match(value); if (!match.Success) { // Check if it's a URL or path-like specifier (basic check) // uv supports these directly. For simplicity, we'll treat them as a Name-only specifier for now. if ( Uri.IsWellFormedUriString(value, UriKind.Absolute) || value.Contains(Path.DirectorySeparatorChar) || value.Contains(Path.AltDirectorySeparatorChar) ) { packageSpecifier = new UvPackageSpecifier { Name = value }; return true; } if (throwOnFailure) { throw new ArgumentException($"Invalid or unsupported package specifier for uv: {value}"); } packageSpecifier = null; return false; } packageSpecifier = new UvPackageSpecifier { Name = match.Groups["package_name"].Value, Constraint = match.Groups["version_constraint"].Value, // Will be empty string if no constraint Version = match.Groups["version"].Value // Will be empty string if no version }; // Ensure Constraint and Version are null if they were empty strings from regex. if (string.IsNullOrEmpty(packageSpecifier.Constraint)) packageSpecifier.Constraint = null; if (string.IsNullOrEmpty(packageSpecifier.Version)) packageSpecifier.Version = null; return true; } /// public override string ToString() { if (Name is null) return string.Empty; return Name + (VersionConstraint ?? string.Empty); } public Argument ToArgument() { if (Name is null) { return new Argument(""); } // Handle URL or path specifiers - they are typically just the value itself. if ( Uri.IsWellFormedUriString(Name, UriKind.Absolute) || Name.Contains(Path.DirectorySeparatorChar) || Name.Contains(Path.AltDirectorySeparatorChar) ) { return new Argument(ProcessRunner.Quote(Name)); // Ensure paths with spaces are quoted } // Normal package specifier with optional version constraint if (VersionConstraint is not null) { // Use Name as key to allow for potential overrides if the builder uses keys // Otherwise, it's just value. For uv install, it's usually just the full string "package==version". return new Argument(key: Name, value: ToString()); } // Handles cases like "--extra-index-url " or other flags passed as package names. // This logic might be more relevant for a generic ArgsBuilder than for a package specifier directly, // unless these are passed in a requirements file and parsed this way. if (Name.Trim().StartsWith('-')) { var parts = Name.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 1) { var key = parts[0]; // Re-join parts, quoting each if necessary (though Name should be the first part here) // This specific case might be better handled by the ArgsBuilder itself. // For a UvPackageSpecifier, if Name starts with '-', it's usually a single argument value (e.g. from req.txt). return Argument.Quoted(key, Name); // Or simply new Argument(Name) if it's a single directive } } return new Argument(ToString()); } public static implicit operator Argument(UvPackageSpecifier specifier) { return specifier.ToArgument(); } public static implicit operator UvPackageSpecifier(string specifier) { return Parse(specifier); } /// /// Regex to match a pip/uv package specifier with name and optional version. /// Does not explicitly match URLs or file paths, those are handled as a fallback. /// (?i) for case-insensitive package names, though PyPI is case-insensitive in practice. /// [GeneratedRegex( @"^(?[a-zA-Z0-9_.-]+)(?:(?[~><=!]=?|[><])\s*(?[a-zA-Z0-9_.*+-]+))?$", RegexOptions.CultureInvariant | RegexOptions.Compiled )] private static partial Regex PackageSpecifierRegex(); } ================================================ FILE: StabilityMatrix.Core/Python/UvPackageSpecifierOverride.cs ================================================ using System.Text.Json.Serialization; namespace StabilityMatrix.Core.Python; public record UvPackageSpecifierOverride : UvPackageSpecifier { public UvPackageSpecifierOverrideAction Action { get; init; } = UvPackageSpecifierOverrideAction.Update; [JsonIgnore] public bool IsUpdate => Action is UvPackageSpecifierOverrideAction.Update; /// public override string ToString() { // The base ToString() from UvPackageSpecifier should be sufficient as it already formats // the package name and version constraint (e.g., "package_name==1.0.0"). // The Action property influences how this specifier is used by an ArgsBuilder, // rather than its string representation as a package. return base.ToString(); } } ================================================ FILE: StabilityMatrix.Core/Python/UvPackageSpecifierOverrideAction.cs ================================================ namespace StabilityMatrix.Core.Python; public enum UvPackageSpecifierOverrideAction { None, Update, Remove } ================================================ FILE: StabilityMatrix.Core/Python/UvPythonInfo.cs ================================================ namespace StabilityMatrix.Core.Python; /// /// Represents information about a Python installation as discovered or managed by UV. /// public readonly record struct UvPythonInfo( PyVersion Version, string InstallPath, // Full path to the root of the Python installation bool IsInstalled, // True if UV reports it as installed string? Source, // e.g., "cpython", "pypy" - from 'uv python list', aka implementation string? Architecture, // e.g., "x86_64" - from 'uv python list' string? Os, // e.g., "unknown-linux-gnu" - from 'uv python list' string? Key, // The unique key/name uv uses, e.g., "cpython@3.10.13" or "3.10.13", string? Variant, // default/freethreaded string? Libc // gnu/musl/gnueabi/gnueabihf/musl/none ); ================================================ FILE: StabilityMatrix.Core/Python/UvPythonListEntry.cs ================================================ using System.Text.Json.Serialization; using NLog; namespace StabilityMatrix.Core.Python; public class UvPythonListEntry { public required string Key { get; set; } public required string Version { get; set; } public string? Path { get; set; } public string? Symlink { get; set; } public Uri? Url { get; set; } public string Os { get; set; } public string Variant { get; set; } public string Implementation { get; set; } public string Arch { get; set; } public string Libc { get; set; } [JsonIgnore] public PyVersion VersionParts { get { if (string.IsNullOrWhiteSpace(Version)) return new PyVersion(0, 0, 0); if (Version.Contains("a")) { // substring to exclude everything after the first "a" (including the first "a") var version = Version.Substring(0, Version.IndexOf("a", StringComparison.OrdinalIgnoreCase)); return PyVersion.Parse(version); } if (Version.Contains("b")) { // substring to exclude everything after the first "b" (including the first "b") var version = Version.Substring(0, Version.IndexOf("b", StringComparison.OrdinalIgnoreCase)); return PyVersion.Parse(version); } if (Version.Contains("rc")) { // substring to exclude everything after the first "rc" (including the first "rc") var version = Version.Substring(0, Version.IndexOf("rc", StringComparison.OrdinalIgnoreCase)); return PyVersion.Parse(version); } try { return PyVersion.Parse(Version); } catch (Exception e) { LogManager .GetCurrentClassLogger() .Error(e, "Failed to parse Python version: {Version}", Version); return new PyVersion(0, 0, 0); } } } [JsonIgnore] public bool IsPrerelease => Version.Contains("a") || Version.Contains("b") || Version.Contains("rc"); } ================================================ FILE: StabilityMatrix.Core/Python/UvVenvRunner.cs ================================================ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using NLog; using Salaros.Configuration; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; public class UvVenvRunner : IPyVenvRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private string? lastSetPyvenvCfgPath; /// /// Relative path to the site-packages folder from the venv root. /// This is platform specific. /// public static string GetRelativeSitePackagesPath(PyVersion? version = null) { var minorVersion = version?.Minor ?? 10; return Compat.Switch( (PlatformKind.Windows, "Lib/site-packages"), (PlatformKind.Unix, $"lib/python3.{minorVersion}/site-packages") ); } /// /// Legacy path for compatibility /// public static string RelativeSitePackagesPath => Compat.Switch( (PlatformKind.Windows, "Lib/site-packages"), (PlatformKind.Unix, "lib/python3.10/site-packages") ); public PyBaseInstall BaseInstall { get; } /// /// The process running the python executable. /// public AnsiProcess? Process { get; private set; } /// /// The path to the venv root directory. /// public DirectoryPath RootPath { get; } /// /// Optional working directory for the python process. /// public DirectoryPath? WorkingDirectory { get; set; } /// /// Optional environment variables for the python process. /// public ImmutableDictionary EnvironmentVariables { get; set; } = ImmutableDictionary.Empty; /// /// Name of the python binary folder. /// 'Scripts' on Windows, 'bin' on Unix. /// public static string RelativeBinPath => Compat.Switch((PlatformKind.Windows, "Scripts"), (PlatformKind.Unix, "bin")); /// /// The relative path to the python executable. /// public static string RelativePythonPath => Compat.Switch( (PlatformKind.Windows, Path.Combine("Scripts", "python.exe")), (PlatformKind.Unix, Path.Combine("bin", "python3")) ); /// /// The full path to the python executable. /// public FilePath PythonPath => RootPath.JoinFile(RelativePythonPath); /// /// The relative path to the pip executable. /// public static string RelativePipPath => Compat.Switch( (PlatformKind.Windows, Path.Combine("Scripts", "pip.exe")), (PlatformKind.Unix, Path.Combine("bin", "pip3")) ); /// /// The full path to the pip executable. /// public FilePath PipPath => RootPath.JoinFile(RelativePipPath); /// /// The Python version of this venv /// public PyVersion Version => BaseInstall.Version; /// /// List of substrings to suppress from the output. /// When a line contains any of these substrings, it will not be forwarded to callbacks. /// A corresponding Info log will be written instead. /// public List SuppressOutput { get; } = new() { "fatal: not a git repository" }; internal UvVenvRunner(PyBaseInstall baseInstall, DirectoryPath rootPath) { BaseInstall = baseInstall; RootPath = rootPath; EnvironmentVariables = EnvironmentVariables.SetItem("VIRTUAL_ENV", rootPath.FullPath); } public void UpdateEnvironmentVariables( Func, ImmutableDictionary> env ) { EnvironmentVariables = env(EnvironmentVariables); } /// True if the venv has a Scripts\python.exe file public bool Exists() => PythonPath.Exists; private FilePath UvExecutablePath => new(GlobalConfig.LibraryDir, "Assets", "uv", Compat.IsWindows ? "uv.exe" : "uv"); /// /// Creates a venv at the configured path. /// public async Task Setup( bool existsOk = false, Action? onConsoleOutput = null, CancellationToken cancellationToken = default ) { if (!existsOk && Exists()) { throw new InvalidOperationException("Venv already exists"); } // Create RootPath if it doesn't exist RootPath.Create(); // Create venv (copy mode if windows) var args = new ProcessArgsBuilder( "venv", RootPath.ToString(), "--allow-existing", "--python", BaseInstall.PythonExePath ); var venvProc = ProcessRunner.StartAnsiProcess( UvExecutablePath, args.ToProcessArgs(), WorkingDirectory?.FullPath, onConsoleOutput ); try { await venvProc.WaitForExitAsync(cancellationToken).ConfigureAwait(false); // Check return code if (venvProc.ExitCode != 0) { throw new ProcessException($"Venv creation failed with code {venvProc.ExitCode}"); } } catch (OperationCanceledException) { venvProc.CancelStreamReaders(); } finally { venvProc.Kill(); venvProc.Dispose(); } } /// /// Set current python path to pyvenv.cfg /// This should be called before using the venv, in case user moves the venv directory. /// private void SetPyvenvCfg(string pythonDirectory, bool force = false) { // Skip if we are not created yet if (!Exists()) return; // Skip if already set to same value if (lastSetPyvenvCfgPath == pythonDirectory && !force) return; // Path to pyvenv.cfg var cfgPath = Path.Combine(RootPath, "pyvenv.cfg"); if (!File.Exists(cfgPath)) { throw new FileNotFoundException("pyvenv.cfg not found", cfgPath); } Logger.Info("Updating pyvenv.cfg with embedded Python directory {PyDir}", pythonDirectory); // Insert a top section var topSection = "[top]" + Environment.NewLine; var cfg = new ConfigParser(topSection + File.ReadAllText(cfgPath)); // Need to set all path keys - home, base-prefix, base-exec-prefix, base-executable cfg.SetValue("top", "home", pythonDirectory); cfg.SetValue("top", "base-prefix", pythonDirectory); cfg.SetValue("top", "base-exec-prefix", pythonDirectory); cfg.SetValue( "top", "base-executable", Path.Combine(pythonDirectory, Compat.IsWindows ? "python.exe" : RelativePythonPath) ); // Convert to string for writing, strip the top section var cfgString = cfg.ToString()!.Replace(topSection, ""); File.WriteAllText(cfgPath, cfgString); // Update last set path lastSetPyvenvCfgPath = pythonDirectory; } /// /// Run a pip install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task PipInstall(ProcessArgs args, Action? outputDataReceived = null) { if (!File.Exists(UvExecutablePath)) { throw new FileNotFoundException("uv not found", UvExecutablePath); } SetPyvenvCfg(BaseInstall.RootPath); // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { Logger.Debug($"Pip output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived?.Invoke(s); }); RunUvDetached( args.Prepend(["pip", "install"]) .Concat(["--index-strategy", "unsafe-first-match", "--python", PythonPath.ToString()]), outputAction ); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a pip uninstall command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task PipUninstall(ProcessArgs args, Action? outputDataReceived = null) { if (!File.Exists(UvExecutablePath)) { throw new FileNotFoundException("uv not found", UvExecutablePath); } SetPyvenvCfg(BaseInstall.RootPath); // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { Logger.Debug($"Pip output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived?.Invoke(s); }); RunUvDetached( args.Prepend(["pip", "uninstall"]).Concat(["--python", PythonPath.ToString()]), outputAction ); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a pip list command, return results as PipPackageInfo objects. /// public async Task> PipList() { if (!File.Exists(UvExecutablePath)) { throw new FileNotFoundException("uv not found", UvExecutablePath); } SetPyvenvCfg(BaseInstall.RootPath); var result = await ProcessRunner .GetProcessResultAsync( UvExecutablePath, ["pip", "list", "--format=json", "--python", PythonPath.ToString()], WorkingDirectory?.FullPath, EnvironmentVariables ) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip list failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } // There may be warning lines before the Json line, or update messages after // Filter to find the first line that starts with [ var jsonLine = result .StandardOutput?.SplitLines( StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ) .Select(line => line.Trim()) .FirstOrDefault(line => line.StartsWith("[", StringComparison.OrdinalIgnoreCase) && line.EndsWith("]", StringComparison.OrdinalIgnoreCase) ); if (jsonLine is null) { return []; } return JsonSerializer.Deserialize>( jsonLine, PipPackageInfoSerializerContext.Default.Options ) ?? []; } /// /// Run a pip show command, return results as PipPackageInfo objects. /// public async Task PipShow(string packageName) { if (!File.Exists(UvExecutablePath)) { throw new FileNotFoundException("uv not found", UvExecutablePath); } SetPyvenvCfg(BaseInstall.RootPath); var result = await ProcessRunner .GetProcessResultAsync( UvExecutablePath, ["pip", "show", packageName, "--python", PythonPath.ToString()], WorkingDirectory?.FullPath, EnvironmentVariables ) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip show failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } if (result.StandardOutput!.StartsWith("WARNING: Package(s) not found:")) { return null; } return PipShowResult.Parse(result.StandardOutput); } /// /// Run a pip index command, return result as PipIndexResult. /// public async Task PipIndex(string packageName, string? indexUrl = null) { if (!File.Exists(PipPath)) { throw new FileNotFoundException("pip not found", PipPath); } SetPyvenvCfg(BaseInstall.RootPath); var args = new ProcessArgsBuilder( "-m", "pip", "index", "versions", packageName, "--no-color", "--disable-pip-version-check" ); if (indexUrl is not null) { args = args.AddKeyedArgs("--index-url", ["--index-url", indexUrl]); } var result = await ProcessRunner .GetProcessResultAsync(PythonPath, args, WorkingDirectory?.FullPath, EnvironmentVariables) .ConfigureAwait(false); // Check return code if (result.ExitCode != 0) { throw new ProcessException( $"pip index failed with code {result.ExitCode}: {result.StandardOutput}, {result.StandardError}" ); } if ( string.IsNullOrEmpty(result.StandardOutput) || result .StandardOutput!.SplitLines() .Any(l => l.StartsWith("ERROR: No matching distribution found")) ) { return null; } return PipIndexResult.Parse(result.StandardOutput); } /// /// Run a custom install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. /// public async Task CustomInstall(ProcessArgs args, Action? outputDataReceived = null) { // Record output for errors var output = new StringBuilder(); var outputAction = outputDataReceived == null ? null : new Action(s => { Logger.Debug($"Install output: {s.Text}"); // Record to output output.Append(s.Text); // Forward to callback outputDataReceived(s); }); RunDetached(args, outputAction); await Process.WaitForExitAsync().ConfigureAwait(false); // Check return code if (Process.ExitCode != 0) { throw new ProcessException( $"install script failed with code {Process.ExitCode}: {output.ToString().ToRepr()}" ); } } /// /// Run a command using the venv Python executable and return the result. /// /// Arguments to pass to the Python executable. public async Task Run(ProcessArgs arguments) { // Record output for errors var output = new StringBuilder(); var outputAction = new Action(s => { if (s == null) return; Logger.Debug("Pip output: {Text}", s); output.Append(s); }); SetPyvenvCfg(BaseInstall.RootPath); using var process = ProcessRunner.StartProcess( PythonPath, arguments, WorkingDirectory?.FullPath, outputAction, EnvironmentVariables ); await process.WaitForExitAsync().ConfigureAwait(false); return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = output.ToString() }; } [MemberNotNull(nameof(Process))] public void RunDetached( ProcessArgs args, Action? outputDataReceived, Action? onExit = null, bool unbuffered = true ) { var arguments = args.ToString(); if (!PythonPath.Exists) { throw new FileNotFoundException("Venv python not found", PythonPath); } SetPyvenvCfg(BaseInstall.RootPath); Logger.Info( "Launching venv process [{PythonPath}] " + "in working directory [{WorkingDirectory}] with args {Arguments}", PythonPath, WorkingDirectory?.ToString(), arguments ); var filteredOutput = outputDataReceived == null ? null : new Action(s => { if (SuppressOutput.Any(s.Text.Contains)) { Logger.Info("Filtered output: {S}", s); return; } outputDataReceived.Invoke(s); }); var env = EnvironmentVariables; // Disable pip caching - uses significant memory for large packages like torch // env["PIP_NO_CACHE_DIR"] = "true"; // On windows, add portable git to PATH and binary as GIT if (Compat.IsWindows) { var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var venvBin = RootPath.JoinDir(RelativeBinPath); var uvFolder = GlobalConfig.LibraryDir.JoinDir("Assets", "uv"); if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem( "PATH", Compat.GetEnvPathWithExtensions(portableGitBin, venvBin, uvFolder, pathValue) ); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(portableGitBin, uvFolder, venvBin)); } env = env.SetItem("GIT", portableGitBin.JoinFile("git.exe")); } else { if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(pathValue)); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions()); } } if (unbuffered) { env = env.SetItem("PYTHONUNBUFFERED", "1"); // If arguments starts with -, it's a flag, insert `u` after it for unbuffered mode if (arguments.StartsWith('-')) { arguments = arguments.Insert(1, "u"); } // Otherwise insert -u at the beginning else { arguments = "-u " + arguments; } } Logger.Info("PATH: {Path}", env["PATH"]); Process = ProcessRunner.StartAnsiProcess( PythonPath, arguments, workingDirectory: WorkingDirectory?.FullPath, outputDataReceived: filteredOutput, environmentVariables: env ); if (onExit != null) { Process.EnableRaisingEvents = true; Process.Exited += (sender, _) => { onExit((sender as AnsiProcess)?.ExitCode ?? -1); }; } } [MemberNotNull(nameof(Process))] private void RunUvDetached( ProcessArgs args, Action? outputDataReceived, Action? onExit = null ) { var arguments = args.ToString(); if (!UvExecutablePath.Exists) { throw new FileNotFoundException("uv not found", PythonPath); } Logger.Info( "Launching uv process [{UvExecutablePath}] " + "in working directory [{WorkingDirectory}] with args {Arguments}", UvExecutablePath, WorkingDirectory?.ToString(), arguments ); var filteredOutput = outputDataReceived == null ? null : new Action(s => { if (SuppressOutput.Any(s.Text.Contains)) { Logger.Info("Filtered output: {S}", s); return; } outputDataReceived.Invoke(s); }); var env = EnvironmentVariables; // Disable pip caching - uses significant memory for large packages like torch // env["PIP_NO_CACHE_DIR"] = "true"; // On windows, add portable git to PATH and binary as GIT if (Compat.IsWindows) { var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var venvBin = RootPath.JoinDir(RelativeBinPath); if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem( "PATH", Compat.GetEnvPathWithExtensions(portableGitBin, venvBin, pathValue) ); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(portableGitBin, venvBin)); } env = env.SetItem("GIT", portableGitBin.JoinFile("git.exe")); } else { if (env.TryGetValue("PATH", out var pathValue)) { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions(pathValue)); } else { env = env.SetItem("PATH", Compat.GetEnvPathWithExtensions()); } } Logger.Info("PATH: {Path}", env["PATH"]); Process = ProcessRunner.StartAnsiProcess( UvExecutablePath, arguments, workingDirectory: WorkingDirectory?.FullPath, outputDataReceived: filteredOutput, environmentVariables: env ); if (onExit != null) { Process.EnableRaisingEvents = true; Process.Exited += (sender, _) => { onExit((sender as AnsiProcess)?.ExitCode ?? -1); }; } } /// /// Get entry points for a package. /// https://packaging.python.org/en/latest/specifications/entry-points/#entry-points /// public async Task GetEntryPoint(string entryPointName) { // ReSharper disable once StringLiteralTypo var code = $""" from importlib.metadata import entry_points results = entry_points(group='console_scripts', name='{entryPointName}') print(tuple(results)[0].value, end='') """; var result = await Run($"-c \"{code}\"").ConfigureAwait(false); if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StandardOutput)) { return result.StandardOutput; } return null; } /// /// Kills the running process and cancels stream readers, does not wait for exit. /// public void Dispose() { if (Process is not null) { Process.CancelStreamReaders(); Process.Kill(true); Process.Dispose(); } Process = null; GC.SuppressFinalize(this); } /// /// Kills the running process, waits for exit. /// public async ValueTask DisposeAsync() { if (Process is { HasExited: false }) { Process.Kill(true); try { await Process.WaitForExitAsync(new CancellationTokenSource(5000).Token).ConfigureAwait(false); } catch (OperationCanceledException e) { Logger.Warn(e, "Venv Process did not exit in time in DisposeAsync"); Process.CancelStreamReaders(); } } Process = null; GC.SuppressFinalize(this); } ~UvVenvRunner() { Dispose(); } } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/DeviceIoControlCode.cs ================================================ namespace StabilityMatrix.Core.ReparsePoints; internal enum DeviceIoControlCode : uint { /// /// FSCTL_SET_REPARSE_POINT /// Command to set the reparse point data block. /// SetReparsePoint = 0x000900A4, /// /// FSCTL_GET_REPARSE_POINT /// Command to get the reparse point data block. /// GetReparsePoint = 0x000900A8, /// /// FSCTL_DELETE_REPARSE_POINT /// Command to delete the reparse point data base. /// DeleteReparsePoint = 0x000900AC, /// /// IO_REPARSE_TAG_MOUNT_POINT /// Reparse point tag used to identify mount points and junction points. /// ReparseTagMountPoint = 0xA0000003, } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Junction.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using Microsoft.Win32.SafeHandles; namespace StabilityMatrix.Core.ReparsePoints; [SupportedOSPlatform("windows")] public static class Junction { /// /// This prefix indicates to NTFS that the path is to be treated as a non-interpreted /// path in the virtual file system. /// private const string NonInterpretedPathPrefix = @"\??\"; [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr CreateFile( [MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [MarshalAs(UnmanagedType.U4)] Win32FileAccess dwDesiredAccess, [MarshalAs(UnmanagedType.U4)] Win32FileShare dwShareMode, IntPtr lpSecurityAttributes, [MarshalAs(UnmanagedType.U4)] Win32CreationDisposition dwCreationDisposition, [MarshalAs(UnmanagedType.U4)] Win32FileAttribute dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, [In] IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, [Out] out uint lpBytesReturned, IntPtr lpOverlapped); /// /// Creates a junction point from the specified directory to the specified target directory. /// /// The junction point path /// The target directory (Must already exist) /// If true overwrites an existing reparse point or empty directory /// Thrown when the junction point could not be created or when /// an existing directory was found and if false public static void Create(string junctionPoint, string targetDir, bool overwrite) { targetDir = Path.GetFullPath(targetDir); if (!Directory.Exists(targetDir)) { throw new IOException("Target path does not exist or is not a directory"); } if (Directory.Exists(junctionPoint)) { if (!overwrite) throw new IOException("Directory already exists and overwrite parameter is false."); } else { Directory.CreateDirectory(junctionPoint); } using var fileHandle = OpenReparsePoint(junctionPoint, Win32FileAccess.GenericWrite); var targetDirBytes = Encoding.Unicode.GetBytes( NonInterpretedPathPrefix + Path.GetFullPath(targetDir)); var reparseDataBuffer = new ReparseDataBuffer { ReparseTag = (uint) DeviceIoControlCode.ReparseTagMountPoint, ReparseDataLength = Convert.ToUInt16(targetDirBytes.Length + 12), SubstituteNameOffset = 0, SubstituteNameLength = Convert.ToUInt16(targetDirBytes.Length), PrintNameOffset = Convert.ToUInt16(targetDirBytes.Length + 2), PrintNameLength = 0, PathBuffer = new byte[0x3ff0] }; Array.Copy(targetDirBytes, reparseDataBuffer.PathBuffer, targetDirBytes.Length); var inBufferSize = Marshal.SizeOf(reparseDataBuffer); var inBuffer = Marshal.AllocHGlobal(inBufferSize); try { Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false); var result = DeviceIoControl( fileHandle, (uint) DeviceIoControlCode.SetReparsePoint, inBuffer, Convert.ToUInt32(targetDirBytes.Length + 20), IntPtr.Zero, 0, out var bytesReturned, IntPtr.Zero); Debug.WriteLine($"bytesReturned: {bytesReturned}"); if (!result) { ThrowLastWin32Error($"Unable to create junction point" + $" {junctionPoint} -> {targetDir}"); } } finally { Marshal.FreeHGlobal(inBuffer); } } /// /// Deletes a junction point at the specified source directory along with the directory itself. /// Does nothing if the junction point does not exist. /// /// The junction point path public static void Delete(string junctionPoint) { if (!Directory.Exists(junctionPoint)) { if (File.Exists(junctionPoint)) throw new IOException("Path is not a junction point."); return; } using var fileHandle = OpenReparsePoint(junctionPoint, Win32FileAccess.GenericWrite); var reparseDataBuffer = new ReparseDataBuffer { ReparseTag = (uint) DeviceIoControlCode.ReparseTagMountPoint, ReparseDataLength = 0, PathBuffer = new byte[0x3ff0] }; var inBufferSize = Marshal.SizeOf(reparseDataBuffer); var inBuffer = Marshal.AllocHGlobal(inBufferSize); try { Marshal.StructureToPtr(reparseDataBuffer, inBuffer, false); var result = DeviceIoControl(fileHandle, (uint) DeviceIoControlCode.DeleteReparsePoint, inBuffer, 8, IntPtr.Zero, 0, out var bytesReturned, IntPtr.Zero); Debug.WriteLine($"bytesReturned: {bytesReturned}"); if (!result) { ThrowLastWin32Error($"Unable to delete junction point {junctionPoint}"); } } finally { Marshal.FreeHGlobal(inBuffer); } try { Directory.Delete(junctionPoint); } catch (IOException ex) { throw new IOException("Unable to delete junction point.", ex); } } /// /// Determines whether the specified path exists and refers to a junction point. /// /// The junction point path /// True if the specified path represents a junction point /// Thrown if the specified path is invalid /// or some other error occurs public static bool Exists(string path) { if (!Directory.Exists(path)) return false; using var handle = OpenReparsePoint(path, Win32FileAccess.GenericRead); var target = InternalGetTarget(handle); return target != null; } /// /// Gets the target of the specified junction point. /// /// The junction point path /// The target of the junction point /// Thrown when the specified path does not /// exist, is invalid, is not a junction point, or some other error occurs public static string GetTarget(string junctionPoint) { using var handle = OpenReparsePoint(junctionPoint, Win32FileAccess.GenericRead); var target = InternalGetTarget(handle); if (target == null) { throw new IOException("Path is not a junction point."); } return target; } private static string? InternalGetTarget(SafeFileHandle handle) { var outBufferSize = Marshal.SizeOf(typeof(ReparseDataBuffer)); var outBuffer = Marshal.AllocHGlobal(outBufferSize); try { var result = DeviceIoControl( handle, (uint) DeviceIoControlCode.GetReparsePoint, IntPtr.Zero, 0, outBuffer, (uint) outBufferSize, out var bytesReturned, IntPtr.Zero); Debug.WriteLine($"bytesReturned: {bytesReturned}"); // Errors if (!result) { var error = Marshal.GetLastWin32Error(); if (error == (int) Win32ErrorCode.NotAReparsePoint) { return null; } else { ThrowLastWin32Error("Unable to get information about junction point."); } } // Check output if (outBuffer == IntPtr.Zero) return null; // Safe interpret as ReparseDataBuffer type if (Marshal.PtrToStructure(outBuffer, typeof(ReparseDataBuffer)) is not ReparseDataBuffer reparseDataBuffer) { return null; } // Check if it's a mount point if (reparseDataBuffer.ReparseTag != (uint) DeviceIoControlCode.ReparseTagMountPoint) { return null; } // Get the target dir string var targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength); if (targetDir.StartsWith(NonInterpretedPathPrefix)) { targetDir = targetDir[NonInterpretedPathPrefix.Length..]; } return targetDir; } finally { Marshal.FreeHGlobal(outBuffer); } } private static SafeFileHandle OpenReparsePoint(string reparsePoint, Win32FileAccess accessMode) { var filePtr = CreateFile( reparsePoint, accessMode, Win32FileShare.Read | Win32FileShare.Write | Win32FileShare.Delete, IntPtr.Zero, Win32CreationDisposition.OpenExisting, Win32FileAttribute.FlagBackupSemantics | Win32FileAttribute.FlagOpenReparsePoint, IntPtr.Zero); var handle = new SafeFileHandle(filePtr, true); if (Marshal.GetLastWin32Error() != 0) { ThrowLastWin32Error($"Unable to open reparse point {reparsePoint}"); } return handle; } [DoesNotReturn] private static void ThrowLastWin32Error(string message) { throw new IOException(message, Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())); } } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/ReparseDataBuffer.cs ================================================ using System.Runtime.InteropServices; namespace StabilityMatrix.Core.ReparsePoints; /// /// Because the tag we're using is IO_REPARSE_TAG_MOUNT_POINT, /// we use the MountPointReparseBuffer struct in the DUMMYUNIONNAME union. /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct ReparseDataBuffer { /// /// Reparse point tag. Must be a Microsoft reparse point tag. /// public uint ReparseTag; /// /// Size, in bytes, of the reparse data in the buffer that points to. /// This can be calculated by: /// (4 * sizeof(ushort)) + SubstituteNameLength + PrintNameLength + /// (namesAreNullTerminated ? 2 * sizeof(char) : 0); /// public ushort ReparseDataLength; /// /// Reserved; do not use. /// #pragma warning disable CS0169 // Field is never used private ushort Reserved; #pragma warning restore CS0169 // Field is never used /// /// Offset, in bytes, of the substitute name string in the array. /// public ushort SubstituteNameOffset; /// /// Length, in bytes, of the substitute name string. If this string is null-terminated, /// does not include space for the null character. /// public ushort SubstituteNameLength; /// /// Offset, in bytes, of the print name string in the array. /// public ushort PrintNameOffset; /// /// Length, in bytes, of the print name string. If this string is null-terminated, /// does not include space for the null character. /// public ushort PrintNameLength; /// /// A buffer containing the unicode-encoded path string. The path string contains /// the substitute name string and print name string. /// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)] public byte[] PathBuffer; } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Win32CreationDisposition.cs ================================================ namespace StabilityMatrix.Core.ReparsePoints; internal enum Win32CreationDisposition : uint { New = 1, CreateAlways = 2, OpenExisting = 3, OpenAlways = 4, TruncateExisting = 5, } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Win32ErrorCode.cs ================================================ namespace StabilityMatrix.Core.ReparsePoints; internal enum Win32ErrorCode { /// /// The file or directory is not a reparse point. /// ERROR_NOT_A_REPARSE_POINT /// NotAReparsePoint = 4390, /// /// The reparse point attribute cannot be set because it conflicts with an existing attribute. /// ERROR_REPARSE_ATTRIBUTE_CONFLICT /// ReparseAttributeConflict = 4391, /// /// The data present in the reparse point buffer is invalid. /// ERROR_INVALID_REPARSE_DATA /// InvalidReparseData = 4392, /// /// The tag present in the reparse point buffer is invalid. /// ERROR_REPARSE_TAG_INVALID /// ReparseTagInvalid = 4393, /// /// There is a mismatch between the tag specified in the request and the tag present in the reparse point. /// ERROR_REPARSE_TAG_MISMATCH /// ReparseTagMismatch = 4394, } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Win32FileAccess.cs ================================================ namespace StabilityMatrix.Core.ReparsePoints; [Flags] internal enum Win32FileAccess : uint { GenericRead = 0x80000000U, GenericWrite = 0x40000000U, GenericExecute = 0x20000000U, GenericAll = 0x10000000U } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Win32FileAttribute.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.ReparsePoints; [Flags] [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum Win32FileAttribute : uint { AttributeReadOnly = 0x1U, AttributeHidden = 0x2U, AttributeSystem = 0x4U, AttributeDirectory = 0x10U, AttributeArchive = 0x20U, AttributeDevice = 0x40U, AttributeNormal = 0x80U, AttributeTemporary = 0x100U, AttributeSparseFile = 0x200U, AttributeReparsePoint = 0x400U, AttributeCompressed = 0x800U, AttributeOffline = 0x1000U, AttributeNotContentIndexed = 0x2000U, AttributeEncrypted = 0x4000U, AttributeIntegrityStream = 0x8000U, AttributeVirtual = 0x10000U, AttributeNoScrubData = 0x20000U, AttributeEA = 0x40000U, AttributeRecallOnOpen = 0x40000U, AttributePinned = 0x80000U, AttributeUnpinned = 0x100000U, AttributeRecallOnDataAccess = 0x400000U, FlagOpenNoRecall = 0x100000U, /// /// Normal reparse point processing will not occur; CreateFile will attempt to open the reparse point. When a file is opened, a file handle is returned, /// whether or not the filter that controls the reparse point is operational. ///
This flag cannot be used with the flag. ///
If the file is not a reparse point, then this flag is ignored. ///
FlagOpenReparsePoint = 0x200000U, FlagSessionAware = 0x800000U, FlagPosixSemantics = 0x1000000U, /// /// You must set this flag to obtain a handle to a directory. A directory handle can be passed to some functions instead of a file handle. /// FlagBackupSemantics = 0x2000000U, FlagDeleteOnClose = 0x4000000U, FlagSequentialScan = 0x8000000U, FlagRandomAccess = 0x10000000U, FlagNoBuffering = 0x20000000U, FlagOverlapped = 0x40000000U, FlagWriteThrough = 0x80000000U } ================================================ FILE: StabilityMatrix.Core/ReparsePoints/Win32FileShare.cs ================================================ namespace StabilityMatrix.Core.ReparsePoints; [Flags] internal enum Win32FileShare : uint { None = 0x00000000, Read = 0x00000001, Write = 0x00000002, Delete = 0x00000004, } ================================================ FILE: StabilityMatrix.Core/Services/DownloadService.cs ================================================ using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Polly.Contrib.WaitAndRetry; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Services; [RegisterSingleton] public class DownloadService : IDownloadService { private readonly ILogger logger; private readonly IHttpClientFactory httpClientFactory; private readonly ISecretsManager secretsManager; private const int BufferSize = ushort.MaxValue; public DownloadService( ILogger logger, IHttpClientFactory httpClientFactory, ISecretsManager secretsManager ) { this.logger = logger; this.httpClientFactory = httpClientFactory; this.secretsManager = secretsManager; } public async Task DownloadToFileAsync( string downloadUrl, string downloadPath, IProgress? progress = null, string? httpClientName = null, CancellationToken cancellationToken = default ) { using var client = string.IsNullOrWhiteSpace(httpClientName) ? httpClientFactory.CreateClient() : httpClientFactory.CreateClient(httpClientName); client.Timeout = TimeSpan.FromMinutes(10); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0")); await AddConditionalHeaders(client, new Uri(downloadUrl)).ConfigureAwait(false); await using var file = new FileStream( downloadPath, FileMode.Create, FileAccess.Write, FileShare.None ); long contentLength = 0; var response = await client .GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); contentLength = response.Content.Headers.ContentLength ?? 0; var delays = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(50), retryCount: 3); foreach (var delay in delays) { if (contentLength > 0) break; logger.LogDebug("Retrying get-headers for content-length"); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); response = await client .GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); contentLength = response.Content.Headers.ContentLength ?? 0; } var isIndeterminate = contentLength == 0; if (contentLength > 0) { // check free space if ( SystemInfo.GetDiskFreeSpaceBytes(Path.GetDirectoryName(downloadPath)) is { } freeSpace && freeSpace < contentLength ) { throw new ApplicationException( $"Not enough free space to download file. Free: {freeSpace} bytes, Required: {contentLength} bytes" ); } } await using var stream = await response .Content.ReadAsStreamAsync(cancellationToken) .ConfigureAwait(false); var stopwatch = Stopwatch.StartNew(); var totalBytesRead = 0L; var buffer = new byte[BufferSize]; while (true) { var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) break; await file.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; var elapsedSeconds = stopwatch.Elapsed.TotalSeconds; var speedInMBps = (totalBytesRead / elapsedSeconds) / (1024 * 1024); if (isIndeterminate) { progress?.Report(new ProgressReport(-1, isIndeterminate: true)); } else { progress?.Report( new ProgressReport( current: Convert.ToUInt64(totalBytesRead), total: Convert.ToUInt64(contentLength), message: "Downloading...", printToConsole: false, speedInMBps: speedInMBps ) ); } } await file.FlushAsync(cancellationToken).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, message: "Download complete!")); } /// public async Task ResumeDownloadToFileAsync( string downloadUrl, string downloadPath, long existingFileSize, IProgress? progress = null, string? httpClientName = null, CancellationToken cancellationToken = default ) { using var client = string.IsNullOrWhiteSpace(httpClientName) ? httpClientFactory.CreateClient() : httpClientFactory.CreateClient(httpClientName); using var noRedirectClient = httpClientFactory.CreateClient("DontFollowRedirects"); client.Timeout = TimeSpan.FromMinutes(10); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0")); await AddConditionalHeaders(client, new Uri(downloadUrl)).ConfigureAwait(false); await AddConditionalHeaders(noRedirectClient, new Uri(downloadUrl)).ConfigureAwait(false); // Create file if it doesn't exist if (!File.Exists(downloadPath)) { logger.LogInformation("Resume file doesn't exist, creating file {DownloadPath}", downloadPath); File.Create(downloadPath).Close(); } await using var file = new FileStream( downloadPath, FileMode.Append, FileAccess.Write, FileShare.None ); // Remaining content length long remainingContentLength = 0; // Total of the original content long originalContentLength = 0; using var noRedirectRequest = new HttpRequestMessage(); noRedirectRequest.Method = HttpMethod.Get; noRedirectRequest.RequestUri = new Uri(downloadUrl); noRedirectRequest.Headers.Range = new RangeHeaderValue(existingFileSize, null); HttpResponseMessage? response = null; foreach ( var delay in Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(50), retryCount: 4) ) { var noRedirectResponse = await noRedirectClient .SendAsync(noRedirectRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); if ((int)noRedirectResponse.StatusCode > 299 && (int)noRedirectResponse.StatusCode < 400) { var redirectUrl = noRedirectResponse.Headers.Location?.ToString(); if (redirectUrl != null && redirectUrl.Contains("reason=download-auth")) { throw new UnauthorizedAccessException(); } } else if (noRedirectResponse.StatusCode == HttpStatusCode.Unauthorized) { if ( noRedirectRequest.RequestUri?.Host.Equals( "huggingface.co", StringComparison.OrdinalIgnoreCase ) == true ) { throw new HuggingFaceLoginRequiredException(); } if ( noRedirectRequest.RequestUri?.Host.Equals( "civitai.com", StringComparison.OrdinalIgnoreCase ) == true ) { var responseContent = await noRedirectResponse .Content.ReadAsStringAsync(cancellationToken) .ConfigureAwait(false); if (responseContent.Contains("The creator of this asset has disabled downloads")) { throw new CivitDownloadDisabledException(); } throw new CivitLoginRequiredException(); } throw new UnauthorizedAccessException(); } else if (noRedirectResponse.StatusCode == HttpStatusCode.Forbidden) { throw new EarlyAccessException(); } using var redirectRequest = new HttpRequestMessage(); redirectRequest.Method = HttpMethod.Get; redirectRequest.RequestUri = new Uri(downloadUrl); redirectRequest.Headers.Range = new RangeHeaderValue(existingFileSize, null); response = await client .SendAsync(redirectRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); remainingContentLength = response.Content.Headers.ContentLength ?? 0; originalContentLength = response.Content.Headers.ContentRange?.Length.GetValueOrDefault() ?? 0; if (remainingContentLength > 0) break; logger.LogDebug("Retrying get-headers for content-length"); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); } if (response == null) { throw new ApplicationException("Response is null"); } var isIndeterminate = remainingContentLength == 0; await using var stream = await response .Content.ReadAsStreamAsync(cancellationToken) .ConfigureAwait(false); var totalBytesRead = 0L; var stopwatch = Stopwatch.StartNew(); var buffer = new byte[BufferSize]; while (true) { cancellationToken.ThrowIfCancellationRequested(); var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) break; await file.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); totalBytesRead += bytesRead; var elapsedSeconds = stopwatch.Elapsed.TotalSeconds; var speedInMBps = (totalBytesRead / elapsedSeconds) / (1024 * 1024); if (isIndeterminate) { progress?.Report(new ProgressReport(-1, isIndeterminate: true)); } else { progress?.Report( new ProgressReport( // Report the current as session current + original start size current: Convert.ToUInt64(totalBytesRead + existingFileSize), // Total as the original total total: Convert.ToUInt64(originalContentLength), message: "Downloading...", speedInMBps: speedInMBps ) ); } } await file.FlushAsync(cancellationToken).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, message: "Download complete!")); } /// public async Task GetFileSizeAsync( string downloadUrl, string? httpClientName = null, CancellationToken cancellationToken = default ) { using var client = string.IsNullOrWhiteSpace(httpClientName) ? httpClientFactory.CreateClient() : httpClientFactory.CreateClient(httpClientName); client.Timeout = TimeSpan.FromMinutes(10); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0")); await AddConditionalHeaders(client, new Uri(downloadUrl)).ConfigureAwait(false); var contentLength = 0L; foreach ( var delay in Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(50), retryCount: 3) ) { var response = await client .GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); contentLength = response.Content.Headers.ContentLength ?? -1; if (contentLength > 0) break; logger.LogDebug("Retrying get-headers for content-length"); await Task.Delay(delay, cancellationToken).ConfigureAwait(false); } return contentLength; } public async Task GetImageStreamFromUrl(string url) { using var client = httpClientFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0")); await AddConditionalHeaders(client, new Uri(url)).ConfigureAwait(false); try { var response = await client.GetAsync(url).ConfigureAwait(false); return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } catch (Exception e) { logger.LogError(e, "Failed to get image stream from url {Url}", url); return null; } } public async Task GetContentAsync(string url, CancellationToken cancellationToken = default) { using var client = httpClientFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "2.0")); await AddConditionalHeaders(client, new Uri(url)).ConfigureAwait(false); var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false); return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); } /// /// Adds conditional headers to the HttpClient for the given URL /// private async Task AddConditionalHeaders(HttpClient client, Uri url) { // Check if civit download if (url.Host.Equals("civitai.com", StringComparison.OrdinalIgnoreCase)) { // Add auth if we have it if (await secretsManager.SafeLoadAsync().ConfigureAwait(false) is { CivitApi: { } civitApi }) { logger.LogTrace( "Adding Civit auth header {Signature} for download {Url}", ObjectHash.GetStringSignature(civitApi.ApiToken), url ); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", civitApi.ApiToken ); } } // Check if Hugging Face download else if (url.Host.Equals("huggingface.co", StringComparison.OrdinalIgnoreCase)) { var secrets = await secretsManager.SafeLoadAsync().ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(secrets.HuggingFaceToken)) { logger.LogTrace("Adding Hugging Face auth header for download {Url}", url); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", secrets.HuggingFaceToken ); } } } } ================================================ FILE: StabilityMatrix.Core/Services/IDownloadService.cs ================================================ using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Services; public interface IDownloadService { Task DownloadToFileAsync( string downloadUrl, string downloadPath, IProgress? progress = null, string? httpClientName = null, CancellationToken cancellationToken = default ); Task ResumeDownloadToFileAsync( string downloadUrl, string downloadPath, long existingFileSize, IProgress? progress = null, string? httpClientName = null, CancellationToken cancellationToken = default ); Task GetFileSizeAsync( string downloadUrl, string? httpClientName = null, CancellationToken cancellationToken = default ); Task GetImageStreamFromUrl(string url); Task GetContentAsync(string url, CancellationToken cancellationToken = default); } ================================================ FILE: StabilityMatrix.Core/Services/IImageIndexService.cs ================================================ using DynamicData.Binding; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Services; public interface IImageIndexService { IndexCollection InferenceImages { get; } /// /// Refresh index for all collections /// Task RefreshIndexForAllCollections(); Task RefreshIndex(IndexCollection indexCollection); /// /// Refreshes the index of local images in the background /// void BackgroundRefreshIndex(); } ================================================ FILE: StabilityMatrix.Core/Services/IMetadataImportService.cs ================================================ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Services; public interface IMetadataImportService { Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress? progress = null); Task GetMetadataForFile( FilePath filePath, IProgress? progress = null, bool forceReimport = false ); Task UpdateExistingMetadata(DirectoryPath directory, IProgress? progress = null); } ================================================ FILE: StabilityMatrix.Core/Services/IModelIndexService.cs ================================================ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Core.Services; public interface IModelIndexService { Dictionary> ModelIndex { get; } /// /// Set of all files Blake3 hashes. /// Synchronized with internal changes to . /// IReadOnlySet ModelIndexBlake3Hashes { get; } /// /// Refreshes the local model file index. /// Task RefreshIndex(); /// /// Starts a background task to refresh the local model file index. /// void BackgroundRefreshIndex(); /// /// Get all models of the specified type from the existing (in-memory) index. /// IEnumerable FindByModelType(SharedFolderType types); /// /// Gets all models in a hierarchical structure. /// Task> FindAllFolders(); /// /// Find all models of the specified SharedFolderType. /// Task> FindByModelTypeAsync(SharedFolderType type); /// /// Find all models with the specified Blake3 hash. /// Task> FindByHashAsync(string hashBlake3); /// /// Find all models with the specified Sha256 hash /// Task> FindBySha256Async(string hashSha256); /// /// Remove a model from the index. /// Task RemoveModelAsync(LocalModelFile model); Task RemoveModelsAsync(IEnumerable models); Task CheckModelsForUpdateAsync(); } ================================================ FILE: StabilityMatrix.Core/Services/IPipWheelService.cs ================================================ using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Services; /// /// Service for installing pip wheel packages from GitHub releases. /// All install methods are safe to call regardless of platform/GPU support - /// they will silently no-op if the package is not applicable. /// public interface IPipWheelService { /// /// Installs Triton. Windows uses triton-windows, Linux uses triton. /// No-ops on macOS. /// Task InstallTritonAsync( IPyVenvRunner venv, IProgress? progress = null, string? version = null ); /// /// Installs SageAttention from pre-built wheels or source. /// No-ops on macOS or non-NVIDIA GPUs. /// Task InstallSageAttentionAsync( IPyVenvRunner venv, GpuInfo? gpuInfo = null, IProgress? progress = null, string? version = null ); /// /// Installs Nunchaku from pre-built wheels. /// No-ops on macOS or GPUs with compute capability < 7.5. /// Task InstallNunchakuAsync( IPyVenvRunner venv, GpuInfo? gpuInfo = null, IProgress? progress = null, string? version = null ); /// /// Installs FlashAttention from pre-built wheels. /// Windows only. No-ops on Linux/macOS. /// Task InstallFlashAttentionAsync( IPyVenvRunner venv, IProgress? progress = null, string? version = null ); } ================================================ FILE: StabilityMatrix.Core/Services/ISecretsManager.cs ================================================ using StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Services; /// /// Interface for managing secure settings and tokens. /// public interface ISecretsManager { /// /// Load and return the secrets. /// Task LoadAsync(); /// /// Load and return the secrets, or save and return a new instance on error. /// Task SafeLoadAsync(); Task SaveAsync(Secrets secrets); } ================================================ FILE: StabilityMatrix.Core/Services/ISettingsManager.cs ================================================ using System.ComponentModel; using System.Linq.Expressions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Core.Services; public interface ISettingsManager { bool IsPortableMode { get; } DirectoryPath LibraryDir { get; } bool IsLibraryDirSet { get; } string ModelsDirectory { get; } string DownloadsDirectory { get; } DirectoryPath TagsDirectory { get; } DirectoryPath ImagesDirectory { get; } DirectoryPath ImagesInferenceDirectory { get; } DirectoryPath ConsolidatedImagesDirectory { get; } Settings Settings { get; } List PackageInstallsInProgress { get; set; } DirectoryPath WorkflowDirectory { get; } DirectoryPath ExtensionPackDirectory { get; } /// /// Event fired when the library directory is changed /// event EventHandler? LibraryDirChanged; /// /// Event fired when a property of Settings is changed /// event EventHandler? SettingsPropertyChanged; /// /// Event fired when Settings are loaded from disk /// event EventHandler? Loaded; /// /// Set an override for the library directory. /// void SetLibraryDirOverride(DirectoryPath path); /// /// Register a handler that fires once when LibraryDir is first set. /// Will fire instantly if it is already set. /// void RegisterOnLibraryDirSet(Action handler); /// /// Return a SettingsTransaction that can be used to modify Settings /// Saves on Dispose. /// SettingsTransaction BeginTransaction(); /// /// Execute a function that modifies Settings /// Commits changes after the function returns. /// /// Function accepting Settings to modify /// Ignore missing library dir when committing changes void Transaction(Action func, bool ignoreMissingLibraryDir = false); /// /// Modify a settings property by expression and commit changes. /// This will notify listeners of SettingsPropertyChanged. /// void Transaction(Expression> expression, TValue value); /// /// Register a source observable object and property to be relayed to Settings /// IDisposable RelayPropertyFor( T source, Expression> sourceProperty, Expression> settingsProperty, bool setInitial = false, TimeSpan? delay = null ) where T : INotifyPropertyChanged; /// /// Register an Action to be called on change of the settings property. /// IDisposable RegisterPropertyChangedHandler( Expression> settingsProperty, Action onPropertyChanged ); /// /// Creates an observable sequence that notifies when the specified settings property changes. /// Emits the initial value upon subscription and subsequent changes. /// /// The type of the property value. /// An expression representing the settings property to observe. /// An observable sequence of the property's values. IObservable ObservePropertyChanged(Expression> settingsProperty); /// /// Attempts to locate and set the library path /// Return true if found, false otherwise /// /// Force reload even if library is already set bool TryFindLibrary(bool forceReload = false); /// /// Save a new library path to %APPDATA%/StabilityMatrix/library.json /// void SetLibraryPath(string path); /// /// Enable and create settings files for portable mode /// Creates the ./Data directory and the `.sm-portable` marker file /// void SetPortableMode(); void SaveLaunchArgs(Guid packageId, IEnumerable launchArgs); bool IsEulaAccepted(); void SetEulaAccepted(); /// /// Cancels any scheduled delayed save of settings and flushes immediately. /// Task FlushAsync(CancellationToken cancellationToken); } ================================================ FILE: StabilityMatrix.Core/Services/ITrackedDownloadService.cs ================================================ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Services; public interface ITrackedDownloadService { IEnumerable Downloads { get; } event EventHandler? DownloadAdded; TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath); TrackedDownload NewDownload(string downloadUrl, FilePath downloadPath) => NewDownload(new Uri(downloadUrl), downloadPath); Task TryStartDownload(TrackedDownload download); Task TryResumeDownload(TrackedDownload download); void UpdateMaxConcurrentDownloads(int newMax); } ================================================ FILE: StabilityMatrix.Core/Services/ImageIndexService.cs ================================================ using System.Collections.Concurrent; using System.Diagnostics; using AsyncAwaitBestPractices; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Services; [RegisterSingleton] public class ImageIndexService : IImageIndexService { private readonly ILogger logger; private readonly ISettingsManager settingsManager; /// public IndexCollection InferenceImages { get; } public ImageIndexService(ILogger logger, ISettingsManager settingsManager) { this.logger = logger; this.settingsManager = settingsManager; InferenceImages = new IndexCollection(this, file => file.AbsolutePath) { RelativePath = "Inference" }; EventManager.Instance.ImageFileAdded += OnImageFileAdded; } public Task RefreshIndexForAllCollections() { return RefreshIndex(InferenceImages); } public async Task RefreshIndex(IndexCollection indexCollection) { if (indexCollection.RelativePath is not { } subPath) return; var imagesDir = settingsManager.ImagesDirectory; var searchDir = imagesDir.JoinDir(indexCollection.RelativePath); if (!searchDir.Exists) { return; } // Start var stopwatch = Stopwatch.StartNew(); logger.LogInformation("Refreshing images index at {SearchDir}...", searchDir.ToString()); var errors = 0; var toAdd = new ConcurrentBag(); await Task.Run(() => { var files = searchDir .EnumerateFiles("*", EnumerationOptionConstants.AllDirectories) .Where(file => LocalImageFile.SupportedImageExtensions.Contains(file.Extension)); Parallel.ForEach( files, f => { try { toAdd.Add(LocalImageFile.FromPath(f)); } catch (Exception e) { Interlocked.Increment(ref errors); logger.LogWarning( e, "Failed to add indexed image file at {Path}, skipping", f.FullPath ); } } ); }) .ConfigureAwait(false); var indexElapsed = stopwatch.Elapsed; indexCollection.ItemsSource.EditDiff(toAdd); // End stopwatch.Stop(); var editElapsed = stopwatch.Elapsed - indexElapsed; logger.LogInformation( "Image index updated for {Prefix} with ({Added}/{Total}) files, took {IndexDuration:F1}ms ({EditDuration:F1}ms edit)", subPath, toAdd.Count, toAdd.Count + errors, indexElapsed.TotalMilliseconds, editElapsed.TotalMilliseconds ); } private void OnImageFileAdded(object? sender, FilePath filePath) { var imagesFolder = settingsManager.ImagesDirectory.JoinDir(InferenceImages.RelativePath!); if (string.IsNullOrEmpty(Path.GetRelativePath(imagesFolder, filePath))) { logger.LogWarning( "Image file {Path} added outside of relative directory {DirPath}, skipping", filePath, imagesFolder ); return; } try { InferenceImages.Add(LocalImageFile.FromPath(filePath)); } catch (Exception e) { logger.LogWarning(e, "Failed to add image file at {Path}", filePath); } } /// public void BackgroundRefreshIndex() { RefreshIndexForAllCollections().SafeFireAndForget(); } } ================================================ FILE: StabilityMatrix.Core/Services/MetadataImportService.cs ================================================ using System.Diagnostics; using System.Text.Json; using Injectio.Attributes; using Microsoft.Extensions.Logging; 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; [RegisterTransient] public class MetadataImportService( ILogger logger, IDownloadService downloadService, ModelFinder modelFinder ) : IMetadataImportService { public async Task ScanDirectoryForMissingInfo( DirectoryPath directory, IProgress? progress = null ) { progress?.Report(new ProgressReport(-1f, message: "Scanning directory...", isIndeterminate: true)); var checkpointsWithoutMetadata = directory .EnumerateFiles("*", EnumerationOptionConstants.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, message: "Scanning directory..." ) ); } else { progress?.Report( new ProgressReport( current: scanned, total: checkpointsWithoutMetadata.Count, message: $"{success} files imported successfully" ) ); } var fileNameWithoutExtension = checkpointFilePath.NameWithoutExtension; var cmInfoPath = checkpointFilePath.Directory?.JoinFile( $"{fileNameWithoutExtension}.cm-info.json" ); var cmInfoExists = File.Exists(cmInfoPath); if (cmInfoExists) continue; var hashProgress = new Progress(report => { progress?.Report( new ProgressReport( current: report.Current ?? 0, total: report.Total ?? 0, message: $"Scanning file {scanned}/{checkpointsWithoutMetadata.Count} ... {report.Percentage}%", printToConsole: false ) ); }); try { 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)) && img.Type == "image" ); if (image == null) { scanned++; success++; continue; } await DownloadImage(image, checkpointFilePath, progress).ConfigureAwait(false); scanned++; success++; } catch (Exception e) { logger.LogError(e, "Error while scanning {checkpointFilePath}", checkpointFilePath); scanned++; } } progress?.Report( new ProgressReport( current: scanned, total: checkpointsWithoutMetadata.Count, message: $"Metadata found for {success}/{checkpointsWithoutMetadata.Count} files" ) ); } private static bool FileHasNoCmInfo(FilePath file) { return LocalModelFile.SupportedCheckpointExtensions.Contains(file.Extension) && !File.Exists(file.Directory?.JoinFile($"{file.NameWithoutExtension}.cm-info.json")); } public async Task UpdateExistingMetadata( DirectoryPath directory, IProgress? progress = null ) { progress?.Report(new ProgressReport(-1f, message: "Scanning directory...", isIndeterminate: true)); var cmInfoList = new Dictionary(); foreach ( var cmInfoPath in directory.EnumerateFiles( "*.cm-info.json", EnumerationOptionConstants.AllDirectories ) ) { ConnectedModelInfo? cmInfo; try { cmInfo = JsonSerializer.Deserialize( await cmInfoPath.ReadAllTextAsync().ConfigureAwait(false) ); } catch (JsonException) { cmInfo = null; } 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, message: $"Updating metadata {success}/{cmInfoList.Count}" ) ); try { 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)) && img.Type == "image" ); if (image == null) continue; await DownloadImage(image, filePath, progress).ConfigureAwait(false); success++; } catch (Exception e) { logger.LogError(e, "Error while updating {filePath}", filePath); } } } public async Task GetMetadataForFile( FilePath filePath, IProgress? progress = null, bool forceReimport = false ) { progress?.Report(new ProgressReport(-1f, message: "Getting metadata...", isIndeterminate: true)); var fileNameWithoutExtension = filePath.NameWithoutExtension; var cmInfoPath = filePath.Directory?.JoinFile($"{fileNameWithoutExtension}.cm-info.json"); var cmInfoExists = File.Exists(cmInfoPath); if (cmInfoExists && !forceReimport) return null; var hashProgress = new Progress(report => { progress?.Report( new ProgressReport( current: report.Current ?? 0, total: report.Total ?? 0, message: $"Getting metadata for {fileNameWithoutExtension} ... {report.Percentage}%", printToConsole: false ) ); }); 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)) && img.Type == "image" ); if (image == null) return updatedCmInfo; var imagePath = await DownloadImage(image, filePath, progress).ConfigureAwait(false); updatedCmInfo.ThumbnailImageUrl = imagePath; return updatedCmInfo; } private static async Task GetBlake3Hash( FilePath? cmInfoPath, FilePath checkpointFilePath, IProgress hashProgress ) { if (string.IsNullOrWhiteSpace(cmInfoPath?.ToString()) || !File.Exists(cmInfoPath)) { return await FileHash.GetBlake3Async(checkpointFilePath, hashProgress).ConfigureAwait(false); } var cmInfo = JsonSerializer.Deserialize( await cmInfoPath.ReadAllTextAsync().ConfigureAwait(false) ); return cmInfo?.Hashes.BLAKE3; } private async Task DownloadImage( CivitImage image, FilePath modelFilePath, IProgress? progress ) { var imageExt = Path.GetExtension(image.Url).TrimStart('.'); var nameWithoutCmInfo = modelFilePath.NameWithoutExtension.Replace(".cm-info", string.Empty); var imageDownloadPath = Path.GetFullPath( Path.Combine(modelFilePath.Directory, $"{nameWithoutCmInfo}.preview.{imageExt}") ); await downloadService .DownloadToFileAsync(image.Url, imageDownloadPath, progress) .ConfigureAwait(false); return imageDownloadPath; } } ================================================ FILE: StabilityMatrix.Core/Services/ModelIndexService.cs ================================================ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.Text; using AsyncAwaitBestPractices; using AutoCtor; using Injectio.Attributes; using KGySoft.CoreLibraries; using LiteDB; using LiteDB.Async; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using JsonSerializer = System.Text.Json.JsonSerializer; namespace StabilityMatrix.Core.Services; [RegisterSingleton] [AutoConstruct] public partial class ModelIndexService : IModelIndexService { private readonly ILogger logger; private readonly ISettingsManager settingsManager; private readonly ILiteDbContext liteDbContext; private readonly ModelFinder modelFinder; private DateTimeOffset lastUpdateCheck = DateTimeOffset.MinValue; private Dictionary> _modelIndex = new(); private HashSet? _modelIndexBlake3Hashes; /// /// Whether the database has been initially loaded. /// private bool IsDbLoaded { get; set; } public Dictionary> ModelIndex { get => _modelIndex; private set { _modelIndex = value; OnModelIndexReset(); } } public IReadOnlySet ModelIndexBlake3Hashes => _modelIndexBlake3Hashes ??= CollectModelHashes(ModelIndex.Values.SelectMany(x => x)); [AutoPostConstruct] private void Initialize() { // Start background index when library dir is set settingsManager.RegisterOnLibraryDirSet(_ => { // Skip if already loaded if (IsDbLoaded) { return; } Task.Run(async () => { // Build db indexes await liteDbContext .LocalModelFiles.EnsureIndexAsync(m => m.HashBlake3) .ConfigureAwait(false); await liteDbContext .LocalModelFiles.EnsureIndexAsync(m => m.SharedFolderType) .ConfigureAwait(false); // Load models first from db, then do index refresh await EnsureLoadedAsync().ConfigureAwait(false); await RefreshIndex().ConfigureAwait(false); }) .SafeFireAndForget(ex => { logger.LogError(ex, "Error loading model index"); }); }); } // Ensure the in memory cache is loaded private async Task EnsureLoadedAsync() { if (!IsDbLoaded) { await LoadFromDbAsync().ConfigureAwait(false); } } /// /// Populates from the database. /// private async Task LoadFromDbAsync() { var timer = Stopwatch.StartNew(); logger.LogInformation("Loading models from database..."); // Handle enum deserialize exceptions from changes var allModels = await liteDbContext .TryQueryWithClearOnExceptionAsync( liteDbContext.LocalModelFiles, liteDbContext.LocalModelFiles.IncludeAll().FindAllAsync() ) .ConfigureAwait(false); if (allModels is not null) { ModelIndex = allModels.GroupBy(m => m.SharedFolderType).ToDictionary(g => g.Key, g => g.ToList()); } else { ModelIndex.Clear(); } IsDbLoaded = true; EventManager.Instance.OnModelIndexChanged(); timer.Stop(); logger.LogInformation( "Loaded {Count} models from database in {Time:F2}ms", ModelIndex.Count, timer.Elapsed.TotalMilliseconds ); } /// public async Task> FindAllFolders() { var modelFiles = await liteDbContext.LocalModelFiles.FindAllAsync().ConfigureAwait(false); var rootFolders = new Dictionary(); foreach (var modelFile in modelFiles) { var pathParts = modelFile.RelativePath.Split(Path.DirectorySeparatorChar); var currentFolder = rootFolders.GetOrAdd( modelFile.SharedFolderType, _ => new LocalModelFolder { RelativePath = pathParts[0] } ); for (var i = 1; i < pathParts.Length - 1; i++) { var folderName = pathParts[i]; var folder = currentFolder.Folders.GetValueOrDefault(folderName); if (folder == null) { folder = new LocalModelFolder { RelativePath = folderName }; currentFolder.Folders[folderName] = folder; } currentFolder = folder; } currentFolder.Files[modelFile.RelativePath] = modelFile; } return rootFolders; } /// public IEnumerable FindByModelType(SharedFolderType types) { return ModelIndex.Where(kvp => (kvp.Key & types) != 0).SelectMany(kvp => kvp.Value); } /// public Task> FindByModelTypeAsync(SharedFolderType type) { // To list of types var types = Enum.GetValues() .Where(folderType => type.HasFlag(folderType)) .ToArray(); return types.Length switch { 0 => Task.FromResult(Enumerable.Empty()), 1 => liteDbContext.LocalModelFiles.FindAsync(m => m.SharedFolderType == type), _ => liteDbContext.LocalModelFiles.FindAsync(m => types.Contains(m.SharedFolderType)), }; } /// public Task> FindByHashAsync(string hashBlake3) { return liteDbContext.LocalModelFiles.FindAsync(m => m.HashBlake3 == hashBlake3); } public Task> FindBySha256Async(string hashSha256) { return liteDbContext.LocalModelFiles.FindAsync(m => m.HashSha256 == hashSha256); } /// public Task RefreshIndex() { return RefreshIndexParallelCore(); } private async Task RefreshIndexCore() { if (!settingsManager.IsLibraryDirSet) { logger.LogTrace("Model index refresh skipped, library directory not set"); return; } if (new DirectoryPath(settingsManager.ModelsDirectory) is not { Exists: true } modelsDir) { logger.LogTrace("Model index refresh skipped, model directory does not exist"); return; } logger.LogInformation("Refreshing model index..."); // Start var stopwatch = Stopwatch.StartNew(); var newIndex = new Dictionary>(); var newIndexFlat = new List(); var paths = Directory .EnumerateFiles(modelsDir, "*", EnumerationOptionConstants.AllDirectories) .ToHashSet(); foreach (var path in paths) { // Skip if not supported extension if (!LocalModelFile.SupportedCheckpointExtensions.Contains(Path.GetExtension(path))) { continue; } var relativePath = Path.GetRelativePath(modelsDir, path); // Get shared folder name var sharedFolderName = relativePath.Split( Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries )[0]; // Try Convert to enum if (!Enum.TryParse(sharedFolderName, out var sharedFolderType)) { continue; } // Since RelativePath is the database key, for LiteDB this is limited to 1021 bytes if (Encoding.UTF8.GetByteCount(relativePath) is var byteCount and > 1021) { logger.LogWarning( "Skipping model {Path} because it's path is too long ({Length} bytes)", relativePath, byteCount ); continue; } var localModel = new LocalModelFile { RelativePath = relativePath, SharedFolderType = sharedFolderType, }; // Try to find a connected model info var fileDirectory = new DirectoryPath(Path.GetDirectoryName(path)!); var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); var jsonPath = fileDirectory.JoinFile($"{fileNameWithoutExtension}.cm-info.json"); if (paths.Contains(jsonPath)) { try { await using var stream = jsonPath.Info.OpenRead(); var connectedModelInfo = await JsonSerializer .DeserializeAsync( stream, ConnectedModelInfoSerializerContext.Default.ConnectedModelInfo ) .ConfigureAwait(false); localModel.ConnectedModelInfo = connectedModelInfo; } catch (Exception e) { logger.LogWarning( e, "Failed to deserialize connected model info for {Path}, skipping", jsonPath ); } } // Try to find a preview image var previewImagePath = LocalModelFile .SupportedImageExtensions.Select(ext => fileDirectory.JoinFile($"{fileNameWithoutExtension}.preview{ext}") ) .FirstOrDefault(filePath => paths.Contains(filePath)); if (previewImagePath is not null) { localModel.PreviewImageRelativePath = Path.GetRelativePath(modelsDir, previewImagePath); } // Try to find a config file (same name as model file but with .yaml extension) var configFile = fileDirectory.JoinFile($"{fileNameWithoutExtension}.yaml"); if (paths.Contains(configFile)) { localModel.ConfigFullPath = configFile; } // Add to index newIndexFlat.Add(localModel); var list = newIndex.GetOrAdd(sharedFolderType); list.Add(localModel); } ModelIndex = newIndex; stopwatch.Stop(); var indexTime = stopwatch.Elapsed; // Insert to db as transaction stopwatch.Restart(); using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false); var localModelFiles = db.GetCollection("LocalModelFiles")!; await localModelFiles.DeleteAllAsync().ConfigureAwait(false); await localModelFiles.InsertBulkAsync(newIndexFlat).ConfigureAwait(false); await db.CommitAsync().ConfigureAwait(false); stopwatch.Stop(); var dbTime = stopwatch.Elapsed; logger.LogInformation( "Model index refreshed with {Entries} entries, took (index: {IndexDuration}), (db: {DbDuration})", newIndexFlat.Count, CodeTimer.FormatTime(indexTime), CodeTimer.FormatTime(dbTime) ); EventManager.Instance.OnModelIndexChanged(); } private async Task RefreshIndexParallelCore() { if (!settingsManager.IsLibraryDirSet) { logger.LogTrace("Model index refresh skipped, library directory not set"); return; } if (new DirectoryPath(settingsManager.ModelsDirectory) is not { Exists: true } modelsDir) { logger.LogTrace("Model index refresh skipped, model directory does not exist"); return; } // Start var stopwatch = Stopwatch.StartNew(); logger.LogInformation("Refreshing model index..."); var newIndexFlat = new ConcurrentBag(); var paths = Directory .EnumerateFiles(modelsDir, "*", EnumerationOptionConstants.AllDirectories) .ToHashSet(); var partitioner = Partitioner.Create(paths, EnumerablePartitionerOptions.NoBuffering); var numThreads = Environment.ProcessorCount switch { >= 20 => Environment.ProcessorCount / 3 - 1, > 1 => Environment.ProcessorCount, _ => 1, }; Parallel.ForEach( partitioner, new ParallelOptions { MaxDegreeOfParallelism = numThreads }, path => { // Skip if not supported extension if (!LocalModelFile.SupportedCheckpointExtensions.Contains(Path.GetExtension(path))) { return; } var relativePath = Path.GetRelativePath(modelsDir, path); // Get shared folder name var sharedFolderName = relativePath.Split( Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries )[0]; // Try Convert to enum if (!Enum.TryParse(sharedFolderName, out var sharedFolderType)) { sharedFolderType = SharedFolderType.Unknown; } // Since RelativePath is the database key, for LiteDB this is limited to 1021 bytes if (Encoding.UTF8.GetByteCount(relativePath) is var byteCount and > 1021) { logger.LogWarning( "Skipping model {Path} because it's path is too long ({Length} bytes)", relativePath, byteCount ); return; } var localModel = new LocalModelFile { RelativePath = relativePath, SharedFolderType = sharedFolderType, }; // Try to find a connected model info var fileDirectory = new DirectoryPath(Path.GetDirectoryName(path)!); var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); var jsonPath = fileDirectory.JoinFile($"{fileNameWithoutExtension}.cm-info.json"); if (paths.Contains(jsonPath)) { try { using var stream = jsonPath.Info.OpenRead(); var connectedModelInfo = JsonSerializer.Deserialize( stream, ConnectedModelInfoSerializerContext.Default.ConnectedModelInfo ); // Seems there is a limitation of LiteDB datetime resolution, so drop nanoseconds on load // Otherwise new loaded models with ns will cause mismatching equality with models loaded from db with no ns if (connectedModelInfo?.ImportedAt is { } importedAt && importedAt.Nanosecond != 0) { connectedModelInfo.ImportedAt = new DateTimeOffset( importedAt.Year, importedAt.Month, importedAt.Day, importedAt.Hour, importedAt.Minute, importedAt.Second, importedAt.Millisecond, importedAt.Offset ); } localModel.ConnectedModelInfo = connectedModelInfo; } catch (Exception e) { logger.LogWarning( e, "Failed to deserialize connected model info for {Path}, skipping", jsonPath ); } } // Try to find a preview image var previewImagePath = LocalModelFile .SupportedImageExtensions.Select(ext => fileDirectory.JoinFile($"{fileNameWithoutExtension}.preview{ext}") ) .FirstOrDefault(filePath => paths.Contains(filePath)); if (previewImagePath is not null) { localModel.PreviewImageRelativePath = Path.GetRelativePath(modelsDir, previewImagePath); } // Try to find a config file (same name as model file but with .yaml extension) var configFile = fileDirectory.JoinFile($"{fileNameWithoutExtension}.yaml"); if (paths.Contains(configFile)) { localModel.ConfigFullPath = configFile; } // Add to index newIndexFlat.Add(localModel); } ); var newIndexComplete = newIndexFlat.ToArray(); var modelsDict = ModelIndex .Values.SelectMany(x => x) .DistinctBy(f => f.RelativePath) .ToDictionary(f => f.RelativePath, file => file); var newIndex = new Dictionary>(); foreach (var model in newIndexComplete) { if (modelsDict.TryGetValue(model.RelativePath, out var dbModel)) { model.HasUpdate = dbModel.HasUpdate; model.HasEarlyAccessUpdateOnly = dbModel.HasEarlyAccessUpdateOnly; model.LastUpdateCheck = dbModel.LastUpdateCheck; model.LatestModelInfo = dbModel.LatestModelInfo; } if (model.LatestModelInfo == null && model.HasCivitMetadata) { // Handle enum deserialize exceptions from changes if ( await liteDbContext .TryQueryWithClearOnExceptionAsync( liteDbContext.CivitModels, liteDbContext .CivitModels.Include(m => m.ModelVersions) .FindByIdAsync(model.ConnectedModelInfo.ModelId) ) .ConfigureAwait(false) is { } latestModel ) { model.LatestModelInfo = latestModel; } } var list = newIndex.GetOrAdd(model.SharedFolderType); list.Add(model); } ModelIndex = newIndex; stopwatch.Stop(); var indexTime = stopwatch.Elapsed; // Insert to db as transaction stopwatch.Restart(); using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false); var localModelFiles = db.GetCollection("LocalModelFiles")!; await localModelFiles.DeleteAllAsync().ConfigureAwait(false); await localModelFiles.InsertBulkAsync(newIndexComplete).ConfigureAwait(false); await db.CommitAsync().ConfigureAwait(false); stopwatch.Stop(); var dbTime = stopwatch.Elapsed; logger.LogInformation( "Model index refreshed with {Entries} entries, took {IndexDuration} ({DbDuration} db)", newIndexFlat.Count, CodeTimer.FormatTime(indexTime), CodeTimer.FormatTime(dbTime) ); EventManager.Instance.OnModelIndexChanged(); } /// public void BackgroundRefreshIndex() { Task.Run(async () => await RefreshIndex().ConfigureAwait(false)) .SafeFireAndForget(ex => { logger.LogError(ex, "Error in background model indexing"); }); } /// public async Task RemoveModelAsync(LocalModelFile model) { // Remove from database if (await liteDbContext.LocalModelFiles.DeleteAsync(model.RelativePath).ConfigureAwait(false)) { // Remove from index if (ModelIndex.TryGetValue(model.SharedFolderType, out var list)) { list.RemoveAll(x => x.RelativePath == model.RelativePath); OnModelIndexReset(); EventManager.Instance.OnModelIndexChanged(); } return true; } return false; } public async Task RemoveModelsAsync(IEnumerable models) { var modelsList = models.ToList(); var paths = modelsList.Select(m => m.RelativePath).ToList(); var result = true; foreach (var path in paths) { result &= await liteDbContext.LocalModelFiles.DeleteAsync(path).ConfigureAwait(false); } foreach (var model in modelsList) { if (ModelIndex.TryGetValue(model.SharedFolderType, out var list)) { list.RemoveAll(x => x.RelativePath == model.RelativePath); } } OnModelIndexReset(); EventManager.Instance.OnModelIndexChanged(); return result; } public async Task CheckModelsForUpdateAsync() { if (DateTimeOffset.UtcNow < lastUpdateCheck.AddMinutes(5)) { return; } lastUpdateCheck = DateTimeOffset.UtcNow; var installedHashes = ModelIndexBlake3Hashes; var dbModels = ( await liteDbContext.LocalModelFiles.FindAllAsync().ConfigureAwait(false) ?? [] ).ToList(); var ids = dbModels .Where(x => x.ConnectedModelInfo?.ModelId != null) .Select(x => x.ConnectedModelInfo!.ModelId.Value) .Distinct(); var remoteModels = (await modelFinder.FindRemoteModelsById(ids).ConfigureAwait(false)).ToList(); // update the civitmodels cache with this new result await liteDbContext.UpsertCivitModelAsync(remoteModels).ConfigureAwait(false); var localModelsToUpdate = new List(); foreach (var dbModel in dbModels) { if (dbModel.ConnectedModelInfo == null) continue; var remoteModel = remoteModels.FirstOrDefault(m => m.Id == dbModel.ConnectedModelInfo!.ModelId); var latestVersion = remoteModel?.ModelVersions?.FirstOrDefault(); if (latestVersion?.Files is not { } latestVersionFiles) { continue; } var latestHashes = latestVersionFiles .Where(f => f.Type == CivitFileType.Model) .Select(f => f.Hashes.BLAKE3) .Where(hash => hash is not null) .ToList(); dbModel.HasUpdate = !latestHashes.Any(hash => installedHashes.Contains(hash!)); dbModel.HasEarlyAccessUpdateOnly = GetHasEarlyAccessUpdateOnly(dbModel, remoteModel); dbModel.LastUpdateCheck = DateTimeOffset.UtcNow; dbModel.LatestModelInfo = remoteModel; localModelsToUpdate.Add(dbModel); } await liteDbContext.LocalModelFiles.UpsertAsync(localModelsToUpdate).ConfigureAwait(false); await LoadFromDbAsync().ConfigureAwait(false); } public async Task UpsertModelAsync(LocalModelFile model) { await liteDbContext.LocalModelFiles.UpsertAsync(model).ConfigureAwait(false); await LoadFromDbAsync().ConfigureAwait(false); } private void OnModelIndexReset() { _modelIndexBlake3Hashes = null; } private static HashSet CollectModelHashes(IEnumerable models) { var hashes = new HashSet(); foreach (var model in models) { if (model.ConnectedModelInfo?.Hashes?.BLAKE3 is { } hashBlake3) { hashes.Add(hashBlake3); } } return hashes; } private static bool GetHasEarlyAccessUpdateOnly(LocalModelFile model, CivitModel? remoteModel) { if (!model.HasUpdate || !model.HasCivitMetadata) return false; var versions = remoteModel?.ModelVersions; if (versions == null || versions.Count == 0) return false; var installedVersionId = model.ConnectedModelInfo?.VersionId; if (installedVersionId == null) return false; var installedIndex = versions.FindIndex(version => version.Id == installedVersionId.Value); if (installedIndex <= 0) return false; return versions.Take(installedIndex).All(version => version.IsEarlyAccess); } } ================================================ FILE: StabilityMatrix.Core/Services/OpenModelDbManager.cs ================================================ using System.Diagnostics.CodeAnalysis; using Apizr; using Apizr.Caching; using Apizr.Caching.Attributes; using Apizr.Configuring.Manager; using Apizr.Connecting; using Apizr.Mapping; using Fusillade; using Polly.Registry; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Models.Api.OpenModelsDb; namespace StabilityMatrix.Core.Services; public class OpenModelDbManager( ILazyFactory lazyWebApi, IConnectivityHandler connectivityHandler, ICacheHandler cacheHandler, IMappingHandler mappingHandler, ILazyFactory> lazyResiliencePipelineRegistry, IApizrManagerOptions apizrOptions ) : ApizrManager( lazyWebApi, connectivityHandler, cacheHandler, mappingHandler, lazyResiliencePipelineRegistry, apizrOptions ) { public Uri UsersBaseUri => new("https://openmodeldb.info/users"); public Uri ModelsBaseUri => new("https://openmodeldb.info/models"); public IReadOnlyDictionary? Tags { get; private set; } public IReadOnlyDictionary? Architectures { get; private set; } [MemberNotNull(nameof(Tags), nameof(Architectures))] public async Task EnsureMetadataLoadedAsync(Priority priority = default) { if (Tags is null) { Tags = await ExecuteAsync(api => api.GetTags()).ConfigureAwait(false); } if (Architectures is null) { Architectures = await ExecuteAsync(api => api.GetArchitectures()).ConfigureAwait(false); } } } ================================================ FILE: StabilityMatrix.Core/Services/PipWheelService.cs ================================================ using System.Text.RegularExpressions; using Injectio.Attributes; using NLog; using Octokit; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Services; /// /// Service for installing pip wheel packages from GitHub releases. /// [RegisterSingleton] public class PipWheelService( IGithubApiCache githubApi, IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper ) : IPipWheelService { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); #region Triton /// public async Task InstallTritonAsync( IPyVenvRunner venv, IProgress? progress = null, string? version = null ) { // No-op on macOS if (Compat.IsMacOS) { Logger.Info("Skipping Triton installation - not supported on macOS"); return; } var packageName = Compat.IsWindows ? "triton-windows" : "triton"; var versionSpec = string.IsNullOrWhiteSpace(version) ? "" : $"=={version}"; progress?.Report(new ProgressReport(-1f, $"Installing {packageName}", isIndeterminate: true)); await venv.PipInstall($"{packageName}{versionSpec}", progress.AsProcessOutputHandler()) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Triton installed", isIndeterminate: false)); } #endregion #region SageAttention /// public async Task InstallSageAttentionAsync( IPyVenvRunner venv, GpuInfo? gpuInfo = null, IProgress? progress = null, string? version = null ) { // No-op on macOS if (Compat.IsMacOS) { Logger.Info("Skipping SageAttention installation - not supported on macOS"); return; } // No-op for non-NVIDIA GPUs (SageAttention requires CUDA) if (gpuInfo is not null && !gpuInfo.IsNvidia) { Logger.Info("Skipping SageAttention installation - requires NVIDIA GPU"); return; } // On Linux, can use pip directly if (Compat.IsLinux) { var versionSpec = string.IsNullOrWhiteSpace(version) ? "" : $"=={version}"; progress?.Report(new ProgressReport(-1f, "Installing SageAttention", isIndeterminate: true)); await venv.PipInstall($"sageattention{versionSpec}", progress.AsProcessOutputHandler()) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "SageAttention installed", isIndeterminate: false)); return; } // Windows: find wheel from GitHub releases await InstallSageAttentionWindowsAsync(venv, gpuInfo, progress, version).ConfigureAwait(false); } private async Task InstallSageAttentionWindowsAsync( IPyVenvRunner venv, GpuInfo? gpuInfo, IProgress? progress, string? version ) { var torchInfo = await venv.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) { Logger.Warn("Cannot install SageAttention - torch not installed"); return; } progress?.Report(new ProgressReport(-1f, "Finding SageAttention wheel", isIndeterminate: true)); // Get releases from GitHub var releases = await githubApi.GetAllReleases("woct0rdho", "SageAttention").ConfigureAwait(false); var releaseList = releases .Where(r => r.TagName.Contains("windows")) .OrderByDescending(r => r.CreatedAt) .ToList(); if (releaseList.Count == 0) { Logger.Warn("No SageAttention Windows releases found"); await InstallSageAttentionFromSourceAsync(venv, progress).ConfigureAwait(false); return; } // Find matching wheel from release assets var wheelUrl = FindMatchingWheelAsset(releaseList, torchInfo, venv.Version, version); if (!string.IsNullOrWhiteSpace(wheelUrl)) { progress?.Report( new ProgressReport(-1f, "Installing Triton & SageAttention", isIndeterminate: true) ); // Install triton-windows first, then sage with --no-deps to prevent torch reinstall var pipArgs = new PipInstallArgs("triton-windows").AddArg("--no-deps").AddArg(wheelUrl); await venv.PipInstall(pipArgs, progress.AsProcessOutputHandler()).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "SageAttention installed", isIndeterminate: false)); return; } // No wheel found - fall back to building from source Logger.Info("No matching SageAttention wheel found, building from source"); await InstallSageAttentionFromSourceAsync(venv, progress).ConfigureAwait(false); } private static string? FindMatchingWheelAsset( IEnumerable releases, PipShowResult torchInfo, PyVersion pyVersion, string? targetVersion ) { // Parse torch info var torchVersionStr = torchInfo.Version; var plusIndex = torchVersionStr.IndexOf('+'); var baseTorchVersion = plusIndex >= 0 ? torchVersionStr[..plusIndex] : torchVersionStr; var cudaIndex = plusIndex >= 0 ? torchVersionStr[(plusIndex + 1)..] : ""; // Get major.minor of torch var torchParts = baseTorchVersion.Split('.'); var shortTorch = torchParts.Length >= 2 ? $"{torchParts[0]}.{torchParts[1]}" : baseTorchVersion; // Get python version string (e.g., "cp312") var shortPy = $"cp3{pyVersion.Minor}"; foreach (var release in releases) { // If a specific version is requested, filter releases if (!string.IsNullOrWhiteSpace(targetVersion) && !release.TagName.Contains(targetVersion)) continue; foreach (var asset in release.Assets) { var name = asset.Name; // Must be a wheel file if (!name.EndsWith(".whl")) continue; // Must be for Windows if (!name.Contains("win_amd64")) continue; // Check Python version compatibility (cp39-abi3 works for cp39+, or specific version) var matchesPython = name.Contains($"{shortPy}-{shortPy}") || name.Contains("cp39-abi3") || (pyVersion.Minor >= 9 && name.Contains("abi3")); if (!matchesPython) continue; // Check torch version match // Assets use patterns like: cu128torch2.9.0 or cu130torch2.9.0andhigher var matchesTorch = name.Contains($"torch{shortTorch}") || name.Contains($"torch{baseTorchVersion}") || (name.Contains("andhigher") && CompareTorchVersions(baseTorchVersion, name)); // Check CUDA index match var matchesCuda = !string.IsNullOrEmpty(cudaIndex) && name.Contains(cudaIndex); if (matchesTorch && matchesCuda) { Logger.Info("Found matching SageAttention wheel: {Name}", name); return asset.BrowserDownloadUrl; } } } return null; } private static bool CompareTorchVersions(string installedTorch, string assetName) { // Extract torch version from asset name (e.g., "torch2.9.0andhigher" -> "2.9.0") var match = Regex.Match(assetName, @"torch(\d+\.\d+\.\d+)"); if (!match.Success) return false; if (!Version.TryParse(installedTorch, out var installed)) return false; if (!Version.TryParse(match.Groups[1].Value, out var required)) return false; // "andhigher" means installed version must be >= required version return installed >= required; } private async Task InstallSageAttentionFromSourceAsync( IPyVenvRunner venv, IProgress? progress ) { // Check prerequisites if (!prerequisiteHelper.IsVcBuildToolsInstalled) { Logger.Warn("Cannot build SageAttention from source - VS Build Tools not installed"); return; } var nvccPath = await Utilities.WhichAsync("nvcc").ConfigureAwait(false); if (string.IsNullOrWhiteSpace(nvccPath)) { var cuda126Path = new DirectoryPath( @"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\bin" ); var cuda128Path = new DirectoryPath( @"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.8\bin" ); if (!cuda126Path.Exists && !cuda128Path.Exists) { Logger.Warn("Cannot build SageAttention from source - CUDA Toolkit not found"); return; } nvccPath = cuda128Path.Exists ? cuda128Path.JoinFile("nvcc.exe").ToString() : cuda126Path.JoinFile("nvcc.exe").ToString(); } // Set up CUDA environment var cudaBinPath = Path.GetDirectoryName(nvccPath)!; var cudaHome = Path.GetDirectoryName(cudaBinPath)!; venv.UpdateEnvironmentVariables(env => { env = env.TryGetValue("PATH", out var pathValue) ? env.SetItem("PATH", $"{cudaBinPath}{Path.PathSeparator}{pathValue}") : env.Add("PATH", cudaBinPath); if (!env.ContainsKey("CUDA_HOME")) { env = env.Add("CUDA_HOME", cudaHome); } return env; }); progress?.Report(new ProgressReport(-1f, "Installing Triton", isIndeterminate: true)); await venv.PipInstall("triton-windows", progress.AsProcessOutputHandler()).ConfigureAwait(false); venv.UpdateEnvironmentVariables(env => env.SetItem("SETUPTOOLS_USE_DISTUTILS", "setuptools")); // Download python libs for building await AddMissingLibsToVenvAsync(venv, progress).ConfigureAwait(false); var sageDir = venv.WorkingDirectory?.JoinDir("SageAttention") ?? new DirectoryPath("SageAttention"); if (!sageDir.Exists) { progress?.Report(new ProgressReport(-1f, "Downloading SageAttention", isIndeterminate: true)); await prerequisiteHelper .RunGit( ["clone", "https://github.com/thu-ml/SageAttention.git", sageDir.ToString()], progress.AsProcessOutputHandler() ) .ConfigureAwait(false); } progress?.Report(new ProgressReport(-1f, "Building SageAttention", isIndeterminate: true)); await venv.PipInstall([sageDir.ToString()], progress.AsProcessOutputHandler()).ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "SageAttention built and installed", isIndeterminate: false)); } private async Task AddMissingLibsToVenvAsync(IPyVenvRunner venv, IProgress? progress) { var venvLibsDir = venv.RootPath.JoinDir("libs"); var venvIncludeDir = venv.RootPath.JoinDir("include"); if ( venvLibsDir.Exists && venvIncludeDir.Exists && venvLibsDir.JoinFile("python3.lib").Exists && venvLibsDir.JoinFile("python310.lib").Exists ) { return; } const string pythonLibsUrl = "https://cdn.lykos.ai/python_libs_for_sage.zip"; var downloadPath = venv.RootPath.JoinFile("python_libs_for_sage.zip"); progress?.Report(new ProgressReport(-1f, "Downloading Python libraries", isIndeterminate: true)); await downloadService .DownloadToFileAsync(pythonLibsUrl, downloadPath, progress) .ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Extracting Python libraries", isIndeterminate: true)); await ArchiveHelper.Extract7Z(downloadPath, venv.RootPath, progress).ConfigureAwait(false); var includeFolder = venv.RootPath.JoinDir("include"); var scriptsIncludeFolder = venv.RootPath.JoinDir("Scripts", "include"); await includeFolder.CopyToAsync(scriptsIncludeFolder).ConfigureAwait(false); await downloadPath.DeleteAsync().ConfigureAwait(false); } #endregion #region Nunchaku /// public async Task InstallNunchakuAsync( IPyVenvRunner venv, GpuInfo? gpuInfo = null, IProgress? progress = null, string? version = null ) { // No-op on macOS if (Compat.IsMacOS) { Logger.Info("Skipping Nunchaku installation - not supported on macOS"); return; } // No-op for GPUs with compute capability < 7.5 if (gpuInfo?.ComputeCapabilityValue is < 7.5m) { Logger.Info("Skipping Nunchaku installation - GPU compute capability < 7.5"); return; } var torchInfo = await venv.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) { Logger.Warn("Cannot install Nunchaku - torch not installed"); return; } progress?.Report(new ProgressReport(-1f, "Finding Nunchaku wheel", isIndeterminate: true)); // Get releases from GitHub var releases = await githubApi.GetAllReleases("nunchaku-ai", "nunchaku").ConfigureAwait(false); var releaseList = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.CreatedAt).ToList(); if (releaseList.Count == 0) { Logger.Warn("No Nunchaku releases found"); return; } var wheelUrl = FindMatchingNunchakuWheelAsset(releaseList, torchInfo, venv.Version, version); if (string.IsNullOrWhiteSpace(wheelUrl)) { Logger.Warn("No compatible Nunchaku wheel found for torch {TorchVersion}", torchInfo.Version); return; } progress?.Report(new ProgressReport(-1f, "Installing Nunchaku", isIndeterminate: true)); // Use --no-deps to prevent reinstalling torch without CUDA await venv.PipInstall( new PipInstallArgs("--no-deps").AddArg(wheelUrl), progress.AsProcessOutputHandler() ) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "Nunchaku installed", isIndeterminate: false)); } private static string? FindMatchingNunchakuWheelAsset( IEnumerable releases, PipShowResult torchInfo, PyVersion pyVersion, string? targetVersion ) { // Parse torch version var torchVersionStr = torchInfo.Version; var plusIndex = torchVersionStr.IndexOf('+'); var baseTorchVersion = plusIndex >= 0 ? torchVersionStr[..plusIndex] : torchVersionStr; var torchParts = baseTorchVersion.Split('.'); var shortTorch = torchParts.Length >= 2 ? $"{torchParts[0]}.{torchParts[1]}" : baseTorchVersion; // Get python version string var shortPy = $"cp3{pyVersion.Minor}"; // Get platform var platform = Compat.IsWindows ? "win_amd64" : "linux_x86_64"; Logger.Debug( "Searching for Nunchaku wheel: Python={ShortPy}, Torch={ShortTorch}, Platform={Platform}", shortPy, shortTorch, platform ); foreach (var release in releases) { // If a specific version is requested, filter releases if (!string.IsNullOrWhiteSpace(targetVersion) && !release.TagName.Contains(targetVersion)) continue; foreach (var asset in release.Assets) { var name = asset.Name; if (!name.EndsWith(".whl")) continue; if (!name.Contains(platform)) continue; // Check Python version if (!name.Contains($"{shortPy}-{shortPy}")) continue; // Check torch version (assets use patterns like: torch2.7 or torch2.8) if (!name.Contains($"torch{shortTorch}")) continue; Logger.Info( "Found matching Nunchaku wheel: {Name} (Python={ShortPy}, Torch={ShortTorch})", name, shortPy, shortTorch ); return asset.BrowserDownloadUrl; } } return null; } #endregion #region FlashAttention /// public async Task InstallFlashAttentionAsync( IPyVenvRunner venv, IProgress? progress = null, string? version = null ) { // Windows only if (!Compat.IsWindows) { Logger.Info("Skipping FlashAttention installation - Windows only"); return; } var torchInfo = await venv.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) { Logger.Warn("Cannot install FlashAttention - torch not installed"); return; } progress?.Report(new ProgressReport(-1f, "Finding FlashAttention wheel", isIndeterminate: true)); // Get releases from GitHub var releases = await githubApi .GetAllReleases("mjun0812", "flash-attention-prebuild-wheels") .ConfigureAwait(false); var releaseList = releases.OrderByDescending(r => r.CreatedAt).ToList(); if (releaseList.Count == 0) { Logger.Warn("No FlashAttention releases found"); return; } var wheelUrl = FindMatchingFlashAttentionWheelAsset(releaseList, torchInfo, venv.Version, version); if (string.IsNullOrWhiteSpace(wheelUrl)) { Logger.Warn( "No compatible FlashAttention wheel found for torch {TorchVersion}", torchInfo.Version ); return; } progress?.Report(new ProgressReport(-1f, "Installing FlashAttention", isIndeterminate: true)); // Use --no-deps to prevent reinstalling torch without CUDA await venv.PipInstall( new PipInstallArgs("--no-deps").AddArg(wheelUrl), progress.AsProcessOutputHandler() ) .ConfigureAwait(false); progress?.Report(new ProgressReport(1f, "FlashAttention installed", isIndeterminate: false)); } private static string? FindMatchingFlashAttentionWheelAsset( IEnumerable releases, PipShowResult torchInfo, PyVersion pyVersion, string? targetVersion ) { // Parse torch version and CUDA index var torchVersionStr = torchInfo.Version; var plusIndex = torchVersionStr.IndexOf('+'); var baseTorchVersion = plusIndex >= 0 ? torchVersionStr[..plusIndex] : torchVersionStr; var cudaIndex = plusIndex >= 0 ? torchVersionStr[(plusIndex + 1)..] : ""; var torchParts = baseTorchVersion.Split('.'); var shortTorch = torchParts.Length >= 2 ? $"{torchParts[0]}.{torchParts[1]}" : baseTorchVersion; // Get python version string var shortPy = $"cp3{pyVersion.Minor}"; foreach (var release in releases) { foreach (var asset in release.Assets) { var name = asset.Name; if (!name.EndsWith(".whl")) continue; if (!name.Contains("win_amd64")) continue; // Check for specific version if requested if ( !string.IsNullOrWhiteSpace(targetVersion) && !name.Contains($"flash_attn-{targetVersion}") ) continue; // Check Python version if (!name.Contains($"{shortPy}-{shortPy}")) continue; // Check torch version if (!name.Contains($"torch{shortTorch}")) continue; // Check CUDA index if (!string.IsNullOrEmpty(cudaIndex) && !name.Contains(cudaIndex)) continue; Logger.Info("Found matching FlashAttention wheel: {Name}", name); return asset.BrowserDownloadUrl; } } return null; } #endregion } ================================================ FILE: StabilityMatrix.Core/Services/SecretsManager.cs ================================================ using System.Reactive.Concurrency; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Services; /// /// Default implementation of . /// Data is encrypted at rest in %APPDATA%\StabilityMatrix\user-secrets.data /// [RegisterSingleton] public class SecretsManager : ISecretsManager { private readonly ILogger logger; private static FilePath GlobalFile => GlobalConfig.HomeDir.JoinFile("user-secrets.data"); private static SemaphoreSlim GlobalFileLock { get; } = new(1, 1); public SecretsManager(ILogger logger) { this.logger = logger; } /// public async Task LoadAsync() { if (!GlobalFile.Exists) { return new Secrets(); } var fileBytes = await GlobalFile.ReadAllBytesAsync().ConfigureAwait(false); return GlobalEncryptedSerializer.Deserialize(fileBytes); } /// public async Task SafeLoadAsync() { try { return await LoadAsync().ConfigureAwait(false); } catch (Exception e) { logger.LogError(e, "Failed to load secrets ({ExcType}), saving new instance", e.GetType().Name); var secrets = new Secrets(); await SaveAsync(secrets).ConfigureAwait(false); return secrets; } } /// public async Task SaveAsync(Secrets secrets) { await GlobalFileLock.WaitAsync().ConfigureAwait(false); try { var fileBytes = GlobalEncryptedSerializer.Serialize(secrets); await GlobalFile.WriteAllBytesAsync(fileBytes).ConfigureAwait(false); } finally { GlobalFileLock.Release(); } } } ================================================ FILE: StabilityMatrix.Core/Services/SettingsManager.cs ================================================ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; using System.Text.Json; using AsyncAwaitBestPractices; using CompiledExpressions; using Injectio.Attributes; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Services; [RegisterSingleton] public class SettingsManager(ILogger logger) : ISettingsManager { private static string GlobalSettingsPath => Path.Combine(Compat.AppDataHome, "global.json"); private readonly SemaphoreSlim fileLock = new(1, 1); private bool isLoaded; private DirectoryPath? libraryDirOverride; // Library properties public bool IsPortableMode { get; private set; } private DirectoryPath? libraryDir; public DirectoryPath LibraryDir { get { if (libraryDir is null) { throw new InvalidOperationException("LibraryDir is not set"); } return libraryDir; } private set { var isChanged = libraryDir != value; libraryDir = value; // Only invoke if different if (isChanged) { LibraryDirChanged?.Invoke(this, value); } } } [MemberNotNullWhen(true, nameof(libraryDir))] public bool IsLibraryDirSet => libraryDir is not null; // Dynamic paths from library private FilePath SettingsFile => LibraryDir.JoinFile("settings.json"); public string ModelsDirectory => Settings.ModelDirectoryOverride ?? Path.Combine(LibraryDir, "Models"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); public DirectoryPath WorkflowDirectory => LibraryDir.JoinDir("Workflows"); public DirectoryPath TagsDirectory => LibraryDir.JoinDir("Tags"); public DirectoryPath ImagesDirectory => LibraryDir.JoinDir("Images"); public DirectoryPath ImagesInferenceDirectory => ImagesDirectory.JoinDir("Inference"); public DirectoryPath ConsolidatedImagesDirectory => ImagesDirectory.JoinDir("Consolidated"); public DirectoryPath ExtensionPackDirectory => LibraryDir.JoinDir("ExtensionPacks"); public Settings Settings { get; private set; } = new(); public List PackageInstallsInProgress { get; set; } = []; /// public event EventHandler? LibraryDirChanged; /// public event EventHandler? SettingsPropertyChanged; /// public event EventHandler? Loaded; /// public void SetLibraryDirOverride(DirectoryPath path) { libraryDirOverride = path; } /// public void RegisterOnLibraryDirSet(Action handler) { if (IsLibraryDirSet) { handler(LibraryDir); return; } LibraryDirChanged += Handler; return; void Handler(object? sender, string dir) { LibraryDirChanged -= Handler; handler(dir); } } /// public SettingsTransaction BeginTransaction() { if (!IsLibraryDirSet) { throw new InvalidOperationException("LibraryDir not set when BeginTransaction was called"); } return new SettingsTransaction(this, () => SaveSettings(), () => SaveSettingsAsync()); } /// public void Transaction(Action func, bool ignoreMissingLibraryDir = false) { if (!IsLibraryDirSet) { if (ignoreMissingLibraryDir) { func(Settings); return; } throw new InvalidOperationException("LibraryDir not set when Transaction was called"); } using var transaction = BeginTransaction(); func(transaction.Settings); } /// public void Transaction(Expression> expression, TValue value) { var accessor = CompiledExpression.CreateAccessor(expression); // Set value using var transaction = BeginTransaction(); accessor.Set(transaction.Settings, value); // Invoke property changed event SettingsPropertyChanged?.Invoke(this, new RelayPropertyChangedEventArgs(accessor.FullName)); } /// public IDisposable RelayPropertyFor( T source, Expression> sourceProperty, Expression> settingsProperty, bool setInitial = false, TimeSpan? delay = null ) where T : INotifyPropertyChanged { var sourceInstanceAccessor = CompiledExpression.CreateAccessor(sourceProperty).WithInstance(source); var settingsAccessor = CompiledExpression.CreateAccessor(settingsProperty); var sourcePropertyPath = sourceInstanceAccessor.FullName; var settingsPropertyPath = settingsAccessor.FullName; var sourceTypeName = source.GetType().Name; // Update source when settings change void OnSettingsPropertyChanged(object? sender, RelayPropertyChangedEventArgs args) { if (args.PropertyName != settingsPropertyPath) return; // Skip if event is relay and the sender is the source, to prevent duplicate if (args.IsRelay && ReferenceEquals(sender, source)) return; logger.LogTrace( "[RelayPropertyFor] " + "Settings.{SettingsProperty:l} -> {SourceType:l}.{SourceProperty:l}", settingsPropertyPath, sourceTypeName, sourcePropertyPath ); sourceInstanceAccessor.Set(source, settingsAccessor.Get(Settings)); } // Set and Save settings when source changes void OnSourcePropertyChanged(object? sender, PropertyChangedEventArgs args) { if (args.PropertyName != sourcePropertyPath) return; // If TValue is a primitive type, check if there are changes first. // If not, skip saving and property changed event. if (typeof(TValue).IsPrimitive || typeof(TValue).IsEnum) { var settingsValue = settingsAccessor.Get(Settings); var sourceValue = sourceInstanceAccessor.Get(); if (EqualityComparer.Default.Equals(settingsValue, sourceValue)) { /*logger.LogTrace( "[RelayPropertyFor] {SourceType:l}.{SourceProperty:l} -> Settings.{SettingsProperty:l} ()", sourceTypeName, sourcePropertyPath, settingsPropertyPath );*/ return; } } logger.LogTrace( "[RelayPropertyFor] {SourceType:l}.{SourceProperty:l} -> Settings.{SettingsProperty:l}", sourceTypeName, sourcePropertyPath, settingsPropertyPath ); settingsAccessor.Set(Settings, sourceInstanceAccessor.Get()); if (IsLibraryDirSet) { if (delay != null) { SaveSettingsDelayed(delay.Value).SafeFireAndForget(); } else { SaveSettingsAsync().SafeFireAndForget(); } } else { logger.LogWarning( "[RelayPropertyFor] LibraryDir not set when saving ({SourceType:l}.{SourceProperty:l} -> Settings.{SettingsProperty:l})", sourceTypeName, sourcePropertyPath, settingsPropertyPath ); } // Invoke property changed event, passing along sender SettingsPropertyChanged?.Invoke( sender, new RelayPropertyChangedEventArgs(settingsPropertyPath, true) ); } var subscription = Disposable.Create(() => { source.PropertyChanged -= OnSourcePropertyChanged; SettingsPropertyChanged -= OnSettingsPropertyChanged; }); try { SettingsPropertyChanged += OnSettingsPropertyChanged; source.PropertyChanged += OnSourcePropertyChanged; // Set initial value if requested if (setInitial) { sourceInstanceAccessor.Set(settingsAccessor.Get(Settings)); } } catch { subscription.Dispose(); throw; } return subscription; } /// public IDisposable RegisterPropertyChangedHandler( Expression> settingsProperty, Action onPropertyChanged ) { var handlerName = onPropertyChanged.Method.Name; var settingsAccessor = CompiledExpression.CreateAccessor(settingsProperty); return Observable .FromEventPattern, RelayPropertyChangedEventArgs>( h => SettingsPropertyChanged += h, h => SettingsPropertyChanged -= h ) .Where(args => args.EventArgs.PropertyName == settingsAccessor.FullName) .Subscribe(_ => { logger.LogTrace( "[RegisterPropertyChangedHandler] Settings.{SettingsProperty:l} -> Handler ({Action})", settingsAccessor.FullName, handlerName ); onPropertyChanged(settingsAccessor.Get(Settings)); }); } /// public IObservable ObservePropertyChanged(Expression> settingsProperty) { var settingsAccessor = CompiledExpression.CreateAccessor(settingsProperty); return Observable .FromEventPattern, RelayPropertyChangedEventArgs>( h => SettingsPropertyChanged += h, h => SettingsPropertyChanged -= h ) .Where(args => args.EventArgs.PropertyName == settingsAccessor.FullName) .Select(_ => settingsAccessor.Get(Settings)); } /// /// Attempts to locate and set the library path /// Return true if found, false otherwise /// public bool TryFindLibrary(bool forceReload = false) { if (IsLibraryDirSet && !forceReload) return true; // 0. Check Override if (libraryDirOverride is not null) { logger.LogInformation("Using library override path {Path}", libraryDirOverride.FullPath); LibraryDir = libraryDirOverride; SetStaticLibraryPaths(); LoadSettings(); return true; } // 1. Check portable mode var appDir = Compat.AppCurrentDir; IsPortableMode = File.Exists(Path.Combine(appDir, "Data", ".sm-portable")); if (IsPortableMode) { LibraryDir = appDir + "Data"; SetStaticLibraryPaths(); LoadSettings(); return true; } // 2. Check %APPDATA%/StabilityMatrix/library.json FilePath libraryJsonFile = Compat.AppDataHome + "library.json"; if (!libraryJsonFile.Exists) return false; try { var libraryJson = libraryJsonFile.ReadAllText(); var librarySettings = JsonSerializer.Deserialize(libraryJson); if ( !string.IsNullOrWhiteSpace(librarySettings?.LibraryPath) && Directory.Exists(librarySettings.LibraryPath) ) { LibraryDir = librarySettings.LibraryPath; SetStaticLibraryPaths(); LoadSettings(); return true; } } catch (Exception e) { logger.LogWarning("Failed to read library.json in AppData: {Message}", e.Message); } return false; } // Set static classes requiring library path private void SetStaticLibraryPaths() { GlobalConfig.LibraryDir = LibraryDir; ArchiveHelper.HomeDir = LibraryDir; PyRunner.HomeDir = LibraryDir; GlobalConfig.ModelsDir = ModelsDirectory; } /// /// Save a new library path to %APPDATA%/StabilityMatrix/library.json /// public void SetLibraryPath(string path) { Compat.AppDataHome.Create(); var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json"); var library = new LibrarySettings { LibraryPath = path }; var libraryJson = JsonSerializer.Serialize( library, new JsonSerializerOptions { WriteIndented = true } ); libraryJsonFile.WriteAllText(libraryJson); // actually create the LibraryPath directory Directory.CreateDirectory(path); } /// /// Enable and create settings files for portable mode /// Creates the ./Data directory and the `.sm-portable` marker file /// public void SetPortableMode() { // Get app directory var appDir = Compat.AppCurrentDir; // Create data directory var dataDir = appDir.JoinDir("Data"); dataDir.Create(); // Create marker file dataDir.JoinFile(".sm-portable").Create(); } public void SaveLaunchArgs(Guid packageId, IEnumerable launchArgs) { var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId); if (packageData == null) { return; } // Only save if not null or default var toSave = launchArgs.Where(opt => !opt.IsEmptyOrDefault()).ToList(); packageData.LaunchArgs = toSave; SaveSettings(); } public bool IsEulaAccepted() { if (!File.Exists(GlobalSettingsPath)) { Directory.CreateDirectory(Path.GetDirectoryName(GlobalSettingsPath)!); File.Create(GlobalSettingsPath).Close(); File.WriteAllText(GlobalSettingsPath, "{}"); return false; } var json = File.ReadAllText(GlobalSettingsPath); var globalSettings = JsonSerializer.Deserialize(json); return globalSettings?.EulaAccepted ?? false; } public void SetEulaAccepted() { var globalSettings = new GlobalSettings { EulaAccepted = true }; var json = JsonSerializer.Serialize(globalSettings); File.WriteAllText(GlobalSettingsPath, json); } /// /// Loads settings from the settings file. Continues without loading if the file does not exist or is empty. /// Will set to true when finished in any case. /// protected virtual void LoadSettings(CancellationToken cancellationToken = default) { fileLock.Wait(cancellationToken); try { if (!SettingsFile.Exists) { return; } using var fileStream = SettingsFile.Info.OpenRead(); if (fileStream.Length == 0) { logger.LogWarning("Settings file is empty, using default settings"); return; } var loadedSettings = JsonSerializer.Deserialize( fileStream, SettingsSerializerContext.Default.Settings ); if (loadedSettings is not null) { Settings = loadedSettings; } } finally { fileLock.Release(); isLoaded = true; Loaded?.Invoke(this, EventArgs.Empty); } } /// /// Loads settings from the settings file. Continues without loading if the file does not exist or is empty. /// Will set to true when finished in any case. /// protected virtual async Task LoadSettingsAsync(CancellationToken cancellationToken = default) { await fileLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { if (!SettingsFile.Exists) { return; } await using var fileStream = SettingsFile.Info.OpenRead(); if (fileStream.Length == 0) { logger.LogWarning("Settings file is empty, using default settings"); return; } var loadedSettings = await JsonSerializer .DeserializeAsync(fileStream, SettingsSerializerContext.Default.Settings, cancellationToken) .ConfigureAwait(false); if (loadedSettings is not null) { Settings = loadedSettings; } Loaded?.Invoke(this, EventArgs.Empty); } finally { fileLock.Release(); isLoaded = true; Loaded?.Invoke(this, EventArgs.Empty); } } protected virtual void SaveSettings(CancellationToken cancellationToken = default) { // Skip saving if not loaded yet if (!isLoaded) return; fileLock.Wait(cancellationToken); try { // Create empty settings file if it doesn't exist if (!SettingsFile.Exists) { SettingsFile.Directory?.Create(); SettingsFile.Create(); } // Check disk space if (SystemInfo.GetDiskFreeSpaceBytes(SettingsFile) is < 1 * SystemInfo.Mebibyte) { logger.LogWarning("Not enough disk space to save settings"); return; } var jsonBytes = JsonSerializer.SerializeToUtf8Bytes( Settings, SettingsSerializerContext.Default.Settings ); if (jsonBytes.Length == 0) { logger.LogError("JsonSerializer returned empty bytes for some reason"); return; } using var fs = File.Open(SettingsFile, FileMode.Open); if (fs.CanWrite) { fs.Write(jsonBytes, 0, jsonBytes.Length); fs.Flush(); fs.SetLength(jsonBytes.Length); } } finally { fileLock.Release(); } } protected virtual async Task SaveSettingsAsync(CancellationToken cancellationToken = default) { // Skip saving if not loaded yet if (!isLoaded) return; await fileLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { // Create empty settings file if it doesn't exist if (!SettingsFile.Exists) { SettingsFile.Directory?.Create(); SettingsFile.Create(); } // Check disk space if (SystemInfo.GetDiskFreeSpaceBytes(SettingsFile) is < 1 * SystemInfo.Mebibyte) { logger.LogWarning("Not enough disk space to save settings"); return; } var jsonBytes = JsonSerializer.SerializeToUtf8Bytes( Settings, SettingsSerializerContext.Default.Settings ); if (jsonBytes.Length == 0) { logger.LogError("JsonSerializer returned empty bytes for some reason"); return; } await using var fs = File.Open(SettingsFile, FileMode.Open); if (fs.CanWrite) { await fs.WriteAsync(jsonBytes, cancellationToken).ConfigureAwait(false); await fs.FlushAsync(cancellationToken).ConfigureAwait(false); fs.SetLength(jsonBytes.Length); } } finally { fileLock.Release(); } } private volatile CancellationTokenSource? delayedSaveCts; private Task SaveSettingsDelayed(TimeSpan delay) { var cts = new CancellationTokenSource(); var oldCancellationToken = Interlocked.Exchange(ref delayedSaveCts, cts); try { oldCancellationToken?.Cancel(); } catch (ObjectDisposedException) { } return Task.Run( async () => { try { await Task.Delay(delay, cts.Token).ConfigureAwait(false); await SaveSettingsAsync(cts.Token).ConfigureAwait(false); } catch (TaskCanceledException) { } finally { cts.Dispose(); } }, CancellationToken.None ); } public Task FlushAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } if (!isLoaded) { return Task.CompletedTask; } // Cancel any delayed save tasks try { Interlocked.Exchange(ref delayedSaveCts, null)?.Cancel(); } catch (ObjectDisposedException) { } return SaveSettingsAsync(cancellationToken); } } ================================================ FILE: StabilityMatrix.Core/Services/TrackedDownloadService.cs ================================================ using System.Collections.Concurrent; using System.Text; using System.Text.Json; using AsyncAwaitBestPractices; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; namespace StabilityMatrix.Core.Services; public class TrackedDownloadService : ITrackedDownloadService, IDisposable { private readonly ILogger logger; private readonly IDownloadService downloadService; private readonly ISettingsManager settingsManager; private readonly IModelIndexService modelIndexService; private readonly ConcurrentDictionary downloads = new(); private readonly ConcurrentQueue pendingDownloads = new(); private readonly SemaphoreSlim downloadSemaphore; public IEnumerable Downloads => downloads.Values.Select(x => x.Download); public IEnumerable PendingDownloads => pendingDownloads; /// public event EventHandler? DownloadAdded; public event EventHandler? DownloadStarted; private int MaxConcurrentDownloads { get; set; } private bool IsQueueEnabled => MaxConcurrentDownloads > 0; public int ActiveDownloads => downloads.Count(kvp => kvp.Value.Download.ProgressState == ProgressState.Working); public TrackedDownloadService( ILogger logger, IDownloadService downloadService, IModelIndexService modelIndexService, ISettingsManager settingsManager ) { this.logger = logger; this.downloadService = downloadService; this.settingsManager = settingsManager; this.modelIndexService = modelIndexService; // Index for in-progress downloads when library dir loaded settingsManager.RegisterOnLibraryDirSet(path => { var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); // Ignore if not exist if (!downloadsDir.Exists) return; LoadInProgressDownloads(downloadsDir); }); MaxConcurrentDownloads = settingsManager.Settings.MaxConcurrentDownloads; downloadSemaphore = new SemaphoreSlim(MaxConcurrentDownloads); } private void OnDownloadAdded(TrackedDownload download) { logger.LogInformation("Download added: ({Download}, {State})", download.Id, download.ProgressState); DownloadAdded?.Invoke(this, download); } private void OnDownloadStarted(TrackedDownload download) { logger.LogInformation("Download started: ({Download}, {State})", download.Id, download.ProgressState); DownloadStarted?.Invoke(this, download); } /// /// Creates a new tracked download with backed json file and adds it to the dictionary. /// /// private void AddDownload(TrackedDownload download) { // Set download service download.SetDownloadService(downloadService); // Create json file var downloadsDir = new DirectoryPath(settingsManager.DownloadsDirectory); downloadsDir.Create(); var jsonFile = downloadsDir.JoinFile($"{download.Id}.json"); var jsonFileStream = jsonFile.Info.Open(FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read); // Serialize to json var json = JsonSerializer.Serialize(download); jsonFileStream.Write(Encoding.UTF8.GetBytes(json)); jsonFileStream.Flush(); // Add to dictionary downloads.TryAdd(download.Id, (download, jsonFileStream)); // Connect to state changed event to update json file AttachHandlers(download); OnDownloadAdded(download); } public async Task TryStartDownload(TrackedDownload download) { if (IsQueueEnabled && ActiveDownloads >= MaxConcurrentDownloads) { logger.LogDebug("Download {Download} is pending", download.FileName); pendingDownloads.Enqueue(download); download.SetPending(); UpdateJsonForDownload(download); return; } if (!IsQueueEnabled || await downloadSemaphore.WaitAsync(0).ConfigureAwait(false)) { logger.LogDebug("Starting download {Download}", download.FileName); download.Start(); OnDownloadStarted(download); } else { logger.LogDebug("Download {Download} is pending", download.FileName); pendingDownloads.Enqueue(download); download.SetPending(); UpdateJsonForDownload(download); } } public async Task TryResumeDownload(TrackedDownload download) { if (IsQueueEnabled && ActiveDownloads >= MaxConcurrentDownloads) { logger.LogDebug("Download {Download} is pending", download.FileName); pendingDownloads.Enqueue(download); download.SetPending(); UpdateJsonForDownload(download); return; } if (!IsQueueEnabled || await downloadSemaphore.WaitAsync(0).ConfigureAwait(false)) { logger.LogDebug("Resuming download {Download}", download.FileName); download.Resume(); OnDownloadStarted(download); } else { logger.LogDebug("Download {Download} is pending", download.FileName); pendingDownloads.Enqueue(download); download.SetPending(); UpdateJsonForDownload(download); } } public void UpdateMaxConcurrentDownloads(int newMax) { if (newMax <= 0) { MaxConcurrentDownloads = 0; return; } var oldMax = MaxConcurrentDownloads; MaxConcurrentDownloads = newMax; if (oldMax == newMax) return; logger.LogInformation("Updating max concurrent downloads from {OldMax} to {NewMax}", oldMax, newMax); if (newMax > oldMax) { downloadSemaphore.Release(newMax - oldMax); ProcessPendingDownloads().SafeFireAndForget(); } // When reducing, we don't need to do anything immediately. // The system will naturally adjust as downloads complete or are paused/resumed. AdjustSemaphoreCount(); } private void AdjustSemaphoreCount() { var currentCount = downloadSemaphore.CurrentCount; var targetCount = MaxConcurrentDownloads - ActiveDownloads; if (currentCount < targetCount) { downloadSemaphore.Release(targetCount - currentCount); } else if (currentCount > targetCount) { for (var i = 0; i < currentCount - targetCount; i++) { downloadSemaphore.Wait(0); } } } /// /// Update the json file for the download. /// private void UpdateJsonForDownload(TrackedDownload download) { // Serialize to json var json = JsonSerializer.Serialize(download); var jsonBytes = Encoding.UTF8.GetBytes(json); // Write to file var (_, fs) = downloads[download.Id]; fs.Seek(0, SeekOrigin.Begin); fs.Write(jsonBytes); fs.Flush(); } private void AttachHandlers(TrackedDownload download) { download.ProgressStateChanged += TrackedDownload_OnProgressStateChanged; } private async Task ProcessPendingDownloads() { while (pendingDownloads.TryPeek(out var nextDownload)) { if (ActiveDownloads >= MaxConcurrentDownloads) { break; } if (pendingDownloads.TryDequeue(out nextDownload)) { if (nextDownload.DownloadedBytes > 0) { await TryResumeDownload(nextDownload).ConfigureAwait(false); } else { await TryStartDownload(nextDownload).ConfigureAwait(false); } } else { break; } } } /// /// Handler when the download's state changes /// private void TrackedDownload_OnProgressStateChanged(object? sender, ProgressState e) { if (sender is not TrackedDownload download) { return; } // Update json file UpdateJsonForDownload(download); // If the download is completed, remove it from the dictionary and delete the json file if (e is ProgressState.Success or ProgressState.Failed or ProgressState.Cancelled) { if (downloads.TryRemove(download.Id, out var downloadInfo)) { downloadInfo.Item2.Dispose(); // Delete json file new DirectoryPath(settingsManager.DownloadsDirectory) .JoinFile($"{download.Id}.json") .Delete(); logger.LogDebug("Removed download {Download}", download.FileName); if (IsQueueEnabled) { try { downloadSemaphore.Release(); } catch (SemaphoreFullException) { // Ignore } ProcessPendingDownloads().SafeFireAndForget(); } } } else if (e is ProgressState.Paused && IsQueueEnabled) { downloadSemaphore.Release(); ProcessPendingDownloads().SafeFireAndForget(); } // On successes, run the continuation action if (e == ProgressState.Success) { if (download.ContextAction is not null) { logger.LogDebug("Running context action for {Download}", download.FileName); } switch (download.ContextAction) { case CivitPostDownloadContextAction action: action.Invoke(settingsManager, modelIndexService); break; case ModelPostDownloadContextAction action: action.Invoke(modelIndexService); break; } } } private void LoadInProgressDownloads(DirectoryPath downloadsDir) { logger.LogDebug("Indexing in-progress downloads at {DownloadsDir}...", downloadsDir); var jsonFiles = downloadsDir.Info.EnumerateFiles("*.json", EnumerationOptionConstants.TopLevelOnly); // Add to dictionary, the file name is the guid foreach (var file in jsonFiles) { // Try to get a shared write handle try { var fileStream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read); // Deserialize json and add to dictionary var download = JsonSerializer.Deserialize(fileStream)!; // If the download is marked as working, pause it if (download.ProgressState is ProgressState.Working or ProgressState.Pending) { download.ProgressState = ProgressState.Paused; } else if ( download.ProgressState != ProgressState.Inactive && download.ProgressState != ProgressState.Paused && download.ProgressState != ProgressState.Pending ) { // If the download is not inactive, skip it logger.LogWarning( "Skipping download {Download} with state {State}", download.FileName, download.ProgressState ); fileStream.Dispose(); // Delete json file logger.LogDebug( "Deleting json file for {Download} with unsupported state", download.FileName ); file.Delete(); continue; } download.SetDownloadService(downloadService); downloads.TryAdd(download.Id, (download, fileStream)); if (download.ProgressState == ProgressState.Pending) { pendingDownloads.Enqueue(download); } AttachHandlers(download); OnDownloadAdded(download); logger.LogDebug("Loaded in-progress download {Download}", download.FileName); } catch (Exception e) { logger.LogInformation(e, "Could not open file {File} for reading", file.Name); } } } public TrackedDownload NewDownload(Uri downloadUrl, FilePath downloadPath) { var download = new TrackedDownload { Id = Guid.NewGuid(), SourceUrl = downloadUrl, DownloadDirectory = downloadPath.Directory!, FileName = downloadPath.Name, TempFileName = NewTempFileName(downloadPath.Directory!), }; AddDownload(download); return download; } /// /// Generate a new temp file name that is unique in the given directory. /// In format of "Unconfirmed {id}.smdownload" /// /// /// private static string NewTempFileName(DirectoryPath parentDir) { FilePath? tempFile = null; for (var i = 0; i < 10; i++) { if (tempFile is { Exists: false }) { return tempFile.Name; } var id = Random.Shared.Next(1000000, 9999999); tempFile = parentDir.JoinFile($"Unconfirmed {id}.smdownload"); } throw new Exception("Failed to generate a unique temp file name."); } /// public void Dispose() { foreach (var (download, fs) in downloads.Values) { if (download.ProgressState == ProgressState.Working) { try { download.Pause(); } catch (Exception e) { logger.LogWarning(e, "Failed to pause download {Download}", download.FileName); } } } GC.SuppressFinalize(this); } } ================================================ FILE: StabilityMatrix.Core/StabilityMatrix.Core.csproj ================================================ true true true MSBuild:GenerateCodeFromAttributes MSBuild:GenerateCodeFromAttributes ================================================ FILE: StabilityMatrix.Core/StabilityMatrix.Core.csproj.DotSettings ================================================  Library ================================================ FILE: StabilityMatrix.Core/Updater/IUpdateHelper.cs ================================================ using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Core.Updater; public interface IUpdateHelper { event EventHandler? UpdateStatusChanged; Task StartCheckingForUpdates(); Task CheckForUpdate(); Task DownloadUpdate(UpdateInfo updateInfo, IProgress progress); } ================================================ FILE: StabilityMatrix.Core/Updater/SignatureChecker.cs ================================================ using System.Text; using NSec.Cryptography; namespace StabilityMatrix.Core.Updater; public class SignatureChecker { private static readonly SignatureAlgorithm Algorithm = SignatureAlgorithm.Ed25519; private const string UpdatePublicKey = "-----BEGIN PUBLIC KEY-----\n" + "MCowBQYDK2VwAyEAqYXhKG1b0iOMnAZGBSBdFlFEWpFBIbIPQk0TtyE2SfI=\n" + "-----END PUBLIC KEY-----\n"; private readonly PublicKey publicKey; /// /// Initializes a new instance of SignatureChecker. /// /// Pkix format public key. Defaults to update verification key. public SignatureChecker(string? publicKeyPkix = null) { publicKey = PublicKey.Import( Algorithm, Encoding.ASCII.GetBytes(publicKeyPkix ?? UpdatePublicKey), KeyBlobFormat.PkixPublicKeyText); } /// /// Verifies the signature of provided data. /// /// Data to verify /// Signature in base64 encoding /// True if verified public bool Verify(string data, string signature) { var signatureBytes = Convert.FromBase64String(signature); return Algorithm.Verify(publicKey, Encoding.UTF8.GetBytes(data), signatureBytes); } /// /// Verifies the signature of provided data. /// /// Data to verify /// Signature in base64 encoding /// True if verified public bool Verify(ReadOnlySpan data, ReadOnlySpan signature) { return Algorithm.Verify(publicKey, data, signature); } } ================================================ FILE: StabilityMatrix.Core/Updater/UpdateHelper.cs ================================================ using System.Text.Json; using System.Web; using Injectio.Attributes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Configs; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Updater; [RegisterSingleton] public class UpdateHelper : IUpdateHelper { private readonly ILogger logger; private readonly IHttpClientFactory httpClientFactory; private readonly IDownloadService downloadService; private readonly ISettingsManager settingsManager; private readonly ILykosAuthApiV2 lykosAuthApi; private readonly DebugOptions debugOptions; private readonly System.Timers.Timer timer = new(TimeSpan.FromMinutes(60)); private string UpdateManifestUrl => debugOptions.UpdateManifestUrl ?? "https://cdn.lykos.ai/update-v3.json"; public const string UpdateFolderName = ".StabilityMatrixUpdate"; public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName); public static IPathObject ExecutablePath => Compat.IsMacOS ? UpdateFolder.JoinDir(Compat.GetAppName()) : UpdateFolder.JoinFile(Compat.GetAppName()); /// public event EventHandler? UpdateStatusChanged; public UpdateHelper( ILogger logger, IHttpClientFactory httpClientFactory, IDownloadService downloadService, IOptions debugOptions, ISettingsManager settingsManager, ILykosAuthApiV2 lykosAuthApi ) { this.logger = logger; this.httpClientFactory = httpClientFactory; this.downloadService = downloadService; this.settingsManager = settingsManager; this.lykosAuthApi = lykosAuthApi; this.debugOptions = debugOptions.Value; timer.Elapsed += async (_, _) => { await CheckForUpdate().ConfigureAwait(false); }; settingsManager.RegisterOnLibraryDirSet(_ => { timer.Start(); }); } public async Task StartCheckingForUpdates() { timer.Enabled = true; timer.Start(); await CheckForUpdate().ConfigureAwait(false); } public async Task DownloadUpdate(UpdateInfo updateInfo, IProgress progress) { UpdateFolder.Create(); UpdateFolder.Info.Attributes |= FileAttributes.Hidden; var downloadFile = UpdateFolder.JoinFile(Path.GetFileName(updateInfo.Url.ToString())); var extractDir = UpdateFolder.JoinDir("extract"); try { var url = updateInfo.Url.ToString(); // check if need authenticated download const string authedPathPrefix = "/lykos-s1/"; if ( updateInfo.Url.Host.Equals("cdn.lykos.ai", StringComparison.OrdinalIgnoreCase) && updateInfo.Url.PathAndQuery.StartsWith( authedPathPrefix, StringComparison.OrdinalIgnoreCase ) ) { logger.LogInformation("Handling authenticated update download: {Url}", updateInfo.Url); var path = updateInfo.Url.PathAndQuery.StripStart(authedPathPrefix); path = HttpUtility.UrlDecode(path); url = ( await lykosAuthApi.ApiV2FilesDownload(path).ConfigureAwait(false) ).DownloadUrl.ToString(); } // Download update await downloadService .DownloadToFileAsync(url, downloadFile, progress: progress, httpClientName: "UpdateClient") .ConfigureAwait(false); // Unzip if needed if (downloadFile.Extension == ".zip") { if (extractDir.Exists) { await extractDir.DeleteAsync(true).ConfigureAwait(false); } extractDir.Create(); progress.Report(new ProgressReport(-1, isIndeterminate: true, type: ProgressType.Extract)); await ArchiveHelper.Extract(downloadFile, extractDir).ConfigureAwait(false); progress.Report(new ProgressReport(1, isIndeterminate: true, type: ProgressType.Extract)); // Find binary and move it up to the root var binaryFile = extractDir .EnumerateFiles("*", EnumerationOptionConstants.AllDirectories) .First(f => f.Extension.ToLowerInvariant() is ".exe" or ".appimage"); await binaryFile.MoveToAsync((FilePath)ExecutablePath).ConfigureAwait(false); } else if (downloadFile.Extension == ".dmg") { if (!Compat.IsMacOS) throw new NotSupportedException(".dmg is only supported on macOS"); if (extractDir.Exists) { await extractDir.DeleteAsync(true).ConfigureAwait(false); } extractDir.Create(); // Extract dmg contents await ArchiveHelper.ExtractDmg(downloadFile, extractDir).ConfigureAwait(false); // Find app dir and move it up to the root var appBundle = extractDir.EnumerateDirectories("*.app").First(); await appBundle.MoveToAsync((DirectoryPath)ExecutablePath).ConfigureAwait(false); } // Otherwise just rename else { downloadFile.Rename(ExecutablePath.Name); } progress.Report(new ProgressReport(1d)); } finally { // Clean up original download await downloadFile.DeleteAsync().ConfigureAwait(false); // Clean up extract dir if (extractDir.Exists) { await extractDir.DeleteAsync(true).ConfigureAwait(false); } } } public async Task CheckForUpdate() { try { var httpClient = httpClientFactory.CreateClient("UpdateClient"); var response = await httpClient.GetAsync(UpdateManifestUrl).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { logger.LogWarning( "Error while checking for update {StatusCode} - {Content}", response.StatusCode, await response.Content.ReadAsStringAsync().ConfigureAwait(false) ); return; } var updateManifest = await JsonSerializer .DeserializeAsync( await response.Content.ReadAsStreamAsync().ConfigureAwait(false), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } ) .ConfigureAwait(false); if (updateManifest is null) { logger.LogError("UpdateManifest is null"); return; } foreach ( var channel in Enum.GetValues(typeof(UpdateChannel)) .Cast() .Where(c => c > UpdateChannel.Unknown && c <= settingsManager.Settings.PreferredUpdateChannel ) ) { if ( updateManifest.Updates.TryGetValue(channel, out var platforms) && platforms.GetInfoForCurrentPlatform() is { } update && ValidateUpdate(update) ) { OnUpdateStatusChanged( new UpdateStatusChangedEventArgs { LatestUpdate = update, UpdateChannels = updateManifest .Updates.Select(kv => (kv.Key, kv.Value.GetInfoForCurrentPlatform())) .Where(kv => kv.Item2 is not null) .ToDictionary(kv => kv.Item1, kv => kv.Item2)!, } ); return; } } logger.LogInformation("No update available"); var args = new UpdateStatusChangedEventArgs { UpdateChannels = updateManifest .Updates.Select(kv => (kv.Key, kv.Value.GetInfoForCurrentPlatform())) .Where(kv => kv.Item2 is not null) .ToDictionary(kv => kv.Item1, kv => kv.Item2)!, }; OnUpdateStatusChanged(args); } catch (Exception e) { logger.LogError(e, "Couldn't check for update"); } } private bool ValidateUpdate(UpdateInfo? update) { if (update is null) return false; // Verify signature var checker = new SignatureChecker(); var signedData = update.GetSignedData(); if (!checker.Verify(signedData, update.Signature)) { logger.LogError( "UpdateInfo signature {Signature} is invalid, Data = {Data}, UpdateInfo = {Info}", update.Signature, signedData, update ); return false; } switch (update.Version.ComparePrecedenceTo(Compat.AppVersion)) { case > 0: // Newer version available return true; case 0: { // Same version available, check if we both have commit hash metadata var updateHash = update.Version.Metadata; var appHash = Compat.AppVersion.Metadata; // Always assume update if (We don't have hash && Update has hash) if (string.IsNullOrEmpty(appHash) && !string.IsNullOrEmpty(updateHash)) { return true; } // Trim both to the lower length, to a minimum of 7 characters var minLength = Math.Min(7, Math.Min(updateHash.Length, appHash.Length)); updateHash = updateHash[..minLength]; appHash = appHash[..minLength]; // If different, we can update if (updateHash != appHash) { return true; } break; } } return false; } private void OnUpdateStatusChanged(UpdateStatusChangedEventArgs args) { UpdateStatusChanged?.Invoke(this, args); if (args.LatestUpdate is { } update) { logger.LogInformation( "Update available {AppVer} -> {UpdateVer}", Compat.AppVersion, update.Version ); EventManager.Instance.OnUpdateAvailable(update); } } private void NotifyUpdateAvailable(UpdateInfo update) { logger.LogInformation("Update available {AppVer} -> {UpdateVer}", Compat.AppVersion, update.Version); EventManager.Instance.OnUpdateAvailable(update); } } ================================================ FILE: StabilityMatrix.Core/Updater/UpdateStatusChangedEventArgs.cs ================================================ using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Core.Updater; public class UpdateStatusChangedEventArgs : EventArgs { public UpdateInfo? LatestUpdate { get; init; } public IReadOnlyDictionary UpdateChannels { get; init; } = new Dictionary(); public DateTimeOffset CheckedAt { get; init; } = DateTimeOffset.UtcNow; } ================================================ FILE: StabilityMatrix.Core/Validators/RequiresMatchAttribute.cs ================================================ using System.ComponentModel.DataAnnotations; namespace StabilityMatrix.Core.Validators; /// /// Validator that requires equality to another property /// i.e. Confirm password must match password /// public sealed class RequiresMatchAttribute : ValidationAttribute where T : IEquatable { public string PropertyName { get; } public RequiresMatchAttribute(string propertyName) { PropertyName = propertyName; } public RequiresMatchAttribute(string propertyName, string errorMessage) { PropertyName = propertyName; ErrorMessage = errorMessage; } protected override ValidationResult IsValid(object? value, ValidationContext validationContext) { var instance = validationContext.ObjectInstance; var otherProperty = instance.GetType().GetProperty(PropertyName) ?? throw new ArgumentException($"Property {PropertyName} not found"); if (otherProperty.PropertyType != typeof(T)) { throw new ArgumentException($"Property {PropertyName} is not of type {typeof(T)}"); } var otherValue = otherProperty.GetValue(instance); if (otherValue == null && value == null) { return ValidationResult.Success!; } if (((IEquatable?)otherValue)!.Equals(value)) { return ValidationResult.Success!; } return new ValidationResult( $"{validationContext.DisplayName} does not match {PropertyName}" ); } } ================================================ FILE: StabilityMatrix.Native/NativeFileOperations.cs ================================================ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using StabilityMatrix.Native.Abstractions; namespace StabilityMatrix.Native; [PublicAPI] public static class NativeFileOperations { public static INativeRecycleBinProvider? RecycleBin { get; } [MemberNotNullWhen(true, nameof(RecycleBin))] public static bool IsRecycleBinAvailable => RecycleBin is not null; static NativeFileOperations() { #if Windows if (!OperatingSystem.IsWindows()) { Debug.Fail( $"Assembly of {nameof(NativeFileOperations)} was compiled for Windows, " + $"the current OS is '{Environment.OSVersion}'" ); return; } RecycleBin = new Windows.NativeRecycleBinProvider(); #elif OSX if (!OperatingSystem.IsMacOS()) { Debug.Fail( $"Assembly of {nameof(NativeFileOperations)} was compiled for macOS, " + $"the current OS is '{Environment.OSVersion}'" ); return; } RecycleBin = new macOS.NativeRecycleBinProvider(); #endif } } ================================================ FILE: StabilityMatrix.Native/StabilityMatrix.Native.csproj ================================================ Windows true OSX true Linux true false ================================================ FILE: StabilityMatrix.Native.Abstractions/INativeRecycleBinProvider.cs ================================================ namespace StabilityMatrix.Native.Abstractions; public interface INativeRecycleBinProvider { /// /// Moves a file to the recycle bin. /// /// The path of the file to be moved. /// The flags to be used for the operation. void MoveFileToRecycleBin(string path, NativeFileOperationFlags flags = default); /// /// Asynchronously moves a file to the recycle bin. /// /// The path of the file to be moved. /// The flags to be used for the operation. /// A task representing the asynchronous operation. Task MoveFileToRecycleBinAsync(string path, NativeFileOperationFlags flags = default); /// /// Moves the specified files to the recycle bin. /// /// The paths of the files to be moved. /// The flags to be used for the operation. void MoveFilesToRecycleBin(IEnumerable paths, NativeFileOperationFlags flags = default); /// /// Asynchronously moves the specified files to the recycle bin. /// /// The paths of the files to be moved. /// The flags to be used for the operation. /// A task representing the asynchronous operation. Task MoveFilesToRecycleBinAsync(IEnumerable paths, NativeFileOperationFlags flags = default); /// /// Moves the specified directory to the recycle bin. /// /// The path of the directory to be moved. /// The flags to be used for the operation. void MoveDirectoryToRecycleBin(string path, NativeFileOperationFlags flags = default); /// /// Moves a directory to the recycle bin asynchronously. /// /// The path of the directory to be moved. /// The flags to be used for the operation. /// A task representing the asynchronous operation. Task MoveDirectoryToRecycleBinAsync(string path, NativeFileOperationFlags flags = default); /// /// Moves the specified directories to the recycle bin. /// /// The paths of the directories to be moved. /// The flags to be used for the operation. void MoveDirectoriesToRecycleBin(IEnumerable paths, NativeFileOperationFlags flags = default); /// /// Moves the specified directories to the recycle bin asynchronously. /// /// The paths of the directories to be moved. /// The flags to be used for the operation. /// A task representing the asynchronous operation. Task MoveDirectoriesToRecycleBinAsync( IEnumerable paths, NativeFileOperationFlags flags = default ); } ================================================ FILE: StabilityMatrix.Native.Abstractions/NativeFileOperationFlags.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Native.Abstractions; [Flags] public enum NativeFileOperationFlags : uint { /// /// Do not display a progress dialog. /// Silent = 1 << 0, /// /// Display a warning if files are being permanently deleted. /// WarnOnPermanentDelete = 1 << 1, /// /// Do not ask the user to confirm the operation. /// NoConfirmation = 1 << 2, } public static class NativeFileOperationFlagsExtensions { [SuppressMessage("ReSharper", "CommentTypo")] public static void ToWindowsFileOperationFlags( this NativeFileOperationFlags flags, ref uint windowsFileOperationFlags ) { if (flags.HasFlag(NativeFileOperationFlags.Silent)) { windowsFileOperationFlags |= 0x0004; // FOF_SILENT } if (flags.HasFlag(NativeFileOperationFlags.WarnOnPermanentDelete)) { windowsFileOperationFlags |= 0x4000; // FOF_WANTNUKEWARNING } if (flags.HasFlag(NativeFileOperationFlags.NoConfirmation)) { windowsFileOperationFlags |= 0x0010; // FOF_NOCONFIRMATION } } } ================================================ FILE: StabilityMatrix.Native.Abstractions/StabilityMatrix.Native.Abstractions.csproj ================================================  ================================================ FILE: StabilityMatrix.Native.Windows/AssemblyInfo.cs ================================================ using System.Runtime.Versioning; [assembly: SupportedOSPlatform("windows")] ================================================ FILE: StabilityMatrix.Native.Windows/FileOperations/FileOperationWrapper.cs ================================================ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using StabilityMatrix.Native.Windows.Interop; namespace StabilityMatrix.Native.Windows.FileOperations; [SuppressMessage("ReSharper", "InconsistentNaming")] internal partial class FileOperationWrapper : IDisposable { private bool _disposed; private readonly IFileOperation _fileOperation; private readonly IFileOperationProgressSink? _callbackSink; private readonly uint _sinkCookie; [PublicAPI] public FileOperationWrapper() : this(null) { } public FileOperationWrapper(IFileOperationProgressSink? callbackSink) : this(callbackSink, IntPtr.Zero) { } public FileOperationWrapper(IFileOperationProgressSink? callbackSink, IntPtr ownerHandle) { _callbackSink = callbackSink; _fileOperation = (IFileOperation?)Activator.CreateInstance(FileOperationType) ?? throw new NullReferenceException("Failed to create FileOperation instance."); if (_callbackSink != null) _sinkCookie = _fileOperation.Advise(_callbackSink); if (ownerHandle != IntPtr.Zero) _fileOperation.SetOwnerWindow((uint)ownerHandle); } public void SetOperationFlags(FileOperationFlags operationFlags) { _fileOperation.SetOperationFlags(operationFlags); } [PublicAPI] public void CopyItem(string source, string destination, string newName) { ThrowIfDisposed(); using var sourceItem = CreateShellItem(source); using var destinationItem = CreateShellItem(destination); _fileOperation.CopyItem(sourceItem.Item, destinationItem.Item, newName, null); } [PublicAPI] public void MoveItem(string source, string destination, string newName) { ThrowIfDisposed(); using var sourceItem = CreateShellItem(source); using var destinationItem = CreateShellItem(destination); _fileOperation.MoveItem(sourceItem.Item, destinationItem.Item, newName, null); } [PublicAPI] public void RenameItem(string source, string newName) { ThrowIfDisposed(); using var sourceItem = CreateShellItem(source); _fileOperation.RenameItem(sourceItem.Item, newName, null); } public void DeleteItem(string source) { ThrowIfDisposed(); using var sourceItem = CreateShellItem(source); _fileOperation.DeleteItem(sourceItem.Item, null); } /*public void DeleteItems(params string[] sources) { ThrowIfDisposed(); using var sourceItems = CreateShellItemArray(sources); _fileOperation.DeleteItems(sourceItems.Item); }*/ public void DeleteItems(string[] sources) { ThrowIfDisposed(); var pidlArray = new IntPtr[sources.Length]; try { // Convert paths to PIDLs for (var i = 0; i < sources.Length; i++) { pidlArray[i] = ILCreateFromPathW(sources[i]); if (pidlArray[i] == IntPtr.Zero) throw new Exception($"Failed to create PIDL for path: {sources[i]}"); } // Create ShellItemArray from PIDLs var shellItemArray = SHCreateShellItemArrayFromIDLists((uint)sources.Length, pidlArray); // Use the IFileOperation interface to delete items _fileOperation.DeleteItems(shellItemArray); } finally { // Free PIDLs foreach (var pidl in pidlArray) { if (pidl != IntPtr.Zero) { Marshal.FreeCoTaskMem(pidl); } } } } [PublicAPI] public void NewItem(string folderName, string name, FileAttributes attrs) { ThrowIfDisposed(); using var folderItem = CreateShellItem(folderName); _fileOperation.NewItem(folderItem.Item, attrs, name, string.Empty, _callbackSink); } public void PerformOperations() { ThrowIfDisposed(); _fileOperation.PerformOperations(); } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().Name); } } public void Dispose() { if (!_disposed) { _disposed = true; if (_callbackSink != null) { _fileOperation.Unadvise(_sinkCookie); } Marshal.FinalReleaseComObject(_fileOperation); } } private static ComReleaser CreateShellItem(string path) { // Normalize path slashes path = path.Replace('/', '\\'); return new ComReleaser( (IShellItem)SHCreateItemFromParsingName(path, IntPtr.Zero, ref _shellItemGuid) ); } private static ComReleaser CreateShellItemArray(params string[] paths) { var pidls = new IntPtr[paths.Length]; try { for (var i = 0; i < paths.Length; i++) { if (SHParseDisplayName(paths[i], IntPtr.Zero, out var pidl, 0, out _) != 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } pidls[i] = pidl; } return new ComReleaser( SHCreateShellItemArrayFromIDLists((uint)pidls.Length, pidls) ); } finally { foreach (var pidl in pidls) { Marshal.FreeCoTaskMem(pidl); } } } [LibraryImport("shell32.dll", SetLastError = true)] private static partial int SHParseDisplayName( [MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, // IBindCtx out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut ); [LibraryImport("shell32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] private static partial IntPtr ILCreateFromPathW(string pszPath); [DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode, PreserveSig = false)] [return: MarshalAs(UnmanagedType.Interface)] private static extern object SHCreateItemFromParsingName( [MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, // IBindCtx ref Guid riid ); [DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode, PreserveSig = false)] [return: MarshalAs(UnmanagedType.Interface)] private static extern IShellItemArray SHCreateShellItemArrayFromIDLists( uint cidl, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStruct)] IntPtr[] rgpidl ); private static readonly Guid ClsidFileOperation = new("3ad05575-8857-4850-9277-11b85bdb8e09"); private static readonly Type FileOperationType = Type.GetTypeFromCLSID(ClsidFileOperation) ?? throw new NullReferenceException("Failed to get FileOperation type from CLSID"); private static Guid _shellItemGuid = typeof(IShellItem).GUID; } ================================================ FILE: StabilityMatrix.Native.Windows/GlobalUsings.cs ================================================ // Global using directives global using System.Runtime.InteropServices; global using System.Runtime.InteropServices.Marshalling; ================================================ FILE: StabilityMatrix.Native.Windows/Interop/ComReleaser.cs ================================================ namespace StabilityMatrix.Native.Windows.Interop { internal sealed class ComReleaser : IDisposable where T : class { public T? Item { get; private set; } public ComReleaser(T obj) { ArgumentNullException.ThrowIfNull(obj); if (!Marshal.IsComObject(obj)) throw new ArgumentOutOfRangeException(nameof(obj)); Item = obj; } public void Dispose() { if (Item != null) { Marshal.FinalReleaseComObject(Item); Item = null; } } } } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/FileOperationFlags.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Native.Windows.Interop; [Flags] [SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "IdentifierTypo")] internal enum FileOperationFlags : uint { FOF_MULTIDESTFILES = 0x0001, FOF_CONFIRMMOUSE = 0x0002, FOF_WANTMAPPINGHANDLE = 0x0020, // Fill in SHFILEOPSTRUCT.hNameMappings FOF_FILESONLY = 0x0080, // on *.*, do only files FOF_NOCONFIRMMKDIR = 0x0200, // don't confirm making any needed dirs FOF_NOCOPYSECURITYATTRIBS = 0x0800, // dont copy NT file Security Attributes FOF_NORECURSION = 0x1000, // don't recurse into directories. FOF_NO_CONNECTED_ELEMENTS = 0x2000, // don't operate on connected file elements. FOF_NORECURSEREPARSE = 0x8000, // treat reparse points as objects, not containers /// /// Do not show a dialog during the process /// FOF_SILENT = 0x0004, FOF_RENAMEONCOLLISION = 0x0008, /// /// Do not ask the user to confirm selection /// FOF_NOCONFIRMATION = 0x0010, /// /// Delete the file to the recycle bin. (Required flag to send a file to the bin /// FOF_ALLOWUNDO = 0x0040, /// /// Do not show the names of the files or folders that are being recycled. /// FOF_SIMPLEPROGRESS = 0x0100, /// /// Surpress errors, if any occur during the process. /// FOF_NOERRORUI = 0x0400, /// /// Warn if files are too big to fit in the recycle bin and will need /// to be deleted completely. /// FOF_WANTNUKEWARNING = 0x4000, FOFX_ADDUNDORECORD = 0x20000000, FOFX_NOSKIPJUNCTIONS = 0x00010000, FOFX_PREFERHARDLINK = 0x00020000, FOFX_SHOWELEVATIONPROMPT = 0x00040000, FOFX_EARLYFAILURE = 0x00100000, FOFX_PRESERVEFILEEXTENSIONS = 0x00200000, FOFX_KEEPNEWERFILE = 0x00400000, FOFX_NOCOPYHOOKS = 0x00800000, FOFX_NOMINIMIZEBOX = 0x01000000, FOFX_MOVEACLSACROSSVOLUMES = 0x02000000, FOFX_DONTDISPLAYSOURCEPATH = 0x04000000, FOFX_DONTDISPLAYDESTPATH = 0x08000000, FOFX_RECYCLEONDELETE = 0x00080000, FOFX_REQUIREELEVATION = 0x10000000, FOFX_COPYASDOWNLOAD = 0x40000000, FOFX_DONTDISPLAYLOCATIONS = 0x80000000, } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/FileOperationProgressSinkTcs.cs ================================================ namespace StabilityMatrix.Native.Windows.Interop; [GeneratedComClass] [Guid("04b0f1a7-9490-44bc-96e1-4296a31252e2")] public partial class FileOperationProgressSinkTcs : TaskCompletionSource, IFileOperationProgressSink { private readonly IProgress<(uint WorkTotal, uint WorkSoFar)>? progress; public FileOperationProgressSinkTcs() { } public FileOperationProgressSinkTcs(IProgress<(uint, uint)> progress) { this.progress = progress; } /// public virtual void StartOperations() { } /// public virtual void FinishOperations(uint hrResult) { SetResult(hrResult); } /// public virtual void PreRenameItem(uint dwFlags, IShellItem psiItem, string pszNewName) { } /// public virtual void PostRenameItem( uint dwFlags, IShellItem psiItem, string pszNewName, uint hrRename, IShellItem psiNewlyCreated ) { } /// public virtual void PreMoveItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, string pszNewName ) { } /// public virtual void PostMoveItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, string pszNewName, uint hrMove, IShellItem psiNewlyCreated ) { } /// public virtual void PreCopyItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, string pszNewName ) { } /// public virtual void PostCopyItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, string pszNewName, uint hrCopy, IShellItem psiNewlyCreated ) { } /// public virtual void PreDeleteItem(uint dwFlags, IShellItem psiItem) { } /// public virtual void PostDeleteItem( uint dwFlags, IShellItem psiItem, uint hrDelete, IShellItem psiNewlyCreated ) { } /// public virtual void PreNewItem(uint dwFlags, IShellItem psiDestinationFolder, string pszNewName) { } /// public virtual void PostNewItem( uint dwFlags, IShellItem psiDestinationFolder, string pszNewName, string pszTemplateName, uint dwFileAttributes, uint hrNew, IShellItem psiNewItem ) { } /// public virtual void UpdateProgress(uint iWorkTotal, uint iWorkSoFar) { progress?.Report((iWorkTotal, iWorkSoFar)); } /// public virtual void ResetTimer() { } /// public virtual void PauseTimer() { } /// public virtual void ResumeTimer() { } } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/IFileOperation.cs ================================================ namespace StabilityMatrix.Native.Windows.Interop; [GeneratedComInterface] [Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal partial interface IFileOperation { uint Advise(IFileOperationProgressSink pfops); void Unadvise(uint dwCookie); void SetOperationFlags(FileOperationFlags dwOperationFlags); void SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string pszMessage); void SetProgressDialog([MarshalAs(UnmanagedType.Interface)] object popd); void SetProperties([MarshalAs(UnmanagedType.Interface)] object pproparray); void SetOwnerWindow(uint hwndParent); void ApplyPropertiesToItem(IShellItem psiItem); void ApplyPropertiesToItems([MarshalAs(UnmanagedType.Interface)] object punkItems); void RenameItem( IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IFileOperationProgressSink? pfopsItem ); void RenameItems( [MarshalAs(UnmanagedType.Interface)] object pUnkItems, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName ); void MoveItem( IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IFileOperationProgressSink? pfopsItem ); void MoveItems([MarshalAs(UnmanagedType.Interface)] object punkItems, IShellItem psiDestinationFolder); void CopyItem( IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszCopyName, IFileOperationProgressSink? pfopsItem ); void CopyItems([MarshalAs(UnmanagedType.Interface)] object punkItems, IShellItem psiDestinationFolder); void DeleteItem(IShellItem psiItem, IFileOperationProgressSink? pfopsItem); void DeleteItems([MarshalAs(UnmanagedType.Interface)] object punkItems); uint NewItem( IShellItem psiDestinationFolder, FileAttributes dwFileAttributes, [MarshalAs(UnmanagedType.LPWStr)] string pszName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, IFileOperationProgressSink? pfopsItem ); void PerformOperations(); [return: MarshalAs(UnmanagedType.Bool)] bool GetAnyOperationsAborted(); } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/IFileOperationProgressSink.cs ================================================ namespace StabilityMatrix.Native.Windows.Interop; [GeneratedComInterface] [Guid("04b0f1a7-9490-44bc-96e1-4296a31252e2")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public partial interface IFileOperationProgressSink { void StartOperations(); void FinishOperations(uint hrResult); void PreRenameItem(uint dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); void PostRenameItem( uint dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, uint hrRename, IShellItem psiNewlyCreated ); void PreMoveItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName ); void PostMoveItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, uint hrMove, IShellItem psiNewlyCreated ); void PreCopyItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName ); void PostCopyItem( uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, uint hrCopy, IShellItem psiNewlyCreated ); void PreDeleteItem(uint dwFlags, IShellItem psiItem); void PostDeleteItem(uint dwFlags, IShellItem psiItem, uint hrDelete, IShellItem psiNewlyCreated); void PreNewItem( uint dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName ); void PostNewItem( uint dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, uint dwFileAttributes, uint hrNew, IShellItem psiNewItem ); void UpdateProgress(uint iWorkTotal, uint iWorkSoFar); void ResetTimer(); void PauseTimer(); void ResumeTimer(); } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/IShellItem.cs ================================================ namespace StabilityMatrix.Native.Windows.Interop; [GeneratedComInterface] [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public partial interface IShellItem { [return: MarshalAs(UnmanagedType.Interface)] object BindToHandler( IntPtr pbc, // IBindCTX ref Guid bhid, ref Guid riid ); IShellItem GetParent(); [return: MarshalAs(UnmanagedType.LPWStr)] string GetDisplayName(SIGDN sigdnName); uint GetAttributes(uint sfgaoMask); int Compare(IShellItem psi, uint hint); } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/IShellItemArray.cs ================================================ using System.Runtime.CompilerServices; namespace StabilityMatrix.Native.Windows.Interop; [GeneratedComInterface] [Guid("b63ea76d-1f85-456f-a19c-48159efa858b")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public partial interface IShellItemArray { // uint BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out object ppvOut); /*[return: MarshalAs(UnmanagedType.Interface)] IShellItem GetItemAt(uint dwIndex); [return: MarshalAs(UnmanagedType.U4)] uint GetCount();*/ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] void BindToHandler( [MarshalAs(UnmanagedType.Interface)] IntPtr pbc, ref Guid rbhid, ref Guid riid, out IntPtr ppvOut ); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] void GetPropertyStore(int flags, ref Guid riid, out IntPtr ppv); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] int GetCount(); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] IShellItem GetItemAt(int dwIndex); [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); } ================================================ FILE: StabilityMatrix.Native.Windows/Interop/SIGDN.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Native.Windows.Interop { [SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "IdentifierTypo")] public enum SIGDN : uint { SIGDN_NORMALDISPLAY = 0x00000000, SIGDN_PARENTRELATIVEPARSING = 0x80018001, SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8001c001, SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000, SIGDN_PARENTRELATIVEEDITING = 0x80031001, SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000, SIGDN_FILESYSPATH = 0x80058000, SIGDN_URL = 0x80068000 } } ================================================ FILE: StabilityMatrix.Native.Windows/NativeRecycleBinProvider.cs ================================================ using JetBrains.Annotations; using StabilityMatrix.Native.Abstractions; using StabilityMatrix.Native.Windows.FileOperations; using StabilityMatrix.Native.Windows.Interop; namespace StabilityMatrix.Native.Windows; [PublicAPI] public class NativeRecycleBinProvider : INativeRecycleBinProvider { /// public void MoveFileToRecycleBin(string path, NativeFileOperationFlags flags = default) { using var fo = new FileOperationWrapper(); var fileOperationFlags = default(uint); flags.ToWindowsFileOperationFlags(ref fileOperationFlags); fo.SetOperationFlags( (FileOperationFlags)fileOperationFlags | FileOperationFlags.FOFX_RECYCLEONDELETE ); fo.DeleteItem(path); fo.PerformOperations(); } /// public Task MoveFileToRecycleBinAsync(string path, NativeFileOperationFlags flags = default) { return Task.Run(() => MoveFileToRecycleBin(path, flags)); } /// public void MoveFilesToRecycleBin(IEnumerable paths, NativeFileOperationFlags flags = default) { using var fo = new FileOperationWrapper(); var fileOperationFlags = default(uint); flags.ToWindowsFileOperationFlags(ref fileOperationFlags); fo.SetOperationFlags( (FileOperationFlags)fileOperationFlags | FileOperationFlags.FOFX_RECYCLEONDELETE ); fo.DeleteItems(paths.ToArray()); fo.PerformOperations(); } /// public Task MoveFilesToRecycleBinAsync( IEnumerable paths, NativeFileOperationFlags flags = default ) { return Task.Run(() => MoveFilesToRecycleBin(paths, flags)); } /// public void MoveDirectoryToRecycleBin(string path, NativeFileOperationFlags flags = default) { using var fo = new FileOperationWrapper(); var fileOperationFlags = default(uint); flags.ToWindowsFileOperationFlags(ref fileOperationFlags); fo.SetOperationFlags( (FileOperationFlags)fileOperationFlags | FileOperationFlags.FOFX_RECYCLEONDELETE ); fo.DeleteItem(path); fo.PerformOperations(); } /// public Task MoveDirectoryToRecycleBinAsync(string path, NativeFileOperationFlags flags = default) { return Task.Run(() => MoveDirectoryToRecycleBin(path, flags)); } /// public void MoveDirectoriesToRecycleBin( IEnumerable paths, NativeFileOperationFlags flags = default ) { using var fo = new FileOperationWrapper(); var fileOperationFlags = default(uint); flags.ToWindowsFileOperationFlags(ref fileOperationFlags); fo.SetOperationFlags( (FileOperationFlags)fileOperationFlags | FileOperationFlags.FOFX_RECYCLEONDELETE ); fo.DeleteItems(paths.ToArray()); fo.PerformOperations(); } /// public Task MoveDirectoriesToRecycleBinAsync( IEnumerable paths, NativeFileOperationFlags flags = default ) { return Task.Run(() => MoveDirectoriesToRecycleBin(paths, flags)); } } ================================================ FILE: StabilityMatrix.Native.Windows/StabilityMatrix.Native.Windows.csproj ================================================  win-x64 true true true ================================================ FILE: StabilityMatrix.Native.macOS/AssemblyInfo.cs ================================================ using System.Runtime.Versioning; [assembly: SupportedOSPlatform("macos")] [assembly: SupportedOSPlatform("osx")] ================================================ FILE: StabilityMatrix.Native.macOS/NativeRecycleBinProvider.cs ================================================ using System.Diagnostics; using JetBrains.Annotations; using StabilityMatrix.Native.Abstractions; namespace StabilityMatrix.Native.macOS; [PublicAPI] public class NativeRecycleBinProvider : INativeRecycleBinProvider { /// public void MoveFileToRecycleBin(string path, NativeFileOperationFlags flags = default) { MoveFileToRecycleBinAsync(path, flags).GetAwaiter().GetResult(); } /// public async Task MoveFileToRecycleBinAsync(string path, NativeFileOperationFlags flags = default) { await RunAppleScriptAsync($"tell application \\\"Finder\\\" to delete POSIX file \\\"{path}\\\""); } /// public void MoveFilesToRecycleBin(IEnumerable paths, NativeFileOperationFlags flags = default) { MoveFilesToRecycleBinAsync(paths, flags).GetAwaiter().GetResult(); } /// public async Task MoveFilesToRecycleBinAsync( IEnumerable paths, NativeFileOperationFlags flags = default ) { var pathsArrayString = string.Join(", ", paths.Select(p => $"POSIX file \\\"{p}\\\"")); await RunAppleScriptAsync($"tell application \\\"Finder\\\" to delete {{{pathsArrayString}}}"); } /// public void MoveDirectoryToRecycleBin(string path, NativeFileOperationFlags flags = default) { MoveDirectoryToRecycleBinAsync(path, flags).GetAwaiter().GetResult(); } /// public async Task MoveDirectoryToRecycleBinAsync(string path, NativeFileOperationFlags flags = default) { await RunAppleScriptAsync( $"tell application \\\"Finder\\\" to delete folder POSIX file \\\"{path}\\\"" ); } /// public void MoveDirectoriesToRecycleBin( IEnumerable paths, NativeFileOperationFlags flags = default ) { MoveDirectoriesToRecycleBinAsync(paths, flags).GetAwaiter().GetResult(); } /// public async Task MoveDirectoriesToRecycleBinAsync( IEnumerable paths, NativeFileOperationFlags flags = default ) { var pathsArrayString = string.Join(", ", paths.Select(p => $"folder POSIX file \\\"{p}\\\"")); await RunAppleScriptAsync($"tell application \\\"Finder\\\" to delete {{{pathsArrayString}}}"); } /// /// Runs an AppleScript script. /// private static async Task RunAppleScriptAsync( string script, CancellationToken cancellationToken = default ) { using var process = new Process(); process.StartInfo = new ProcessStartInfo { FileName = "/usr/bin/osascript", Arguments = $"-e \"{script}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; process.Start(); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (process.ExitCode != 0) { var stdOut = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var stdErr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); throw new InvalidOperationException( $"The AppleScript script failed with exit code {process.ExitCode}: (StdOut = {stdOut}, StdErr = {stdErr})" ); } } } ================================================ FILE: StabilityMatrix.Native.macOS/StabilityMatrix.Native.macOS.csproj ================================================  osx-x64;osx-arm64 ================================================ FILE: StabilityMatrix.Tests/Avalonia/CheckpointFileViewModelTests.cs ================================================ using Microsoft.Extensions.Logging; using NSubstitute; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class CheckpointFileViewModelTests { [TestMethod] public void HasStandardUpdate_IsFalse_WhenUpdateIsEarlyAccessOnly() { var vm = CreateViewModel(CreateCheckpointFile(hasUpdate: true, hasEarlyAccessUpdateOnly: true)); Assert.IsTrue(vm.HasEarlyAccessUpdateOnly); Assert.IsFalse(vm.HasStandardUpdate); } [TestMethod] public void CheckpointFile_Setter_RaisesDerivedUpdatePropertyNotifications() { var vm = CreateViewModel(CreateCheckpointFile(hasUpdate: true, hasEarlyAccessUpdateOnly: true)); var changed = new List(); vm.PropertyChanged += (_, e) => { if (!string.IsNullOrWhiteSpace(e.PropertyName)) { changed.Add(e.PropertyName!); } }; vm.CheckpointFile = CreateCheckpointFile(hasUpdate: true, hasEarlyAccessUpdateOnly: false); Assert.IsFalse(vm.HasEarlyAccessUpdateOnly); Assert.IsTrue(vm.HasStandardUpdate); CollectionAssert.Contains(changed, nameof(CheckpointFileViewModel.HasEarlyAccessUpdateOnly)); CollectionAssert.Contains(changed, nameof(CheckpointFileViewModel.HasStandardUpdate)); } private static CheckpointFileViewModel CreateViewModel(LocalModelFile checkpointFile) { var settingsManager = Substitute.For(); settingsManager.Settings.Returns(new Settings { ShowNsfwInCheckpointsPage = true }); settingsManager.IsLibraryDirSet.Returns(false); return new CheckpointFileViewModel( settingsManager, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For>(), Substitute.For(), checkpointFile ); } private static LocalModelFile CreateCheckpointFile(bool hasUpdate, bool hasEarlyAccessUpdateOnly) { return new LocalModelFile { RelativePath = "StableDiffusion/test-vm.safetensors", SharedFolderType = SharedFolderType.StableDiffusion, HasUpdate = hasUpdate, HasEarlyAccessUpdateOnly = hasEarlyAccessUpdateOnly, ConnectedModelInfo = new ConnectedModelInfo { ModelId = 77, VersionId = 700, Source = ConnectedModelSource.Civitai, ModelName = "VM Test Model", ModelDescription = string.Empty, VersionName = "v700", Tags = [], Hashes = new CivitFileHashes(), }, }; } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/Converters/NullableDefaultNumericConverterTests.cs ================================================ using System.Globalization; using StabilityMatrix.Avalonia.Converters; namespace StabilityMatrix.Tests.Avalonia.Converters; [TestClass] public class NullableDefaultNumericConverterTests { [TestMethod] public void Convert_IntToDecimal_ValueReturnsNullable() { const int value = 123; var converter = NullableDefaultNumericConverters.IntToDecimal; var result = converter.Convert(value, typeof(decimal?), null, CultureInfo.InvariantCulture); Assert.AreEqual((decimal?)123, result); } [TestMethod] public void ConvertBack_IntToDecimal_NullableReturnsDefault() { decimal? value = null; var converter = NullableDefaultNumericConverters.IntToDecimal; var result = converter.ConvertBack(value, typeof(int), null, CultureInfo.InvariantCulture); Assert.AreEqual(0, result); } [TestMethod] public void ConvertBack_IntToDouble_NanReturnsDefault() { const double value = double.NaN; var converter = new NullableDefaultNumericConverter(); var result = converter.ConvertBack(value, typeof(int), null, CultureInfo.InvariantCulture); Assert.AreEqual(0, result); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/DesignDataTests.cs ================================================ using System.Reflection; using StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class DesignDataTests { [ClassInitialize] public static void ClassInitialize(TestContext context) { SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); DesignData.Initialize(); } // Return all properties public static IEnumerable DesignDataProperties => typeof(DesignData).GetProperties() .Select(p => new object[] { p }); [TestMethod] [DynamicData(nameof(DesignDataProperties))] public void Property_ShouldBeNotNull(PropertyInfo property) { var value = property.GetValue(null); Assert.IsNotNull(value); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs ================================================ using System.ComponentModel.DataAnnotations; using StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class FileNameFormatProviderTests { [TestMethod] public void TestFileNameFormatProviderValidate_Valid_ShouldNotThrow() { var provider = new FileNameFormatProvider(); var result = provider.Validate("{date}_{time}-{model_name}-{seed}"); Assert.AreEqual(ValidationResult.Success, result); } [TestMethod] public void TestFileNameFormatProviderValidate_Invalid_ShouldThrow() { var provider = new FileNameFormatProvider(); var result = provider.Validate("{date}_{time}-{model_name}-{seed}-{invalid}"); Assert.AreNotEqual(ValidationResult.Success, result); Assert.AreEqual("Unknown variable 'invalid'", result.ErrorMessage); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs ================================================ using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Inference; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class FileNameFormatTests { [TestMethod] public void TestFileNameFormatParse() { var provider = new FileNameFormatProvider { GenerationParameters = new GenerationParameters { Seed = 123 }, ProjectName = "uwu", ProjectType = InferenceProjectType.TextToImage, }; var format = FileNameFormat.Parse("{project_type} - {project_name} ({seed})", provider); Assert.AreEqual("TextToImage - uwu (123)", format.GetFileName()); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NSubstitute; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Base; #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Tests.Avalonia; // Example subclass public class TestLoadableViewModel : LoadableViewModelBase { [JsonInclude] public string? Included { get; set; } public int Id { get; set; } [JsonIgnore] public int Ignored { get; set; } } public class TestLoadableViewModelReadOnly : LoadableViewModelBase { public int ReadOnly { get; } public TestLoadableViewModelReadOnly(int readOnly) { ReadOnly = readOnly; } } public class TestLoadableViewModelReadOnlyLoadable : LoadableViewModelBase { public TestLoadableViewModel ReadOnlyLoadable { get; } = new(); } public partial class TestLoadableViewModelObservable : LoadableViewModelBase { [ObservableProperty] [property: JsonIgnore] private string? title; [ObservableProperty] private int id; [RelayCommand] private void TestCommand() { throw new NotImplementedException(); } } public class TestLoadableViewModelNestedInterface : LoadableViewModelBase { public IJsonLoadableState? NestedState { get; set; } } public class TestLoadableViewModelNested : LoadableViewModelBase { public TestLoadableViewModel? NestedState { get; set; } } [TestClass] public class LoadableViewModelBaseTests { [TestMethod] public void TestSaveStateToJsonObject_JsonIgnoreAttribute() { var vm = new TestLoadableViewModel { Included = "abc", Id = 123, Ignored = 456, }; var state = vm.SaveStateToJsonObject(); // [JsonInclude] and not marked property should be serialized. // Ignored property should be ignored. Assert.AreEqual(2, state.Count); Assert.AreEqual("abc", state["Included"].Deserialize()); Assert.AreEqual(123, state["Id"].Deserialize()); } [TestMethod] public void TestSaveStateToJsonObject_Observable() { // Mvvm ObservableProperty should be serialized. var vm = new TestLoadableViewModelObservable { Title = "abc", Id = 123, }; var state = vm.SaveStateToJsonObject(); // Title should be ignored since it has [JsonIgnore] // Command should be ignored from excluded type rules // Id should be serialized Assert.AreEqual(1, state.Count); Assert.AreEqual(123, state["Id"].Deserialize()); } [TestMethod] public void TestSaveStateToJsonObject_IJsonLoadableState() { // Properties of type IJsonLoadableState should be serialized by calling their // SaveStateToJsonObject method. // Make a mock IJsonLoadableState var mockState = Substitute.For(); var vm = new TestLoadableViewModelNestedInterface { NestedState = mockState }; // Serialize var state = vm.SaveStateToJsonObject(); // Check results Assert.AreEqual(1, state.Count); // Check that SaveStateToJsonObject was called mockState.Received().SaveStateToJsonObject(); } [TestMethod] public void TestLoadStateFromJsonObject() { // Simple round trip save / load var vm = new TestLoadableViewModel { Included = "abc", Id = 123, Ignored = 456, }; var state = vm.SaveStateToJsonObject(); // Create a new instance and load the state var vm2 = new TestLoadableViewModel(); vm2.LoadStateFromJsonObject(state); // Check [JsonInclude] and not marked property was loaded Assert.AreEqual("abc", vm2.Included); Assert.AreEqual(123, vm2.Id); // Check ignored property was not loaded Assert.AreEqual(0, vm2.Ignored); } [TestMethod] public void TestLoadStateFromJsonObject_Nested_DefaultCtor() { // Round trip save / load with nested IJsonLoadableState property var nested = new TestLoadableViewModel { Included = "abc", Id = 123, Ignored = 456, }; var vm = new TestLoadableViewModelNested { NestedState = nested }; var state = vm.SaveStateToJsonObject(); // Create a new instance with null NestedState, rely on default ctor var vm2 = new TestLoadableViewModelNested(); vm2.LoadStateFromJsonObject(state); // Check nested state was loaded Assert.IsNotNull(vm2.NestedState); var loadedNested = (TestLoadableViewModel)vm2.NestedState; Assert.AreEqual("abc", loadedNested.Included); Assert.AreEqual(123, loadedNested.Id); Assert.AreEqual(0, loadedNested.Ignored); } [TestMethod] public void TestLoadStateFromJsonObject_Nested_Existing() { // Round trip save / load with nested IJsonLoadableState property var nested = new TestLoadableViewModel { Included = "abc", Id = 123, Ignored = 456, }; var vm = new TestLoadableViewModelNestedInterface { NestedState = nested }; var state = vm.SaveStateToJsonObject(); // Create a new instance with existing NestedState var vm2 = new TestLoadableViewModelNestedInterface { NestedState = new TestLoadableViewModel() }; vm2.LoadStateFromJsonObject(state); // Check nested state was loaded Assert.IsNotNull(vm2.NestedState); var loadedNested = (TestLoadableViewModel)vm2.NestedState; Assert.AreEqual("abc", loadedNested.Included); Assert.AreEqual(123, loadedNested.Id); Assert.AreEqual(0, loadedNested.Ignored); } [TestMethod] public void TestLoadStateFromJsonObject_ReadOnly() { var vm = new TestLoadableViewModelReadOnly(456); var state = vm.SaveStateToJsonObject(); // Check no properties were serialized Assert.AreEqual(0, state.Count); // Create a new instance and load the state var vm2 = new TestLoadableViewModelReadOnly(123); vm2.LoadStateFromJsonObject(state); // Read only property should have been ignored Assert.AreEqual(123, vm2.ReadOnly); } [TestMethod] public void TestLoadStateFromJsonObject_ReadOnlyLoadable() { var vm = new TestLoadableViewModelReadOnlyLoadable { ReadOnlyLoadable = { Included = "abc-123" } }; var state = vm.SaveStateToJsonObject(); // Check readonly loadable property was serialized Assert.AreEqual(1, state.Count); Assert.AreEqual( "abc-123", state["ReadOnlyLoadable"].Deserialize()!.Included ); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/PromptTests.cs ================================================ using System.Globalization; using System.Reflection; using NSubstitute; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Core.Models.Tokens; using TextMateSharp.Grammars; using TextMateSharp.Registry; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class PromptTests { private ITokenizerProvider tokenizerProvider = null!; [TestInitialize] public void TestInitialize() { tokenizerProvider = Substitute.For(); var promptSyntaxFile = Assembly .GetExecutingAssembly() .GetManifestResourceStream("StabilityMatrix.Tests.ImagePrompt.tmLanguage.json")!; var registry = new Registry(new RegistryOptions(ThemeName.DarkPlus)); var grammar = registry.LoadGrammarFromStream(promptSyntaxFile); tokenizerProvider.TokenizeLine(Arg.Any()).Returns(x => grammar.TokenizeLine(x.Arg())); } [TestMethod] public void TestPromptProcessedText() { var prompt = Prompt.FromRawText("test", tokenizerProvider); prompt.Process(); Assert.AreEqual("test", prompt.ProcessedText); } [TestMethod] public void TestPromptWeightParsing() { var prompt = Prompt.FromRawText("", tokenizerProvider); prompt.Process(); // Output should have no loras Assert.AreEqual("", prompt.ProcessedText); var network = prompt.ExtraNetworks[0]; Assert.AreEqual(PromptExtraNetworkType.Lora, network.Type); Assert.AreEqual("my_model", network.Name); Assert.AreEqual(1.5f, network.ModelWeight); } /// /// Tests that we can parse decimal numbers with different cultures /// [TestMethod] public void TestPromptWeightParsing_DecimalSeparatorCultures_ShouldParse() { var prompt = Prompt.FromRawText("", tokenizerProvider); // Cultures like de-DE use commas as decimal separators, check that we can parse those too ExecuteWithCulture(() => prompt.Process(), CultureInfo.GetCultureInfo("de-DE")); // Output should have no loras Assert.AreEqual("", prompt.ProcessedText); var network = prompt.ExtraNetworks![0]; Assert.AreEqual(PromptExtraNetworkType.Lora, network.Type); Assert.AreEqual("my_model", network.Name); Assert.AreEqual(1.5f, network.ModelWeight); } private static T? ExecuteWithCulture(Func func, CultureInfo culture) { var result = default(T); var thread = new Thread(() => { result = func(); }) { CurrentCulture = culture }; thread.Start(); thread.Join(); return result; } private static void ExecuteWithCulture(Action func, CultureInfo culture) { var thread = new Thread(() => { func(); }) { CurrentCulture = culture }; thread.Start(); thread.Join(); } } ================================================ FILE: StabilityMatrix.Tests/Avalonia/UpdateViewModelTests.cs ================================================ using Semver; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Tests.Avalonia; [TestClass] public class UpdateViewModelTests { [TestMethod] public void FormatChangelogTest() { // Arrange const string markdown = """ # Changelog 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.4.6 ### Added - Stuff ### Changed - Things ## v2.4.5 ### Fixed - Fixed bug ## v2.4.4 ### Changed - Changed stuff """; // Act var result = UpdateViewModel.FormatChangelog(markdown, SemVersion.Parse("2.4.5")); var resultPre = UpdateViewModel.FormatChangelog( markdown, SemVersion.Parse("2.4.5-pre.1+1a7b4e4") ); // Assert const string expected = """ ## v2.4.6 ### Added - Stuff ### Changed - Things """; Assert.AreEqual(expected, result); // Pre-release should include the current release const string expectedPre = """ ## v2.4.6 ### Added - Stuff ### Changed - Things ## v2.4.5 ### Fixed - Fixed bug """; Assert.AreEqual(expectedPre, resultPre); } [TestMethod] public void FormatChangelogWithChannelTest() { // Arrange const string markdown = """ # Changelog 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.4.6 ### Added - Stuff ### Changed - Things ## v2.4.6-pre.1 ### Fixed - Fixed bug ## v2.4.6-dev.1 ### Fixed - Fixed bug ## v2.4.5 ### Changed - Changed stuff """; // Act var result = UpdateViewModel.FormatChangelog( markdown, SemVersion.Parse("2.4.0"), UpdateChannel.Preview ); // Assert const string expected = """ ## v2.4.6 ### Added - Stuff ### Changed - Things ## v2.4.6-pre.1 ### Fixed - Fixed bug ## v2.4.5 ### Changed - Changed stuff """; // Should include pre but not dev Assert.AreEqual(expected, result); } } ================================================ FILE: StabilityMatrix.Tests/Core/AnsiParserTests.cs ================================================ using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Tests.Core; [TestClass] public class AnsiParserTests { [DataTestMethod] [DataRow("\u001b[0m", "\u001b[0m")] [DataRow("\u001b[A", "\u001b[A")] [DataRow("\u001b[A\r\n", "\u001b[A")] public void TestAnsiRegex(string source, string expectedMatch) { var pattern = AnsiParser.AnsiEscapeSequenceRegex(); var match = pattern.Match(source); Assert.IsTrue(match.Success); Assert.AreEqual(expectedMatch, match.Value); } } ================================================ FILE: StabilityMatrix.Tests/Core/AsyncStreamReaderTests.cs ================================================ using System.Text; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Tests.Core; /// /// Tests AsyncStreamReader and ApcMessage parsing /// [TestClass] public class AsyncStreamReaderTests { [DataTestMethod] // Test newlines handling for \r\n, \n [DataRow("a\r\nb\nc", "a\r\n", "b\n", "c", null)] // Carriage returns \r should be sent as is [DataRow("a\rb\rc", "a", "\rb", "\rc", null)] [DataRow("a1\ra2\nb1\rb2", "a1", "\ra2\n", "b1", "\rb2", null)] // Ansi escapes should be seperated [DataRow("\x1b[A\x1b[A", "\x1b[A", "\x1b[A", null)] // Mixed Ansi and newlines [DataRow("a \x1b[A\r\n\r xyz", "a ", "\x1b[A", "\r\n", "\r xyz", null)] public async Task TestRead(string source, params string?[] expected) { var results = new List(); var callback = new Action(s => { results.Add(s); }); using var stream = new MemoryStream(Encoding.UTF8.GetBytes(source)); using (var reader = new AsyncStreamReader(stream, callback, Encoding.UTF8)) { // Begin read line and wait until finish reader.BeginReadLine(); // Wait for maximum 1 second await reader.EOF.WaitAsync(new CancellationTokenSource(1000).Token); } // Check expected output matches Assert.IsTrue(expected.SequenceEqual(results.ToArray()), "Results [{0}] do not match expected [{1}]", string.Join(", ", results.Select(s => s?.ToRepr() ?? "")), string.Join(", ", expected.Select(s => s?.ToRepr() ?? ""))); } [TestMethod] public async Task TestCarriageReturnHandling() { var expected = new[] {"dog\r\n", "cat", "\r123", "\r456", null}; var results = new List(); var callback = new Action(s => { results.Add(s); }); // The previous buffer should be sent when \r is encountered const string source = "dog\r\ncat\r123\r456"; // Make the reader using var stream = new MemoryStream(Encoding.UTF8.GetBytes(source)); using (var reader = new AsyncStreamReader(stream, callback, Encoding.UTF8)) { // Begin read line and wait until finish reader.BeginReadLine(); // Wait for maximum 1 second await reader.EOF.WaitAsync(new CancellationTokenSource(1000).Token); } // Check if all expected strings were read Assert.IsTrue(expected.SequenceEqual(results.ToArray()), "Results [{0}] do not match expected [{1}]", string.Join(", ", results.Select(s => s?.ToRepr() ?? "")), string.Join(", ", expected.Select(s => s?.ToRepr() ?? ""))); } } ================================================ FILE: StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs ================================================ using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Tests.Core; [TestClass] public class DefaultUnknownEnumConverterTests { [TestMethod] [ExpectedException(typeof(JsonException))] public void TestDeserialize_NormalEnum_ShouldError() { const string json = "\"SomeUnknownValue\""; JsonSerializer.Deserialize(json); } [TestMethod] public void TestDeserialize_UnknownEnum_ShouldConvert() { const string json = "\"SomeUnknownValue\""; var result = JsonSerializer.Deserialize(json); Assert.AreEqual(UnknownEnum.Unknown, result); } [TestMethod] public void TestDeserialize_DefaultEnum_ShouldConvert() { const string json = "\"SomeUnknownValue\""; var result = JsonSerializer.Deserialize(json); Assert.AreEqual(DefaultEnum.CustomDefault, result); } [TestMethod] public void TestSerialize_UnknownEnum_ShouldConvert() { const string expected = "\"Unknown\""; var result = JsonSerializer.Serialize(UnknownEnum.Unknown); Assert.AreEqual(expected, result); } [TestMethod] public void TestDeserialize_UnknownEnum_ShouldUseEnumMemberValue() { const string json = "\"Value 2\""; var result = JsonSerializer.Deserialize(json); Assert.AreEqual(UnknownEnum.Value2, result); } [TestMethod] public void TestSerialize_DefaultEnum_ShouldConvert() { const string expected = "\"CustomDefault\""; var result = JsonSerializer.Serialize(DefaultEnum.CustomDefault); Assert.AreEqual(expected, result); } [TestMethod] public void TestSerialize_UnknownEnum_ShouldUseEnumMemberValue() { const string json = "\"Value 2\""; var result = JsonSerializer.Deserialize(json); Assert.AreEqual(UnknownEnum.Value2, result); } [TestMethod] public void TestSerialize_ComplexObject_ShouldUseEnumMemberValue() { const string expected = "{\"Key\":\"Value 2\"}"; var result = JsonSerializer.Serialize(new { Key = UnknownEnum.Value2 }); Assert.AreEqual(expected, result); } private enum NormalEnum { Unknown, Value1, Value2 } [JsonConverter(typeof(DefaultUnknownEnumConverter))] private enum UnknownEnum { Unknown, Value1, [EnumMember(Value = "Value 2")] Value2 } [JsonConverter(typeof(DefaultUnknownEnumConverter))] private enum DefaultEnum { CustomDefault, Value1, Value2 } } ================================================ FILE: StabilityMatrix.Tests/Core/FileSystemPathTests.cs ================================================ using System.Runtime.Versioning; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Tests.Core; [TestClass] public class FileSystemPathTests { [SupportedOSPlatform("windows")] [DataTestMethod] [DataRow("M:\\Path", "M:\\Path")] [DataRow("root/abc", "root/abc")] [DataRow("root\\abc", "root\\abc")] public void TestFilePathEqualsWin(string left, string right) { // Arrange var leftPath = new FilePath(left); var rightPath = new FilePath(right); // Act var resultEquals = leftPath.Equals(rightPath); var resultOperator = leftPath == rightPath; var resultNotOperator = leftPath != rightPath; // Assert Assert.IsTrue(resultEquals); Assert.IsTrue(resultOperator); Assert.IsFalse(resultNotOperator); } [DataTestMethod] [DataRow("M:/Path", "M:/Path")] [DataRow("root/abc", "root/abc")] [DataRow("root/abc", "root/abc")] public void TestFilePathEquals(string left, string right) { // Arrange var leftPath = new FilePath(left); var rightPath = new FilePath(right); // Act var resultEquals = leftPath.Equals(rightPath); var resultOperator = leftPath == rightPath; var resultNotOperator = leftPath != rightPath; // Assert Assert.IsTrue(resultEquals); Assert.IsTrue(resultOperator); Assert.IsFalse(resultNotOperator); } [DataTestMethod] [DataRow("M:/Path", "M:/Path2")] [DataRow("root/abc", "root/abc2")] public void TestFilePathNotEquals(string left, string right) { // Arrange var leftPath = new FilePath(left); var rightPath = new FilePath(right); // Act var resultEquals = leftPath.Equals(rightPath); var resultOperator = leftPath == rightPath; var resultNotOperator = leftPath != rightPath; // Assert Assert.IsFalse(resultEquals); Assert.IsFalse(resultOperator); Assert.IsTrue(resultNotOperator); } [DataTestMethod] [DataRow("root/abc", "root/abc")] [DataRow("root/abc", "root/abc/")] public void TestDirectoryPathEquals(string left, string right) { // Arrange var leftPath = new DirectoryPath(left); var rightPath = new DirectoryPath(right); // Act var resultEquals = leftPath.Equals(rightPath); var resultOperator = leftPath == rightPath; var resultNotOperator = leftPath != rightPath; // Assert Assert.IsTrue(resultEquals); Assert.IsTrue(resultOperator); Assert.IsFalse(resultNotOperator); } } ================================================ FILE: StabilityMatrix.Tests/Core/GlobalEncryptedSerializerTests.cs ================================================ using System.Security; using System.Security.Cryptography; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Lykos; #pragma warning disable CS0618 // Type or member is obsolete namespace StabilityMatrix.Tests.Core; [TestClass] public class GlobalEncryptedSerializerTests { [TestMethod] public void Serialize_ShouldDeserializeToSameObject() { // Arrange var secrets = new Secrets { LykosAccount = new LykosAccountV1Tokens("123", "456"), }; // Act var serialized = GlobalEncryptedSerializer.Serialize(secrets); var deserialized = GlobalEncryptedSerializer.Deserialize(serialized); // Assert Assert.AreEqual(secrets, deserialized); } [TestMethod] public void SerializeV1_ShouldDeserializeToSameObject() { // Arrange var secrets = new Secrets { LykosAccount = new LykosAccountV1Tokens("123", "456"), }; // Act var serialized = GlobalEncryptedSerializer.Serialize(secrets, GlobalEncryptedSerializer.KeyInfoV1); var deserialized = GlobalEncryptedSerializer.Deserialize(serialized); // Assert Assert.AreEqual(secrets, deserialized); } [TestMethod] public void SerializeV2_ShouldDeserializeToSameObject() { // Arrange var secrets = new Secrets { LykosAccount = new LykosAccountV1Tokens("123", "456"), }; // Act var serialized = GlobalEncryptedSerializer.Serialize(secrets, GlobalEncryptedSerializer.KeyInfoV2); var deserialized = GlobalEncryptedSerializer.Deserialize(serialized); // Assert Assert.AreEqual(secrets, deserialized); } [TestMethod] public void SerializeWithNonDefaultKeyInfo_ShouldDeserializeToSameObject() { // Arrange var secrets = new Secrets { LykosAccount = new LykosAccountV1Tokens("123", "456"), }; // Act var serialized = GlobalEncryptedSerializer.Serialize( secrets, GlobalEncryptedSerializer.KeyInfoV2 with { Iterations = GlobalEncryptedSerializer.KeyInfoV2.Iterations + 10, } ); var deserialized = GlobalEncryptedSerializer.Deserialize(serialized); // Assert Assert.AreEqual(secrets, deserialized); } [TestMethod] public void EncryptAndDecryptBytesWithKeyInfoV2_ShouldReturnSameBytes() { // Arrange var data = "hello"u8.ToArray(); var keyInfo = GlobalEncryptedSerializer.KeyInfoV2; var password = GetSecureString("password"); // Act var (encrypted, salt) = GlobalEncryptedSerializer.EncryptBytes(data, password, keyInfo); var decrypted = GlobalEncryptedSerializer.DecryptBytes(encrypted, salt, password, keyInfo); // Assert CollectionAssert.AreEqual(data, decrypted); } [TestMethod] public void EncryptAndDecryptBytesWithKeyInfoV2_DifferentPassword_ShouldFail() { // Arrange var data = "hello"u8.ToArray(); var keyInfo = GlobalEncryptedSerializer.KeyInfoV2; var encryptPassword = GetSecureString("password"); var decryptPassword = GetSecureString("a_different_password"); // Act var (encrypted, salt) = GlobalEncryptedSerializer.EncryptBytes(data, encryptPassword, keyInfo); // Assert Assert.ThrowsException( () => GlobalEncryptedSerializer.DecryptBytes(encrypted, salt, decryptPassword, keyInfo) ); } private static SecureString GetSecureString(string value) { var secureString = new SecureString(); foreach (var c in value) { secureString.AppendChar(c); } return secureString; } } ================================================ FILE: StabilityMatrix.Tests/Core/ModelIndexServiceTests.cs ================================================ using System.Reflection; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Tests.Core; [TestClass] public class ModelIndexServiceTests { [TestMethod] public void GetHasEarlyAccessUpdateOnly_ReturnsTrue_WhenAllNewerVersionsAreEarlyAccess() { var model = CreateLocalModel(installedVersionId: 100, hasUpdate: true); var remoteModel = CreateRemoteModel( CreateVersion(id: 300, isEarlyAccess: true), CreateVersion(id: 200, isEarlyAccess: true), CreateVersion(id: 100, isEarlyAccess: false) ); var result = InvokeGetHasEarlyAccessUpdateOnly(model, remoteModel); Assert.IsTrue(result); } [TestMethod] public void GetHasEarlyAccessUpdateOnly_ReturnsFalse_WhenAnyNewerVersionIsPublic() { var model = CreateLocalModel(installedVersionId: 100, hasUpdate: true); var remoteModel = CreateRemoteModel( CreateVersion(id: 300, isEarlyAccess: true), CreateVersion(id: 200, isEarlyAccess: false), CreateVersion(id: 100, isEarlyAccess: false) ); var result = InvokeGetHasEarlyAccessUpdateOnly(model, remoteModel); Assert.IsFalse(result); } [TestMethod] public void GetHasEarlyAccessUpdateOnly_ReturnsFalse_WhenInstalledVersionIsLatest() { var model = CreateLocalModel(installedVersionId: 100, hasUpdate: true); var remoteModel = CreateRemoteModel( CreateVersion(id: 100, isEarlyAccess: false), CreateVersion(id: 90, isEarlyAccess: true) ); var result = InvokeGetHasEarlyAccessUpdateOnly(model, remoteModel); Assert.IsFalse(result); } [TestMethod] public void GetHasEarlyAccessUpdateOnly_ReturnsFalse_WhenModelHasNoUpdate() { var model = CreateLocalModel(installedVersionId: 100, hasUpdate: false); var remoteModel = CreateRemoteModel( CreateVersion(id: 300, isEarlyAccess: true), CreateVersion(id: 200, isEarlyAccess: true), CreateVersion(id: 100, isEarlyAccess: false) ); var result = InvokeGetHasEarlyAccessUpdateOnly(model, remoteModel); Assert.IsFalse(result); } [TestMethod] public void GetHasEarlyAccessUpdateOnly_ReturnsFalse_WhenInstalledVersionIsNotInRemoteList() { var model = CreateLocalModel(installedVersionId: 100, hasUpdate: true); var remoteModel = CreateRemoteModel( CreateVersion(id: 300, isEarlyAccess: true), CreateVersion(id: 200, isEarlyAccess: true), CreateVersion(id: 150, isEarlyAccess: false) ); var result = InvokeGetHasEarlyAccessUpdateOnly(model, remoteModel); Assert.IsFalse(result); } private static bool InvokeGetHasEarlyAccessUpdateOnly(LocalModelFile model, CivitModel? remoteModel) { var method = typeof(ModelIndexService).GetMethod( "GetHasEarlyAccessUpdateOnly", BindingFlags.NonPublic | BindingFlags.Static ); Assert.IsNotNull(method); var result = method.Invoke(null, [model, remoteModel]); Assert.IsNotNull(result); return (bool)result; } private static LocalModelFile CreateLocalModel(int installedVersionId, bool hasUpdate) { return new LocalModelFile { RelativePath = "StableDiffusion/test-model.safetensors", SharedFolderType = SharedFolderType.StableDiffusion, HasUpdate = hasUpdate, ConnectedModelInfo = new ConnectedModelInfo { ModelId = 123, VersionId = installedVersionId, Source = ConnectedModelSource.Civitai, ModelName = "Test Model", ModelDescription = string.Empty, VersionName = $"v{installedVersionId}", Tags = [], Hashes = new CivitFileHashes(), }, }; } private static CivitModel CreateRemoteModel(params CivitModelVersion[] versions) { return new CivitModel { Id = 123, Name = "Test Model", Description = string.Empty, Type = CivitModelType.Unknown, Tags = [], Stats = new CivitModelStats(), ModelVersions = versions.ToList(), }; } private static CivitModelVersion CreateVersion(int id, bool isEarlyAccess) { return new CivitModelVersion { Id = id, Name = $"v{id}", Description = string.Empty, DownloadUrl = string.Empty, TrainedWords = [], Availability = isEarlyAccess ? "EarlyAccess" : "Public", Stats = new CivitModelStats(), }; } } ================================================ FILE: StabilityMatrix.Tests/Core/PipInstallArgsTests.cs ================================================ using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Tests.Core; [TestClass] public class PipInstallArgsTests { [TestMethod] public void TestGetTorch() { // Arrange const string version = "==2.1.0"; // Act var args = new PipInstallArgs().WithTorch(version).ToProcessArgs().ToString(); // Assert Assert.AreEqual("torch==2.1.0", args); } [TestMethod] public void TestGetTorchWithExtraIndex() { // Arrange const string version = ">=2.0.0"; const string index = "cu118"; // Act var args = new PipInstallArgs() .WithTorch(version) .WithTorchVision() .WithTorchExtraIndex(index) .ToProcessArgs() .ToString(); // Assert Assert.AreEqual( "torch>=2.0.0 torchvision --extra-index-url https://download.pytorch.org/whl/cu118", args ); } [TestMethod] public void TestGetTorchWithMoreStuff() { // Act var args = new PipInstallArgs() .AddArg("--pre") .WithTorch("~=2.0.0") .WithTorchVision() .WithTorchExtraIndex("nightly/cpu") .ToString(); // Assert Assert.AreEqual( "--pre torch~=2.0.0 torchvision --extra-index-url https://download.pytorch.org/whl/nightly/cpu", args ); } [TestMethod] public void TestParsedFromRequirementsTxt() { // Arrange const string requirements = """ torch~=2.0.0 torchvision # comment --extra-index-url https://example.org """; // Act var args = new PipInstallArgs().WithParsedFromRequirementsTxt(requirements); // Assert CollectionAssert.AreEqual( new[] { "torch~=2.0.0", "torchvision", "--extra-index-url https://example.org" }, args.ToProcessArgs().Select(arg => arg.GetQuotedValue()).ToArray() ); Assert.AreEqual("torch~=2.0.0 torchvision --extra-index-url https://example.org", args.ToString()); } [TestMethod] public void TestWithUserOverrides() { // Arrange var args = new PipInstallArgs() .AddArg("numpy") .WithTorch("==1.0.0") .WithExtraIndex("https://download.pytorch.org/whl/cu121"); var overrides = new List { new() { Name = "torch", Constraint = ">=", Version = "2.0.0", Action = PipPackageSpecifierOverrideAction.Update }, new() { Name = "--extra-index-url https://download.pytorch.org/whl/nightly/cpu", Action = PipPackageSpecifierOverrideAction.Update } }; // Act var resultArgs = args.WithUserOverrides(overrides); // Assert Assert.AreEqual( "numpy torch==1.0.0 --extra-index-url https://download.pytorch.org/whl/cu121", args.ToString() ); Assert.AreEqual( "numpy torch>=2.0.0 --extra-index-url https://download.pytorch.org/whl/nightly/cpu", resultArgs.ToString() ); } } ================================================ FILE: StabilityMatrix.Tests/Core/PipShowResultsTests.cs ================================================ using System.IO; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Tests.Core; [TestClass] public class PipShowResultsTests { [TestMethod] public void TestSinglePackage() { var input = """ Name: package-a Version: 1.0.0 Summary: A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: MIT Location: /path/to/package Requires: Required-by: """; var result = PipShowResult.Parse(input); Assert.IsNotNull(result); Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); Assert.AreEqual("A test package", result.Summary); } [TestMethod] public void TestMultiplePackages() { var input = """ Name: package-a Version: 1.0.0 Summary: A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: MIT Location: /path/to/package Requires: Required-by: --- Name: package-b Version: 2.0.0 Summary: Another test package Home-page: https://example.com Author: Jane Doe Author-email: jane.doe@example.com License: Apache-2.0 Location: /path/to/another/package Requires: package-a Required-by: """; var result = PipShowResult.Parse(input); Assert.IsNotNull(result); Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); Assert.AreNotEqual("package-b", result.Name); } [TestMethod] public void TestMalformedPackage() { var input = """ Name: package-a Version: 1.0.0 Summary A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: MIT Location: /path/to/package Requires: Required-by: """; var result = PipShowResult.Parse(input); Assert.IsNotNull(result); Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); Assert.IsNull(result.Summary); } [TestMethod] public void TestMultiLineLicense() { var input = """ Name: package-a Version: 1.0.0 Summary: A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: The MIT License (MIT) Copyright (c) 2015 John Doe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Location: /path/to/package Requires: Required-by: """; var result = PipShowResult.Parse(input); Assert.IsNotNull(result); Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); Assert.IsTrue(result.License?.StartsWith("License: The MIT License (MIT)")); } /// /// This test simulates the input that caused the crash reported in Sentry issue b125504f. /// The old implementation of PipShowResult.Parse used ToDictionary, which would throw an /// ArgumentException if the input contained multiple packages, as the "Name" key would be /// duplicated. The new implementation uses a foreach loop and TryAdd to prevent this crash. /// [TestMethod] public void TestDuplicatePackageNameInOutput() { var input = """ Name: package-a Version: 1.0.0 Summary: A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: MIT Location: /path/to/package Requires: Required-by: --- Name: package-a Version: 1.0.0 Summary: A test package Home-page: https://example.com Author: John Doe Author-email: john.doe@example.com License: MIT Location: /path/to/package Requires: Required-by: """; var result = PipShowResult.Parse(input); Assert.IsNotNull(result); Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); } [TestMethod] public void TestEmptyInputThrowsFormatException() { var input = ""; Assert.ThrowsException(() => PipShowResult.Parse(input)); } } ================================================ FILE: StabilityMatrix.Tests/Core/ServiceProviderExtensionsTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using NSubstitute; using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Tests.Core; [TestClass] public class ServiceProviderExtensionsTests { public abstract class TestDisposable : IDisposable { public abstract void Dispose(); } public abstract class TestAsyncDisposable : IAsyncDisposable { public abstract ValueTask DisposeAsync(); } [TestMethod] public void GetDisposables_ReturnsEmptyList_WhenNoDisposables() { // Arrange var services = new ServiceCollection(); var serviceProvider = services.BuildServiceProvider(); // Act var disposables = serviceProvider.GetDisposables(); // Assert Assert.AreEqual(0, disposables.Count); } [TestMethod] public void GetDisposables_ReturnsEmptyList_WhenNoMaterializedDisposables() { // Arrange var services = new ServiceCollection(); services.AddSingleton(_ => Substitute.For()); services.AddSingleton(_ => Substitute.For()); var serviceProvider = services.BuildServiceProvider(); // Act var disposables = serviceProvider.GetDisposables(); // Assert Assert.AreEqual(0, disposables.Count); } [TestMethod] public void GetDisposables_ReturnsDisposables_WhenMaterializedDisposables() { // Arrange var services = new ServiceCollection(); services.AddSingleton(_ => Substitute.For()); services.AddSingleton(_ => Substitute.For()); var serviceProvider = services.BuildServiceProvider(); // Act var testDisposable = serviceProvider.GetRequiredService(); var testAsyncDisposable = serviceProvider.GetRequiredService(); var disposables = serviceProvider.GetDisposables(); // Assert Assert.AreEqual(2, disposables.Count); CollectionAssert.Contains(disposables, testDisposable); CollectionAssert.Contains(disposables, testAsyncDisposable); } [TestMethod] public void GetDisposables_ReturnsMutableListReference() { // Arrange var services = new ServiceCollection(); services.AddSingleton(_ => Substitute.For()); var serviceProvider = services.BuildServiceProvider(); // Act // Clearing the list should result in TestDisposable not being disposed by the ServiceProvider var testDisposable = serviceProvider.GetRequiredService(); var disposables = serviceProvider.GetDisposables(); disposables.Clear(); serviceProvider.Dispose(); // Assert testDisposable.DidNotReceive().Dispose(); } } ================================================ FILE: StabilityMatrix.Tests/Helper/EventManagerTests.cs ================================================ using System.Diagnostics.CodeAnalysis; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Tests.Helper; [TestClass] public class EventManagerTests { private EventManager eventManager = null!; [TestInitialize] public void TestInitialize() { eventManager = EventManager.Instance; } [TestMethod] public void GlobalProgressChanged_ShouldBeInvoked() { // Arrange var progress = 0; eventManager.GlobalProgressChanged += (sender, args) => progress = args; // Act eventManager.OnGlobalProgressChanged(100); // Assert Assert.AreEqual(100, progress); } } ================================================ FILE: StabilityMatrix.Tests/Helper/ImageProcessorTests.cs ================================================ using StabilityMatrix.Avalonia.Helpers; namespace StabilityMatrix.Tests.Helper; [TestClass] public class ImageProcessorTests { [DataTestMethod] [DataRow(0, 1, 1)] [DataRow(1, 1, 1)] [DataRow(4, 2, 2)] [DataRow(8, 2, 4)] [DataRow(12, 3, 4)] [DataRow(20, 4, 5)] public void TestGetGridDimensionsFromImageCount(int count, int expectedRow, int expectedCols) { var result = ImageProcessor.GetGridDimensionsFromImageCount(count); Assert.AreEqual(expectedRow, result.rows); Assert.AreEqual(expectedCols, result.columns); } } ================================================ FILE: StabilityMatrix.Tests/Helper/PackageFactoryTests.cs ================================================ using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Tests.Helper; [TestClass] public class PackageFactoryTests { private PackageFactory packageFactory = null!; private IEnumerable fakeBasePackages = null!; [TestInitialize] public void Setup() { fakeBasePackages = new List { new DankDiffusion(null!, null!, null!, null!, null!, null!), }; packageFactory = new PackageFactory( fakeBasePackages, null!, null!, null!, null!, null!, null!, null! ); } [TestMethod] public void GetAllAvailablePackages_ReturnsAllPackages() { var result = packageFactory.GetAllAvailablePackages(); Assert.AreEqual(1, result.Count()); } [TestMethod] public void FindPackageByName_ReturnsPackage() { var result = packageFactory.FindPackageByName("dank-diffusion"); Assert.IsNotNull(result); } [TestMethod] public void FindPackageByName_ReturnsNull() { var result = packageFactory.FindPackageByName("not-a-package"); Assert.IsNull(result); } } ================================================ FILE: StabilityMatrix.Tests/Models/GenerationParametersTests.cs ================================================ using StabilityMatrix.Core.Models; namespace StabilityMatrix.Tests.Models; [TestClass] public class GenerationParametersTests { [TestMethod] public void TestParse() { const string data = """ test123 Negative prompt: test, easy negative Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 3589107295, Size: 1024x1028, Model hash: 9aa0c3e54d, Model: nightvisionXL_v0770_BakedVAE, VAE hash: 235745af8d, VAE: sdxl_vae.safetensors, Style Selector Enabled: True, Style Selector Randomize: False, Style Selector Style: base, Version: 1.6.0 """; Assert.IsTrue(GenerationParameters.TryParse(data, out var result)); Assert.AreEqual("test123", result.PositivePrompt); Assert.AreEqual("test, easy negative", result.NegativePrompt); Assert.AreEqual(20, result.Steps); Assert.AreEqual("Euler a", result.Sampler); Assert.AreEqual(7, result.CfgScale); Assert.AreEqual(3589107295, result.Seed); Assert.AreEqual(1024, result.Width); Assert.AreEqual(1028, result.Height); Assert.AreEqual("9aa0c3e54d", result.ModelHash); Assert.AreEqual("nightvisionXL_v0770_BakedVAE", result.ModelName); } [TestMethod] public void TestParse_NoNegative() { const string data = """ test123 Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 3589107295, Size: 1024x1028, Model hash: 9aa0c3e54d, Model: nightvisionXL_v0770_BakedVAE, VAE hash: 235745af8d, VAE: sdxl_vae.safetensors, Style Selector Enabled: True, Style Selector Randomize: False, Style Selector Style: base, Version: 1.6.0 """; Assert.IsTrue(GenerationParameters.TryParse(data, out var result)); Assert.AreEqual("test123", result.PositivePrompt); Assert.IsNull(result.NegativePrompt); Assert.AreEqual(20, result.Steps); Assert.AreEqual("Euler a", result.Sampler); Assert.AreEqual(7, result.CfgScale); Assert.AreEqual(3589107295, result.Seed); Assert.AreEqual(1024, result.Width); Assert.AreEqual(1028, result.Height); Assert.AreEqual("9aa0c3e54d", result.ModelHash); Assert.AreEqual("nightvisionXL_v0770_BakedVAE", result.ModelName); } [TestMethod] // basic data [DataRow( """Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1""", 7, "30", "DPM++ 2M Karras", "7", "2216407431", "640x896", "eb2h052f91", "anime_v1", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Size", "Model hash", "Model" } )] // duplicated keys [DataRow( """Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1, Steps: 40, Sampler: Whatever, CFG scale: 1, Seed: 1234567890, Size: 1024x1024, Model hash: 1234567890, Model: anime_v2""", 7, "30", "DPM++ 2M Karras", "7", "2216407431", "640x896", "eb2h052f91", "anime_v1", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Size", "Model hash", "Model" } )] public void TestParseLineFields( string line, int totalFields, string? expectedSteps, string? expectedSampler, string? expectedCfgScale, string? expectedSeed, string? expectedSize, string? expectedModelHash, string? expectedModel, string[] expectedKeys ) { var fields = GenerationParameters.ParseLine(line); Assert.AreEqual(totalFields, fields.Count); Assert.AreEqual(expectedSteps, fields["Steps"]); Assert.AreEqual(expectedSampler, fields["Sampler"]); Assert.AreEqual(expectedCfgScale, fields["CFG scale"]); Assert.AreEqual(expectedSeed, fields["Seed"]); Assert.AreEqual(expectedSize, fields["Size"]); Assert.AreEqual(expectedModelHash, fields["Model hash"]); Assert.AreEqual(expectedModel, fields["Model"]); CollectionAssert.AreEqual(expectedKeys, fields.Keys); } [TestMethod] // empty line [DataRow("", new string[] { })] [DataRow(" ", new string[] { })] // basic data [DataRow( "Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Size", "Model hash", "Model" } )] // no spaces [DataRow( "Steps:30,Sampler:DPM++2MKarras,CFGscale:7,Seed:2216407431,Size:640x896,Modelhash:eb2h052f91,Model:anime_v1", new string[] { "Steps", "Sampler", "CFGscale", "Seed", "Size", "Modelhash", "Model" } )] // extra commas [DataRow( "Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896,,,,,, Model hash: eb2h052f91, Model: anime_v1,,,,,,,", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Size", "Model hash", "Model" } )] // quoted string [DataRow( """Name: "John, Doe", Json: {"key:with:colon": "value, with, comma"}, It still: should work""", new string[] { "Name", "Json", "It still" } )] // extra ending brackets [DataRow( """Name: "John, Doe", Json: {"key:with:colon": "value, with, comma"}}}}}}}})))>>, It still: should work""", new string[] { "Name", "Json", "It still" } )] // civitai [DataRow( """Steps: 8, Sampler: Euler, CFG scale: 1, Seed: 12346789098, Size: 832x1216, Clip skip: 2, Created Date: 2024-12-22T01:01:01.0222111Z, Civitai resources: [{"type":"checkpoint","modelVersionId":123456,"modelName":"Some model name here [Pony XL] which hopefully doesnt contains half pair of quotes and brackets","modelVersionName":"v2.0"},{"type":"lycoris","weight":0.7,"modelVersionId":11111111,"modelName":"some style","modelVersionName":"v1.0 pony"},{"type":"lora","weight":1,"modelVersionId":222222222,"modelName":"another name","modelVersionName":"v1.0"},{"type":"lora","modelVersionId":3333333,"modelName":"name for 33333333333","modelVersionName":"version name here"}], Civitai metadata: {"remixOfId":11111100000}""", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Size", "Clip skip", "Created Date", "Civitai resources", "Civitai metadata" } )] // github.com/nkchocoai/ComfyUI-SaveImageWithMetaData [DataRow( """Steps: 20, Sampler: DPM++ SDE Karras, CFG scale: 6.0, Seed: 1111111111111, Clip skip: 2, Size: 1024x1024, Model: the_main_model.safetensors, Model hash: ababababab, Lora_0 Model name: name_of_the_first_lora.safetensors, Lora_0 Model hash: ababababab, Lora_0 Strength model: -1.1, Lora_0 Strength clip: -1.1, Lora_1 Model name: name_of_the_second_lora.safetensors, Lora_1 Model hash: ababababab, Lora_1 Strength model: 1, Lora_1 Strength clip: 1, Lora_2 Model name: name_of_the_third_lora.safetensors, Lora_2 Model hash: ababababab, Lora_2 Strength model: 0.9, Lora_2 Strength clip: 0.9, Hashes: {"model": "ababababab", "lora:name_of_the_first_lora": "ababababab", "lora:name_of_the_second_lora": "ababababab", "lora:name_of_the_third_lora": "ababababab"}""", new string[] { "Steps", "Sampler", "CFG scale", "Seed", "Clip skip", "Size", "Model", "Model hash", "Lora_0 Model name", "Lora_0 Model hash", "Lora_0 Strength model", "Lora_0 Strength clip", "Lora_1 Model name", "Lora_1 Model hash", "Lora_1 Strength model", "Lora_1 Strength clip", "Lora_2 Model name", "Lora_2 Model hash", "Lora_2 Strength model", "Lora_2 Strength clip", "Hashes" } )] // asymmetrical bracket [DataRow( """Steps: 20, Missing closing bracket: {"name": "Someone did not close [this bracket"}, But: the parser, should: still return, the: fields before it""", new string[] { "Steps", "Missing closing bracket" } )] public void TestParseLineEdgeCases(string line, string[] expectedKeys) { var fields = GenerationParameters.ParseLine(line); Assert.AreEqual(expectedKeys.Length, fields.Count); CollectionAssert.AreEqual(expectedKeys, fields.Keys); } [TestMethod] public void TestParseLine() { var fields = GenerationParameters.ParseLine( """Steps: 8, Sampler: Euler, CFG scale: 1, Seed: 12346789098, Size: 832x1216, Clip skip: 2, """ + """Created Date: 2024-12-22T01:01:01.0222111Z, Civitai resources: [{"type":"checkpoint","modelVersionId":123456,"modelName":"Some model name here [Pony XL] which hopefully doesnt contains half pair of quotes and brackets","modelVersionName":"v2.0"},{"type":"lycoris","weight":0.7,"modelVersionId":11111111,"modelName":"some style","modelVersionName":"v1.0 pony"},{"type":"lora","weight":1,"modelVersionId":222222222,"modelName":"another name","modelVersionName":"v1.0"},{"type":"lora","modelVersionId":3333333,"modelName":"name for 33333333333","modelVersionName":"version name here"}], Civitai metadata: {"remixOfId":11111100000},""" + """Hashes: {"model": "1234455678", "lora:aaaaaaa": "1234455678", "lora:bbbbbb": "1234455678", "lora:cccccccc": "1234455678"}""" ); Assert.AreEqual(10, fields.Count); Assert.AreEqual("8", fields["Steps"]); Assert.AreEqual("Euler", fields["Sampler"]); Assert.AreEqual("1", fields["CFG scale"]); Assert.AreEqual("12346789098", fields["Seed"]); Assert.AreEqual("832x1216", fields["Size"]); Assert.AreEqual("2", fields["Clip skip"]); Assert.AreEqual("2024-12-22T01:01:01.0222111Z", fields["Created Date"]); Assert.AreEqual( """[{"type":"checkpoint","modelVersionId":123456,"modelName":"Some model name here [Pony XL] which hopefully doesnt contains half pair of quotes and brackets","modelVersionName":"v2.0"},{"type":"lycoris","weight":0.7,"modelVersionId":11111111,"modelName":"some style","modelVersionName":"v1.0 pony"},{"type":"lora","weight":1,"modelVersionId":222222222,"modelName":"another name","modelVersionName":"v1.0"},{"type":"lora","modelVersionId":3333333,"modelName":"name for 33333333333","modelVersionName":"version name here"}]""", fields["Civitai resources"] ); Assert.AreEqual("""{"remixOfId":11111100000}""", fields["Civitai metadata"]); Assert.AreEqual( """{"model": "1234455678", "lora:aaaaaaa": "1234455678", "lora:bbbbbb": "1234455678", "lora:cccccccc": "1234455678"}""", fields["Hashes"] ); } } ================================================ FILE: StabilityMatrix.Tests/Models/InstalledPackageTests.cs ================================================ using System.Runtime.InteropServices; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Tests.Models; [TestClass] public class InstalledPackageTests { [DataTestMethod] [DataRow("C:\\User\\AppData\\StabilityMatrix", "C:\\User\\Other", null)] [DataRow("C:\\Data", "D:\\Data\\abc", null)] [DataRow("C:\\Data", "C:\\Data\\abc", "abc")] [DataRow("C:\\User\\AppData\\StabilityMatrix", "C:\\User\\AppData\\StabilityMatrix\\Packages\\abc", "Packages\\abc")] public void TestGetSubPath(string relativeTo, string path, string? expected) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { relativeTo = relativeTo.Replace("C:\\", $"{Path.DirectorySeparatorChar}") .Replace('\\', Path.DirectorySeparatorChar); path = path.Replace("C:\\", $"{Path.DirectorySeparatorChar}") .Replace('\\', Path.DirectorySeparatorChar); expected = expected?.Replace("C:\\", $"{Path.DirectorySeparatorChar}") .Replace('\\', Path.DirectorySeparatorChar); } var result = InstalledPackage.GetSubPath(relativeTo, path); Assert.AreEqual(expected, result); } } ================================================ FILE: StabilityMatrix.Tests/Models/LocalModelFileTests.cs ================================================ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Tests.Models; [TestClass] public class LocalModelFileTests { [TestMethod] public void Equals_ReturnsFalse_WhenEarlyAccessOnlyFlagDiffers() { var standardUpdateModel = CreateLocalModelFile(hasEarlyAccessUpdateOnly: false); var earlyAccessOnlyModel = standardUpdateModel with { HasEarlyAccessUpdateOnly = true }; Assert.IsFalse(standardUpdateModel.Equals(earlyAccessOnlyModel)); Assert.IsFalse( LocalModelFile.RelativePathConnectedModelInfoComparer.Equals( standardUpdateModel, earlyAccessOnlyModel ) ); } [TestMethod] public void RelativePathConnectedModelInfoComparer_TreatsEarlyAccessFlagAsDistinct() { var standardUpdateModel = CreateLocalModelFile(hasEarlyAccessUpdateOnly: false); var earlyAccessOnlyModel = standardUpdateModel with { HasEarlyAccessUpdateOnly = true }; var set = new HashSet(LocalModelFile.RelativePathConnectedModelInfoComparer) { standardUpdateModel, earlyAccessOnlyModel, }; Assert.AreEqual(2, set.Count); } private static LocalModelFile CreateLocalModelFile(bool hasEarlyAccessUpdateOnly) { return new LocalModelFile { RelativePath = "StableDiffusion/model-a.safetensors", SharedFolderType = SharedFolderType.StableDiffusion, HasUpdate = true, HasEarlyAccessUpdateOnly = hasEarlyAccessUpdateOnly, ConnectedModelInfo = new ConnectedModelInfo { ModelId = 123, VersionId = 101, Source = ConnectedModelSource.Civitai, ModelName = "Model A", ModelDescription = string.Empty, VersionName = "v101", Tags = [], Hashes = new CivitFileHashes(), }, }; } } ================================================ FILE: StabilityMatrix.Tests/Models/Packages/PackageHelper.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Tests.Models.Packages; public static class PackageHelper { /// /// Get all BasePackage implementations in the assembly. /// public static IEnumerable GetPackages() { var services = new ServiceCollection(); services .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()) .AddSingleton(Substitute.For()); var assembly = typeof(BasePackage).Assembly; var packageTypes = assembly .GetTypes() .Where(t => t.IsSubclassOf(typeof(BasePackage)) && !t.IsAbstract) .Where(t => t != typeof(DankDiffusion) && t != typeof(UnknownPackage)) .ToList(); // Register all package types services.TryAddEnumerable( packageTypes.Select(t => ServiceDescriptor.Singleton(typeof(BasePackage), t)) ); var serviceProvider = services.BuildServiceProvider(); return serviceProvider.GetServices(); } } ================================================ FILE: StabilityMatrix.Tests/Models/Packages/PackageLinkTests.cs ================================================ using System.Net.Http.Headers; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Retry; using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Tests.Models.Packages; /// /// Tests that URL links on Packages should be valid. Requires internet connection. /// [TestClass] [TestCategory("Http")] public sealed class PackageLinkTests { private static HttpClient HttpClient { get; } = new() { DefaultRequestHeaders = { { "User-Agent", "StabilityMatrix/2.0" } } }; private static IEnumerable PackagesData => PackageHelper.GetPackages().Where(x => x is not ComfyZluda).Select(p => new object[] { p }); private static readonly AsyncRetryPolicy RetryPolicy = Policy .HandleResult(response => response.StatusCode == System.Net.HttpStatusCode.TooManyRequests) .WaitAndRetryAsync( Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromMilliseconds(200), 3), onRetry: (outcome, timespan, retryAttempt, context) => { // Log retry attempt if needed Console.WriteLine($"Retry attempt {retryAttempt}, waiting {timespan.TotalSeconds} seconds"); } ); [TestMethod] [DynamicData(nameof(PackagesData))] public async Task TestPreviewImageUri(BasePackage package) { var imageUri = package.PreviewImageUri; // If is GitHub Uri, use jsdelivr instead due to rate limiting imageUri = GitHubToJsDelivr(imageUri); // Test http head is successful with retry policy var response = await RetryPolicy.ExecuteAsync(async () => await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, imageUri)) ); Assert.IsTrue( response.IsSuccessStatusCode, "Failed to get PreviewImageUri at {0}: {1}", imageUri, response ); } [TestMethod] [DynamicData(nameof(PackagesData))] public async Task TestLicenseUrl(BasePackage package) { if (string.IsNullOrEmpty(package.LicenseUrl)) { Assert.Inconclusive($"No LicenseUrl for package {package.GetType().Name} '{package.Name}'"); } var licenseUri = new Uri(package.LicenseUrl); // If is GitHub Uri, use jsdelivr instead due to rate limiting licenseUri = GitHubToJsDelivr(licenseUri); // Test http head is successful with retry policy var response = await RetryPolicy.ExecuteAsync(async () => await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, licenseUri)) ); Assert.IsTrue( response.IsSuccessStatusCode, "Failed to get LicenseUrl at {0}: {1}", licenseUri, response ); } private static Uri GitHubToJsDelivr(Uri uri) { // Like https://github.com/user/Repo/blob/main/LICENSE // becomes: https://cdn.jsdelivr.net/gh/user/Repo@main/LICENSE if (uri.Host.Equals("github.com", StringComparison.OrdinalIgnoreCase)) { var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments is [var user, var repo, "blob", var branch, ..]) { var path = string.Join("/", segments.Skip(4)); return new Uri($"https://cdn.jsdelivr.net/gh/{user}/{repo}@{branch}/{path}"); } } return uri; } } ================================================ FILE: StabilityMatrix.Tests/Models/Packages/SharedFolderConfigHelperTests.cs ================================================ using System.Collections.Immutable; using System.Text; using System.Text.Json.Nodes; using FreneticUtilities.FreneticDataSyntax; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Packages.Config; using YamlDotNet.RepresentationModel; namespace StabilityMatrix.Tests.Models.Packages; [TestClass] public class SharedFoldersConfigHelperTests { // Define mock paths used across tests private const string MockPackageRoot = @"C:\SM\Packages\TestPackage"; // Use OS-specific or normalized private const string MockSharedModelsRoot = @"C:\SM\Models"; // Helper to run the target method and return the resulting stream content as string private async Task RunHelperAndGetOutput( SharedFolderLayout layout, string packageRoot, string sharedModelsRoot, bool useSharedMode // True for SharedAsync, False for DefaultAsync ) { using var stream = new MemoryStream(); if (useSharedMode) { await SharedFoldersConfigHelper.UpdateConfigFileForSharedAsync( layout, packageRoot, sharedModelsRoot, stream ); } else { await SharedFoldersConfigHelper.UpdateConfigFileForDefaultAsync(layout, packageRoot, stream); } stream.Position = 0; // Rewind stream to read the output using var reader = new StreamReader(stream, Encoding.UTF8); return await reader.ReadToEndAsync(); } // Helper to normalize paths in expected strings for cross-platform compatibility private string NormalizeExpectedPath(string path) => path.Replace('/', Path.DirectorySeparatorChar); // --- JSON Tests --- [TestMethod] public async Task Json_UpdateForShared_WritesCorrectPaths() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = "config.json", ConfigFileType = ConfigFileType.Json, ConfigSharingOptions = ConfigSharingOptions.Default, // Use default options Rules = ImmutableList.Create( new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], ConfigDocumentPaths = ["ckpt_dir"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], ConfigDocumentPaths = ["lora_dirs"] } // Test multiple sources -> array ) }; // Act var outputJson = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: true ); var jsonNode = JsonNode.Parse(outputJson); // Assert Assert.IsNotNull(jsonNode); var expectedCkptPath = Path.Combine(MockSharedModelsRoot, "StableDiffusion").Replace('\\', '/'); // JSON usually uses / var expectedLoraPath = Path.Combine(MockSharedModelsRoot, "Lora").Replace('\\', '/'); var expectedLycoPath = Path.Combine(MockSharedModelsRoot, "LyCORIS").Replace('\\', '/'); Assert.AreEqual(expectedCkptPath, jsonNode["ckpt_dir"]?.GetValue()); var loraDirs = jsonNode["lora_dirs"] as JsonArray; Assert.IsNotNull(loraDirs); Assert.AreEqual(2, loraDirs.Count); Assert.IsTrue(loraDirs.Any(n => n != null && n.GetValue() == expectedLoraPath)); Assert.IsTrue(loraDirs.Any(n => n != null && n.GetValue() == expectedLycoPath)); } [TestMethod] public async Task Json_UpdateForDefault_WritesCorrectPaths() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = "config.json", ConfigFileType = ConfigFileType.Json, ConfigSharingOptions = ConfigSharingOptions.Default, Rules = ImmutableList.Create( new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["models/checkpoints"], ConfigDocumentPaths = ["ckpt_dir"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora], TargetRelativePaths = ["models/loras"], ConfigDocumentPaths = ["lora_dirs"] } // Assume single default path ) }; var expectedCkptPath = Path.Combine(MockPackageRoot, "models", "checkpoints").Replace('\\', '/'); var expectedLoraPath = Path.Combine(MockPackageRoot, "models", "loras").Replace('\\', '/'); // Act var outputJson = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: false ); // Default Mode var jsonNode = JsonNode.Parse(outputJson); // Assert Assert.IsNotNull(jsonNode); Assert.AreEqual(expectedCkptPath, jsonNode["ckpt_dir"]?.GetValue()); // Since default writes single target path, expect string, not array Assert.AreEqual(expectedLoraPath, jsonNode["lora_dirs"]?.GetValue()); } [TestMethod] public async Task Json_NestedPaths_UpdateForShared_WritesCorrectly() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = "config.json", ConfigFileType = ConfigFileType.Json, ConfigSharingOptions = ConfigSharingOptions.Default, Rules = ImmutableList.Create( new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], ConfigDocumentPaths = ["paths.models.vae"] } ) }; var expectedVaePath = Path.Combine(MockSharedModelsRoot, "VAE").Replace('\\', '/'); // Act var outputJson = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: true ); var jsonNode = JsonNode.Parse(outputJson); // Assert Assert.IsNotNull(jsonNode); Assert.AreEqual(expectedVaePath, jsonNode?["paths"]?["models"]?["vae"]?.GetValue()); } // --- YAML Tests --- [TestMethod] public async Task Yaml_UpdateForShared_WritesCorrectPathsWithRootKey() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = "extra_paths.yaml", ConfigFileType = ConfigFileType.Yaml, ConfigSharingOptions = ConfigSharingOptions.Default with { RootKey = "stability_matrix" }, // Set RootKey Rules = ImmutableList.Create( new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], ConfigDocumentPaths = ["vae"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], ConfigDocumentPaths = ["loras"] } ) }; var expectedVaePath = Path.Combine(MockSharedModelsRoot, "VAE").Replace('\\', '/'); var expectedLoraPath = Path.Combine(MockSharedModelsRoot, "Lora").Replace('\\', '/'); var expectedLycoPath = Path.Combine(MockSharedModelsRoot, "LyCORIS").Replace('\\', '/'); // Act var outputYaml = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: true ); // Assert using YamlDotNet.RepresentationModel var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(outputYaml)); var rootMapping = yamlStream.Documents[0].RootNode as YamlMappingNode; Assert.IsNotNull(rootMapping); var smNode = rootMapping.Children[new YamlScalarNode("stability_matrix")] as YamlMappingNode; Assert.IsNotNull(smNode); // Scalars var vaeNode = smNode.Children[new YamlScalarNode("vae")] as YamlScalarNode; Assert.IsNotNull(vaeNode); Assert.AreEqual(expectedVaePath, vaeNode.Value); var lorasNode = smNode.Children[new YamlScalarNode("loras")] as YamlScalarNode; Assert.IsNotNull(lorasNode); // Split into sequences var loras = lorasNode.Value?.SplitLines() ?? []; CollectionAssert.Contains(loras, expectedLoraPath); CollectionAssert.Contains(loras, expectedLycoPath); // Sequence support /*var vaeNode = smNode.Children[new YamlScalarNode("vae")] as YamlSequenceNode; Assert.IsNotNull(vaeNode); Assert.AreEqual(1, vaeNode.Children.Count); Assert.AreEqual(expectedVaePath, (vaeNode.Children[0] as YamlScalarNode)?.Value); var lorasNode = smNode.Children[new YamlScalarNode("loras")] as YamlSequenceNode; Assert.IsNotNull(lorasNode); Assert.AreEqual(2, lorasNode.Children.Count); Assert.IsTrue(lorasNode.Children.Any(n => n is YamlScalarNode ns && ns.Value == expectedLoraPath)); Assert.IsTrue(lorasNode.Children.Any(n => n is YamlScalarNode ns && ns.Value == expectedLycoPath));*/ } [TestMethod] public async Task Yaml_UpdateForDefault_RelativePaths() { // Arrange var initialYamlContent = """ # Existing content some_other_key: value stability_matrix: vae: - C:\SM\Models/VAE loras: - C:\SM\Models/Lora - C:\SM\Models/LyCORIS another_key: 123 """; var layout = new SharedFolderLayout { RelativeConfigPath = "extra_paths.yaml", ConfigFileType = ConfigFileType.Yaml, ConfigSharingOptions = ConfigSharingOptions.Default with { RootKey = "stability_matrix", ConfigDefaultType = ConfigDefaultType.TargetRelativePaths // Configure relative paths }, Rules = ImmutableList.Create( // Define rules so helper knows which keys to clear under RootKey new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], ConfigDocumentPaths = ["vae"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["models/loras"], ConfigDocumentPaths = ["loras"] } ) }; // Act - Write initial content, then run Default Mode using var stream = new MemoryStream(); await using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) { await writer.WriteAsync(initialYamlContent); } stream.Position = 0; // Reset for the helper await SharedFoldersConfigHelper.UpdateConfigFileForDefaultAsync(layout, MockPackageRoot, stream); // Use overload that reads layout options stream.Position = 0; using var reader = new StreamReader(stream); var outputYaml = await reader.ReadToEndAsync(); // Assert var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(outputYaml)); var rootMapping = yamlStream.Documents[0].RootNode as YamlMappingNode; Assert.IsNotNull(rootMapping); // Check that stability_matrix key is not gone (or empty) Assert.IsTrue( rootMapping.Children.ContainsKey(new YamlScalarNode("stability_matrix")), "stability_matrix key should exist." ); // Check that other keys remain Assert.IsTrue(rootMapping.Children.ContainsKey(new YamlScalarNode("some_other_key"))); Assert.IsTrue(rootMapping.Children.ContainsKey(new YamlScalarNode("another_key"))); } [TestMethod] public async Task Yaml_UpdateForDefault_RemovesSmRootKey() { // Arrange var initialYamlContent = """ # Existing content some_other_key: value stability_matrix: vae: - C:\SM\Models/VAE loras: - C:\SM\Models/Lora - C:\SM\Models/LyCORIS another_key: 123 """; var layout = new SharedFolderLayout { RelativeConfigPath = "extra_paths.yaml", ConfigFileType = ConfigFileType.Yaml, ConfigSharingOptions = ConfigSharingOptions.Default with { RootKey = "stability_matrix", ConfigDefaultType = ConfigDefaultType.ClearRoot // Configure clearing of RootKey }, Rules = ImmutableList.Create( // Define rules so helper knows which keys to clear under RootKey new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], ConfigDocumentPaths = ["vae"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.Lora, SharedFolderType.LyCORIS], TargetRelativePaths = ["models/loras"], ConfigDocumentPaths = ["loras"] } ) }; // Act - Write initial content, then run Default Mode using var stream = new MemoryStream(); await using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) { await writer.WriteAsync(initialYamlContent); } stream.Position = 0; // Reset for the helper await SharedFoldersConfigHelper.UpdateConfigFileForDefaultAsync(layout, MockPackageRoot, stream); // Use overload that reads layout options stream.Position = 0; using var reader = new StreamReader(stream); var outputYaml = await reader.ReadToEndAsync(); // Assert var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(outputYaml)); var rootMapping = yamlStream.Documents[0].RootNode as YamlMappingNode; Assert.IsNotNull(rootMapping); // Check that stability_matrix key is gone (or empty) Assert.IsFalse( rootMapping.Children.ContainsKey(new YamlScalarNode("stability_matrix")), "stability_matrix key should be removed." ); // Check that other keys remain Assert.IsTrue(rootMapping.Children.ContainsKey(new YamlScalarNode("some_other_key"))); Assert.IsTrue(rootMapping.Children.ContainsKey(new YamlScalarNode("another_key"))); } // --- FDS Tests --- [TestMethod] public async Task Fds_UpdateForShared_WritesCorrectPathsWithRoot() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = Path.Combine("Data", "Settings.fds"), ConfigFileType = ConfigFileType.Fds, ConfigSharingOptions = ConfigSharingOptions.Default, // RootKey not used by FDS strategy directly Rules = ImmutableList.Create( new SharedFolderLayoutRule { ConfigDocumentPaths = ["ModelRoot"], IsRoot = true }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], ConfigDocumentPaths = ["SDModelFolder"] } ) }; var expectedModelRoot = MockSharedModelsRoot.Replace('/', Path.DirectorySeparatorChar); var expectedSdModelFolder = Path.Combine(MockSharedModelsRoot, "StableDiffusion") .Replace('/', Path.DirectorySeparatorChar); // Act var outputFds = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: true ); var fdsSection = new FDSSection(outputFds); // Assert Assert.IsNotNull(fdsSection); var pathsSection = fdsSection.GetSection("Paths"); Assert.IsNotNull(pathsSection); Assert.AreEqual(expectedModelRoot, pathsSection.GetString("ModelRoot")); Assert.AreEqual(expectedSdModelFolder, pathsSection.GetString("SDModelFolder")); } [TestMethod] public async Task Fds_UpdateForDefault_WritesCorrectPaths() { // Arrange var layout = new SharedFolderLayout { RelativeConfigPath = Path.Combine("Data", "Settings.fds"), ConfigFileType = ConfigFileType.Fds, ConfigSharingOptions = ConfigSharingOptions.Default, Rules = ImmutableList.Create( // Root rule should result in ModelRoot being *removed* in Default mode new SharedFolderLayoutRule { ConfigDocumentPaths = ["ModelRoot"], IsRoot = true }, // Regular rule should write the default path new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.StableDiffusion], TargetRelativePaths = ["Models/Stable-Diffusion"], ConfigDocumentPaths = ["SDModelFolder"] } ) }; var expectedSdModelFolder = Path.Combine(MockPackageRoot, "Models", "Stable-Diffusion") .Replace('/', Path.DirectorySeparatorChar); // Act var outputFds = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: false ); // Default Mode var fdsSection = new FDSSection(outputFds); // Assert Assert.IsNotNull(fdsSection); var pathsSection = fdsSection.GetSection("Paths"); // May or may not exist depending on if SDModelFolder was only key if (pathsSection != null) { Assert.IsNull( pathsSection.GetString("ModelRoot"), "ModelRoot should be removed in Default mode." ); // Check ModelRoot is gone Assert.AreEqual(expectedSdModelFolder, pathsSection.GetString("SDModelFolder")); } else { // If only ModelRoot was defined, Paths section itself might be removed, which is also ok Assert.IsNull( fdsSection.GetSection("Paths"), "Paths section should be removed if only ModelRoot existed." ); } } [TestMethod] public async Task Json_SplitRule_UpdateForShared_WritesCorrectArray() { // Arrange: Simulate SDFX IP-Adapter rules var layout = new SharedFolderLayout { RelativeConfigPath = "config.json", ConfigFileType = ConfigFileType.Json, ConfigSharingOptions = ConfigSharingOptions.Default with { AlwaysWriteArray = true }, // Force array Rules = ImmutableList.Create( new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.IpAdapter], ConfigDocumentPaths = ["paths.models.ipadapter"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.IpAdapters15], ConfigDocumentPaths = ["paths.models.ipadapter"] }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.IpAdaptersXl], ConfigDocumentPaths = ["paths.models.ipadapter"] } ) }; var expectedIpBasePath = Path.Combine(MockSharedModelsRoot, "IpAdapter").Replace('\\', '/'); var expectedIp15Path = Path.Combine(MockSharedModelsRoot, "IpAdapters15").Replace('\\', '/'); // SM SourceTypes map like this var expectedIpXlPath = Path.Combine(MockSharedModelsRoot, "IpAdaptersXl").Replace('\\', '/'); // Act var outputJson = await RunHelperAndGetOutput( layout, MockPackageRoot, MockSharedModelsRoot, useSharedMode: true ); var jsonNode = JsonNode.Parse(outputJson); // Assert Assert.IsNotNull(jsonNode); var ipAdapterNode = jsonNode?["paths"]?["models"]?["ipadapter"]; Assert.IsInstanceOfType(ipAdapterNode, typeof(JsonArray)); var ipAdapterArray = ipAdapterNode as JsonArray; Assert.AreEqual(3, ipAdapterArray?.Count); Assert.IsTrue(ipAdapterArray.Any(n => n?.GetValue() == expectedIpBasePath)); Assert.IsTrue(ipAdapterArray.Any(n => n?.GetValue() == expectedIp15Path)); Assert.IsTrue(ipAdapterArray.Any(n => n?.GetValue() == expectedIpXlPath)); } // Add more tests: // - Starting with an existing config file and modifying it. // - Testing specific ConfigSharingOptions (AlwaysWriteArray for JSON, different RootKey for YAML). // - Testing removal of keys when rules are removed from the layout. // - Edge cases like empty layouts or layouts with no matching rules. } ================================================ FILE: StabilityMatrix.Tests/Models/ProcessArgsTests.cs ================================================ using System.Collections.Immutable; using NSubstitute; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Tests.Models; [TestClass] public class ProcessArgsTests { [DataTestMethod] [DataRow("pip", new[] { "pip" })] [DataRow("pip install torch", new[] { "pip", "install", "torch" })] [DataRow("pip install -r \"file spaces/here\"", new[] { "pip", "install", "-r", "file spaces/here" })] [DataRow("pip install -r \"file spaces\\here\"", new[] { "pip", "install", "-r", "file spaces\\here" })] public void TestStringToArray(string input, string[] expected) { // Implicit (string -> ProcessArgs) ProcessArgs args = input; var result = args.ToArgumentArray().Select(arg => arg.Value).ToArray(); // Assert CollectionAssert.AreEqual(expected, result); } [DataTestMethod] [DataRow(new[] { "pip" }, "pip")] [DataRow(new[] { "pip", "install", "torch" }, "pip install torch")] [DataRow(new[] { "pip", "install", "-r", "file spaces/here" }, "pip install -r \"file spaces/here\"")] [DataRow(new[] { "pip", "install", "-r", "file spaces\\here" }, "pip install -r \"file spaces\\here\"")] public void TestArrayToString(string[] input, string expected) { ProcessArgs args = input; string result = args; Assert.AreEqual(expected, result); } [TestMethod] public void TestIsQuoted() { // Arrange var args = new ProcessArgsBuilder( "-test", // This should be quoted (has space) "--arg 123", // Should not be quoted in result Argument.Quoted("--arg 123") ).ToProcessArgs(); // Act var argString = args.ToString(); // Assert Assert.AreEqual(argString, "-test \"--arg 123\" --arg 123"); } } ================================================ FILE: StabilityMatrix.Tests/Models/SafetensorMetadataTests.cs ================================================ using System.Buffers.Binary; using System.Text; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Tests.Models; [TestClass] public class SafetensorMetadataTests { [TestMethod] public async Task TestParseStreamAsync() { const string SOURCE_JSON = """ { "anything":[1,2,3,4,"",{ "a": 1, "b": 2, "c": 3 }], "__metadata__":{"ss_network_module":"some network module","modelspec.architecture":"some architecture", "ss_tag_frequency":"{\"aaa\":{\"tag1\":59,\"tag2\":2},\"bbb\":{\"tag1\":4,\"tag3\":1}}" }, "someotherdata":{ "a": 1, "b": 2, "c": 3 } } """; var stream = new MemoryStream(); Span buffer = stackalloc byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(buffer, (ulong)SOURCE_JSON.Length); stream.Write(buffer); stream.Write(Encoding.UTF8.GetBytes(SOURCE_JSON)); stream.Position = 0; var metadata = await SafetensorMetadata.ParseAsync(stream); // Assert.AreEqual("some network module", metadata.NetworkModule); // Assert.AreEqual("some architecture", metadata.ModelSpecArchitecture); Assert.IsNotNull(metadata); Assert.IsNotNull(metadata.TagFrequency); CollectionAssert.AreEqual( new List { new("tag1", 63), new("tag2", 2), new("tag3", 1) }, metadata.TagFrequency ); } } ================================================ FILE: StabilityMatrix.Tests/Models/SharedFoldersTests.cs ================================================ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Tests.Models; [TestClass] public class SharedFoldersTests { private string tempFolder = string.Empty; private string TempModelsFolder => Path.Combine(tempFolder, "models"); private string TempPackageFolder => Path.Combine(tempFolder, "package"); private readonly Dictionary sampleDefinitions = new() { [SharedFolderType.StableDiffusion] = "models/Stable-diffusion", [SharedFolderType.ESRGAN] = "models/ESRGAN", [SharedFolderType.Embeddings] = "embeddings", }; [TestInitialize] public void Initialize() { tempFolder = Path.GetTempFileName(); File.Delete(tempFolder); Directory.CreateDirectory(tempFolder); } [TestCleanup] public void Cleanup() { if (string.IsNullOrEmpty(tempFolder)) return; TempFiles.DeleteDirectory(tempFolder); } private void CreateSampleJunctions() { var definitions = new Dictionary> { [SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" }, [SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" }, [SharedFolderType.Embeddings] = new[] { "embeddings" }, }; SharedFolders .UpdateLinksForPackage(definitions, TempModelsFolder, TempPackageFolder) .GetAwaiter() .GetResult(); } [TestMethod] public void SetupLinks_CreatesJunctions() { CreateSampleJunctions(); // Check model folders foreach (var (folderType, relativePath) in sampleDefinitions) { var packagePath = Path.Combine(TempPackageFolder, relativePath); var modelFolder = Path.Combine(TempModelsFolder, folderType.GetStringValue()); // Should exist and be a junction Assert.IsTrue(Directory.Exists(packagePath), $"Package folder {packagePath} does not exist."); var info = new DirectoryInfo(packagePath); Assert.IsTrue( info.Attributes.HasFlag(FileAttributes.ReparsePoint), $"Package folder {packagePath} is not a junction." ); // Check junction target should be in models folder Assert.AreEqual( modelFolder, info.LinkTarget, $"Package folder {packagePath} does not point to {modelFolder}." ); } } [TestMethod] public void SetupLinks_CanDeleteJunctions() { CreateSampleJunctions(); var modelFolder = Path.Combine( tempFolder, "models", SharedFolderType.StableDiffusion.GetStringValue() ); var packagePath = Path.Combine( tempFolder, "package", sampleDefinitions[SharedFolderType.StableDiffusion] ); // Write a file to a model folder File.Create(Path.Combine(modelFolder, "AFile")).Close(); Assert.IsTrue( File.Exists(Path.Combine(modelFolder, "AFile")), $"File should exist in {modelFolder}." ); // Should exist in the package folder Assert.IsTrue( File.Exists(Path.Combine(packagePath, "AFile")), $"File should exist in {packagePath}." ); // Now delete the junction Directory.Delete(packagePath, false); Assert.IsFalse(Directory.Exists(packagePath), $"Package folder {packagePath} should not exist."); // The file should still exist in the model folder Assert.IsTrue( File.Exists(Path.Combine(modelFolder, "AFile")), $"File should exist in {modelFolder}." ); } } ================================================ FILE: StabilityMatrix.Tests/Native/NativeRecycleBinProviderTests.cs ================================================ using System.Runtime.InteropServices; using StabilityMatrix.Native; using StabilityMatrix.Native.Abstractions; namespace StabilityMatrix.Tests.Native; [TestClass] public class NativeRecycleBinProviderTests { private string tempFolder = string.Empty; [TestInitialize] public void Initialize() { if ( !( RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ) ) { Assert.IsFalse(NativeFileOperations.IsRecycleBinAvailable); Assert.IsNull(NativeFileOperations.RecycleBin); Assert.Inconclusive("Recycle bin is only available on Windows and macOS."); return; } Assert.IsTrue(NativeFileOperations.IsRecycleBinAvailable); Assert.IsNotNull(NativeFileOperations.RecycleBin); tempFolder = Path.GetTempFileName(); File.Delete(tempFolder); Directory.CreateDirectory(tempFolder); } [TestCleanup] public void Cleanup() { if (string.IsNullOrEmpty(tempFolder)) return; TempFiles.DeleteDirectory(tempFolder); } [TestMethod] public void RecycleFile() { var targetFile = Path.Combine(tempFolder, $"{nameof(RecycleFile)}_{Guid.NewGuid().ToString()}"); File.Create(targetFile).Close(); Assert.IsTrue(File.Exists(targetFile)); NativeFileOperations.RecycleBin!.MoveFileToRecycleBin( targetFile, NativeFileOperationFlags.Silent | NativeFileOperationFlags.NoConfirmation ); Assert.IsFalse(File.Exists(targetFile)); } [TestMethod] public void RecycleFiles() { var targetFiles = Enumerable .Range(0, 8) .Select(i => Path.Combine(tempFolder, $"{nameof(RecycleFiles)}_{i}_{Guid.NewGuid().ToString()}")) .ToArray(); foreach (var targetFile in targetFiles) { File.Create(targetFile).Close(); Assert.IsTrue(File.Exists(targetFile)); } NativeFileOperations.RecycleBin!.MoveFilesToRecycleBin( targetFiles, NativeFileOperationFlags.Silent | NativeFileOperationFlags.NoConfirmation ); foreach (var targetFile in targetFiles) { Assert.IsFalse(File.Exists(targetFile)); } } [TestMethod] public void RecycleDirectory() { var targetDirectory = Path.Combine( tempFolder, $"{nameof(RecycleDirectory)}_{Guid.NewGuid().ToString()}" ); Directory.CreateDirectory(targetDirectory); Assert.IsTrue(Directory.Exists(targetDirectory)); NativeFileOperations.RecycleBin!.MoveDirectoryToRecycleBin( targetDirectory, NativeFileOperationFlags.Silent | NativeFileOperationFlags.NoConfirmation ); Assert.IsFalse(Directory.Exists(targetDirectory)); } [TestMethod] public void RecycleDirectories() { var targetDirectories = Enumerable .Range(0, 2) .Select( i => Path.Combine(tempFolder, $"{nameof(RecycleDirectories)}_{i}_{Guid.NewGuid().ToString()}") ) .ToArray(); foreach (var targetDirectory in targetDirectories) { Directory.CreateDirectory(targetDirectory); Assert.IsTrue(Directory.Exists(targetDirectory)); } NativeFileOperations.RecycleBin!.MoveDirectoriesToRecycleBin( targetDirectories, NativeFileOperationFlags.Silent | NativeFileOperationFlags.NoConfirmation ); foreach (var targetDirectory in targetDirectories) { Assert.IsFalse(Directory.Exists(targetDirectory)); } } } ================================================ FILE: StabilityMatrix.Tests/ReparsePoints/JunctionTests.cs ================================================ using System.Runtime.InteropServices; using System.Runtime.Versioning; using StabilityMatrix.Core.ReparsePoints; namespace StabilityMatrix.Tests.ReparsePoints; using System.IO; [TestClass] [SupportedOSPlatform("windows")] public class JunctionTest { private string tempFolder = string.Empty; [TestInitialize] public void Initialize() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Assert.Inconclusive("Test cannot be run on anything but Windows currently."); return; } tempFolder = Path.GetTempFileName(); File.Delete(tempFolder); Directory.CreateDirectory(tempFolder); } [TestCleanup] public void Cleanup() { if (string.IsNullOrEmpty(tempFolder)) return; TempFiles.DeleteDirectory(tempFolder); } [TestMethod] public void Exists_NoSuchFile() { Assert.IsFalse(Junction.Exists(Path.Combine(tempFolder, "$$$NoSuchFolder$$$"))); } [TestMethod] public void Exists_IsADirectory() { File.Create(Path.Combine(tempFolder, "AFile")).Close(); Assert.IsFalse(Junction.Exists(Path.Combine(tempFolder, "AFile"))); } [TestMethod] public void Create_VerifyExists_GetTarget_Delete() { var targetFolder = Path.Combine(tempFolder, "ADirectory"); var junctionPoint = Path.Combine(tempFolder, "SymLink"); Directory.CreateDirectory(targetFolder); File.Create(Path.Combine(targetFolder, "AFile")).Close(); // Verify behavior before junction point created. Assert.IsFalse( File.Exists(Path.Combine(junctionPoint, "AFile")), "File should not be located until junction point created." ); Assert.IsFalse(Junction.Exists(junctionPoint), "Junction point not created yet."); // Create junction point and confirm its properties. Junction.Create( junctionPoint, targetFolder, false /*don't overwrite*/ ); Assert.IsTrue(Junction.Exists(junctionPoint), "Junction point exists now."); Assert.AreEqual(targetFolder, Junction.GetTarget(junctionPoint)); Assert.IsTrue( File.Exists(Path.Combine(junctionPoint, "AFile")), "File should be accessible via the junction point." ); // Delete junction point. Junction.Delete(junctionPoint); Assert.IsFalse(Junction.Exists(junctionPoint), "Junction point should not exist now."); Assert.IsFalse( File.Exists(Path.Combine(junctionPoint, "AFile")), "File should not be located after junction point deleted." ); Assert.IsFalse(Directory.Exists(junctionPoint), "Ensure directory was deleted too."); // Cleanup File.Delete(Path.Combine(targetFolder, "AFile")); } [TestMethod] [ExpectedException( typeof(IOException), "Directory already exists and overwrite parameter is false." )] public void Create_ThrowsIfOverwriteNotSpecifiedAndDirectoryExists() { var targetFolder = Path.Combine(tempFolder, "ADirectory"); var junctionPoint = Path.Combine(tempFolder, "SymLink"); Directory.CreateDirectory(junctionPoint); Junction.Create(junctionPoint, targetFolder, false); } [TestMethod] public void Create_OverwritesIfSpecifiedAndDirectoryExists() { var targetFolder = Path.Combine(tempFolder, "ADirectory"); var junctionPoint = Path.Combine(tempFolder, "SymLink"); Directory.CreateDirectory(junctionPoint); Directory.CreateDirectory(targetFolder); Junction.Create(junctionPoint, targetFolder, true); Assert.AreEqual(targetFolder, Junction.GetTarget(junctionPoint)); } [TestMethod] [ExpectedException(typeof(IOException), "Target path does not exist or is not a directory.")] public void Create_ThrowsIfTargetDirectoryDoesNotExist() { var targetFolder = Path.Combine(tempFolder, "ADirectory"); var junctionPoint = Path.Combine(tempFolder, "SymLink"); Junction.Create(junctionPoint, targetFolder, false); } [TestMethod] [ExpectedException(typeof(IOException), "Unable to open reparse point.")] public void GetTarget_NonExistentJunctionPoint() { Junction.GetTarget(Path.Combine(tempFolder, "SymLink")); } [TestMethod] [ExpectedException(typeof(IOException), "Path is not a junction point.")] public void GetTarget_CalledOnADirectoryThatIsNotAJunctionPoint() { Junction.GetTarget(tempFolder); } [TestMethod] [ExpectedException(typeof(IOException), "Path is not a junction point.")] public void GetTarget_CalledOnAFile() { File.Create(Path.Combine(tempFolder, "AFile")).Close(); Junction.GetTarget(Path.Combine(tempFolder, "AFile")); } [TestMethod] public void Delete_NonExistentJunctionPoint() { // Should do nothing. Junction.Delete(Path.Combine(tempFolder, "SymLink")); } [TestMethod] [ExpectedException(typeof(IOException), "Unable to delete junction point.")] public void Delete_CalledOnADirectoryThatIsNotAJunctionPoint() { Junction.Delete(tempFolder); } [TestMethod] [ExpectedException(typeof(IOException), "Path is not a junction point.")] public void Delete_CalledOnAFile() { File.Create(Path.Combine(tempFolder, "AFile")).Close(); Junction.Delete(Path.Combine(tempFolder, "AFile")); } } ================================================ FILE: StabilityMatrix.Tests/StabilityMatrix.Tests.csproj ================================================ true false true all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: StabilityMatrix.Tests/TempFiles.cs ================================================ namespace StabilityMatrix.Tests; public static class TempFiles { // Deletes directory while handling junction folders public static void DeleteDirectory(string directory) { // Enumerate to delete any directory links foreach (var item in Directory.EnumerateDirectories(directory)) { var info = new DirectoryInfo(item); if (info.Exists && info.Attributes.HasFlag(FileAttributes.ReparsePoint)) { info.Delete(); } else { DeleteDirectory(item); } } } } ================================================ FILE: StabilityMatrix.Tests/Usings.cs ================================================ global using Microsoft.VisualStudio.TestTools.UnitTesting; ================================================ FILE: StabilityMatrix.UITests/Attributes/TestPriorityAttribute.cs ================================================ namespace StabilityMatrix.UITests.Attributes; [AttributeUsage(AttributeTargets.Method)] public class TestPriorityAttribute : Attribute { public int Priority { get; private set; } public TestPriorityAttribute(int priority) { Priority = priority; } } ================================================ FILE: StabilityMatrix.UITests/Extensions/VisualExtensions.cs ================================================ using Avalonia.Controls; namespace StabilityMatrix.UITests.Extensions; public static class VisualExtensions { public static Rect GetRelativeBounds(this Visual visual, TopLevel topLevel) { var origin = visual.TranslatePoint(new Point(0, 0), topLevel) ?? throw new NullReferenceException("Origin is null"); var bounds = new Rect(origin, visual.Bounds.Size); return bounds; } } ================================================ FILE: StabilityMatrix.UITests/Extensions/WindowExtensions.cs ================================================ using Avalonia.Controls; using Avalonia.Threading; using Avalonia.VisualTree; namespace StabilityMatrix.UITests.Extensions; /// /// Window extensions for UI tests /// public static class WindowExtensions { public static void ClickTarget(this TopLevel topLevel, Control target) { // Check target is part of the visual tree var targetVisualRoot = target.GetVisualRoot(); if (targetVisualRoot is not TopLevel) { throw new ArgumentException("Target is not part of the visual tree"); } if (targetVisualRoot.Equals(topLevel)) { throw new ArgumentException("Target is not part of the same visual tree as the top level"); } var point = target.TranslatePoint(new Point(target.Bounds.Width / 2, target.Bounds.Height / 2), topLevel) ?? throw new NullReferenceException("Point is null"); topLevel.MouseMove(point); topLevel.MouseDown(point, MouseButton.Left); topLevel.MouseUp(point, MouseButton.Left); // Return mouse to outside of window topLevel.MouseMove(new Point(-50, -50)); } public static async Task ClickTargetAsync(this TopLevel topLevel, Control target) { // Check target is part of the visual tree var targetVisualRoot = target.GetVisualRoot(); if (targetVisualRoot is not TopLevel) { throw new ArgumentException("Target is not part of the visual tree"); } if (!targetVisualRoot.Equals(topLevel)) { throw new ArgumentException("Target is not part of the same visual tree as the top level"); } var point = target.TranslatePoint(new Point(target.Bounds.Width / 2, target.Bounds.Height / 2), topLevel) ?? throw new NullReferenceException("Point is null"); topLevel.MouseMove(point); topLevel.MouseDown(point, MouseButton.Left); topLevel.MouseUp(point, MouseButton.Left); await Task.Delay(40); // Return mouse to outside of window topLevel.MouseMove(new Point(-50, -50)); await Task.Delay(300); } } ================================================ FILE: StabilityMatrix.UITests/MainWindowTests.cs ================================================ using Avalonia.Controls; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.UITests.Extensions; namespace StabilityMatrix.UITests; [Collection("TempDir")] [TestCaseOrderer("StabilityMatrix.UITests.PriorityOrderer", "StabilityMatrix.UITests")] public class MainWindowTests : TestBase { [AvaloniaFact, TestPriority(1)] public async Task MainWindow_ShouldOpen() { var (window, _) = GetMainWindow(); window.Show(); await Task.Delay(300); await DoInitialSetup(); await Task.Delay(1000); await Verify(window, Settings); } [AvaloniaFact, TestPriority(2)] public async Task MainWindowViewModel_ShouldOk() { var viewModel = Services.GetRequiredService(); await Verify(viewModel, Settings); } [AvaloniaFact, TestPriority(3)] public async Task NavigateToModelBrowser_ShouldWork() { var (window, viewModel) = GetMainWindow(); await DoInitialSetup(); var y = window .FindDescendantOfType()! .GetVisualDescendants() .OfType() .FirstOrDefault(i => i.Content?.ToString() == "Model Browser")!; await window.ClickTargetAsync(y); var frame = window.FindControl("FrameView"); Assert.IsType(frame!.Content); await Task.Delay(1000); SaveScreenshot(window); await Verify(window, Settings); } } ================================================ FILE: StabilityMatrix.UITests/ModelBrowser/CivitAiBrowserTests.cs ================================================ namespace StabilityMatrix.UITests.ModelBrowser; [Collection("TempDir")] public class CivitAiBrowserTests : TestBase; ================================================ FILE: StabilityMatrix.UITests/ModuleInit.cs ================================================ using System.Runtime.CompilerServices; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace StabilityMatrix.UITests; public static class ModuleInit { [ModuleInitializer] public static void Init() => VerifyAvalonia.Initialize(); [ModuleInitializer] public static void InitOther() => VerifierSettings.InitializePlugins(); [ModuleInitializer] public static void ConfigureVerify() { VerifyPhash.RegisterComparer("png"); DerivePathInfo( (sourceFile, projectDirectory, type, method) => new PathInfo( directory: Path.Combine(projectDirectory, "Snapshots"), typeName: type.Name, methodName: method.Name ) ); } } ================================================ FILE: StabilityMatrix.UITests/PriorityOrderer.cs ================================================ using StabilityMatrix.UITests.Attributes; using Xunit.Abstractions; using Xunit.Sdk; namespace StabilityMatrix.UITests; public class PriorityOrderer : ITestCaseOrderer { public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase { var sortedMethods = new SortedDictionary>(); foreach (var testCase in testCases) { var priority = 0; foreach ( var attr in testCase.TestMethod.Method.GetCustomAttributes( typeof(TestPriorityAttribute).AssemblyQualifiedName ) ) { priority = attr.GetNamedArgument("Priority"); } GetOrCreate(sortedMethods, priority).Add(testCase); } foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority])) { list.Sort( (x, y) => StringComparer.OrdinalIgnoreCase.Compare( x.TestMethod.Method.Name, y.TestMethod.Method.Name ) ); foreach (var testCase in list) { yield return testCase; } } } private static TValue GetOrCreate(IDictionary dictionary, TKey key) where TValue : new() { if (dictionary.TryGetValue(key, out var result)) return result; result = new TValue(); dictionary[key] = result; return result; } } ================================================ FILE: StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindowViewModel_ShouldOk.verified.txt ================================================ { Greeting: Welcome to Avalonia!, ProgressManagerViewModel: { Title: Download Manager, IconSource: { Type: SymbolIconSource }, IsOpen: false, CanNavigateNext: false, CanNavigatePrevious: false, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false }, UpdateViewModel: { Title: , IsUpdateAvailable: true, UpdateInfo: { Version: { Major: 2, Minor: 999, Prerelease: , IsPrerelease: false, IsRelease: true, Metadata: }, ReleaseDate: DateTimeOffset_1, Channel: Stable, Type: Normal, Url: https://example.org, Changelog: https://example.org, HashBlake3: 46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0, Signature: IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA== }, IsProgressIndeterminate: false, ShowProgressBar: false, NewVersionText: v2.999.0, InstallUpdateCommand: UpdateViewModel.InstallUpdate(), RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false }, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false } ================================================ FILE: StabilityMatrix.UITests/Snapshots/MainWindowTests.MainWindow_ShouldOpen.verified.txt ================================================ { Type: MainWindow, Title: Stability Matrix, Icon: {}, TransparencyLevelHint: [ {}, {}, {} ], TransparencyBackgroundFallback: Transparent, Content: { Type: Grid, Children: [ { Type: Grid, Background: Transparent, Height: 32.0, Name: TitleBarHost, Children: [ { Type: Image, Source: { Dpi: { X: 96.0, Y: 96.0, Length: 135.7645019878171, SquaredLength: 18432.0 }, Size: { AspectRatio: 1.0, Width: 256.0, Height: 256.0 }, PixelSize: { AspectRatio: 1.0, Width: 256, Height: 256 }, Format: { BitsPerPixel: 32 } }, IsHitTestVisible: false, Width: 18.0, Height: 18.0, Margin: 12,4,12,4, IsVisible: true, Name: WindowIcon }, { Type: TextBlock, FontSize: 12.0, Text: Stability Matrix, IsHitTestVisible: false, VerticalAlignment: Center, IsVisible: true }, { Type: Border, Padding: 6 } ] }, { Type: NavigationView, Content: { Type: Frame, Content: { Type: LaunchPageView, Content: { Type: Grid, Children: [ { Type: Grid, Margin: 0,8,0,8, Children: [ { Type: Grid, Margin: 16,8,0,0, Children: [ { Type: Grid, Column: 0, Row: 0, Name: LaunchButtonGrid, Children: [ { Type: Button, Command: LaunchPageViewModel.LaunchAsync(string command), Content: Launch, Width: 95.0, HorizontalAlignment: Left, VerticalAlignment: Stretch, IsVisible: false }, { Type: SplitButton, Command: LaunchPageViewModel.LaunchAsync(string command), Flyout: { Type: FAMenuFlyout }, Content: Launch, Width: 104.0, HorizontalAlignment: Left, VerticalAlignment: Stretch, IsVisible: false } ] }, { Type: TeachingTip, Name: TeachingTip1 }, { Type: Grid, Column: 0, Row: 0, IsVisible: false, Name: StopButtonGrid, Children: [ { Type: Button, Command: {}, Content: Stop, Width: 95.0, HorizontalAlignment: Left, VerticalAlignment: Stretch, IsVisible: false }, { Type: Button, Command: {}, Content: Stop, Width: 104.0, HorizontalAlignment: Left, VerticalAlignment: Stretch, IsVisible: false } ] }, { Type: Button, Command: LaunchPageViewModel.Config(), Content: { Type: SymbolIcon }, FontSize: 16.0, Width: 48.0, Margin: 8,0,0,0, HorizontalAlignment: Left, VerticalAlignment: Stretch } ] }, { Type: ComboBox, SelectedIndex: -1, Selection: { SingleSelect: true, SelectedIndex: -1, AnchorIndex: -1 }, SelectionMode: Single, ItemTemplate: { DataType: InstalledPackage, Content: { Type: Func, Target: XamlIlRuntimeHelpers.<>c__DisplayClass1_0, Method: System.Object DeferredTransformationFactoryV2(System.IServiceProvider) } }, IsEnabled: true, Margin: 8,8,0,0, HorizontalAlignment: Stretch, VerticalAlignment: Top, Name: SelectPackageComboBox }, { Type: ToggleButton, IsChecked: true, Content: { Type: Icon, Template: { Content: { Type: Func, Target: XamlIlRuntimeHelpers.<>c__DisplayClass1_0, Method: System.Object DeferredTransformationFactoryV2(System.IServiceProvider) } }, RenderTransform: { Type: TransformGroup, Children: [ { Type: RotateTransform } ] } }, FontSize: 16.0, Width: 48.0, Margin: 8,8,0,0, HorizontalAlignment: Left, VerticalAlignment: Stretch }, { Type: ToggleButton, IsChecked: false, Content: { Type: SymbolIcon }, FontSize: 16.0, Width: 48.0, Margin: 8,8,16,0, HorizontalAlignment: Left, VerticalAlignment: Stretch } ] }, { Type: TextEditor, FontFamily: Cascadia Code, Margin: 8,8,16,10, DataContext: { IsUpdatesRunning: false, WriteCursorLockTimeout: 00:00:00.1000000, Document: { _undoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, Text: , Version: {}, IsInUpdate: false, Lines: [ { IsDeleted: false, LineNumber: 1 } ], LineTrackers: [ {} ], UndoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, LineCount: 1 } }, Name: Console }, { Type: Grid, Row: 1, Children: [ { Type: StackPanel, Spacing: 4.0, Margin: 8, Children: [ { Type: InfoBar, Margin: 0 }, { Type: InfoBar, Margin: 0 } ] } ] }, { Type: Button, Command: {}, Content: Open Web UI, FontSize: 12.0, Margin: 24,0,24,8, HorizontalAlignment: Stretch, IsVisible: false } ] }, DataContext: { Title: Launch, IconSource: { Type: SymbolIconSource }, Console: { IsUpdatesRunning: false, WriteCursorLockTimeout: 00:00:00.1000000, Document: { _undoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, Text: , Version: {}, IsInUpdate: false, Lines: [ { IsDeleted: false, LineNumber: 1 } ], LineTrackers: [ {} ], UndoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, LineCount: 1 } }, LaunchButtonVisibility: false, StopButtonVisibility: false, IsLaunchTeachingTipsOpen: false, ShowWebUiButton: false, AutoScrollToEnd: true, ShowManualInputPrompt: false, ShowConfirmInputPrompt: false, LaunchCommand: LaunchPageViewModel.LaunchAsync(string command), ConfigCommand: LaunchPageViewModel.Config(), SendConfirmInputCommand: LaunchPageViewModel.SendConfirmInput(bool value), SendManualInputCommand: LaunchPageViewModel.SendManualInput(string input), CanNavigateNext: false, CanNavigatePrevious: false, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false } }, Name: FrameView }, Name: NavigationView }, { Type: TeachingTip, Name: UpdateAvailableTeachingTip } ] }, Background: #ff101010, FontFamily: Segoe UI Variable Text, Width: 1400.0, Height: 900.0, IsVisible: true, DataContext: { Greeting: Welcome to Avalonia!, ProgressManagerViewModel: { Title: Download Manager, IconSource: { Type: SymbolIconSource }, IsOpen: false, CanNavigateNext: false, CanNavigatePrevious: false, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false }, UpdateViewModel: { Title: , IsUpdateAvailable: true, UpdateInfo: { Version: { Major: 2, Minor: 999, Prerelease: , IsPrerelease: false, IsRelease: true, Metadata: }, ReleaseDate: DateTimeOffset_1, Channel: Stable, Type: Normal, Url: https://example.org, Changelog: https://example.org, HashBlake3: 46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0, Signature: IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA== }, IsProgressIndeterminate: false, ShowProgressBar: false, NewVersionText: v2.999.0, InstallUpdateCommand: UpdateViewModel.InstallUpdate(), RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false }, SelectedCategory: { Title: Launch, IconSource: { Type: SymbolIconSource }, Console: { IsUpdatesRunning: false, WriteCursorLockTimeout: 00:00:00.1000000, Document: { _undoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, Text: , Version: {}, IsInUpdate: false, Lines: [ { IsDeleted: false, LineNumber: 1 } ], LineTrackers: [ {} ], UndoStack: { IsOriginalFile: true, AcceptChanges: true, CanUndo: false, CanRedo: false, SizeLimit: 2147483647 }, LineCount: 1 } }, LaunchButtonVisibility: false, StopButtonVisibility: false, IsLaunchTeachingTipsOpen: false, ShowWebUiButton: false, AutoScrollToEnd: true, ShowManualInputPrompt: false, ShowConfirmInputPrompt: false, LaunchCommand: LaunchPageViewModel.LaunchAsync(string command), ConfigCommand: LaunchPageViewModel.Config(), SendConfirmInputCommand: LaunchPageViewModel.SendConfirmInput(bool value), SendManualInputCommand: LaunchPageViewModel.SendManualInput(string input), CanNavigateNext: false, CanNavigatePrevious: false, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false }, RemoveFromParentListCommand: ViewModelBase.RemoveFromParentList(), HasErrors: false } } ================================================ FILE: StabilityMatrix.UITests/StabilityMatrix.UITests.csproj ================================================ false true runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: StabilityMatrix.UITests/TempDirFixture.cs ================================================ using System.Runtime.CompilerServices; namespace StabilityMatrix.UITests; public class TempDirFixture : IDisposable { public static string ModuleTempDir { get; set; } static TempDirFixture() { var tempDir = Path.Combine(Path.GetTempPath(), "StabilityMatrixTest"); if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } Directory.CreateDirectory(tempDir); ModuleTempDir = tempDir; // ReSharper disable once LocalizableElement Console.WriteLine($"Using temp dir: {ModuleTempDir}"); } /// public void Dispose() { if (Directory.Exists(ModuleTempDir)) { // ReSharper disable once LocalizableElement Console.WriteLine($"Deleting temp dir: {ModuleTempDir}"); Directory.Delete(ModuleTempDir, true); } GC.SuppressFinalize(this); } } [CollectionDefinition("TempDir")] public class TempDirCollection : ICollectionFixture { } ================================================ FILE: StabilityMatrix.UITests/TestAppBuilder.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.Extensions; using Semver; using StabilityMatrix.Avalonia; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using StabilityMatrix.UITests; [assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] namespace StabilityMatrix.UITests; public static class TestAppBuilder { public static AppBuilder BuildAvaloniaApp() { ConfigureGlobals(); Program.SetupAvaloniaApp(); App.BeforeBuildServiceProvider += (_, x) => ConfigureAppServices(x); return AppBuilder .Configure() .UseSkia() .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }); } private static void ConfigureGlobals() { var tempDir = TempDirFixture.ModuleTempDir; var globalSettings = Path.Combine(tempDir, "AppDataHome"); Compat.SetAppDataHome(globalSettings); } private static void ConfigureAppServices(IServiceCollection serviceCollection) { // ISettingsManager var settingsManager = Substitute.ForPartsOf(); serviceCollection.AddSingleton(settingsManager); // IUpdateHelper var mockUpdateInfo = new UpdateInfo() { Version = SemVersion.Parse("2.999.0"), ReleaseDate = DateTimeOffset.UnixEpoch, Channel = UpdateChannel.Stable, Type = UpdateType.Normal, Url = new Uri("https://example.org"), Changelog = new Uri("https://example.org"), HashBlake3 = "46e11a5216c55d4c9d3c54385f62f3e1022537ae191615237f05e06d6f8690d0", Signature = "IX5/CCXWJQG0oGkYWVnuF34gTqF/dJSrDrUd6fuNMYnncL39G3HSvkXrjvJvR18MA2rQNB5z13h3/qBSf9c7DA==" }; var updateHelper = Substitute.For(); updateHelper .Configure() .StartCheckingForUpdates() .Returns(Task.CompletedTask) .AndDoes(_ => EventManager.Instance.OnUpdateAvailable(mockUpdateInfo)); serviceCollection.AddSingleton(updateHelper); // UpdateViewModel var updateViewModel = Substitute.ForPartsOf( Substitute.For>(), settingsManager, null, updateHelper ); updateViewModel.Configure().GetReleaseNotes("").Returns("Test"); serviceCollection.AddSingleton(updateViewModel); } } ================================================ FILE: StabilityMatrix.UITests/TestBase.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Media.Imaging; using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Windowing; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.UITests.Extensions; namespace StabilityMatrix.UITests; public class TestBase { internal static IServiceProvider Services => App.Services; internal static (AppWindow, MainWindowViewModel)? currentMainWindow; internal virtual VerifySettings Settings { get { var settings = new VerifySettings(); settings.IgnoreMembers( vm => vm.Pages, vm => vm.FooterPages, vm => vm.CurrentPage ); settings.IgnoreMember(vm => vm.CurrentVersionText); settings.DisableDiff(); return settings; } } internal static (AppWindow, MainWindowViewModel) GetMainWindow() { if (currentMainWindow is not null) { return currentMainWindow.Value; } var window = Services.GetRequiredService(); var viewModel = Services.GetRequiredService(); window.DataContext = viewModel; window.Width = 1400; window.Height = 900; App.VisualRoot = window; currentMainWindow = (window, viewModel); return currentMainWindow.Value; } internal static async Task DoInitialSetup() { var (window, _) = GetMainWindow(); if (!window.IsVisible) { window.Show(); await Task.Delay(300); Dispatcher.UIThread.RunJobs(); } try { var dialog = await WaitHelper.WaitForNotNullAsync(() => GetWindowDialog(window)); if (dialog.Content is SelectDataDirectoryDialog selectDataDirectoryDialog) { // Click continue button var continueButton = selectDataDirectoryDialog .GetVisualDescendants() .OfType