diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1b2d547d2..2234f0836 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -23,6 +23,13 @@ ], "rollForward": false }, + "refitter": { + "version": "1.4.1", + "commands": [ + "refitter" + ], + "rollForward": false + }, "dotnet-script": { "version": "1.6.0", "commands": [ diff --git a/.husky/task-runner.json b/.husky/task-runner.json index cedfb34b1..c8c0d739e 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -13,6 +13,12 @@ "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"] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a54cb91c..2c7d93464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,32 @@ 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.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 -### Added -- Added Safetensor Metadata viewer to the Checkpoint Manager context menu - thanks to @genteure! ### Changed - "Remove symbolic links on shutdown" option now also removes links from Output Sharing ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a63e62f7..4c3ac44e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,48 @@ -# Stability Matrix Code Style Guidelines +# 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 @@ -47,4 +91,4 @@ if (alreadyAteLunch) - 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 \ No newline at end of file +- Refit interfaces should go in the `Api\` folder diff --git a/Directory.Build.props b/Directory.Build.props index cac749720..bde27a37c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ - 11.2.2 + 11.2.5 diff --git a/Directory.Packages.props b/Directory.Packages.props index f0c7fda08..f2f2e7d3a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -53,6 +53,8 @@ + + @@ -125,4 +127,4 @@ - + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index e4d835a51..586e46fd6 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; @@ -37,6 +38,8 @@ using NLog.Extensions.Logging; using NLog.Targets; using Octokit; +using OpenIddict.Abstractions; +using OpenIddict.Client; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Extensions.Http; @@ -52,6 +55,7 @@ using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; @@ -67,6 +71,7 @@ using Application = Avalonia.Application; using Logger = NLog.Logger; using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using ProductHeaderValue = Octokit.ProductHeaderValue; #if DEBUG using StabilityMatrix.Avalonia.Diagnostics.LogViewer; using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; @@ -123,6 +128,14 @@ public sealed class App : Application #else public const string LykosAnalyticsApiBaseUrl = "https://analytics.lykos.ai"; #endif +#if DEBUG + // ReSharper disable twice LocalizableElement + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + public static string LykosAccountApiBaseUrl => + Config?["LykosAccountApiBaseUrl"] ?? "https://account.lykos.ai/"; +#else + public const string LykosAccountApiBaseUrl = "https://account.lykos.ai/"; +#endif // ReSharper disable once MemberCanBePrivate.Global public IClassicDesktopStyleApplicationLifetime? DesktopLifetime => @@ -365,7 +378,10 @@ internal static void ConfigurePageViewModels(IServiceCollection services) provider.GetRequiredService>(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService() + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService>(), + provider.GetRequiredService>() ) { Pages = @@ -594,7 +610,7 @@ internal static IServiceCollection ConfigureServices() .AddPolicyHandler(retryPolicyLonger); services - .AddRefitClient(defaultRefitSettings) + .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri(LykosAuthApiBaseUrl); @@ -607,6 +623,21 @@ internal static IServiceCollection ConfigureServices() new TokenAuthHeaderHandler(serviceProvider.GetRequiredService()) ); + services + .AddRefitClient(defaultRefitSettings) + .ConfigureHttpClient(c => + { + c.BaseAddress = new Uri(LykosAuthApiBaseUrl); + c.Timeout = TimeSpan.FromHours(1); + c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ""); + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }) + .AddPolicyHandler(retryPolicy) + .AddHttpMessageHandler( + serviceProvider => + new TokenAuthHeaderHandler(serviceProvider.GetRequiredService()) + ); + services .AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => @@ -643,6 +674,37 @@ internal static IServiceCollection ConfigureServices() } ); + // Add OpenId + services + .AddOpenIddict() + .AddClient(options => + { + options.AllowDeviceCodeFlow().AllowRefreshTokenFlow(); + + options.DisableTokenStorage(); + options.AddEphemeralEncryptionKey().AddEphemeralSigningKey(); + + options.UseSystemNetHttp().SetProductInformation("StabilityMatrix", "2.0"); + + options.AddRegistration( + new OpenIddictClientRegistration + { + ProviderName = OpenIdClientConstants.LykosAccount.ProviderName, + Issuer = new Uri(LykosAccountApiBaseUrl), + ClientId = "ai.lykos.stabilitymatrix", + Scopes = + { + OpenIddictConstants.Scopes.Profile, + OpenIddictConstants.Scopes.Email, + OpenIddictConstants.Scopes.OpenId, + "api", + OpenIddictConstants.Scopes.OfflineAccess + }, + RedirectUri = Program.MessagePipeUri.Append("/callback/login/lykos") + } + ); + }); + ConditionalAddLogViewer(services); var logConfig = ConfigureLogging(); diff --git a/StabilityMatrix.Avalonia/Assets/brands-lykos-oauth-icon.svg b/StabilityMatrix.Avalonia/Assets/brands-lykos-oauth-icon.svg new file mode 100644 index 000000000..e97a94363 --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/brands-lykos-oauth-icon.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index f609ecf81..ee78ff454 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -788,6 +788,13 @@ public static UpdateSettingsViewModel UpdateSettingsViewModel + "redirect_uri=http://localhost:5022/api/oauth/patreon/callback"; }); + public static OAuthDeviceAuthViewModel OAuthDeviceAuthViewModel => + DialogFactory.Get(vm => + { + vm.VerificationUri = new Uri("https://example.org/connect/verify"); + vm.UserCode = "AB23-CD56"; + }); + public static PythonPackageSpecifiersViewModel PythonPackageSpecifiersViewModel => DialogFactory.Get(); diff --git a/StabilityMatrix.Avalonia/Helpers/ClipboardCommands.cs b/StabilityMatrix.Avalonia/Helpers/ClipboardCommands.cs new file mode 100644 index 000000000..5d20175d6 --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/ClipboardCommands.cs @@ -0,0 +1,12 @@ +using System; +using CommunityToolkit.Mvvm.Input; + +namespace StabilityMatrix.Avalonia.Helpers; + +public static class ClipboardCommands +{ + private static readonly Lazy> CopyTextCommandLazy = + new(() => new RelayCommand(text => App.Clipboard.SetTextAsync(text))); + + public static RelayCommand CopyTextCommand => CopyTextCommandLazy.Value; +} diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 06f8c8036..38fc36b20 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -167,6 +167,15 @@ public static string Action_Copy { } } + /// + /// Looks up a localized string similar to Copy and Open. + /// + public static string Action_CopyAndOpen { + get { + return ResourceManager.GetString("Action_CopyAndOpen", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy as Bitmap. /// @@ -248,6 +257,15 @@ public static string Action_ExitApplication { } } + /// + /// Looks up a localized string similar to Go to Settings. + /// + public static string Action_GoToSettings { + get { + return ResourceManager.GetString("Action_GoToSettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide. /// @@ -3431,6 +3449,32 @@ public static string Text_DeleteFollowingItems { } } + /// + /// Looks up a localized string similar to ## Heads Up: Lykos Account Sign-Out & Security Upgrade + /// + ///We've made some important behind-the-scenes improvements to how Stability Matrix handles Lykos accounts, upgrading to a more secure and convenient login system **(OAuth 2.0 with OpenID Connect)**. Because of this, you've been signed out of your Lykos account. + /// + ///### Why the change? + /// + ///Your security and privacy are important to us. This upgrade brings: + /// + ///* **Streamlined Experience:** Sign in once to [account.lykos.ai](https://account.lykos.ai) to c [rest of string was truncated]";. + /// + public static string Text_LykosAccountUpgradeNotice { + get { + return ResourceManager.GetString("Text_LykosAccountUpgradeNotice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open the link in your browser and enter the following code to authorize your account with Stability Matrix.. + /// + public static string Text_OAuthDeviceAuthDescription { + get { + return ResourceManager.GetString("Text_OAuthDeviceAuthDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open the link in your browser and follow the instructions to connect your account.. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.de.resx b/StabilityMatrix.Avalonia/Languages/Resources.de.resx index b65fa5527..a41fdfd06 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.de.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.de.resx @@ -937,4 +937,7 @@ CLIP Stärke + + Zu den Einstellungen + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.es.resx b/StabilityMatrix.Avalonia/Languages/Resources.es.resx index 0bc23e712..98e3f445b 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.es.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.es.resx @@ -1079,4 +1079,7 @@ Se han importado el workflow y los nodos personalizados. + + Ir a la configuración + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx index c0cde7f22..cc8d4fa97 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx @@ -1029,4 +1029,7 @@ Le workflow et les custom nodes ont été importés. + + Aller aux paramètres + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx index dd3bcc3b8..a34ac2eba 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx @@ -683,4 +683,7 @@ CivitAI + + Vai alle impostazioni + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index e3a073520..a3e4b00fb 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -1148,4 +1148,95 @@ Stability Matrixを実行する前に、ZIPファイルからアプリを抽出してください + + コンソールに表示されている行より上にスクロールして戻ることができる行数 + + + アカウント接続に問題が発生しました + + + モデルメタデータの編集 + + + バージョン名 + + + リンクをクリップボードにコピー + + + {0} でサインイン + e.g. 'Sign in with Google' + + + ブラウザのリンクを開き、指示に従ってアカウントを接続してください + + + Python Dependencies Override + + + インストールと更新時に依存関係を追加・置換・削除 + + + Dependency Specifiers + + + { + "packageName": "stable-diffusion-webui", + "packageVersion": "v1.10.0", + "isSuccess": true, + "type": "install", + "timestamp": "2024-09-04T02:14:04.1967404+00:00" +} + + + この動作はいつでも {0} の手順で変更できます。 + e.g. 'You can always change this behavior in Settings > Category > Item.' + + + Stability Matrixの改善にご協力ください。OS, パッケージの種類など改善に必要なデータのみ匿名で送信し、あなた及びあなたのアカウントとの関連付けはいたしません、また個人データ・機密情報は決して送信しません。 + + + 使用された機能、OSのバージョン、インストールされているパッケージの種類などについての匿名データを送信して、Stability Matrixの改善にご協力ください。 + + + 送信されたデータは、あなたまたはあなたのアカウントとは決して関連付けられず、個人データや機密情報を含みません。 + + + 使用状況のデータ + + + プライバシーポリシー + + + 画像が見つかりません + + + 非表示のカテゴリを非表示 + + + NSFW画像を表示 + + + 長いファイル名を有効にする + (Setting to enable long file paths on windows) + + + MAX_PATH limitations を削除する(Windows で長いファイルパスを有効にする設定) + (Setting to enable long file paths on windows) + + + システム設定 + + + 変更を適用する + + + 変更を有効にするには、再起動が必要になる場合があります。 + + + このパッケージではこのリリースを利用できません。 + + + 設定に移動 + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ko-KR.resx b/StabilityMatrix.Avalonia/Languages/Resources.ko-KR.resx index bdd545ed0..f507e12dc 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ko-KR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ko-KR.resx @@ -1188,4 +1188,7 @@ 클립보드에 링크 복사 + + 설정으로 이동 + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx b/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx index 7e62580f2..a3eb9fcae 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.pt-BR.resx @@ -1150,4 +1150,149 @@ Desativar atualização automática + + EXTRAIA O PROGRAMA DO ARQUIVO ZIP ANTES DE EXECUTAR O STABILITY MATRIX + + + Tamanho do Histórico + + + O número de linhas acima daquelas exibidas no console em que você pode rolar de volta + + + Nós tivemos algum problema para conectar sua conta + + + Editar Metadados do Modelo + + + NSFW + + + Tags + + + Nome de Versão + + + Palavras Treinadas + + + Pré-visualizar imagem + + + Tamanho do Lote + + + Lotes + + + Amostrador + + + Agendador + + + Tamanho máximo + + + Usar prompt separado + + + Semente + + + Prompt Negativo + + + Nova Pasta + + + Copiar Link para a Área de Transferência + + + Entrar com {0} + e.g. 'Sign in with Google' + + + Permita que seu navegador abra este aplicativo quando solicitado. + + + Abra o link no seu navegador e siga as instruções para conectar sua conta. + + + Substituir dependências do Python + + + Adicione, substitua ou remova dependências para instalação e atualização + + + Especificadores de dependência + + + { + "packageName": "stable-diffusion-webui", + "packageVersion": "v1.10.0", + "isSuccess": true, + "type": "install", + "timestamp": "2024-09-04T02:14:04.1967404+00:00" +} + + + Dados Analíticos + + + Você sempre pode alterar esse comportamento em {0}. + e.g. 'You can always change this behavior in Settings > Category > Item.' + + + Ajude-nos a melhorar o Stability Matrix enviando dados anônimos sobre recursos usados, versões de sistema operacional, tipos de pacotes instalados, etc. Os dados enviados nunca serão associados a você ou à sua conta e não incluirão dados pessoais ou qualquer informação sensível. + + + Ajude-nos a melhorar o Stability Matrix enviando dados anônimos sobre recursos usados, versões de sistema operacional, tipos de pacotes instalados, etc. + + + Os dados enviados nunca serão associados a você ou à sua conta e não incluirão dados pessoais ou qualquer informação sensível. + + + Dados de Uso + + + Política de Privacidade + + + Imagem ocultada + + + Nenhuma imagem encontrada + + + Ocultar categorias vazias + + + Mostrar imagens NSFW + + + Habilitar caminhos longos + (Setting to enable long file paths on windows) + + + Remover limitações MAX_PATH de funções comuns de arquivo e diretório do Win32 + (Setting to enable long file paths on windows) + + + Configurações do Sistema + + + Alterações Aplicadas + + + Uma reinicialização pode ser necessária para que as alterações do sistema tenham efeito. + + + Este pacote não possui versões disponíveis. + + + Ir para Configurações + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx b/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx index ee79e62c6..86980fda4 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx @@ -937,4 +937,7 @@ CLIP Força a aplicar + + Ir para as Definições + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 9a17d0c9a..53e4f3696 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -1302,4 +1302,43 @@ - or - + + Open the link in your browser and enter the following code to authorize your account with Stability Matrix. + + + Copy and Open + + + Open on OpenModelDB + + + Wildcards + + + ## Heads Up: Lykos Account Sign-Out & Security Upgrade + +We've made some important behind-the-scenes improvements to how Stability Matrix handles Lykos accounts, upgrading to a more secure and convenient login system **(OAuth 2.0 with OpenID Connect)**. Because of this, you've been signed out of your Lykos account. + +### Why the change? + +Your security and privacy are important to us. This upgrade brings: + +* **Streamlined Experience:** Sign in once to [account.lykos.ai](https://account.lykos.ai) to connect with Stability Matrix and other Lykos AI services. +* **More Ways to Sign In:** Use your existing [lykos.ai](https://lykos.ai) account, or sign in with **Apple**, **GitHub**, or **Google**. +* **Enhanced Privacy:** Stability Matrix only requests the permissions it needs. +* **Industry Standard Security:** We're using OAuth 2.0, the gold standard for secure logins. +* **Ready for the Future:** This allows for secure connections with other apps and services. + +### What do I need to do? + +Click the **"Go to Settings"** button, then click **"Connect"** next to **"Lykos account"**. + +### Is a Lykos account required? + +Nope! Stability Matrix is still fully functional without one. But, your Lykos account enables some extra connected features, such as automatic updates to development builds for our Patreon supporters (and more to come!). + + + + Go to Settings + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx index 36e8b6488..c9d621c44 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx @@ -1266,4 +1266,7 @@ Релизы для этого пакета недоступны. + + Перейти к настройкам + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx b/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx index ca8a29abd..6435fb2a3 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx @@ -1192,4 +1192,7 @@ Bağlantıyı Panoya Kopyala + + Ayarlara Git + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx index 73c37d0eb..3e05d8687 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx @@ -1262,4 +1262,30 @@ 系统更改可能需要重启才能生效。 + + 前往设置 + + + ## 重要提示:Lykos 帐户登出和安全升级 + +我们对 Stability Matrix 处理 Lykos 帐户的方式进行了一些重要的幕后改进,升级到更安全、更便捷的登录系统 **(OAuth 2.0 with OpenID Connect)**。 因此,您已登出 Lykos 帐户。 + +### 为什么进行此更改? + +您的安全和隐私对我们很重要。 此升级带来: + +* **简化的体验:** 登录 [account.lykos.ai](https://account.lykos.ai) 一次即可连接到 Stability Matrix 和其他 Lykos AI 服务。 +* **更多登录方式:** 使用您现有的 [lykos.ai](https://lykos.ai) 帐户,或使用 **Apple**、**GitHub** 或 **Google** 登录。 +* **增强的隐私:** Stability Matrix 仅请求其所需的权限。 +* **行业标准安全性:** 我们正在使用 OAuth 2.0,这是安全登录的黄金标准。 +* **为未来做好准备:** 这允许与其他应用程序和服务进行安全连接。 + +### 我需要做什么? + +单击**“前往设置”**按钮,然后单击**“Lykos 帐户”**旁边的**“连接”**。 + +### Lykos 帐户是必需的吗? + +不,Stability Matrix 在没有帐户的情况下仍然可以正常运行。 但是,您的 Lykos 帐户可以启用一些额外的连接功能,例如为我们的 Patreon 支持者自动更新到开发版本(以及更多即将推出的功能)。 + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx index 1ea14f2f6..d02665bd4 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx @@ -642,4 +642,30 @@ CivitAI + + 前往設定 + + + ## 重要提示:Lykos 帳戶登出和安全性升級 + +我們對 Stability Matrix 處理 Lykos 帳戶的方式進行了一些重要的幕後改進,升級到更安全、更便捷的登入系統 **(OAuth 2.0 with OpenID Connect)**。 因此,您已登出 Lykos 帳戶。 + +### 為什麼進行此變更? + +您的安全和隱私對我們很重要。 此升級帶來: + +* **簡化的體驗:** 登入 [account.lykos.ai](https://account.lykos.ai) 一次即可連接到 Stability Matrix 和其他 Lykos AI 服務。 +* **更多登入方式:** 使用您現有的 [lykos.ai](https://lykos.ai) 帳戶,或使用 **Apple**、**GitHub** 或 **Google** 登入。 +* **增強的隱私:** Stability Matrix 僅請求其所需的權限。 +* **行業標準安全性:** 我們正在使用 OAuth 2.0,這是安全登入的黃金標準。 +* **為未來做好準備:** 這允許與其他應用程式和服務進行安全連接。 + +### 我需要做什麼? + +點擊**「前往設定」**按鈕,然後點擊**「Lykos 帳戶」**旁邊的**「連線」**。 + +### Lykos 帳戶是必需的嗎? + +不,Stability Matrix 在沒有帳戶的情況下仍然可以正常運作。 但是,您的 Lykos 帳戶可以啟用一些額外的連接功能,例如為我們的 Patreon 支持者自動更新到開發版本(以及更多即將推出的功能)。 + diff --git a/StabilityMatrix.Avalonia/Services/AccountsService.cs b/StabilityMatrix.Avalonia/Services/AccountsService.cs index 763d51e41..897c505e4 100644 --- a/StabilityMatrix.Avalonia/Services/AccountsService.cs +++ b/StabilityMatrix.Avalonia/Services/AccountsService.cs @@ -1,14 +1,24 @@ using System; using System.Net; +using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Injectio.Attributes; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Client; +using OpenIddict.Client.SystemNetHttp; using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Api.LykosAuthApi; +using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.CivitTRPC; using StabilityMatrix.Core.Models.Api.Lykos; using StabilityMatrix.Core.Services; +using static OpenIddict.Client.OpenIddictClientModels; using ApiException = Refit.ApiException; namespace StabilityMatrix.Avalonia.Services; @@ -18,8 +28,10 @@ public class AccountsService : IAccountsService { private readonly ILogger logger; private readonly ISecretsManager secretsManager; - private readonly ILykosAuthApi lykosAuthApi; + private readonly ILykosAuthApiV1 lykosAuthApi; + private readonly ILykosAuthApiV2 lykosAuthApiV2; private readonly ICivitTRPCApi civitTRPCApi; + private readonly OpenIddictClientService openIdClient; /// public event EventHandler? LykosAccountStatusUpdate; @@ -34,14 +46,18 @@ public class AccountsService : IAccountsService public AccountsService( ILogger logger, ISecretsManager secretsManager, - ILykosAuthApi lykosAuthApi, - ICivitTRPCApi civitTRPCApi + ILykosAuthApiV1 lykosAuthApi, + ILykosAuthApiV2 lykosAuthApiV2, + ICivitTRPCApi civitTRPCApi, + OpenIddictClientService openIdClient ) { this.logger = logger; this.secretsManager = secretsManager; this.lykosAuthApi = lykosAuthApi; + this.lykosAuthApiV2 = lykosAuthApiV2; this.civitTRPCApi = civitTRPCApi; + this.openIdClient = openIdClient; // Update our own status when the Lykos account status changes LykosAccountStatusUpdate += (_, args) => LykosStatus = args; @@ -96,18 +112,35 @@ public async Task LykosLogoutAsync() OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs.Disconnected); } + public async Task LykosAccountV2LoginAsync(LykosAccountV2Tokens tokens) + { + var secrets = await secretsManager.SafeLoadAsync(); + secrets = secrets with { LykosAccountV2 = tokens }; + await secretsManager.SaveAsync(secrets); + + await RefreshLykosAsync(secrets); + } + + public async Task LykosAccountV2LogoutAsync() + { + var secrets = await secretsManager.SafeLoadAsync(); + await secretsManager.SaveAsync(secrets with { LykosAccountV2 = null }); + + OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs.Disconnected); + } + /// public async Task LykosPatreonOAuthLogoutAsync() { var secrets = await secretsManager.SafeLoadAsync(); - if (secrets.LykosAccount is null) + if (secrets.LykosAccountV2 is null) { throw new InvalidOperationException( "Lykos account must be connected in to manage OAuth connections" ); } - await lykosAuthApi.DeletePatreonOAuth(); + await lykosAuthApiV2.ApiV2OauthPatreon(); await RefreshLykosAsync(secrets); } @@ -151,17 +184,29 @@ public async Task RefreshAsync() private async Task RefreshLykosAsync(Secrets secrets) { if ( - secrets.LykosAccount is not null - && !string.IsNullOrWhiteSpace(secrets.LykosAccount?.RefreshToken) - && !string.IsNullOrWhiteSpace(secrets.LykosAccount?.AccessToken) + secrets.LykosAccountV2 is not null + && !string.IsNullOrWhiteSpace(secrets.LykosAccountV2?.RefreshToken) + && !string.IsNullOrWhiteSpace(secrets.LykosAccountV2?.AccessToken) ) { try { - var user = await lykosAuthApi.GetUserSelf(); + var user = await lykosAuthApiV2.ApiV2AccountsMe(); + + ClaimsPrincipal? principal = null; + + if (secrets.LykosAccountV2.IdentityToken is not null) + { + principal = secrets.LykosAccountV2.GetIdentityTokenPrincipal(); + } OnLykosAccountStatusUpdate( - new LykosAccountStatusUpdateEventArgs { IsConnected = true, User = user } + new LykosAccountStatusUpdateEventArgs + { + IsConnected = true, + Principal = principal, + User = user + } ); return; @@ -236,7 +281,7 @@ private void OnLykosAccountStatusUpdate(LykosAccountStatusUpdateEventArgs e) logger.LogInformation( "Lykos account connected: {Id} ({Username})", e.User?.Id, - e.User?.Account.Name + e.Principal?.Identity?.Name ); } diff --git a/StabilityMatrix.Avalonia/Services/IAccountsService.cs b/StabilityMatrix.Avalonia/Services/IAccountsService.cs index 36e91915f..814b15782 100644 --- a/StabilityMatrix.Avalonia/Services/IAccountsService.cs +++ b/StabilityMatrix.Avalonia/Services/IAccountsService.cs @@ -13,14 +13,22 @@ public interface IAccountsService LykosAccountStatusUpdateEventArgs? LykosStatus { get; } + [Obsolete] Task LykosSignupAsync(string email, string password, string username); + [Obsolete] Task LykosLoginAsync(string email, string password); + [Obsolete] Task LykosLoginViaGoogleOAuthAsync(string code, string state, string codeVerifier); + [Obsolete] Task LykosLogoutAsync(); + Task LykosAccountV2LoginAsync(LykosAccountV2Tokens tokens); + + Task LykosAccountV2LogoutAsync(); + Task LykosPatreonOAuthLogoutAsync(); Task CivitLoginAsync(string apiToken); diff --git a/StabilityMatrix.Avalonia/Services/ModelImportService.cs b/StabilityMatrix.Avalonia/Services/ModelImportService.cs index cadde8122..885d2d5ec 100644 --- a/StabilityMatrix.Avalonia/Services/ModelImportService.cs +++ b/StabilityMatrix.Avalonia/Services/ModelImportService.cs @@ -5,11 +5,14 @@ using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; using Injectio.Attributes; +using Python.Runtime; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; +using Dispatcher = Avalonia.Threading.Dispatcher; namespace StabilityMatrix.Avalonia.Services; @@ -117,6 +120,23 @@ public async Task DoImport( modelFile.Name = Path.GetInvalidFileNameChars() .Aggregate(modelFile.Name, (current, c) => current.Replace(c, '_')); + // New code: Ensure unique file name + var originalFileName = modelFile.Name; + var uniqueFileName = GenerateUniqueFileName(downloadFolder.ToString(), originalFileName); + if (!uniqueFileName.Equals(originalFileName, StringComparison.Ordinal)) + { + Dispatcher.UIThread.Post(() => + { + notificationService.Show( + new Notification( + "File renamed", + $"A file with the name \"{originalFileName}\" already exists. The model will be saved as \"{uniqueFileName}\"." + ) + ); + }); + modelFile.Name = uniqueFileName; + } + var downloadPath = downloadFolder.JoinFile(modelFile.Name); // Download model info and preview first @@ -163,4 +183,25 @@ public async Task DoImport( await trackedDownloadService.TryStartDownload(download); } + + private string GenerateUniqueFileName(string folder, string fileName) + { + var fullPath = Path.Combine(folder, fileName); + if (!File.Exists(fullPath)) + return fileName; + + var name = Path.GetFileNameWithoutExtension(fileName); + var extension = Path.GetExtension(fileName); + var count = 1; + string newFileName; + + do + { + newFileName = $"{name} ({count}){extension}"; + fullPath = Path.Combine(folder, newFileName); + count++; + } while (File.Exists(fullPath)); + + return newFileName; + } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 280a138de..c158182b3 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -125,6 +125,8 @@ + + diff --git a/StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml b/StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml index e49ef7547..18d096abb 100644 --- a/StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/ControlThemes/HyperlinkIconButtonStyles.axaml @@ -3,21 +3,20 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent" + xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:ui="using:FluentAvalonia.UI.Controls"> - - - + + + - - - - + + + + @@ -29,42 +28,42 @@ - - - + + + - + - - + + + - - + + - - + - + - + diff --git a/StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml b/StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml index cf02c7d3e..15fcaa402 100644 --- a/StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml +++ b/StabilityMatrix.Avalonia/Styles/Markdown/MarkdownStyleFluentAvalonia.axaml @@ -39,21 +39,23 @@ @@ -67,7 +69,7 @@ - - + + Spacing="16"> - + - + + IsVisible="{Binding IsLoading}" + Spacing="16"> - + Source="{Binding IconUri}"/>--> + + - - + IsIndeterminate="True" + IsVisible="{Binding IsLoading}" /> diff --git a/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml b/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml index 1c18d27e7..a76e893f0 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml @@ -6,10 +6,13 @@ xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" + xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Runtime" xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" + xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:markupExtensions="clr-namespace:StabilityMatrix.Avalonia.MarkupExtensions" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:vendorLabs="clr-namespace:StabilityMatrix.Avalonia.Controls.VendorLabs" @@ -76,13 +79,12 @@ + IsVisible="{Binding UsesVenv}"> @@ -169,7 +171,7 @@ + IsVisible="{Binding UsesVenv}"> @@ -269,6 +271,34 @@ + + + + + + + + + + + + + + + + + + + + + - + @@ -76,34 +77,54 @@ - + Classes="transparent-full" + CornerRadius="8" + IsVisible="{Binding LykosProfileImageUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"> + + + + + + + + - + CornerRadius="8" + Source="{Binding LykosProfileImageUrl}" /> + - + @@ -125,14 +146,14 @@ - + @@ -145,12 +166,15 @@ - + - + + OffDescription="Connect to Download Models that require login" + OnDescriptionExtra="{Binding CivitStatus.UsernameWithParentheses}"> diff --git a/StabilityMatrix.Core/Api/ILykosAuthApi.cs b/StabilityMatrix.Core/Api/ILykosAuthApiV1.cs similarity index 91% rename from StabilityMatrix.Core/Api/ILykosAuthApi.cs rename to StabilityMatrix.Core/Api/ILykosAuthApiV1.cs index 8bfeaa79b..5d277e66f 100644 --- a/StabilityMatrix.Core/Api/ILykosAuthApi.cs +++ b/StabilityMatrix.Core/Api/ILykosAuthApiV1.cs @@ -8,7 +8,8 @@ namespace StabilityMatrix.Core.Api; [Localizable(false)] [Headers("User-Agent: StabilityMatrix")] -public interface ILykosAuthApi +[Obsolete("Use ILykosAuthApiV2")] +public interface ILykosAuthApiV1 { [Headers("Authorization: Bearer")] [Get("/api/Users/{email}")] @@ -19,26 +20,26 @@ public interface ILykosAuthApi Task GetUserSelf(CancellationToken cancellationToken = default); [Post("/api/Accounts")] - Task PostAccount( + Task PostAccount( [Body] PostAccountRequest request, CancellationToken cancellationToken = default ); [Post("/api/Login")] - Task PostLogin( + Task PostLogin( [Body] PostLoginRequest request, CancellationToken cancellationToken = default ); [Headers("Authorization: Bearer")] [Post("/api/Login/Refresh")] - Task PostLoginRefresh( + Task PostLoginRefresh( [Body] PostLoginRefreshRequest request, CancellationToken cancellationToken = default ); [Get("/api/oauth/google/callback")] - Task GetOAuthGoogleCallback( + Task GetOAuthGoogleCallback( [Query] string code, [Query] string state, [Query] string codeVerifier, diff --git a/StabilityMatrix.Core/Api/LykosAuthApi/.refitter b/StabilityMatrix.Core/Api/LykosAuthApi/.refitter new file mode 100644 index 000000000..5a829d2c0 --- /dev/null +++ b/StabilityMatrix.Core/Api/LykosAuthApi/.refitter @@ -0,0 +1,17 @@ +{ + "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" +} diff --git a/StabilityMatrix.Core/Api/LykosAuthApi/Generated/Refitter.g.cs b/StabilityMatrix.Core/Api/LykosAuthApi/Generated/Refitter.g.cs new file mode 100644 index 000000000..f8cb73f3e --- /dev/null +++ b/StabilityMatrix.Core/Api/LykosAuthApi/Generated/Refitter.g.cs @@ -0,0 +1,258 @@ +// +// 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 \ No newline at end of file diff --git a/StabilityMatrix.Core/Api/LykosAuthTokenProvider.cs b/StabilityMatrix.Core/Api/LykosAuthTokenProvider.cs index bef849453..d129da610 100644 --- a/StabilityMatrix.Core/Api/LykosAuthTokenProvider.cs +++ b/StabilityMatrix.Core/Api/LykosAuthTokenProvider.cs @@ -1,28 +1,29 @@ -using Injectio.Attributes; +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 : ITokenProvider +public class LykosAuthTokenProvider( + Lazy lazyLykosAuthApi, + ISecretsManager secretsManager, + OpenIddictClientService openIdClient +) : ITokenProvider { - private readonly ISecretsManager secretsManager; - private readonly Lazy lazyLykosAuthApi; + private readonly Lazy lazyLykosAuthApi = lazyLykosAuthApi; - public LykosAuthTokenProvider(Lazy lazyLykosAuthApi, ISecretsManager secretsManager) - { - // Lazy as instantiating requires the current class to be instantiated. - this.lazyLykosAuthApi = lazyLykosAuthApi; - this.secretsManager = secretsManager; - } + // Lazy as instantiating requires the current class to be instantiated. /// public async Task GetAccessTokenAsync() { var secrets = await secretsManager.SafeLoadAsync().ConfigureAwait(false); - return secrets.LykosAccount?.AccessToken ?? ""; + return secrets.LykosAccountV2?.AccessToken ?? ""; } /// @@ -30,20 +31,37 @@ public async Task GetAccessTokenAsync() { var secrets = await secretsManager.SafeLoadAsync().ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(secrets.LykosAccount?.RefreshToken)) + if (string.IsNullOrWhiteSpace(secrets.LykosAccountV2?.RefreshToken)) { throw new InvalidOperationException("No refresh token found"); } - var lykosAuthApi = lazyLykosAuthApi.Value; - var newTokens = await lykosAuthApi - .PostLoginRefresh(new PostLoginRefreshRequest(secrets.LykosAccount.RefreshToken)) + var result = await openIdClient + .AuthenticateWithRefreshTokenAsync( + new OpenIddictClientModels.RefreshTokenAuthenticationRequest + { + ProviderName = OpenIdClientConstants.LykosAccount.ProviderName, + RefreshToken = secrets.LykosAccountV2.RefreshToken + } + ) .ConfigureAwait(false); - secrets = secrets with { LykosAccount = newTokens }; + 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 (newTokens.AccessToken, newTokens.RefreshToken); + return (result.AccessToken, result.RefreshToken); } } diff --git a/StabilityMatrix.Core/Api/OpenIdClientConstants.cs b/StabilityMatrix.Core/Api/OpenIdClientConstants.cs new file mode 100644 index 000000000..9254e4170 --- /dev/null +++ b/StabilityMatrix.Core/Api/OpenIdClientConstants.cs @@ -0,0 +1,12 @@ +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"; + } +} diff --git a/StabilityMatrix.Core/Api/TokenAuthHeaderHandler.cs b/StabilityMatrix.Core/Api/TokenAuthHeaderHandler.cs index b042d6b7a..78dc65889 100644 --- a/StabilityMatrix.Core/Api/TokenAuthHeaderHandler.cs +++ b/StabilityMatrix.Core/Api/TokenAuthHeaderHandler.cs @@ -14,17 +14,21 @@ public class TokenAuthHeaderHandler : DelegatingHandler 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( - r => - r.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden - && r.RequestMessage?.Headers.Authorization is { Scheme: "Bearer", Parameter: { } param } - && !string.IsNullOrWhiteSpace(param) - ) + .HandleResult(ResponseFilter) .RetryAsync( async (result, _) => { @@ -55,7 +59,7 @@ CancellationToken cancellationToken { // 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 (request.Headers.Authorization is { Scheme: "Bearer" }) + if (RequestFilter(request)) { var accessToken = await tokenProvider.GetAccessTokenAsync().ConfigureAwait(false); diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs index 018763eb1..d23f848c9 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs @@ -27,6 +27,16 @@ public bool IsNvidia } } + public bool IsBlackwellGpu() + { + if (Name is null) + return false; + + return IsNvidia + && Name.Contains("RTX 50", StringComparison.OrdinalIgnoreCase) + && !Name.Contains("RTX 5000", StringComparison.OrdinalIgnoreCase); + } + public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false; public bool IsIntel => Name?.Contains("arc", StringComparison.OrdinalIgnoreCase) ?? false; diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs index b2b8a0059..76f27a5a6 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs @@ -250,6 +250,17 @@ 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 } + && gpu.Name.Contains("RTX 50", StringComparison.OrdinalIgnoreCase) + && !gpu.Name.Contains("RTX 5000", StringComparison.OrdinalIgnoreCase) + ); + } + /// /// Return true if the system has at least one AMD GPU. /// diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountStatusUpdateEventArgs.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountStatusUpdateEventArgs.cs index e1c5b4dbd..31f76bb4d 100644 --- a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountStatusUpdateEventArgs.cs +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountStatusUpdateEventArgs.cs @@ -1,4 +1,9 @@ -namespace StabilityMatrix.Core.Models.Api.Lykos; +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 { @@ -6,7 +11,16 @@ public class LykosAccountStatusUpdateEventArgs : EventArgs public bool IsConnected { get; init; } - public GetUserResponse? User { 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; } diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs deleted file mode 100644 index 193a388f1..000000000 --- a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountTokens.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace StabilityMatrix.Core.Models.Api.Lykos; - -public record LykosAccountTokens(string AccessToken, string RefreshToken); diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV1Tokens.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV1Tokens.cs new file mode 100644 index 000000000..8ac4b7609 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV1Tokens.cs @@ -0,0 +1,4 @@ +namespace StabilityMatrix.Core.Models.Api.Lykos; + +[Obsolete("Use LykosAccountV2Tokens instead")] +public record LykosAccountV1Tokens(string AccessToken, string RefreshToken); diff --git a/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV2Tokens.cs b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV2Tokens.cs new file mode 100644 index 000000000..891f5b66b --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Lykos/LykosAccountV2Tokens.cs @@ -0,0 +1,38 @@ +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; + } +} diff --git a/StabilityMatrix.Core/Models/ExtraPackageCommand.cs b/StabilityMatrix.Core/Models/ExtraPackageCommand.cs new file mode 100644 index 000000000..87398a8c3 --- /dev/null +++ b/StabilityMatrix.Core/Models/ExtraPackageCommand.cs @@ -0,0 +1,7 @@ +namespace StabilityMatrix.Core.Models; + +public class ExtraPackageCommand +{ + public required string CommandName { get; set; } + public required Func Command { get; set; } +} diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 2f5f80a7f..e9f2a276d 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -53,6 +53,13 @@ public abstract class BasePackage(ISettingsManager settingsManager) public abstract PackageDifficulty InstallerSortOrder { get; } public virtual PackageType PackageType => PackageType.SdInference; + public virtual bool UsesVenv => true; + + /// + /// 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, diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 0ce94c424..ad5aed0ee 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -207,36 +207,65 @@ public override async Task InstallPackage( var pipArgs = new PipInstallArgs(); - pipArgs = torchVersion switch + var isBlackwell = + SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu(); + + if (isBlackwell && torchVersion is TorchIndex.Cuda) { - TorchIndex.DirectMl => pipArgs.WithTorchDirectML(), - _ - => pipArgs - .AddArg("--upgrade") - .WithTorch() - .WithTorchVision() - .WithTorchExtraIndex( - torchVersion switch - { - TorchIndex.Cpu => "cpu", - TorchIndex.Cuda => "cu124", - TorchIndex.Rocm => "rocm6.2", - TorchIndex.Mps => "cpu", - _ - => throw new ArgumentOutOfRangeException( - nameof(torchVersion), - torchVersion, - null - ) - } - ) - }; + pipArgs = pipArgs + .AddArg("--upgrade") + .AddArg("--pre") + .WithTorch() + .WithTorchVision() + .WithTorchExtraIndex("nightly/cu128"); + + if (installedPackage.PipOverrides != null) + { + pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); + } + progress?.Report( + new ProgressReport(-1f, "Installing Torch for your shiny new GPU...", isIndeterminate: true) + ); + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); + } + else + { + pipArgs = torchVersion switch + { + TorchIndex.DirectMl => pipArgs.WithTorchDirectML(), + _ + => pipArgs + .AddArg("--upgrade") + .WithTorch() + .WithTorchVision() + .WithTorchExtraIndex( + torchVersion switch + { + TorchIndex.Cpu => "cpu", + TorchIndex.Cuda => "cu126", + TorchIndex.Rocm => "rocm6.2.4", + TorchIndex.Mps => "cpu", + _ + => throw new ArgumentOutOfRangeException( + nameof(torchVersion), + torchVersion, + null + ) + } + ) + }; + } + + if (isBlackwell && torchVersion is TorchIndex.Cuda) + { + pipArgs = new PipInstallArgs(); + } var requirements = new FilePath(installLocation, "requirements.txt"); pipArgs = pipArgs.WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), - excludePattern: "torch$|numpy" + excludePattern: isBlackwell ? "torch$|torchvision$|numpy" : "torch$|numpy" ); // https://github.com/comfyanonymous/ComfyUI/pull/4121 @@ -385,7 +414,7 @@ private async Task SetupModelFoldersConfig(DirectoryPath installDirectory) nodeValue.Children["ultralytics_bbox"] = Path.Combine(modelsDir, "Ultralytics", "bbox"); nodeValue.Children["ultralytics_segm"] = Path.Combine(modelsDir, "Ultralytics", "segm"); nodeValue.Children["sams"] = Path.Combine(modelsDir, "Sams"); - nodeValue.Children["diffusion_models"] = Path.Combine(modelsDir, "unet"); + nodeValue.Children["diffusion_models"] = Path.Combine(modelsDir, "Unet"); } else { @@ -429,7 +458,7 @@ private async Task SetupModelFoldersConfig(DirectoryPath installDirectory) { "ultralytics_bbox", Path.Combine(modelsDir, "Ultralytics", "bbox") }, { "ultralytics_segm", Path.Combine(modelsDir, "Ultralytics", "segm") }, { "sams", Path.Combine(modelsDir, "Sams") }, - { "diffusion_models", Path.Combine(modelsDir, "unet") } + { "diffusion_models", Path.Combine(modelsDir, "Unet") } } ); } diff --git a/StabilityMatrix.Core/Models/Packages/FluxGym.cs b/StabilityMatrix.Core/Models/Packages/FluxGym.cs index 151a5a0f5..073b7ba5d 100644 --- a/StabilityMatrix.Core/Models/Packages/FluxGym.cs +++ b/StabilityMatrix.Core/Models/Packages/FluxGym.cs @@ -89,13 +89,13 @@ public override async Task InstallPackage( // check if sd-scripts is already installed - if so: pull, else: clone if (Directory.Exists(Path.Combine(installLocation, "sd-scripts"))) { - await prerequisiteHelper + await PrerequisiteHelper .RunGit(["pull"], onConsoleOutput, Path.Combine(installLocation, "sd-scripts")) .ConfigureAwait(false); } else { - await prerequisiteHelper + await PrerequisiteHelper .RunGit( ["clone", "-b", "sd3", "https://github.com/kohya-ss/sd-scripts"], onConsoleOutput, diff --git a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs index 5b86a60e7..78ada74ba 100644 --- a/StabilityMatrix.Core/Models/Packages/SDWebForge.cs +++ b/StabilityMatrix.Core/Models/Packages/SDWebForge.cs @@ -146,10 +146,27 @@ public override async Task InstallPackage( var pipArgs = new PipInstallArgs("setuptools==69.5.1"); + var isBlackwell = + SettingsManager.Settings.PreferredGpu?.IsBlackwellGpu() ?? HardwareHelper.HasBlackwellGpu(); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); - if (torchVersion is TorchIndex.DirectMl) + + if (isBlackwell && torchVersion is TorchIndex.Cuda) { - pipArgs = pipArgs.WithTorchDirectML(); + pipArgs = pipArgs + .AddArg("--upgrade") + .AddArg("--pre") + .WithTorch() + .WithTorchVision() + .WithTorchExtraIndex("nightly/cu128"); + + if (installedPackage.PipOverrides != null) + { + pipArgs = pipArgs.WithUserOverrides(installedPackage.PipOverrides); + } + progress?.Report( + new ProgressReport(-1f, "Installing Torch for your shiny new GPU...", isIndeterminate: true) + ); + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } else { @@ -161,13 +178,18 @@ public override async Task InstallPackage( { TorchIndex.Cpu => "cpu", TorchIndex.Cuda => "cu121", - TorchIndex.Rocm => "rocm5.6", + TorchIndex.Rocm => "rocm5.7", TorchIndex.Mps => "cpu", _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null) } ); } + if (isBlackwell && torchVersion is TorchIndex.Cuda) + { + pipArgs = new PipInstallArgs(); + } + pipArgs = pipArgs.WithParsedFromRequirementsTxt(requirementsContent, excludePattern: "torch"); if (installedPackage.PipOverrides != null) diff --git a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs index 376d713ec..61e7801c8 100644 --- a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs +++ b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs @@ -42,6 +42,32 @@ IPrerequisiteHelper prerequisiteHelper [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 => [ @@ -135,7 +161,7 @@ public override async Task InstallPackage( progress?.Report(new ProgressReport(-1f, "Installing SwarmUI...", isIndeterminate: true)); var comfy = settingsManager.Settings.InstalledPackages.FirstOrDefault( - x => x.PackageName == nameof(ComfyUI) + x => x.PackageName is nameof(ComfyUI) or "ComfyUI-Zluda" ); if (comfy == null) @@ -233,16 +259,45 @@ await prerequisiteHelper .Where(arg => !string.IsNullOrWhiteSpace(arg)) ); - dataSection.Set( - "settings", - new ComfyUiSelfStartSettings - { - StartScript = $"../{comfy.DisplayName}/main.py", - DisableInternalArgs = false, - AutoUpdate = false, - ExtraArgs = comfyArgs - }.Save(true) - ); + 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]; + + dataSection.Set( + "settings", + new ComfyUiSelfStartSettings + { + StartScript = zludaPath, + DisableInternalArgs = false, + AutoUpdate = false, + ExtraArgs = args + }.Save(true) + ); + } + else + { + dataSection.Set( + "settings", + new ComfyUiSelfStartSettings + { + StartScript = $"../{comfy.DisplayName}/main.py", + DisableInternalArgs = false, + AutoUpdate = false, + ExtraArgs = comfyArgs + }.Save(true) + ); + } backendsFile.Set("0", dataSection); backendsFile.SaveToFile(GetBackendsPath(installLocation)); @@ -385,6 +440,29 @@ await dotnetProcess 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); diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index 889773668..5d81bd4ec 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -201,6 +201,8 @@ public override async Task InstallPackage( // Setup venv await using var venvRunner = await SetupVenvPure(installLocation).ConfigureAwait(false); + await venvRunner.PipInstall("numpy==1.26.4").ConfigureAwait(false); + if (installedPackage.PipOverrides != null) { var pipArgs = new PipInstallArgs().WithUserOverrides(installedPackage.PipOverrides); diff --git a/StabilityMatrix.Core/Models/Secrets.cs b/StabilityMatrix.Core/Models/Secrets.cs index 5a413205a..cd2e31889 100644 --- a/StabilityMatrix.Core/Models/Secrets.cs +++ b/StabilityMatrix.Core/Models/Secrets.cs @@ -5,7 +5,20 @@ namespace StabilityMatrix.Core.Models; public readonly record struct Secrets { - public LykosAccountTokens? LykosAccount { get; init; } + [Obsolete("Use LykosAccountV2 instead")] + public LykosAccountV1Tokens? LykosAccount { get; init; } public CivitApiTokens? CivitApi { get; init; } + + public LykosAccountV2Tokens? LykosAccountV2 { 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 + } } diff --git a/StabilityMatrix.Core/Models/Settings/TeachingTip.cs b/StabilityMatrix.Core/Models/Settings/TeachingTip.cs index d69595c76..643aca2a8 100644 --- a/StabilityMatrix.Core/Models/Settings/TeachingTip.cs +++ b/StabilityMatrix.Core/Models/Settings/TeachingTip.cs @@ -15,6 +15,7 @@ public record TeachingTip(string Value) : StringValue(Value) 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 override string ToString() diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index 183ef48fc..b3c3e4c11 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -50,6 +50,8 @@ + + diff --git a/StabilityMatrix.Core/Updater/UpdateHelper.cs b/StabilityMatrix.Core/Updater/UpdateHelper.cs index 563fee0dc..7cc8ce028 100644 --- a/StabilityMatrix.Core/Updater/UpdateHelper.cs +++ b/StabilityMatrix.Core/Updater/UpdateHelper.cs @@ -3,7 +3,7 @@ using Injectio.Attributes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Api.LykosAuthApi; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Configs; @@ -21,7 +21,7 @@ public class UpdateHelper : IUpdateHelper private readonly IHttpClientFactory httpClientFactory; private readonly IDownloadService downloadService; private readonly ISettingsManager settingsManager; - private readonly ILykosAuthApi lykosAuthApi; + private readonly ILykosAuthApiV2 lykosAuthApi; private readonly DebugOptions debugOptions; private readonly System.Timers.Timer timer = new(TimeSpan.FromMinutes(60)); @@ -45,7 +45,7 @@ public UpdateHelper( IDownloadService downloadService, IOptions debugOptions, ISettingsManager settingsManager, - ILykosAuthApi lykosAuthApi + ILykosAuthApiV2 lykosAuthApi ) { this.logger = logger; @@ -101,7 +101,7 @@ public async Task DownloadUpdate(UpdateInfo updateInfo, IProgress