You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
18 KiB
440 lines
18 KiB
using System.Diagnostics; |
|
using System.Text.RegularExpressions; |
|
using NLog; |
|
using StabilityMatrix.Core.Attributes; |
|
using StabilityMatrix.Core.Helper; |
|
using StabilityMatrix.Core.Helper.Cache; |
|
using StabilityMatrix.Core.Helper.HardwareInfo; |
|
using StabilityMatrix.Core.Models.FileInterfaces; |
|
using StabilityMatrix.Core.Models.Progress; |
|
using StabilityMatrix.Core.Processes; |
|
using StabilityMatrix.Core.Python; |
|
using StabilityMatrix.Core.Services; |
|
using YamlDotNet.RepresentationModel; |
|
using YamlDotNet.Serialization; |
|
using YamlDotNet.Serialization.NamingConventions; |
|
|
|
namespace StabilityMatrix.Core.Models.Packages; |
|
|
|
[Singleton(typeof(BasePackage))] |
|
public class ComfyUI : BaseGitPackage |
|
{ |
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
|
public override string Name => "ComfyUI"; |
|
public override string DisplayName { get; set; } = "ComfyUI"; |
|
public override string Author => "comfyanonymous"; |
|
public override string LicenseType => "GPL-3.0"; |
|
public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE"; |
|
public override string Blurb => "A powerful and modular stable diffusion GUI and backend"; |
|
public override string LaunchCommand => "main.py"; |
|
|
|
public override Uri PreviewImageUri => |
|
new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png"); |
|
public override bool ShouldIgnoreReleases => true; |
|
public override bool IsInferenceCompatible => true; |
|
public override string OutputFolderName => "output"; |
|
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; |
|
|
|
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; |
|
|
|
public ComfyUI( |
|
IGithubApiCache githubApi, |
|
ISettingsManager settingsManager, |
|
IDownloadService downloadService, |
|
IPrerequisiteHelper prerequisiteHelper |
|
) |
|
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } |
|
|
|
// https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11 |
|
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders => |
|
new() |
|
{ |
|
[SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, |
|
[SharedFolderType.Diffusers] = new[] { "models/diffusers" }, |
|
[SharedFolderType.Lora] = new[] { "models/loras" }, |
|
[SharedFolderType.CLIP] = new[] { "models/clip" }, |
|
[SharedFolderType.TextualInversion] = new[] { "models/embeddings" }, |
|
[SharedFolderType.VAE] = new[] { "models/vae" }, |
|
[SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" }, |
|
[SharedFolderType.ControlNet] = new[] { "models/controlnet" }, |
|
[SharedFolderType.GLIGEN] = new[] { "models/gligen" }, |
|
[SharedFolderType.ESRGAN] = new[] { "models/upscale_models" }, |
|
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, |
|
}; |
|
|
|
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders => |
|
new() { [SharedOutputType.Text2Img] = new[] { "output" } }; |
|
|
|
public override List<LaunchOptionDefinition> LaunchOptions => |
|
new List<LaunchOptionDefinition> |
|
{ |
|
new() |
|
{ |
|
Name = "Host", |
|
Type = LaunchOptionType.String, |
|
DefaultValue = "127.0.0.1", |
|
Options = { "--listen" } |
|
}, |
|
new() |
|
{ |
|
Name = "Port", |
|
Type = LaunchOptionType.String, |
|
DefaultValue = "8188", |
|
Options = { "--port" } |
|
}, |
|
new() |
|
{ |
|
Name = "VRAM", |
|
Type = LaunchOptionType.Bool, |
|
InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch |
|
{ |
|
MemoryLevel.Low => "--lowvram", |
|
MemoryLevel.Medium => "--normalvram", |
|
_ => null |
|
}, |
|
Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } |
|
}, |
|
new() |
|
{ |
|
Name = "Preview Method", |
|
Type = LaunchOptionType.Bool, |
|
InitialValue = "--preview-method auto", |
|
Options = { "--preview-method auto", "--preview-method latent2rgb", "--preview-method taesd" } |
|
}, |
|
new() |
|
{ |
|
Name = "Enable DirectML", |
|
Type = LaunchOptionType.Bool, |
|
InitialValue = HardwareHelper.PreferDirectML(), |
|
Options = { "--directml" } |
|
}, |
|
new() |
|
{ |
|
Name = "Use CPU only", |
|
Type = LaunchOptionType.Bool, |
|
InitialValue = !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(), |
|
Options = { "--cpu" } |
|
}, |
|
new() |
|
{ |
|
Name = "Disable Xformers", |
|
Type = LaunchOptionType.Bool, |
|
InitialValue = !HardwareHelper.HasNvidiaGpu(), |
|
Options = { "--disable-xformers" } |
|
}, |
|
new() |
|
{ |
|
Name = "Disable upcasting of attention", |
|
Type = LaunchOptionType.Bool, |
|
Options = { "--dont-upcast-attention" } |
|
}, |
|
new() |
|
{ |
|
Name = "Auto-Launch", |
|
Type = LaunchOptionType.Bool, |
|
Options = { "--auto-launch" } |
|
}, |
|
LaunchOptionDefinition.Extras |
|
}; |
|
|
|
public override string MainBranch => "master"; |
|
|
|
public override IEnumerable<TorchVersion> AvailableTorchVersions => |
|
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm, TorchVersion.Mps }; |
|
|
|
public override async Task InstallPackage( |
|
string installLocation, |
|
TorchVersion torchVersion, |
|
SharedFolderMethod selectedSharedFolderMethod, |
|
DownloadPackageVersionOptions versionOptions, |
|
IProgress<ProgressReport>? progress = null, |
|
Action<ProcessOutput>? onConsoleOutput = null |
|
) |
|
{ |
|
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); |
|
// Setup venv |
|
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv")); |
|
venvRunner.WorkingDirectory = installLocation; |
|
await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false); |
|
|
|
// Install torch / xformers based on gpu info |
|
switch (torchVersion) |
|
{ |
|
case TorchVersion.Cpu: |
|
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); |
|
break; |
|
case TorchVersion.Cuda: |
|
await venvRunner |
|
.PipInstall( |
|
new PipInstallArgs() |
|
.WithTorch("~=2.1.0") |
|
.WithTorchVision() |
|
.WithXFormers("==0.0.22.post4") |
|
.AddArg("--upgrade") |
|
.WithTorchExtraIndex("cu121"), |
|
onConsoleOutput |
|
) |
|
.ConfigureAwait(false); |
|
break; |
|
case TorchVersion.DirectMl: |
|
await venvRunner |
|
.PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput) |
|
.ConfigureAwait(false); |
|
break; |
|
case TorchVersion.Rocm: |
|
await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); |
|
break; |
|
case TorchVersion.Mps: |
|
await venvRunner |
|
.PipInstall( |
|
new PipInstallArgs() |
|
.AddArg("--pre") |
|
.WithTorch() |
|
.WithTorchVision() |
|
.WithTorchExtraIndex("nightly/cpu"), |
|
onConsoleOutput |
|
) |
|
.ConfigureAwait(false); |
|
break; |
|
default: |
|
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); |
|
} |
|
|
|
// Install requirements file (skip torch) |
|
progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)); |
|
|
|
var requirementsFile = new FilePath(installLocation, "requirements.txt"); |
|
|
|
await venvRunner |
|
.PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch") |
|
.ConfigureAwait(false); |
|
|
|
progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)); |
|
} |
|
|
|
private async Task AutoDetectAndInstallTorch(PyVenvRunner venvRunner, IProgress<ProgressReport>? progress = null) |
|
{ |
|
var gpus = HardwareHelper.IterGpuInfo().ToList(); |
|
if (gpus.Any(g => g.IsNvidia)) |
|
{ |
|
await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false); |
|
} |
|
else if (HardwareHelper.PreferRocm()) |
|
{ |
|
await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false); |
|
} |
|
else if (HardwareHelper.PreferDirectML()) |
|
{ |
|
await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false); |
|
} |
|
else |
|
{ |
|
await InstallCpuTorch(venvRunner, progress).ConfigureAwait(false); |
|
} |
|
} |
|
|
|
public override async Task RunPackage( |
|
string installedPackagePath, |
|
string command, |
|
string arguments, |
|
Action<ProcessOutput>? onConsoleOutput |
|
) |
|
{ |
|
await SetupVenv(installedPackagePath).ConfigureAwait(false); |
|
|
|
void HandleConsoleOutput(ProcessOutput s) |
|
{ |
|
onConsoleOutput?.Invoke(s); |
|
|
|
if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) |
|
{ |
|
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); |
|
var match = regex.Match(s.Text); |
|
if (match.Success) |
|
{ |
|
WebUrl = match.Value; |
|
} |
|
OnStartupComplete(WebUrl); |
|
} |
|
} |
|
|
|
void HandleExit(int i) |
|
{ |
|
Debug.WriteLine($"Venv process exited with code {i}"); |
|
OnExit(i); |
|
} |
|
|
|
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; |
|
|
|
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); |
|
} |
|
|
|
public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) |
|
{ |
|
switch (sharedFolderMethod) |
|
{ |
|
case SharedFolderMethod.None: |
|
return Task.CompletedTask; |
|
case SharedFolderMethod.Symlink: |
|
return base.SetupModelFolders(installDirectory, sharedFolderMethod); |
|
} |
|
|
|
var extraPathsYamlPath = installDirectory + "extra_model_paths.yaml"; |
|
var modelsDir = SettingsManager.ModelsDirectory; |
|
|
|
var exists = File.Exists(extraPathsYamlPath); |
|
if (!exists) |
|
{ |
|
Logger.Info("Creating extra_model_paths.yaml"); |
|
File.WriteAllText(extraPathsYamlPath, string.Empty); |
|
} |
|
var yaml = File.ReadAllText(extraPathsYamlPath); |
|
using var sr = new StringReader(yaml); |
|
var yamlStream = new YamlStream(); |
|
yamlStream.Load(sr); |
|
|
|
if (!yamlStream.Documents.Any()) |
|
{ |
|
yamlStream.Documents.Add(new YamlDocument(new YamlMappingNode())); |
|
} |
|
|
|
var root = yamlStream.Documents[0].RootNode; |
|
if (root is not YamlMappingNode mappingNode) |
|
{ |
|
throw new Exception("Invalid extra_model_paths.yaml"); |
|
} |
|
// check if we have a child called "stability_matrix" |
|
var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(c => c.Key.ToString() == "stability_matrix"); |
|
|
|
if (stabilityMatrixNode.Key != null) |
|
{ |
|
if (stabilityMatrixNode.Value is not YamlMappingNode nodeValue) |
|
return Task.CompletedTask; |
|
|
|
nodeValue.Children["checkpoints"] = Path.Combine(modelsDir, "StableDiffusion"); |
|
nodeValue.Children["vae"] = Path.Combine(modelsDir, "VAE"); |
|
nodeValue.Children["loras"] = |
|
$"{Path.Combine(modelsDir, "Lora")}\n" + $"{Path.Combine(modelsDir, "LyCORIS")}"; |
|
nodeValue.Children["upscale_models"] = |
|
$"{Path.Combine(modelsDir, "ESRGAN")}\n" |
|
+ $"{Path.Combine(modelsDir, "RealESRGAN")}\n" |
|
+ $"{Path.Combine(modelsDir, "SwinIR")}"; |
|
nodeValue.Children["embeddings"] = Path.Combine(modelsDir, "TextualInversion"); |
|
nodeValue.Children["hypernetworks"] = Path.Combine(modelsDir, "Hypernetwork"); |
|
nodeValue.Children["controlnet"] = Path.Combine(modelsDir, "ControlNet"); |
|
nodeValue.Children["clip"] = Path.Combine(modelsDir, "CLIP"); |
|
nodeValue.Children["diffusers"] = Path.Combine(modelsDir, "Diffusers"); |
|
nodeValue.Children["gligen"] = Path.Combine(modelsDir, "GLIGEN"); |
|
nodeValue.Children["vae_approx"] = Path.Combine(modelsDir, "ApproxVAE"); |
|
} |
|
else |
|
{ |
|
stabilityMatrixNode = new KeyValuePair<YamlNode, YamlNode>( |
|
new YamlScalarNode("stability_matrix"), |
|
new YamlMappingNode |
|
{ |
|
{ "checkpoints", Path.Combine(modelsDir, "StableDiffusion") }, |
|
{ "vae", Path.Combine(modelsDir, "VAE") }, |
|
{ "loras", $"{Path.Combine(modelsDir, "Lora")}\n{Path.Combine(modelsDir, "LyCORIS")}" }, |
|
{ |
|
"upscale_models", |
|
$"{Path.Combine(modelsDir, "ESRGAN")}\n{Path.Combine(modelsDir, "RealESRGAN")}\n{Path.Combine(modelsDir, "SwinIR")}" |
|
}, |
|
{ "embeddings", Path.Combine(modelsDir, "TextualInversion") }, |
|
{ "hypernetworks", Path.Combine(modelsDir, "Hypernetwork") }, |
|
{ "controlnet", Path.Combine(modelsDir, "ControlNet") }, |
|
{ "clip", Path.Combine(modelsDir, "CLIP") }, |
|
{ "diffusers", Path.Combine(modelsDir, "Diffusers") }, |
|
{ "gligen", Path.Combine(modelsDir, "GLIGEN") }, |
|
{ "vae_approx", Path.Combine(modelsDir, "ApproxVAE") } |
|
} |
|
); |
|
} |
|
|
|
var newRootNode = new YamlMappingNode(); |
|
foreach (var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix")) |
|
{ |
|
newRootNode.Children.Add(child); |
|
} |
|
|
|
newRootNode.Children.Add(stabilityMatrixNode); |
|
|
|
var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); |
|
var yamlData = serializer.Serialize(newRootNode); |
|
File.WriteAllText(extraPathsYamlPath, yamlData); |
|
|
|
return Task.CompletedTask; |
|
} |
|
|
|
public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) => |
|
sharedFolderMethod switch |
|
{ |
|
SharedFolderMethod.Symlink => base.UpdateModelFolders(installDirectory, sharedFolderMethod), |
|
SharedFolderMethod.Configuration => SetupModelFolders(installDirectory, sharedFolderMethod), |
|
SharedFolderMethod.None => Task.CompletedTask, |
|
_ => Task.CompletedTask |
|
}; |
|
|
|
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) |
|
{ |
|
return sharedFolderMethod switch |
|
{ |
|
SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory), |
|
SharedFolderMethod.None => Task.CompletedTask, |
|
SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), |
|
_ => Task.CompletedTask |
|
}; |
|
} |
|
|
|
private Task RemoveConfigSection(string installDirectory) |
|
{ |
|
var extraPathsYamlPath = Path.Combine(installDirectory, "extra_model_paths.yaml"); |
|
var exists = File.Exists(extraPathsYamlPath); |
|
if (!exists) |
|
{ |
|
return Task.CompletedTask; |
|
} |
|
|
|
var yaml = File.ReadAllText(extraPathsYamlPath); |
|
using var sr = new StringReader(yaml); |
|
var yamlStream = new YamlStream(); |
|
yamlStream.Load(sr); |
|
|
|
if (!yamlStream.Documents.Any()) |
|
{ |
|
return Task.CompletedTask; |
|
} |
|
|
|
var root = yamlStream.Documents[0].RootNode; |
|
if (root is not YamlMappingNode mappingNode) |
|
{ |
|
return Task.CompletedTask; |
|
} |
|
|
|
mappingNode.Children.Remove("stability_matrix"); |
|
|
|
var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); |
|
var yamlData = serializer.Serialize(mappingNode); |
|
File.WriteAllText(extraPathsYamlPath, yamlData); |
|
|
|
return Task.CompletedTask; |
|
} |
|
|
|
private async Task InstallRocmTorch( |
|
PyVenvRunner venvRunner, |
|
IProgress<ProgressReport>? progress = null, |
|
Action<ProcessOutput>? onConsoleOutput = null |
|
) |
|
{ |
|
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)); |
|
|
|
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); |
|
|
|
await venvRunner |
|
.PipInstall( |
|
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.6"), |
|
onConsoleOutput |
|
) |
|
.ConfigureAwait(false); |
|
} |
|
}
|
|
|