Skip to content

Commit

Permalink
Merge pull request #5 from ComfyWorkflows/windows
Browse files Browse the repository at this point in the history
Initial windows support
  • Loading branch information
thecooltechguy authored Feb 26, 2024
2 parents 354c737 + 0837e4d commit ddb2362
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 78 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
comfyui_launcher_models/
comfyui_launcher_projects/
comfyui_launcher_projects/
venv/
.DS_Store
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,4 @@ requests==2.31.0
show-in-file-manager==1.1.4
tqdm==4.66.2
urllib3==2.2.0
virtualenv==20.25.1
Werkzeug==3.0.1
2 changes: 1 addition & 1 deletion server/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"credentials": {"civitai": {"apikey": ""}}}
{"credentials": {"civitai": {"apikey": "asd"}}}
19 changes: 15 additions & 4 deletions server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
get_launcher_state,
is_launcher_json_format,
is_port_in_use,
run_command,
run_command_in_project_comfyui_venv,
set_config,
set_launcher_state_data,
Expand Down Expand Up @@ -123,7 +124,7 @@ def create_project():
create_comfyui_project(
project_path, models_path, id=id, name=name, launcher_json=launcher_json
)
return jsonify({"success": True})
return jsonify({"success": True, "id": id})


@app.route("/api/import_project", methods=["POST"])
Expand All @@ -149,7 +150,7 @@ def import_project():
create_comfyui_project(
project_path, models_path, id=id, name=name, launcher_json=launcher_json
)
return jsonify({"success": True})
return jsonify({"success": True, "id": id})


@app.route("/api/projects/<id>/start", methods=["POST"])
Expand All @@ -167,9 +168,19 @@ def start_project(id):
assert port, "No free port found"
assert not is_port_in_use(port), f"Port {port} is already in use"

# # start the project
# pid = run_command_in_project_comfyui_venv(
# project_path, f"python main.py --port {port}", in_bg=True
# )
# assert pid, "Failed to start the project"

# start the project
command = f"python main.py --port {port}"
if os.name == "nt":
command = f"start \"\" cmd /c \"{command}\""

pid = run_command_in_project_comfyui_venv(
project_path, f"python main.py --port {port}", in_bg=True
project_path, command, in_bg=True
)
assert pid, "Failed to start the project"

Expand All @@ -184,7 +195,7 @@ def start_project(id):
set_launcher_state_data(
project_path, {"state": "running", "port": port, "pid": pid}
)
return jsonify({"success": True})
return jsonify({"success": True, "port": port})


@app.route("/api/projects/<id>/stop", methods=["POST"])
Expand Down
14 changes: 6 additions & 8 deletions server/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@
# create_comfyui_project(project_path, models_path, id="proj2", name="proj2", launcher_json=launcher_json)
# run_command_in_project_comfyui_venv(project_path, f"python main.py --port 4000")

project_path = "./test_projects/proj3"
workflow_json_path = "./latentspace_desert_dancer_comfyworkflows.json"
if not os.path.exists(project_path):
with open(workflow_json_path, "r") as f:
workflow_json = json.load(f)
launcher_json = get_launcher_json_for_workflow_json(workflow_json)
create_comfyui_project(project_path, models_path, id="proj3", name="proj3", launcher_json=launcher_json)
run_command_in_project_comfyui_venv(project_path, f"python main.py --port 4000")
workflow_json_path = "./subby_ramesh_ultra_upscale_comfyworkflows.json"
with open(workflow_json_path, "r") as f:
workflow_json = json.load(f)
launcher_json = get_launcher_json_for_workflow_json(workflow_json)
with open("./subby_ramesh_ultra_upscale_comfyui-launcher.json", "w") as f:
json.dump(launcher_json, f, indent=4)
56 changes: 56 additions & 0 deletions server/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# takes in an argument: the endpoint of the launcher server

# num_total_tests = len(files)
# passed_tests = []
# failed_tests = []

# for each input json file:
# load the contents of the json file into memory

# for each json object in memory, replace image filepaths with our testing image filepath
# do the same for videeo filepaths in the json file

# /import_project name=<uuid> import_json=<json>
# returns an id

# /projects/<id>/start
# returns a port

# use selenium navigate to http://localhost:<port>
# wait 15s for the page to load
# click on Queue prompt button
# wait 5s
# stop selenium

# get request to http://localhost:<port>/queue
# find the client_id and the prompt_id for the prompt we just created in the resopnse above (via selenium)

"""
ws = websocket.WebSocket()
ws.connect("ws://{}/ws?clientId={}".format("localhost:<port>", client_id))
is_success = False
while True:
out = ws.recv()
if isinstance(out, str):
message = json.loads(out)
if message['type'] == 'executing':
data = message['data']
if data['node'] is None and data['prompt_id'] == prompt_id:
is_success = True
break #Execution is done
elif message['type'] == 'execution_error':
is_success = False
break
else:
continue #previews are binary data
if is_success
passed_tests.append(json_file)
else:
failed_tests.append(json_file)
ws.close()
make POST request to /projects/<id>/delete
"""

