Skip to content

Commit 90b7f37

Browse files
authored
Merge branch 'Acly:main' into add_dynamic_prompts
2 parents 18678be + be31d15 commit 90b7f37

File tree

9 files changed

+229
-81
lines changed

9 files changed

+229
-81
lines changed

ai_diffusion/image.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ def restrict(bounds: "Bounds", within: "Bounds"):
202202
"""Restrict bounds to be inside another bounds."""
203203
x = max(within.x, bounds.x)
204204
y = max(within.y, bounds.y)
205-
width = min(within.x + within.width, bounds.x + bounds.width) - x
206-
height = min(within.y + within.height, bounds.y + bounds.height) - y
205+
width = max(0, min(within.x + within.width, bounds.x + bounds.width) - x)
206+
height = max(0, min(within.y + within.height, bounds.y + bounds.height) - y)
207207
return Bounds(x, y, width, height)
208208

209209
@staticmethod

ai_diffusion/presets/models.json

-10
Original file line numberDiff line numberDiff line change
@@ -347,16 +347,6 @@
347347
}
348348
]
349349
},
350-
{
351-
"id": "controlnet-hands-sd15",
352-
"name": "ControlNet Hand Refiner",
353-
"files": [
354-
{
355-
"path": "models/controlnet/control_sd15_inpaint_depth_hand_fp16.safetensors",
356-
"url": "https://huggingface.co/hr16/ControlNet-HandRefiner-pruned/resolve/main/control_sd15_inpaint_depth_hand_fp16.safetensors"
357-
}
358-
]
359-
},
360350
{
361351
"id": "ip_adapter-face-sd15",
362352
"name": "IP-Adapter Face (SD1.5)",

ai_diffusion/server.py

+94-66
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
import shutil
77
import re
8+
import os
89
from typing import Callable, NamedTuple, Optional, Union
910
from PyQt5.QtNetwork import QNetworkAccessManager
1011

@@ -48,11 +49,13 @@ class Server:
4849
comfy_dir: Optional[Path] = None
4950
version: Optional[str] = None
5051

52+
_uv_cmd: Optional[Path] = None
5153
_python_cmd: Optional[Path] = None
5254
_cache_dir: Path
5355
_version_file: Path
5456
_process: Optional[asyncio.subprocess.Process] = None
5557
_task: Optional[asyncio.Task] = None
58+
_installed_backend: Optional[ServerBackend] = None
5659

5760
def __init__(self, path: Optional[str] = None):
5861
self.path = Path(path or settings.server_path)
@@ -66,22 +69,38 @@ def check_install(self):
6669
self._cache_dir = self.path / ".cache"
6770

6871
self._version_file = self.path / ".version"
72+
self.version = None
6973
if self._version_file.exists():
70-
self.version = self._version_file.read_text().strip()
71-
log.info(f"Found server installation v{self.version} at {self.path}")
72-
else:
73-
self.version = None
74+
content = self._version_file.read_text().strip().split()
75+
if len(content) > 0:
76+
self.version = content[0]
77+
if len(content) > 1 and content[1] in ServerBackend.__members__:
78+
self._installed_backend = ServerBackend[content[1]]
79+
if self.version is not None:
80+
backend = f" [{self._installed_backend.name}]" if self._installed_backend else ""
81+
log.info(f"Found server installation v{self.version}{backend} at {self.path}")
7482

7583
comfy_pkg = ["main.py", "nodes.py", "custom_nodes"]
7684
self.comfy_dir = _find_component(comfy_pkg, [self.path / "ComfyUI"])
7785

78-
python_pkg = ["python3.dll", "python.exe"] if is_windows else ["python3", "pip3"]
79-
python_search_paths = [self.path / "python", self.path / "venv" / "bin"]
86+
uv_exe = "uv.exe" if is_windows else "uv"
87+
self._uv_cmd = self.path / "uv" / uv_exe
88+
if not self._uv_cmd.exists():
89+
self._uv_cmd = None
90+
self._uv_cmd = _find_program(uv_exe)
91+
92+
python_pkg = ["python.exe"] if is_windows else ["python3"]
93+
python_search_paths = [
94+
self.path / "python",
95+
self.path / "venv" / "bin",
96+
self.path / "venv" / "Scripts",
97+
]
8098
python_path = _find_component(python_pkg, python_search_paths)
8199
if python_path is None:
82-
self._python_cmd = _find_program(
83-
"python3.12", "python3.11", "python3.10", "python3", "python"
84-
)
100+
if not is_windows:
101+
self._python_cmd = _find_program(
102+
"python3.12", "python3.11", "python3.10", "python3", "python"
103+
)
85104
else:
86105
self._python_cmd = python_path / f"python{_exe}"
87106

@@ -124,16 +143,15 @@ async def _install(self, cb: InternalCB):
124143
self._cache_dir.mkdir(parents=True, exist_ok=True)
125144
self._version_file.write_text("incomplete")
126145

127-
if is_windows and (self.comfy_dir is None or self._python_cmd is None):
128-
# On Windows install an embedded version of Python
129-
python_dir = self.path / "python"
130-
self._python_cmd = python_dir / f"python{_exe}"
131-
await install_if_missing(python_dir, self._install_python, network, cb)
132-
elif not is_windows and (self.comfy_dir is None or not (self.path / "venv").exists()):
133-
# On Linux a system Python is required to create a virtual environment
146+
has_venv = (self.path / "venv").exists()
147+
has_embedded_python = (self.path / "python").exists()
148+
has_uv = self._uv_cmd is not None
149+
if not any((has_venv, has_embedded_python, has_uv)):
150+
await try_install(self.path / "uv", self._install_uv, network, cb)
151+
152+
if self.comfy_dir is None or not (has_venv or has_embedded_python):
134153
python_dir = self.path / "venv"
135154
await install_if_missing(python_dir, self._create_venv, cb)
136-
self._python_cmd = python_dir / "bin" / "python3"
137155
assert self._python_cmd is not None
138156
await self._log_python_version()
139157
await determine_system_encoding(str(self._python_cmd))
@@ -146,57 +164,59 @@ async def _install(self, cb: InternalCB):
146164
dir = comfy_dir / "custom_nodes" / pkg.folder
147165
await install_if_missing(dir, self._install_custom_node, pkg, network, cb)
148166

149-
self._version_file.write_text(resources.version)
167+
self._version_file.write_text(f"{resources.version} {self.backend.name}")
150168
self.state = ServerState.stopped
151169
cb("Finished", f"Installation finished in {self.path}")
152170
self.check_install()
153171

154172
async def _log_python_version(self):
173+
if self._uv_cmd is not None:
174+
uv_ver = await get_python_version_string(self._uv_cmd)
175+
log.info(f"Using uv: {uv_ver}")
155176
if self._python_cmd is not None:
156177
python_ver = await get_python_version_string(self._python_cmd)
157178
log.info(f"Using Python: {python_ver}, {self._python_cmd}")
158-
pip_ver = await get_python_version_string(self._python_cmd, "-m", "pip")
159-
log.info(f"Using pip: {pip_ver}")
160-
161-
def _pip_install(self, *args):
162-
return [self._python_cmd, "-su", "-m", "pip", "install", *args]
163-
164-
async def _install_python(self, network: QNetworkAccessManager, cb: InternalCB):
165-
url = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip"
166-
archive_path = self._cache_dir / "python-3.11.9-embed-amd64.zip"
167-
dir = self.path / "python"
168-
169-
await _download_cached("Python", network, url, archive_path, cb)
170-
await _extract_archive("Python", archive_path, dir, cb)
171-
172-
python_pth = dir / "python311._pth"
173-
cb("Installing Python", f"Patching {python_pth}")
174-
with open(python_pth, "a") as file:
175-
file.write("import site\n")
176-
177-
git_pip_url = "https://bootstrap.pypa.io/get-pip.py"
178-
get_pip_file = dir / "get-pip.py"
179-
await _download_cached("Python", network, git_pip_url, get_pip_file, cb)
180-
await _execute_process("Python", [self._python_cmd, get_pip_file], dir, cb)
181-
await _execute_process("Python", self._pip_install("wheel", "setuptools"), dir, cb)
179+
if self._uv_cmd is None:
180+
pip_ver = await get_python_version_string(self._python_cmd, "-m", "pip")
181+
log.info(f"Using pip: {pip_ver}")
182+
183+
def _pip_install(self, name: str, args: list[str], cb: InternalCB):
184+
env = None
185+
if self._uv_cmd is not None:
186+
env = {"VIRTUAL_ENV": str(self.path / "venv")}
187+
cmd = [self._uv_cmd, "pip", "install", *args]
188+
else:
189+
cmd = [self._python_cmd, "-su", "-m", "pip", "install", *args]
190+
return _execute_process(name, cmd, self.path, cb, env=env)
191+
192+
async def _install_uv(self, network: QNetworkAccessManager, cb: InternalCB):
193+
script_ext = ".ps1" if is_windows else ".sh"
194+
url = f"https://astral.sh/uv/0.6.10/install{script_ext}"
195+
script_path = self._cache_dir / f"install_uv{script_ext}"
196+
await _download_cached("Python", network, url, script_path, cb)
197+
198+
if is_windows:
199+
del os.environ["PSModulePath"] # Don't inherit this from parent process
200+
cmd = ["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script_path)]
201+
else:
202+
cmd = ["/bin/sh", str(script_path)]
203+
env = {"UV_INSTALL_DIR": str(self.path / "uv")}
204+
await _execute_process("Python", cmd, self.path, cb, env=env)
182205

183-
cb("Installing Python", f"Patching {python_pth}")
184-
_prepend_file(python_pth, "../ComfyUI\n")
185-
cb("Installing Python", "Finished installing Python")
206+
self._uv_cmd = self.path / "uv" / ("uv" + _exe)
207+
cb("Installing Python", f"Installed uv at {self._uv_cmd}")
186208

187209
async def _create_venv(self, cb: InternalCB):
188210
cb("Creating Python virtual environment", f"Creating venv in {self.path / 'venv'}")
189-
assert self._python_cmd is not None
190-
python_version, major, minor = await get_python_version(self._python_cmd)
191-
if major is not None and minor is not None and (major < 3 or minor < 9):
192-
raise Exception(
193-
_(
194-
"Python version 3.9 or higher is required, but found {version} at {location}. Please make sure a compatible version of Python is installed and can be found by the Krita process."
195-
).format(version=python_version, location=self._python_cmd)
196-
)
197-
venv_cmd = [self._python_cmd, "-m", "venv", "venv"]
211+
assert self._uv_cmd is not None
212+
venv_cmd = [self._uv_cmd, "venv", "--python", "3.12", str(self.path / "venv")]
198213
await _execute_process("Python", venv_cmd, self.path, cb)
199214

215+
if is_windows:
216+
self._python_cmd = self.path / "venv" / "Scripts" / "python.exe"
217+
else:
218+
self._python_cmd = self.path / "venv" / "bin" / "python3"
219+
200220
async def _install_comfy(self, comfy_dir: Path, network: QNetworkAccessManager, cb: InternalCB):
201221
url = f"{resources.comfy_url}/archive/{resources.comfy_version}.zip"
202222
archive_path = self._cache_dir / f"ComfyUI-{resources.comfy_version}.zip"
@@ -211,10 +231,13 @@ async def _install_comfy(self, comfy_dir: Path, network: QNetworkAccessManager,
211231
torch_args += ["--index-url", "https://download.pytorch.org/whl/cu124"]
212232
elif self.backend is ServerBackend.directml:
213233
torch_args = ["numpy<2", "torch-directml"]
214-
await _execute_process("PyTorch", self._pip_install(*torch_args), self.path, cb)
234+
await self._pip_install("PyTorch", torch_args, cb)
235+
236+
requirements_txt = Path(__file__).parent / "server_requirements.txt"
237+
await self._pip_install("ComfyUI", ["-r", str(requirements_txt)], cb)
215238

216239
requirements_txt = temp_comfy_dir / "requirements.txt"
217-
await _execute_process("ComfyUI", self._pip_install("-r", requirements_txt), self.path, cb)
240+
await self._pip_install("ComfyUI", ["-r", str(requirements_txt)], cb)
218241

219242
_configure_extra_model_paths(temp_comfy_dir)
220243
await rename_extracted_folder("ComfyUI", comfy_dir, resources.comfy_version)
@@ -233,26 +256,22 @@ async def _install_custom_node(
233256
await _download_cached(pkg.name, network, resource_url, resource_zip_path, cb)
234257
await _extract_archive(pkg.name, resource_zip_path, folder.parent, cb)
235258
await rename_extracted_folder(pkg.name, folder, pkg.version)
236-
237-
requirements_txt = folder / "requirements.txt"
238-
if requirements_txt.exists():
239-
await _execute_process(pkg.name, self._pip_install("-r", requirements_txt), folder, cb)
240259
cb(f"Installing {pkg.name}", f"Finished installing {pkg.name}")
241260

242261
async def _install_insightface(self, network: QNetworkAccessManager, cb: InternalCB):
243262
assert self.comfy_dir is not None and self._python_cmd is not None
244263

245264
dependencies = ["onnx==1.16.1", "onnxruntime"] # onnx version pinned due to #1033
246-
await _execute_process("FaceID", self._pip_install(*dependencies), self.path, cb)
265+
await self._pip_install("FaceID", dependencies, cb)
247266

248267
pyver = await get_python_version_string(self._python_cmd)
249268
if is_windows and "3.11" in pyver:
250269
whl_file = self._cache_dir / "insightface-0.7.3-cp311-cp311-win_amd64.whl"
251270
whl_url = "https://github.com/bihailantian655/insightface_wheel/raw/main/insightface-0.7.3-cp311-cp311-win_amd64%20(1).whl"
252271
await _download_cached("FaceID", network, whl_url, whl_file, cb)
253-
await _execute_process("FaceID", self._pip_install(whl_file), self.path, cb)
272+
await self._pip_install("FaceID", [str(whl_file)], cb)
254273
else:
255-
await _execute_process("FaceID", self._pip_install("insightface"), self.path, cb)
274+
await self._pip_install("FaceID", ["insightface"], cb)
256275

257276
async def _install_requirements(
258277
self, requirements: ModelRequirements, network: QNetworkAccessManager, cb: InternalCB
@@ -519,11 +538,18 @@ def can_install(self):
519538

520539
@property
521540
def upgrade_required(self):
541+
backend_mismatch = (
542+
self._installed_backend is not None
543+
and self._installed_backend != self.backend
544+
and self.backend in [ServerBackend.cuda, ServerBackend.directml]
545+
and self._installed_backend
546+
in [ServerBackend.cuda, ServerBackend.directml, ServerBackend.cpu]
547+
)
522548
return (
523549
self.state is not ServerState.not_installed
524550
and self.version is not None
525551
and self.version != "incomplete"
526-
and self.version != resources.version
552+
and (self.version != resources.version or backend_mismatch)
527553
)
528554

529555

@@ -563,12 +589,14 @@ async def _extract_archive(name: str, archive: Path, target: Path, cb: InternalC
563589
zip_file.extractall(target)
564590

565591

566-
async def _execute_process(name: str, cmd: list, cwd: Path, cb: InternalCB):
592+
async def _execute_process(
593+
name: str, cmd: list, cwd: Path, cb: InternalCB, env: dict | None = None
594+
):
567595
errlog = ""
568596

569597
cmd = [str(c) for c in cmd]
570598
cb(f"Installing {name}", f"Executing {' '.join(cmd)}")
571-
process = await create_process(cmd[0], *cmd[1:], cwd=cwd, pipe_stderr=True)
599+
process = await create_process(cmd[0], *cmd[1:], cwd=cwd, additional_env=env, pipe_stderr=True)
572600

573601
async def forward(stream: asyncio.StreamReader):
574602
async for line in stream:

ai_diffusion/server_requirements.txt

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This file was autogenerated by uv via the following command:
2+
# uv pip compile scripts/server_requirements.in.txt --no-deps --no-annotate --universal -o ai_diffusion/server_requirements.txt
3+
addict==2.4.0
4+
aiohttp==3.11.14
5+
albumentations==2.0.5
6+
argostranslate==1.9.6
7+
av==14.2.0
8+
einops==0.8.1
9+
filelock==3.18.0
10+
ftfy==6.3.1
11+
fvcore==0.1.5.post20221221
12+
gguf==0.14.0
13+
huggingface-hub==0.29.3
14+
importlib-metadata==8.6.1
15+
kornia==0.8.0
16+
matplotlib==3.10.1
17+
mediapipe==0.10.21
18+
omegaconf==2.3.0
19+
opencv-python==4.11.0.86
20+
pillow==11.1.0
21+
psutil==7.0.0
22+
python-dateutil==2.9.0.post0
23+
pyyaml==6.0.2
24+
safetensors==0.5.3
25+
scikit-image==0.25.2
26+
scikit-learn==1.6.1
27+
scipy==1.15.2
28+
sentencepiece==0.2.0
29+
soundfile==0.13.1
30+
spandrel==0.4.1
31+
svglib==1.5.1
32+
tokenizers==0.21.1
33+
torchsde==0.2.6
34+
tqdm==4.67.1
35+
transformers==4.50.3
36+
yacs==0.1.8
37+
yapf==0.43.0
38+
yarl==1.18.3

ai_diffusion/ui/actions.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def generate():
1212
model.generate_live()
1313
elif model.workspace is Workspace.animation:
1414
model.animation.generate()
15+
elif model.workspace is Workspace.custom:
16+
model.custom.generate()
1517

1618

1719
def cancel_active():

ai_diffusion/ui/custom_workflow.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def value(self, value: str):
363363
)
364364

365365

366-
def _create_param_widget(param: CustomParam, parent: QWidget) -> CustomParamWidget:
366+
def _create_param_widget(param: CustomParam, parent: "WorkflowParamsWidget") -> CustomParamWidget:
367367
match param.kind:
368368
case ParamKind.image_layer:
369369
return LayerSelect("image", parent)
@@ -378,7 +378,9 @@ def _create_param_widget(param: CustomParam, parent: QWidget) -> CustomParamWidg
378378
case ParamKind.text:
379379
return TextParamWidget(param, parent)
380380
case ParamKind.prompt_positive | ParamKind.prompt_negative:
381-
return PromptParamWidget(param, parent)
381+
w = PromptParamWidget(param, parent)
382+
w.activated.connect(parent.activated)
383+
return w
382384
case ParamKind.choice:
383385
return ChoiceParamWidget(param, parent)
384386
case ParamKind.style:
@@ -427,6 +429,7 @@ def _reset_group(self):
427429

428430
class WorkflowParamsWidget(QWidget):
429431
value_changed = pyqtSignal()
432+
activated = pyqtSignal()
430433

431434
def __init__(self, params: list[CustomParam], parent: QWidget | None = None):
432435
super().__init__(parent)
@@ -858,6 +861,7 @@ def _update_current_workflow(self):
858861
self._params_widget.value = self.model.custom.params # set default values from model
859862
self.model.custom.params = self._params_widget.value # set default values from widgets
860863
self._params_widget.value_changed.connect(self._change_params)
864+
self._params_widget.activated.connect(self._generate)
861865

862866
self._params_scroll.setWidget(self._params_widget)
863867
params_size = min(self.height() // 2, self._params_widget.min_size)

0 commit comments

Comments
 (0)