From e8cf04ce3a4bcf49d53b89a0fc132ee15ffa2892 Mon Sep 17 00:00:00 2001 From: sa_ddam213 Date: Wed, 12 Aug 2026 20:42:07 +1200 Subject: [PATCH 1/2] StableDiffusionCpp host scaffold --- .../Backend/StableDiffusionCppClient.cs | 45 +++++ Apps/Amuse.App/Services/GenerateService.cs | 1 + Apps/Amuse.Common/Config/ServerConfig.cs | 11 ++ Apps/Amuse.Common/Enums.cs | 3 +- .../Amuse.Host.StableDiffusionCpp.csproj | 45 +++++ .../HostServer.cs | 158 ++++++++++++++++++ Apps/Amuse.Host.StableDiffusionCpp/Icon.ico | Bin 0 -> 111379 bytes Apps/Amuse.Host.StableDiffusionCpp/Program.cs | 119 +++++++++++++ TensorStack.Common/Common/BackendType.cs | 7 +- TensorStack.sln | 9 + 10 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 Apps/Amuse.App/Backend/StableDiffusionCppClient.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Amuse.Host.StableDiffusionCpp.csproj create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/HostServer.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Icon.ico create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Program.cs diff --git a/Apps/Amuse.App/Backend/StableDiffusionCppClient.cs b/Apps/Amuse.App/Backend/StableDiffusionCppClient.cs new file mode 100644 index 0000000..8005fbc --- /dev/null +++ b/Apps/Amuse.App/Backend/StableDiffusionCppClient.cs @@ -0,0 +1,45 @@ +using Amuse.App.Services; +using Amuse.Common; +using Amuse.Common.Config; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; + +namespace Amuse.App.Runtime +{ + /// + /// PipelineClient implemntation for Amuse.Host.StableDiffusionCpp + /// Implements the + /// + /// + public sealed class StableDiffusionCppClient : BackendClient + { + /// + /// Initializes a new instance of the class. + /// + /// The settings. + /// The media service. + /// The logger. + public StableDiffusionCppClient(Settings settings, IMediaService mediaService, ILogger logger) + : base(settings, mediaService, logger) { } + + + /// + /// Create PipelineClient targeting Amuse.Host.StableDiffusionCpp. + /// + /// The cancellation token. + /// + protected override async Task CreatePipelineClientAsync(CancellationToken cancellationToken = default) + { + var createOptions = new PipelineCreateOptions(); + var clientConfig = new ClientConfig + { + ServerPath = App.DirectoryServer, + ServerType = ServerType.StableDiffusionCpp, + IsDebugMode = Settings.IsServerDebugEnabled, + }; + return await CreatePipelineClientAsync(clientConfig, createOptions, cancellationToken); + } + + } +} diff --git a/Apps/Amuse.App/Services/GenerateService.cs b/Apps/Amuse.App/Services/GenerateService.cs index c9eb7d6..4e67c62 100644 --- a/Apps/Amuse.App/Services/GenerateService.cs +++ b/Apps/Amuse.App/Services/GenerateService.cs @@ -112,6 +112,7 @@ public async Task LoadAsync(PipelineModel pipeline, IProgress { BackendType.PyTorch => new PyTorchBackendClient(_settings, _mediaService, _environmentService, _logger), BackendType.OnnxRuntime => new OnnxBackendClient(_settings, _mediaService, _logger), + BackendType.StableDiffusionCpp => new StableDiffusionCppClient(_settings, _mediaService, _logger), _ => throw new NotImplementedException() }; diff --git a/Apps/Amuse.Common/Config/ServerConfig.cs b/Apps/Amuse.Common/Config/ServerConfig.cs index 8f72d15..f37cc4a 100644 --- a/Apps/Amuse.Common/Config/ServerConfig.cs +++ b/Apps/Amuse.Common/Config/ServerConfig.cs @@ -45,6 +45,17 @@ public static ServerConfig GetConfig(ServerType serverType, string directoryBase ChannelPipeName = "AmusePyTorch.PipeName", ChannelProgress = "AmusePyTorch.Progress" } + }, + { + ServerType.StableDiffusionCpp, new ServerConfig + { + Name = "AmuseStableDiffusionCpp", + Arguments = [nameof(ServerType.StableDiffusionCpp)], + Executable = "AmuseHost.StableDiffusionCpp.exe", + ChannelCommand = "AmuseStableDiffusionCpp.Command", + ChannelPipeName = "AmuseStableDiffusionCpp.PipeName", + ChannelProgress = "AmuseStableDiffusionCpp.Progress" + } } }; } diff --git a/Apps/Amuse.Common/Enums.cs b/Apps/Amuse.Common/Enums.cs index 12f767f..e5bfb16 100644 --- a/Apps/Amuse.Common/Enums.cs +++ b/Apps/Amuse.Common/Enums.cs @@ -5,7 +5,8 @@ namespace Amuse.Common public enum ServerType { OnnxRuntime = 0, - PyTorch = 10 + PyTorch = 10, + StableDiffusionCpp = 20 } public enum ProcessType diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Amuse.Host.StableDiffusionCpp.csproj b/Apps/Amuse.Host.StableDiffusionCpp/Amuse.Host.StableDiffusionCpp.csproj new file mode 100644 index 0000000..4210c4c --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Amuse.Host.StableDiffusionCpp.csproj @@ -0,0 +1,45 @@ + + + + Exe + AmuseHost.StableDiffusionCpp + net10.0-windows10.0.17763.0 + x64 + Icon.ico + Debug;Release;Release_Installer + false + ..\Amuse.App\bin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apps/Amuse.Host.StableDiffusionCpp/HostServer.cs b/Apps/Amuse.Host.StableDiffusionCpp/HostServer.cs new file mode 100644 index 0000000..9f38cfd --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/HostServer.cs @@ -0,0 +1,158 @@ +using Amuse.Common; +using Amuse.Common.Config; +using Amuse.Common.Message; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using TensorStack.Common.Pipeline; + + +namespace Amuse.Host.StableDiffusionCpp +{ + public sealed class HostServer : PipelineServer + { + private readonly IProgress _progressRelayRunCallback; + + private IPipeline _pipeline; + private PipelineLoadOptions _pipelineOptions; + + public HostServer(ServerConfig channelConfig, ILogger logger) + : base(channelConfig, logger) + { + _progressRelayRunCallback = new Progress(async (p) => await UpdateProgress(p)); + } + + + /// + /// Called when the Channel is opened. + /// + /// Task. + protected override Task ChannelOpenedAsync() + { + return Task.CompletedTask; + } + + + /// + /// Called when the Channel is closed. + /// + protected override Task ChannelClosedAsync() + { + _pipeline?.Dispose(); + return Task.CompletedTask; + } + + + protected override async Task CreatePipelineAsync(PipelineRequest request, CancellationToken cancellationToken) + { + try + { + var timestamp = Stopwatch.GetTimestamp(); + var environmentRequest = request.CreateOptions; + + Logger.LogInformation($"[PipelineServer] [CreatePipeline] Environment created, Elapsed: {Stopwatch.GetElapsedTime(timestamp)}"); + await SendResponse(cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "[PipelineServer] [CreatePipeline] An exception occurred creating environment."); + await SendException(ex, cancellationToken); + } + } + + + protected override async Task LoadPipelineAsync(PipelineRequest request, CancellationToken cancellationToken) + { + try + { + _pipelineOptions = request.LoadOptions; + + //TODO: Create Pipeline + + await _pipeline.LoadAsync(cancellationToken); + await SendResponse(cancellationToken); + + } + catch (Exception ex) + { + Logger.LogError(ex, "[PipelineServer] [LoadPipeline] An exception occurred loading pipeline."); + await SendException(ex, cancellationToken); + } + } + + + protected override async Task ReloadPipelineAsync(PipelineRequest request, CancellationToken cancellationToken) + { + try + { + var reloadOptions = request.ReloadOptions; + _pipelineOptions.ProcessType = reloadOptions.ProcessType; + _pipelineOptions.ControlNet = reloadOptions.ControlNet; + _pipelineOptions.LoraAdapters = reloadOptions.LoraAdapters; + + // TODO: Reload Pipeline + + await SendResponse(cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "[PipelineServer] [ReloadPipeline] An exception occurred reloading pipeline."); + await SendException(ex, cancellationToken); + } + } + + + protected override async Task UnloadPipelineAsync(PipelineRequest request, CancellationToken cancellationToken) + { + try + { + await _pipeline.UnloadAsync(); + await SendResponse(cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "[PipelineServer] [UnloadPipeline] An exception occurred unloading pipeline."); + await SendException(ex, cancellationToken); + } + } + + + protected override async Task RunPipelineAsync(PipelineRequest request, CancellationToken cancellationToken) + { + try + { + request.RunOptions.UnpackTensors(request); + + // TODO: Execute Image/Video + + } + catch (OperationCanceledException ex) + { + Logger.LogError("[PipelineServer] [RunPipeline] {Message}", ex.Message); + await SendException(ex, cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex, "[PipelineServer] [RunPipeline] An exception occurred running pipeline."); + await SendException(ex, cancellationToken); + } + } + + + private async Task UpdateProgress(RunProgress progress) + { + await QueueProgress(new PipelineProgress + { + Key = "Generate", + Subkey = "Step", + Value = progress.Value, + Maximum = progress.Maximum, + Message = progress.Message, + Elapsed = (float)progress.Elapsed.TotalMilliseconds + }); + } + + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Icon.ico b/Apps/Amuse.Host.StableDiffusionCpp/Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..aa4f669dc8b8ec45d4c8739306de85a50198f094 GIT binary patch literal 111379 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 literal 0 HcmV?d00001 diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Program.cs b/Apps/Amuse.Host.StableDiffusionCpp/Program.cs new file mode 100644 index 0000000..b689aa4 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Program.cs @@ -0,0 +1,119 @@ +using Amuse.Common; +using Amuse.Common.Config; +using Microsoft.Extensions.Logging; +using Serilog; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using TensorStack.Common; +using Logger = Microsoft.Extensions.Logging.ILogger; + +namespace Amuse.Host.StableDiffusionCpp +{ + internal class Program + { + private static Logger _logger; + private static string _directoryBase; + private static string _directoryData; + private static ServerConfig _serverConfig; + + /// + /// Defines the entry point of the application. + /// + /// The arguments. + static async Task Main(string[] args) + { + if (!Enum.TryParse(args[0], true, out var serverType)) + throw new InvalidOperationException("Invalid ServerType"); + + _directoryBase = AppDomain.CurrentDomain.BaseDirectory; + _directoryData = GetApplicationDataDirectory(); + _serverConfig = ServerConfig.GetConfig(serverType, _directoryData); + + using (var mutex = new Mutex(true, $"Global\\{_serverConfig.Name}", out var createdNew)) + { + if (!createdNew) + throw new InvalidOperationException("Another instance of this application is already running."); + + _logger = ConfigureLogging(); + await StartAsync(); + } + } + + + /// + /// Start the server + /// + /// The options. + /// A Task representing the asynchronous operation. + private static async Task StartAsync() + { + try + { + _logger.LogInformation("[StartAsync] Starting {Name}...", _serverConfig.Name); + using (var cancellationTokenSource = new CancellationTokenSource()) + { + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => cancellationTokenSource.SafeCancel(); + + // Start Server + using (var hostServer = new HostServer(_serverConfig, _logger)) + { + await hostServer.StartAsync(cancellationTokenSource.Token); + } + } + _logger.LogInformation("[StartAsync] {Name} stopped.", _serverConfig.Name); + } + catch (EndOfStreamException) + { + } + catch (Exception ex) + { + _logger.LogError(ex, "[StartAsync] An unhandled exception occurred."); + } + } + + + /// + /// Configures the logging. + /// + private static Logger ConfigureLogging() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.File(GetLogName(), rollOnFileSizeLimit: true) + .CreateLogger(); + var factory = LoggerFactory.Create(builder => + { + builder.ClearProviders(); + builder.AddSerilog(dispose: true); + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); + return factory.CreateLogger(); + } + + + /// + /// Gets the application data directory. + /// + private static string GetApplicationDataDirectory() + { +#if RELEASE_INSTALLER + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Amuse"); +#else + return _directoryBase; +#endif + } + + + /// + /// Gets the name of the log. + /// + private static string GetLogName() + { + var now = DateTime.Now; + return Path.Combine(_directoryData, @$"Logs\{_serverConfig.Name}-{DateTime.Now:dd-MM-yyyy}-{now.Hour * 3600 + now.Minute * 60 + now.Second}.txt"); + } + } +} diff --git a/TensorStack.Common/Common/BackendType.cs b/TensorStack.Common/Common/BackendType.cs index b3af3bd..193ca20 100644 --- a/TensorStack.Common/Common/BackendType.cs +++ b/TensorStack.Common/Common/BackendType.cs @@ -7,7 +7,10 @@ public enum BackendType [Display(Name = "OnnxRuntime", ShortName = "Onnx", Description = "OnnxRuntime .NET model inference using TensorStack")] OnnxRuntime = 0, - [Display(Name = "PyTorch", ShortName = "Onnx", Description = "PyTorch model inference using HuggingFace Diffusers & Transformers")] - PyTorch = 10 + [Display(Name = "PyTorch", ShortName = "torch", Description = "PyTorch model inference using HuggingFace Diffusers & Transformers")] + PyTorch = 10, + + [Display(Name = "StableDiffusionCpp", ShortName = "SD.cpp", Description = "GGML model inference using StableDiffusionCpp")] + StableDiffusionCpp = 20 } } diff --git a/TensorStack.sln b/TensorStack.sln index ab509c4..ea7b0fb 100644 --- a/TensorStack.sln +++ b/TensorStack.sln @@ -56,6 +56,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amuse.Host.Onnx", "Apps\Amu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amuse.Host.PyTorch", "Apps\Amuse.Host.PyTorch\Amuse.Host.PyTorch.csproj", "{E3E2CB70-0145-7F05-D936-3CE7DB5740FF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amuse.Host.StableDiffusionCpp", "Apps\Amuse.Host.StableDiffusionCpp\Amuse.Host.StableDiffusionCpp.csproj", "{DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DebugApp", "Apps\DebugApp\DebugApp.csproj", "{CD6962D3-18EA-4113-25AE-EF9BC1FBD4FE}" EndProject Global @@ -191,6 +193,12 @@ Global {E3E2CB70-0145-7F05-D936-3CE7DB5740FF}.Release_Installer|Any CPU.Build.0 = Release_Installer|Any CPU {E3E2CB70-0145-7F05-D936-3CE7DB5740FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3E2CB70-0145-7F05-D936-3CE7DB5740FF}.Release|Any CPU.Build.0 = Release|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Release_Installer|Any CPU.ActiveCfg = Release|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Release_Installer|Any CPU.Build.0 = Release|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2}.Release|Any CPU.Build.0 = Release|Any CPU {CD6962D3-18EA-4113-25AE-EF9BC1FBD4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD6962D3-18EA-4113-25AE-EF9BC1FBD4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD6962D3-18EA-4113-25AE-EF9BC1FBD4FE}.Release_Installer|Any CPU.ActiveCfg = Release|Any CPU @@ -206,6 +214,7 @@ Global {EB356593-64EA-142D-AA48-63BF20AC1343} = {F81721F7-8D43-410F-A452-99A4F51AB2A0} {B44ED101-BA40-6245-0389-71F72289FB4B} = {F81721F7-8D43-410F-A452-99A4F51AB2A0} {E3E2CB70-0145-7F05-D936-3CE7DB5740FF} = {F81721F7-8D43-410F-A452-99A4F51AB2A0} + {DEAD9EB8-5CDF-46ED-A612-65F88E5417C2} = {F81721F7-8D43-410F-A452-99A4F51AB2A0} {CD6962D3-18EA-4113-25AE-EF9BC1FBD4FE} = {F81721F7-8D43-410F-A452-99A4F51AB2A0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From d1109d5bd328e233568b91346647ca43baf3263a Mon Sep 17 00:00:00 2001 From: sa_ddam213 Date: Thu, 13 Aug 2026 12:16:22 +1200 Subject: [PATCH 2/2] sd-server client --- .../Common/BackendType.cs | 23 ++++ .../Common/CapabilitiesModel.cs | 19 +++ .../Common/DefaultParams.cs | 14 ++ .../Common/GuidanceParams.cs | 15 +++ .../Common/ImageParams.cs | 71 ++++++++++ .../Common/ImageResult.cs | 14 ++ .../Common/JobModel.cs | 32 +++++ .../Common/JobResult.cs | 47 +++++++ .../Common/JobStatus.cs | 23 ++++ .../Common/LoraParams.cs | 16 +++ .../Common/SampleParams.cs | 32 +++++ .../Common/VaeTilingParams.cs | 31 +++++ .../Common/VideoParams.cs | 74 ++++++++++ .../Config/ModelConfig.cs | 24 ++++ .../Config/ServerConfig.cs | 22 +++ .../StableDiffusionClient.cs | 127 ++++++++++++++++++ 16 files changed, 584 insertions(+) create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/BackendType.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/CapabilitiesModel.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/DefaultParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/GuidanceParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/ImageParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/ImageResult.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/JobModel.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/JobResult.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/JobStatus.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/LoraParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/SampleParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/VaeTilingParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Common/VideoParams.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Config/ModelConfig.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/Config/ServerConfig.cs create mode 100644 Apps/Amuse.Host.StableDiffusionCpp/StableDiffusionClient.cs diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/BackendType.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/BackendType.cs new file mode 100644 index 0000000..84b6b3e --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/BackendType.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public enum BackendType + { + [Display(Name = "", ShortName = "cpu")] + CPU = 0, + + [Display(Name = "", ShortName = "cuda")] + CUDA = 1, + + [Display(Name = "", ShortName = "vulkan")] + Vulkan = 2, + + [Display(Name = "", ShortName = "metal")] + Metal = 3, + + [Display(Name = "", ShortName = "rocm")] + ROCM = 4 + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/CapabilitiesModel.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/CapabilitiesModel.cs new file mode 100644 index 0000000..a21f31c --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/CapabilitiesModel.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record CapabilitiesModel + { + [JsonPropertyName("samplers")] + public string[] Samplers { get; set; } + + + [JsonPropertyName("schedulers")] + public string[] Schedulers { get; set; } + + + [JsonPropertyName("defaults_by_mode")] + public DefaultParams DefaultParams { get; set; } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/DefaultParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/DefaultParams.cs new file mode 100644 index 0000000..fcd44e8 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/DefaultParams.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record DefaultParams + { + [JsonPropertyName("img_gen")] + public ImageParams ImageParams { get; set; } + + [JsonPropertyName("vid_gen")] + public VideoParams VideoParams { get; set; } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/GuidanceParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/GuidanceParams.cs new file mode 100644 index 0000000..c16f03b --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/GuidanceParams.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record GuidanceParams + { + [JsonPropertyName("txt_cfg")] + public float? TxtCfg { get; set; } + + [JsonPropertyName("img_cfg")] + public float? ImgCfg { get; set; } + + [JsonPropertyName("distilled_guidance")] + public float? DistilledGuidance { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageParams.cs new file mode 100644 index 0000000..3bc6976 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageParams.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record ImageParams + { + [JsonPropertyName("prompt")] + public string Prompt { get; set; } + + [JsonPropertyName("negative_prompt")] + public string NegativePrompt { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } + + [JsonPropertyName("seed")] + public int Seed { get; set; } + + [JsonPropertyName("strength")] + public double Strength { get; set; } + + [JsonPropertyName("batch_count")] + public int BatchCount { get;} = 1; + + [JsonPropertyName("clip_skip")] + public int ClipSkip { get; set; } = -1; + + [JsonPropertyName("control_strength")] + public double ControlStrength { get; set; } + + [JsonPropertyName("embed_image_metadata")] + public bool EmbedImageMetadata { get; set; } + + [JsonPropertyName("init_image")] + public string InitImage { get; set; } + + [JsonPropertyName("ref_images")] + public List RefImages { get; set; } = []; + + [JsonPropertyName("mask_image")] + public string MaskImage { get; set; } + + [JsonPropertyName("control_image")] + public string ControlImage { get; set; } + + [JsonPropertyName("sample_params")] + public SampleParams SampleParams { get; set; } + + [JsonPropertyName("lora")] + public List Lora { get; set; } = []; + + [JsonPropertyName("vae_tiling_params")] + public VaeTilingParams VaeTilingParams { get; set; } + + [JsonPropertyName("output_format")] + public string OutputFormat { get; set; } = "png"; + + [JsonPropertyName("output_compression")] + public int OutputCompression { get; set; } = 100; + + [JsonPropertyName("auto_resize_ref_image")] + public bool AutoResizeRefImage { get; set; } + + [JsonPropertyName("increase_ref_index")] + public bool IncreaseRefIndex { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageResult.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageResult.cs new file mode 100644 index 0000000..357919a --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/ImageResult.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public sealed class ImageResult + { + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("b64_json")] + public string B64Json { get; set; } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/JobModel.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobModel.cs new file mode 100644 index 0000000..1a460b4 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobModel.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public sealed class JobModel + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("kind")] + public string Kind { get; set; } + + [JsonPropertyName("status")] + public JobStatus Status { get; set; } + + [JsonPropertyName("created")] + public long Created { get; set; } + + [JsonPropertyName("started")] + public long Started { get; set; } + + [JsonPropertyName("completed")] + public long? Completed { get; set; } + + [JsonPropertyName("queue_position")] + public int QueuePosition { get; set; } + + [JsonPropertyName("result")] + public JobResult Result { get; set; } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/JobResult.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobResult.cs new file mode 100644 index 0000000..db7e4ca --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobResult.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public sealed class JobResult + { + [JsonPropertyName("output_format")] + public string OutputFormat { get; set; } + + [JsonPropertyName("mime_type")] + public string MimeType { get; set; } + + [JsonPropertyName("fps")] + public int FrameRate { get; set; } + + [JsonPropertyName("frame_count")] + public int FrameCount { get; set; } + + [JsonPropertyName("images")] + public List Images { get; set; } = []; + + [JsonPropertyName("b64_json")] + public string Video { get; set; } + + public byte[] GetImageBytes(int index = 0) + { + var image = Images.ElementAtOrDefault(index); + if (image == null) + return null; + + return Convert.FromBase64String(image.B64Json); + } + + + public byte[] GetVideoBytes() + { + if (string.IsNullOrEmpty(Video)) + return null; + + return Convert.FromBase64String(Video); + } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/JobStatus.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobStatus.cs new file mode 100644 index 0000000..03a7fc9 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/JobStatus.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public enum JobStatus + { + [JsonStringEnumMemberName("queued")] + Queued = 0, + + [JsonStringEnumMemberName("generating")] + Generating = 1, + + [JsonStringEnumMemberName("completed")] + Completed = 2, + + [JsonStringEnumMemberName("failed")] + Failed = 3, + + [JsonStringEnumMemberName("cancelled")] + Cancelled = 4 + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/LoraParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/LoraParams.cs new file mode 100644 index 0000000..eb3eb2d --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/LoraParams.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record LoraParams + { + [JsonPropertyName("path")] + public string Path { get; set; } = ""; + + [JsonPropertyName("multiplier")] + public float Multiplier { get; set; } = 1.0f; + + [JsonPropertyName("is_high_noise")] + public bool IsHighNoise { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/SampleParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/SampleParams.cs new file mode 100644 index 0000000..2dcf013 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/SampleParams.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record SampleParams + { + [JsonPropertyName("scheduler")] + public string Scheduler { get; set; } + + [JsonPropertyName("sample_method")] + public string SampleMethod { get; set; } + + [JsonPropertyName("sample_steps")] + public int SampleSteps { get; set; } + + [JsonPropertyName("eta")] + public float? Eta { get; set; } + + [JsonPropertyName("shifted_timestep")] + public int ShiftedTimestep { get; set; } + + [JsonPropertyName("custom_sigmas")] + public List CustomSigmas { get; set; } = []; + + [JsonPropertyName("flow_shift")] + public float? FlowShift { get; set; } + + [JsonPropertyName("guidance")] + public GuidanceParams Guidance { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/VaeTilingParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/VaeTilingParams.cs new file mode 100644 index 0000000..ddcb96d --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/VaeTilingParams.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public record VaeTilingParams + { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("temporal_tiling")] + public bool TemporalTiling { get; set; } + + [JsonPropertyName("tile_size_x")] + public int TileSizeX { get; set; } + + [JsonPropertyName("tile_size_y")] + public int TileSizeY { get; set; } + + [JsonPropertyName("target_overlap")] + public float TargetOverlap { get; set; } = 0.5f; + + [JsonPropertyName("rel_size_x")] + public float RelSizeX { get; set; } + + [JsonPropertyName("rel_size_y")] + public float RelSizeY { get; set; } + + [JsonPropertyName("extra_tiling_args")] + public string ExtraTilingArgs { get; set; } = ""; + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Common/VideoParams.cs b/Apps/Amuse.Host.StableDiffusionCpp/Common/VideoParams.cs new file mode 100644 index 0000000..0c51b80 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Common/VideoParams.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Amuse.Host.StableDiffusionCpp.Common +{ + public class VideoParams + { + [JsonPropertyName("prompt")] + public string Prompt { get; set; } + + [JsonPropertyName("negative_prompt")] + public string NegativePrompt { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } + + [JsonPropertyName("seed")] + public int Seed { get; set; } + + [JsonPropertyName("strength")] + public float Strength { get; set; } = 1f; + + [JsonPropertyName("clip_skip")] + public int ClipSkip { get; set; } = -1; + + [JsonPropertyName("video_frames")] + public int Frames { get; set; } = 33; + + [JsonPropertyName("fps")] + public int FrameRate { get; set; } = 16; + + [JsonPropertyName("moe_boundary")] + public float MoeBoundary { get; set; } = 0.875f; + + [JsonPropertyName("vace_strength")] + public float VaceStrength { get; set; } = 1f; + + [JsonPropertyName("init_image")] + public string ImageFirst { get; set; } + + [JsonPropertyName("end_image")] + public string ImageLast { get; set; } + + [JsonPropertyName("control_frames")] + public List ControlFrames { get; set; } + + [JsonPropertyName("sample_params")] + public SampleParams SampleParams { get; set; } + + [JsonPropertyName("high_noise_sample_params")] + public SampleParams SampleParamsHighNoise { get; set; } + + [JsonPropertyName("lora")] + public List Lora { get; set; } = []; + + [JsonPropertyName("vae_tiling_params")] + public VaeTilingParams VaeTilingParams { get; set; } + + [JsonPropertyName("output_format")] + public string OutputFormat { get; set; } = "webm"; + + [JsonPropertyName("output_compression")] + public int OutputCompression { get; set; } = 100; + + [JsonPropertyName("auto_resize_ref_image")] + public bool AutoResizeRefImage { get; set; } + + [JsonPropertyName("increase_ref_index")] + public bool IncreaseRefIndex { get; set; } + } +} \ No newline at end of file diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Config/ModelConfig.cs b/Apps/Amuse.Host.StableDiffusionCpp/Config/ModelConfig.cs new file mode 100644 index 0000000..91a3613 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Config/ModelConfig.cs @@ -0,0 +1,24 @@ +namespace Amuse.Host.StableDiffusionCpp.Config +{ + public class ModelConfig + { + public string Full { get; set; } + public string ClipL { get; set; } + public string ClipG { get; set; } + public string ClipVison { get; set; } + public string T5XXL { get; set; } + public string LLM { get; set; } + public string VisionLLM { get; set; } + public string Diffusion { get; set; } + public string DiffusionHighNoise { get; set; } + public string DiffusionUncond { get; set; } + public string Connectors { get; set; } + public string Vae { get; set; } + public string VaeAudio { get; set; } + public string Tased { get; set; } + public string ControlNet { get; set; } + public string EmbeddingsDirectory { get; set; } + public string LoraModelDirectory { get; set; } + } + +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/Config/ServerConfig.cs b/Apps/Amuse.Host.StableDiffusionCpp/Config/ServerConfig.cs new file mode 100644 index 0000000..08afbc7 --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/Config/ServerConfig.cs @@ -0,0 +1,22 @@ +using Amuse.Common; +using Amuse.Host.StableDiffusionCpp.Common; +using System.Collections.Generic; + +namespace Amuse.Host.StableDiffusionCpp.Config +{ + public record ServerConfig + { + public short Port { get; set; } = 1234; + public string Address { get; set; } = "127.0.0.1"; + public string BaseUrl => $"http://{Address}:{Port}/"; + public Dictionary ServerVariables { get; set; } + public ModelConfig ModelConfig { get; set; } + + // Device/Memory + public int DeviceId { get; set; } + public BackendType Backend { get; set; } + public int MemoryReserve { get; set; } = 1; + public MemoryModeType MemoryMode { get; set; } + public bool IsFlashAttentionEnabled { get; set; } + } +} diff --git a/Apps/Amuse.Host.StableDiffusionCpp/StableDiffusionClient.cs b/Apps/Amuse.Host.StableDiffusionCpp/StableDiffusionClient.cs new file mode 100644 index 0000000..5f931ab --- /dev/null +++ b/Apps/Amuse.Host.StableDiffusionCpp/StableDiffusionClient.cs @@ -0,0 +1,127 @@ +using Amuse.Host.StableDiffusionCpp.Common; +using Amuse.Host.StableDiffusionCpp.Config; +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Amuse.Host.StableDiffusionCpp +{ + internal sealed class StableDiffusionClient : IDisposable + { + private readonly HttpClient _httpClient; + private readonly ServerConfig _configuration; + private readonly JsonSerializerOptions _serializerOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public StableDiffusionClient(ServerConfig configuration) + { + _configuration = configuration; + _httpClient = new HttpClient + { + BaseAddress = new Uri(_configuration.BaseUrl) + }; + _serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + }; + } + + + /// + /// Get model capabilities + /// + /// The cancellation token. + public async Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) + { + const string endpoint = "sdcpp/v1/capabilities"; + using (var response = await _httpClient.GetAsync(endpoint, cancellationToken)) + { + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(_serializerOptions, cancellationToken: cancellationToken); + } + } + + + /// + /// Get existing job + /// + /// The job. + /// The cancellation token. + public async Task GetJobAsync(JobModel job, CancellationToken cancellationToken = default) + { + const string endpoint = "sdcpp/v1/jobs/{0}"; + using (var response = await _httpClient.GetAsync(string.Format(endpoint, job.Id), cancellationToken)) + { + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(_serializerOptions, cancellationToken: cancellationToken); + } + } + + + /// + /// Create new Image job + /// + /// The parameters. + /// The cancellation token. + public async Task CreateJobAsync(ImageParams parameters, CancellationToken cancellationToken = default) + { + const string endpoint = "sdcpp/v1/img_gen"; + using (var response = await _httpClient.PostAsJsonAsync(endpoint, parameters, cancellationToken)) + { + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(_serializerOptions, cancellationToken: cancellationToken); + } + } + + + /// + /// Create new Video job + /// + /// The parameters. + /// The cancellation token. + public async Task CreateJobAsync(VideoParams parameters, CancellationToken cancellationToken = default) + { + const string endpoint = "sdcpp/v1/vid_gen"; + using (var response = await _httpClient.PostAsJsonAsync(endpoint, parameters, cancellationToken)) + { + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(_serializerOptions, cancellationToken: cancellationToken); + + } + } + + + /// + /// Cancel the specified job + /// + /// The job. + /// The cancellation token. + public async Task CancelJobAsync(JobModel job, CancellationToken cancellationToken = default) + { + const string endpoint = "sdcpp/v1/jobs/{0}/cancel"; + using (var response = await _httpClient.GetAsync(string.Format(endpoint, job.Id), cancellationToken)) + { + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + } + } + + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _httpClient?.Dispose(); + } + + } +}