# at the end, print out the number of tests that passed and the number of tests that failed
147 changes: 84 additions & 63 deletions server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import hashlib
import unicodedata
import re
import subprocess
import threading
from tqdm import tqdm
from urllib.parse import urlparse

Expand Down Expand Up @@ -78,6 +80,23 @@ def recursive_search(data: Union[Dict, List, str], in_nodes: bool, node_type: Op
recursive_search(json_data, False, None)
return file_names


def print_process_output(process):
for line in iter(process.stdout.readline, b''):
print(line.decode(), end='')
process.stdout.close()

def run_command(cmd: List[str], cwd: Optional[str] = None, bg: bool = False) -> None:
process = subprocess.Popen(" ".join(cmd), cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

if bg:
# Create a separate thread to handle the printing of the process's output
threading.Thread(target=print_process_output, args=(process,), daemon=True).start()
return process.pid
else:
print_process_output(process)
assert process.wait() == 0

def get_ckpt_names_with_node_info(workflow_json: Union[Dict, List], is_windows: bool) -> List[ModelFileWithNodeInfo]:
ckpt_names = []
if isinstance(workflow_json, dict):
Expand All @@ -95,72 +114,62 @@ def normalize_model_filepaths_in_workflow_json(workflow_json: dict) -> dict:
workflow_json = json.loads(workflow_json)
return workflow_json

def run_command_in_project_venv(project_folder_path, command):
assert os.path.exists(
os.path.join(project_folder_path, "venv")
), f"Virtualenv does not exist in project folder: {project_folder_path}"
assert (
os.system(
f". {os.path.join(project_folder_path, 'venv', 'bin', 'activate')} && {command}"
)
== 0
)

def run_command_in_project_venv(project_folder_path, command):
if os.name == "nt": # Check if running on Windows
venv_activate = os.path.join(project_folder_path, "venv", "Scripts", "activate.bat")
else:
venv_activate = os.path.join(project_folder_path, "venv", "bin", "activate")

assert os.path.exists(venv_activate), f"Virtualenv does not exist in project folder: {project_folder_path}"

if os.name == "nt":
command = ["call", venv_activate, "&&", command]
else:
command = ["source", venv_activate, "&&", command]

# Run the command using subprocess and capture stdout
run_command(command)

def run_command_in_project_comfyui_venv(project_folder_path, command, in_bg=False):
assert os.path.exists(
os.path.join(project_folder_path, "venv")
), f"Virtualenv does not exist in project folder: {project_folder_path}"

if not in_bg:
assert (
os.system(
f". {os.path.join(project_folder_path, 'venv', 'bin', 'activate')} && cd {os.path.join(project_folder_path, 'comfyui')} && {command}"
)
== 0
)
venv_activate = os.path.join(project_folder_path, "venv", "Scripts", "activate.bat") if os.name == "nt" else os.path.join(project_folder_path, "venv", "bin", "activate")
comfyui_dir = os.path.join(project_folder_path, "comfyui")

assert os.path.exists(venv_activate), f"Virtualenv does not exist in project folder: {project_folder_path}"

if os.name == "nt":
return run_command([venv_activate, "&&", "cd", comfyui_dir, "&&", command], bg=in_bg)
else:
# start a process in the background and return the process id
import subprocess
process = subprocess.Popen(
f". {os.path.join(project_folder_path, 'venv', 'bin', 'activate')} && cd {os.path.join(project_folder_path, 'comfyui')} && {command}",
shell=True,
)
return process.pid
return run_command(["source", venv_activate, "&&", "cd", comfyui_dir, "&&", command], bg=in_bg)


def install_default_custom_nodes(project_folder_path, launcher_json=None):
# install default custom nodes
# comfyui-manager
os.system(
f"git clone https://github.com/ltdrdata/ComfyUI-Manager {os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-Manager')}"
)
run_command(["git", "clone", f"https://github.com/ltdrdata/ComfyUI-Manager", os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-Manager')])

# pip install comfyui-manager
run_command_in_project_venv(
project_folder_path,
f"pip install -r {os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-Manager', 'requirements.txt')}",
)
os.system(f"git clone https://github.com/thecooltechguy/ComfyUI-ComfyWorkflows {os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-ComfyWorkflows')}")
# for development
# os.system(
# f"cp -r ./default_custom_nodes/ComfyUI-ComfyWorkflows {os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-ComfyWorkflows')}"
# )

run_command(["git", "clone", f"https://github.com/thecooltechguy/ComfyUI-ComfyWorkflows", os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-ComfyWorkflows')])

# pip install comfyui-comfyworkflows
run_command_in_project_venv(
project_folder_path,
f"pip install -r {os.path.join(project_folder_path, 'comfyui', 'custom_nodes', 'ComfyUI-ComfyWorkflows', 'requirements.txt')}",
)


def setup_initial_models_folder(models_folder_path):
assert not os.path.exists(
models_folder_path
), f"Models folder already exists: {models_folder_path}"
# os.makedirs(models_folder_path)

# clone just the models/ folder from the comfyui repo

tmp_dir = os.path.join(os.path.dirname(models_folder_path), "tmp_comfyui")
os.system(f"git clone {COMFYUI_REPO_URL} {tmp_dir}")
run_command(["git", "clone", COMFYUI_REPO_URL, tmp_dir])

shutil.move(os.path.join(tmp_dir, "models"), models_folder_path)
shutil.rmtree(tmp_dir)

Expand Down Expand Up @@ -192,9 +201,14 @@ def setup_custom_nodes_from_snapshot(project_folder_path, launcher_json):
custom_node_path = os.path.join(
project_folder_path, "comfyui", "custom_nodes", custom_node_name
)
os.system(f"git clone {custom_node_repo_url} {custom_node_path}")

# Clone the custom node repository
run_command(["git", "clone", custom_node_repo_url, custom_node_path, "--recursive"])

if custom_node_hash:
os.system(f"cd {custom_node_path} && git checkout {custom_node_hash}")
# Checkout the specific hash
run_command(["git", "checkout", custom_node_hash], cwd=custom_node_path)

pip_requirements_path = os.path.join(custom_node_path, "requirements.txt")
if os.path.exists(pip_requirements_path):
run_command_in_project_venv(
Expand Down Expand Up @@ -286,13 +300,13 @@ def setup_files_from_launcher_json(project_folder_path, launcher_json):
download_url, headers=headers, allow_redirects=True, stream=True
) as response:
total_size = int(response.headers.get("content-length", 0))
pb = tqdm(total=total_size, unit="B", unit_scale=True)
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=10 * 1024):
pb.update(len(chunk))
if chunk:
f.write(chunk)

with tqdm(total=total_size, unit="B", unit_scale=True) as pb:
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=10 * 1024):
pb.update(len(chunk))
if chunk:
f.write(chunk)
if compute_sha256_checksum(dest_path) == sha256_checksum:
download_successful = True
if dest_relative_path in missing_download_files:
Expand Down Expand Up @@ -410,18 +424,20 @@ def create_comfyui_project(
project_folder_path,
{"id":id,"name":name, "status_message": "Downloading ComfyUI...", "state": "download_comfyui"},
)

# git clone comfyui into project folder/comfyui
os.system(
f"git clone {COMFYUI_REPO_URL} {os.path.join(project_folder_path, 'comfyui')}"
# Modify the subprocess.run calls to capture and log the stdout
run_command(
["git", "clone", COMFYUI_REPO_URL, os.path.join(project_folder_path, 'comfyui')],
)

if launcher_json:
comfyui_commit_hash = launcher_json["snapshot_json"]["comfyui"]
if comfyui_commit_hash:
os.system(
f"cd {os.path.join(project_folder_path, 'comfyui')} && git checkout {comfyui_commit_hash}"
run_command(
["git", "checkout", comfyui_commit_hash],
cwd=os.path.join(project_folder_path, 'comfyui'),
)
launcher_json['workflow_json'] = normalize_model_filepaths_in_workflow_json(launcher_json['workflow_json'])


# move the comfyui/web/index.html file to comfyui/web/comfyui_index.html
os.rename(
Expand All @@ -445,19 +461,15 @@ def create_comfyui_project(
setup_initial_models_folder(models_folder_path)

# create a folder in project folder/comfyui/models that is a symlink to the models folder
os.symlink(
models_folder_path,
os.path.join(project_folder_path, "comfyui", "models"),
target_is_directory=True,
)
create_symlink(models_folder_path, os.path.join(project_folder_path, "comfyui", "models"))

set_launcher_state_data(
project_folder_path,
{"status_message": "Installing ComfyUI...", "state": "install_comfyui"},
)

# create a new virtualenv in project folder/venv
os.system(f"python -m venv {os.path.join(project_folder_path, 'venv')}")
create_virtualenv(os.path.join(project_folder_path, 'venv'))

# activate the virtualenv + install comfyui requirements
run_command_in_project_venv(
Expand Down Expand Up @@ -506,4 +518,13 @@ def is_port_in_use(port: int) -> bool:
def find_free_port():
with socket.socket() as s:
s.bind(('', 0)) # Bind to a free port provided by the host.
return s.getsockname()[1] # Return the port number assigned.
return s.getsockname()[1] # Return the port number assigned.

def create_symlink(source, target):
if os.name == 'nt': # Check if running on Windows
run_command(['mklink', '/D', target, source])
else:
os.symlink(source, target, target_is_directory=True)

def create_virtualenv(venv_path):
run_command(['python', '-m', 'venv', venv_path])

0 comments on commit ddb2362

Please sign in to comment.