Files
doubletwisted-ComfyUI-Deadl…/deadline_submit.py
T

786 lines
31 KiB
Python

"""
ComfyUI Deadline submission nodes.
This module owns the ComfyUI-side submission flow. It packages the current API
prompt, stages referenced input assets, and submits a render job to Deadline.
"""
import copy
import filecmp
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import uuid
from typing import Any, Dict, List, Optional, Tuple
DEADLINE_COMMAND_PATHS = {
"windows": "C:\\Program Files\\Thinkbox\\Deadline10\\bin\\deadlinecommand.exe",
"linux": "/opt/Thinkbox/Deadline10/bin/deadlinecommand",
}
DEADLINE_SUBMIT_NODE_TYPES = {"DeadlineSubmit", "SaveAndSubmitNode"}
OUTPUT_NODE_TYPES = {"SaveImage", "PreviewImage", "SaveVideo", "VHS_VideoCombine"}
INPUT_LOADER_FIELDS = {
"LoadImage": ("image",),
"LoadImageMask": ("image",),
"LoadAudio": ("audio",),
"LoadVideo": ("file",),
# VideoHelperSuite file-based loaders. Directory loaders such as
# VHS_LoadImagesPath intentionally remain unsupported here.
"VHS_LoadVideo": ("video",),
"VHS_LoadVideoFFmpeg": ("video",),
"VHS_LoadAudioUpload": ("audio",),
}
ABSOLUTE_FILE_LOADER_FIELDS = {
"VHS_LoadVideoPath": ("video",),
"VHS_LoadVideoFFmpegPath": ("video",),
"VHS_LoadImagePath": ("image",),
"VHS_LoadAudio": ("audio_file",),
}
MEDIA_EXTENSIONS = {
".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tif", ".tiff", ".exr",
".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v",
".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac",
}
# A worker that is allowed to reuse a long-running GUI endpoint must prove that
# the endpoint belongs to this installation. A random value created at module
# import also distinguishes a restarted ComfyUI session from the old process.
DEADLINE_ENDPOINT_SESSION_ID = uuid.uuid4().hex
def _coerce_bool(value: Any) -> bool:
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
class NodeDefaults:
JOB_NAME = "ComfyUI via Deadline"
PRIORITY = 50
POOL = "none"
GROUP = "none"
BATCH_COUNT = 1
CHUNK_SIZE = 1
MAX_BATCH_COUNT = 10000
MAX_CHUNK_SIZE = 256
MAX_PRIORITY = 100
class DeadlineCommandHelper:
@staticmethod
def get_deadline_command() -> str:
deadline_bin = os.environ.get("DEADLINE_PATH", "")
if not deadline_bin and os.path.exists("/Users/Shared/Thinkbox/DEADLINE_PATH"):
try:
with open("/Users/Shared/Thinkbox/DEADLINE_PATH", "r", encoding="utf-8") as handle:
deadline_bin = handle.read().strip()
except Exception:
deadline_bin = ""
candidates = []
if deadline_bin:
candidates.append(os.path.join(deadline_bin, "deadlinecommand.exe" if os.name == "nt" else "deadlinecommand"))
candidates.append(DEADLINE_COMMAND_PATHS["windows"] if sys.platform.startswith("win") else DEADLINE_COMMAND_PATHS["linux"])
for candidate in candidates:
if os.path.exists(candidate):
return candidate
return ""
@staticmethod
def call_deadline_command(arguments: List[str], hide_window: bool = True) -> str:
deadline_command = DeadlineCommandHelper.get_deadline_command()
if not deadline_command:
raise RuntimeError("Deadline command not found. Set DEADLINE_PATH or install Deadline Client.")
startupinfo = None
creationflags = 0
if os.name == "nt" and hide_window:
try:
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
except Exception:
startupinfo = None
elif os.name == "nt":
creationflags = 0x08000000
process = subprocess.Popen(
[deadline_command] + arguments,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
creationflags=creationflags,
)
stdout, stderr = process.communicate()
output = stdout.decode(errors="replace") if isinstance(stdout, bytes) else str(stdout)
errors = stderr.decode(errors="replace") if isinstance(stderr, bytes) else str(stderr)
if process.returncode != 0:
raise RuntimeError(f"deadlinecommand failed with code {process.returncode}: {errors or output}")
return output
@staticmethod
def get_job_id_from_submission(submission_results: str) -> str:
for token in submission_results.replace("\r", "\n").split():
if token.startswith("JobID="):
return token.split("=", 1)[1].strip()
return ""
class WorkflowProcessor:
@staticmethod
def normalize_prompt(prompt: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(prompt, dict) or not prompt:
raise ValueError("ComfyUI did not provide a valid API prompt.")
return copy.deepcopy(prompt)
@staticmethod
def prepare_for_worker(prompt: Dict[str, Any]) -> Dict[str, Any]:
prepared = WorkflowProcessor.normalize_prompt(prompt)
removed = []
for node_id, node in list(prepared.items()):
if isinstance(node, dict) and node.get("class_type") in DEADLINE_SUBMIT_NODE_TYPES:
removed.append(node_id)
del prepared[node_id]
if removed:
print(f"Deadline Submission: Removed submit node(s) from worker prompt: {', '.join(map(str, removed))}")
WorkflowProcessor.validate_worker_prompt(prepared)
return prepared
@staticmethod
def validate_worker_prompt(prompt: Dict[str, Any]) -> None:
if not prompt:
raise ValueError("Worker prompt is empty after removing Deadline submit nodes.")
has_output = any(
isinstance(node, dict) and node.get("class_type") in OUTPUT_NODE_TYPES
for node in prompt.values()
)
if not has_output:
print("Deadline Submission: Warning - worker prompt has no known output node.")
class InputAssetStager:
def __init__(self, output_directory: str, job_name: str, submission_id: str):
self.output_directory = os.path.abspath(output_directory)
self.job_name = job_name
self.submission_id = submission_id
def stage_referenced_assets(self, prompt: Dict[str, Any]) -> Tuple[str, str, List[Dict[str, Any]]]:
input_dir = self._get_local_input_directory()
references = self._collect_references(prompt, input_dir)
staging_dir = self._staging_directory()
manifest_path = os.path.join(staging_dir, f"deadline_input_manifest_{self.submission_id}.json")
if not references:
os.makedirs(staging_dir, exist_ok=True)
manifest = {
"submission_id": self.submission_id,
"input_directory": staging_dir,
"assets": [],
}
self._write_manifest(manifest_path, manifest)
return staging_dir, manifest_path, []
os.makedirs(staging_dir, exist_ok=True)
assets = []
for original_reference, source_path in sorted(references.items()):
# Absolute-path loader nodes are converted to a farm-visible staged
# absolute path later. Use only the basename inside the staging
# directory; never reproduce the submitter's local directory tree.
destination_rel_path = (
os.path.basename(source_path)
if os.path.isabs(original_reference)
else original_reference
)
staged_rel_path, destination_path = self._resolve_destination(staging_dir, destination_rel_path, source_path)
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
if not os.path.exists(destination_path):
shutil.copy2(source_path, destination_path)
assets.append({
"original_relative_path": original_reference.replace("\\", "/"),
"staged_relative_path": staged_rel_path.replace("\\", "/"),
"relative_path": staged_rel_path.replace("\\", "/"),
"source": source_path,
"destination": destination_path,
"size": os.path.getsize(destination_path),
})
manifest = {
"submission_id": self.submission_id,
"input_directory": staging_dir,
"assets": assets,
}
self._write_manifest(manifest_path, manifest)
print(f"Deadline Submission: Staged {len(assets)} input asset(s) to {staging_dir}")
return staging_dir, manifest_path, assets
def _get_local_input_directory(self) -> str:
try:
import folder_paths
return os.path.abspath(folder_paths.get_input_directory())
except Exception as exc:
raise RuntimeError(f"Could not resolve ComfyUI input directory: {exc}")
def _collect_references(self, prompt: Dict[str, Any], input_dir: str) -> Dict[str, str]:
references: Dict[str, str] = {}
for node in prompt.values():
if not isinstance(node, dict):
continue
class_type = node.get("class_type", "")
inputs = node.get("inputs", {})
if not isinstance(inputs, dict):
continue
candidate_values: List[Tuple[Any, bool, bool]] = []
for field_name in INPUT_LOADER_FIELDS.get(class_type, ()):
if field_name in inputs:
candidate_values.append((inputs[field_name], True, False))
for field_name in ABSOLUTE_FILE_LOADER_FIELDS.get(class_type, ()):
if field_name in inputs:
candidate_values.append((inputs[field_name], True, True))
for value in inputs.values():
if isinstance(value, str):
candidate_values.append((value, False, False))
for value, strict, allow_absolute in candidate_values:
if not isinstance(value, str):
continue
resolved = self._resolve_input_file(value, input_dir, strict, allow_absolute)
if not resolved:
continue
rel_path, source_path = resolved
references[rel_path] = source_path
return references
def _resolve_destination(self, staging_dir: str, rel_path: str, source_path: str) -> Tuple[str, str]:
destination_path = os.path.abspath(os.path.join(staging_dir, rel_path))
if not os.path.exists(destination_path):
return rel_path, destination_path
if os.path.isfile(destination_path) and filecmp.cmp(source_path, destination_path, shallow=False):
return rel_path, destination_path
stem, extension = os.path.splitext(rel_path)
staged_rel_path = f"{stem}_{self.submission_id}{extension}"
return staged_rel_path, os.path.abspath(os.path.join(staging_dir, staged_rel_path))
def _resolve_input_file(
self,
value: str,
input_dir: str,
strict: bool,
allow_absolute: bool = False,
) -> Optional[Tuple[str, str]]:
clean_value, annotation = self._strip_annotation(value)
if annotation in {"output", "temp"}:
return None
if not clean_value:
return None
if os.path.splitext(clean_value)[1].lower() not in MEDIA_EXTENSIONS:
return None
if os.path.isabs(clean_value):
if not allow_absolute:
return None
candidate = os.path.abspath(clean_value)
if not os.path.isfile(candidate):
if not strict:
return None
raise FileNotFoundError(f"Referenced input asset was not found: {value} ({candidate})")
return clean_value, candidate
candidate = os.path.abspath(os.path.join(input_dir, clean_value))
try:
common = os.path.commonpath([input_dir, candidate])
except ValueError:
common = ""
if common != input_dir:
raise ValueError(f"Input asset escapes ComfyUI input directory: {value}")
if not os.path.isfile(candidate):
if not strict:
return None
raise FileNotFoundError(f"Referenced input asset was not found: {value} ({candidate})")
rel_path = os.path.relpath(candidate, input_dir)
return rel_path, candidate
def _strip_annotation(self, value: str) -> Tuple[str, Optional[str]]:
match = re.match(r"^(.*)\s+\[(input|output|temp)\]\s*$", value)
if not match:
return value.strip().replace("/", os.sep), None
return match.group(1).strip().replace("/", os.sep), match.group(2)
def _staging_directory(self) -> str:
parent = os.path.dirname(self.output_directory.rstrip("\\/"))
return os.path.join(parent, "input")
def _write_manifest(self, manifest_path: str, manifest: Dict[str, Any]) -> None:
with open(manifest_path, "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2)
class DeadlineJobSubmitter:
def __init__(self, workflow_data: Dict[str, Any], job_config: Dict[str, Any]):
self.workflow_data = workflow_data
self.job_config = job_config
def submit_job(self) -> Tuple[bool, str]:
try:
submission_dir = tempfile.mkdtemp(prefix="comfy_deadline_job_")
job_info_file, plugin_info_file, auxiliary_files = self._create_submission_files(submission_dir)
result = DeadlineCommandHelper.call_deadline_command([job_info_file, plugin_info_file] + auxiliary_files)
job_id = DeadlineCommandHelper.get_job_id_from_submission(result)
if not job_id:
return False, f"Deadline submission did not return a JobID. Output: {result}"
return True, job_id
except Exception as exc:
return False, str(exc)
def _create_submission_files(self, submission_dir: str) -> Tuple[str, str, List[str]]:
job_info_file = os.path.join(submission_dir, "job_info.txt")
plugin_info_file = os.path.join(submission_dir, "plugin_info.txt")
prompt_file = os.path.join(submission_dir, "prompt_to_execute.json")
standard_workflow_file = os.path.join(submission_dir, "workflow.json")
with open(prompt_file, "w", encoding="utf-8") as handle:
json.dump(self.workflow_data, handle, indent=2)
auxiliary_files = [prompt_file]
standard_workflow = self.job_config.get("standard_workflow")
if standard_workflow:
with open(standard_workflow_file, "w", encoding="utf-8") as handle:
json.dump(standard_workflow, handle, indent=2)
auxiliary_files.append(standard_workflow_file)
self._write_job_info(job_info_file)
self._write_plugin_info(plugin_info_file)
return job_info_file, plugin_info_file, auxiliary_files
def _write_job_info(self, path: str) -> None:
config = self.job_config
batch_count = int(config["batch_count"])
chunk_size = max(1, int(config["chunk_size"]))
output_dir = os.path.abspath(config["output_directory"])
with open(path, "w", encoding="utf-8") as handle:
handle.write("Plugin=ComfyUI\n")
handle.write(f"Name={config['job_name']}\n")
handle.write(f"Comment={config.get('comment', '')}\n")
handle.write(f"Department={config.get('department', '')}\n")
handle.write(f"Pool={'' if config['pool'] == 'none' else config['pool']}\n")
handle.write(f"Group={'' if config['group'] == 'none' else config['group']}\n")
handle.write(f"Priority={int(config['priority'])}\n")
handle.write(f"Frames=0-{batch_count - 1}\n")
handle.write(f"ChunkSize={chunk_size}\n")
handle.write(f"OutputDirectory0={output_dir}\n")
machine_list = config.get("machine_list", "")
if machine_list:
if not re.fullmatch(r"[A-Za-z0-9_,.\-]+", machine_list):
raise ValueError("Machine list must contain comma-separated Worker names.")
handle.write(f"Whitelist={machine_list}\n")
def _write_plugin_info(self, path: str) -> None:
config = self.job_config
entries = {
"StandardWorkflowFile": "workflow.json" if config.get("standard_workflow") else "",
"JobOutputDirectory": os.path.abspath(config["output_directory"]),
"JobInputDirectory": os.path.abspath(config["input_directory"]),
"InputManifestFile": os.path.abspath(config["input_manifest"]),
"SubmissionId": config["submission_id"],
"BatchCount": str(int(config["batch_count"])),
"BatchMode": "True",
"DefaultCudaDeviceZero": "True",
"SeedMode": "fixed",
"WorkerMode": "False",
"DistributedMode": "False",
"ForceNewInstance": "True",
"DisableDynamicVRAM": str(bool(config.get("disable_dynamic_vram", False))),
"ReserveVRAM": str(max(0.0, min(64.0, float(config.get("reserve_vram", 0.0))))),
}
with open(path, "w", encoding="utf-8") as handle:
for key, value in entries.items():
if value != "":
handle.write(f"{key}={value}\n")
class DeadlineSeed:
"""
Deadline-compatible seed node.
Batch mode varies by Deadline task ID; distributed mode varies by worker ID.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {
"default": 1125899906842,
"min": 0,
"max": 1125899906842624,
"forceInput": False,
}),
},
"hidden": {
"task_id": ("INT", {"default": 0}),
"batch_mode": ("BOOLEAN", {"default": False}),
"is_worker": ("BOOLEAN", {"default": False}),
"worker_id": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("seed",)
FUNCTION = "distribute"
CATEGORY = "deadline"
def distribute(self, seed, task_id=0, batch_mode=False, is_worker=False, worker_id=""):
seed = int(seed)
if _coerce_bool(batch_mode):
try:
task_id = int(task_id)
except (TypeError, ValueError):
task_id = 0
return (seed + task_id,)
if _coerce_bool(is_worker):
try:
worker_id = str(worker_id)
if worker_id.startswith("worker_"):
worker_index = int(worker_id.split("_")[1])
else:
worker_index = int(worker_id)
return (seed + worker_index + 1,)
except (TypeError, ValueError, IndexError):
return (seed,)
return (seed,)
class LegacyDeadlineSeedAlias(DeadlineSeed):
DEPRECATED = True
class DeadlineSubmitNode:
@classmethod
def INPUT_TYPES(cls):
pools = cls._get_deadline_pools()
groups = cls._get_deadline_groups()
return {
"required": {
"output_directory": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Farm-visible output directory",
}),
"batch_count": ("INT", {
"default": NodeDefaults.BATCH_COUNT,
"min": 1,
"max": NodeDefaults.MAX_BATCH_COUNT,
"step": 1,
}),
"chunk_size": ("INT", {
"default": NodeDefaults.CHUNK_SIZE,
"min": 1,
"max": NodeDefaults.MAX_CHUNK_SIZE,
"step": 1,
}),
"priority": ("INT", {
"default": NodeDefaults.PRIORITY,
"min": 0,
"max": NodeDefaults.MAX_PRIORITY,
}),
"pool": (pools, {"default": NodeDefaults.POOL}),
"group": (groups, {"default": NodeDefaults.GROUP}),
"job_name": ("STRING", {"default": NodeDefaults.JOB_NAME}),
},
"optional": {
"comment": ("STRING", {"default": ""}),
"department": ("STRING", {"default": ""}),
"machine_list": ("STRING", {"default": "", "tooltip": "Optional comma-separated Deadline Worker allowlist."}),
"disable_dynamic_vram": ("BOOLEAN", {"default": False}),
"reserve_vram": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 64.0, "step": 0.5, "tooltip": "GB reserved for activations and other GPU users."}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("job_id",)
FUNCTION = "submit_to_deadline"
CATEGORY = "deadline"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, **kwargs):
return f"deadline_submit_{time.time()}_{uuid.uuid4()}"
@classmethod
def _get_deadline_pools(cls) -> List[str]:
try:
output = DeadlineCommandHelper.call_deadline_command(["-pools"])
pools = [line.strip() for line in output.splitlines() if line.strip()]
return pools or [NodeDefaults.POOL]
except Exception as exc:
print(f"Deadline Submission: Could not query Deadline pools: {exc}")
return [NodeDefaults.POOL]
@classmethod
def _get_deadline_groups(cls) -> List[str]:
try:
output = DeadlineCommandHelper.call_deadline_command(["-groups"])
groups = [line.strip() for line in output.splitlines() if line.strip()]
return groups or [NodeDefaults.GROUP]
except Exception as exc:
print(f"Deadline Submission: Could not query Deadline groups: {exc}")
return [NodeDefaults.GROUP]
def submit_to_deadline(
self,
output_directory,
batch_count,
chunk_size,
priority,
pool,
group,
job_name,
comment="",
department="",
machine_list="",
disable_dynamic_vram=False,
reserve_vram=0.0,
prompt=None,
extra_pnginfo=None,
**_legacy_inputs,
):
try:
output_directory = self._prepare_output_directory(output_directory)
batch_count = max(1, int(batch_count))
chunk_size = max(1, min(int(chunk_size), batch_count))
submission_id = uuid.uuid4().hex[:12]
if prompt is None:
raise ValueError("ComfyUI did not inject the current API prompt.")
worker_prompt = WorkflowProcessor.prepare_for_worker(prompt)
stager = InputAssetStager(output_directory, job_name, submission_id)
input_dir, manifest_file, assets = stager.stage_referenced_assets(worker_prompt)
self._rewrite_prompt_asset_references(worker_prompt, assets)
standard_workflow = self._extract_standard_workflow(extra_pnginfo)
self._rewrite_standard_workflow_assets(standard_workflow, assets)
job_config = {
"submission_id": submission_id,
"job_name": job_name.strip() or NodeDefaults.JOB_NAME,
"priority": int(priority),
"pool": pool,
"group": group,
"batch_count": batch_count,
"chunk_size": chunk_size,
"output_directory": output_directory,
"input_directory": input_dir,
"input_manifest": manifest_file,
"standard_workflow": standard_workflow,
"comment": comment,
"department": department,
"machine_list": ",".join(part.strip() for part in machine_list.split(",") if part.strip()),
"disable_dynamic_vram": disable_dynamic_vram,
"reserve_vram": reserve_vram,
}
submitter = DeadlineJobSubmitter(worker_prompt, job_config)
success, result = submitter.submit_job()
if not success:
raise RuntimeError(result)
print(f"Deadline Submission: Submitted job {result} with {batch_count} variation(s), chunk size {chunk_size}, {len(assets)} staged asset(s).")
return (result,)
except Exception as exc:
raise
def _rewrite_prompt_asset_references(self, prompt: Dict[str, Any], assets: List[Dict[str, Any]]) -> None:
staged_by_original = self._asset_map(assets, "staged_relative_path", allow_absolute=True)
staged_absolute_by_original = self._asset_map(assets, "destination", allow_absolute=True)
if not staged_by_original:
return
for node in prompt.values():
if not isinstance(node, dict):
continue
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
class_type = node.get("class_type", "")
for field_name in ABSOLUTE_FILE_LOADER_FIELDS.get(class_type, ()):
value = inputs.get(field_name)
normalized = self._normalize_asset_reference(value, allow_absolute=True)
if normalized in staged_absolute_by_original:
inputs[field_name] = staged_absolute_by_original[normalized]
for field_name in INPUT_LOADER_FIELDS.get(class_type, ()):
value = inputs.get(field_name)
normalized = self._normalize_asset_reference(value)
if normalized in staged_by_original:
inputs[field_name] = staged_by_original[normalized]
for field_name, value in list(inputs.items()):
normalized = self._normalize_asset_reference(value)
if normalized in staged_by_original:
inputs[field_name] = staged_by_original[normalized]
def _rewrite_standard_workflow_assets(self, workflow: Optional[Dict[str, Any]], assets: List[Dict[str, Any]]) -> None:
staged_absolute_by_original = self._asset_map(assets, "destination", allow_absolute=True)
if not workflow or not staged_absolute_by_original:
return
nodes = workflow.get("nodes", [])
if not isinstance(nodes, list):
return
for node in nodes:
if not isinstance(node, dict):
continue
widgets = node.get("widgets_values")
if not isinstance(widgets, list):
continue
for index, value in enumerate(widgets):
normalized = self._normalize_asset_reference(value)
if normalized in staged_absolute_by_original:
widgets[index] = staged_absolute_by_original[normalized]
def _asset_map(
self,
assets: List[Dict[str, Any]],
target_key: str,
allow_absolute: bool = False,
) -> Dict[str, str]:
mapping = {}
for asset in assets:
original = self._normalize_asset_reference(
asset.get("original_relative_path"), allow_absolute=allow_absolute
)
target = asset.get(target_key)
if original and target:
mapping[original] = target
return mapping
def _normalize_asset_reference(self, value: Any, allow_absolute: bool = False) -> Optional[str]:
if not isinstance(value, str) or not value:
return None
cleaned = re.sub(r"\s+\[(input|output|temp)\]\s*$", "", value.strip())
if os.path.isabs(cleaned) and not allow_absolute:
return None
return os.path.normpath(cleaned.replace("/", os.sep)).replace("\\", "/")
def _extract_standard_workflow(self, extra_pnginfo: Any) -> Optional[Dict[str, Any]]:
if not isinstance(extra_pnginfo, dict):
return None
workflow = extra_pnginfo.get("workflow")
if isinstance(workflow, dict):
return copy.deepcopy(workflow)
if isinstance(workflow, str):
try:
parsed = json.loads(workflow)
return parsed if isinstance(parsed, dict) else None
except json.JSONDecodeError:
return None
return None
def _prepare_output_directory(self, output_directory: str) -> str:
output_directory = (output_directory or "").strip().strip("\"")
if not output_directory:
raise ValueError(
"Set DeadlineSubmit output_directory to a shared farm path, "
"e.g. \\\\server\\share\\output."
)
output_directory = os.path.abspath(os.path.expandvars(output_directory))
os.makedirs(output_directory, exist_ok=True)
if not os.path.isdir(output_directory):
raise ValueError(f"Output path is not a directory: {output_directory}")
return output_directory
def on_prompt(json_data: Dict[str, Any]) -> Dict[str, Any]:
prompt = json_data.get("prompt")
if not isinstance(prompt, dict):
return json_data
submit_ids = [
str(node_id)
for node_id, node in prompt.items()
if isinstance(node, dict) and node.get("class_type") in DEADLINE_SUBMIT_NODE_TYPES
]
if submit_ids:
json_data["partial_execution_targets"] = submit_ids[:1]
if len(submit_ids) > 1:
print(f"Deadline Submission: Multiple submit nodes detected; only node {submit_ids[0]} will execute locally.")
return json_data
def register_on_prompt_handler() -> None:
try:
import server
instance = getattr(getattr(server, "PromptServer", None), "instance", None)
if instance and hasattr(instance, "add_on_prompt_handler"):
instance.add_on_prompt_handler(on_prompt)
print("Deadline Submission: Registered submit-only prompt handler.")
except Exception as exc:
print(f"Deadline Submission: Could not register prompt handler: {exc}")
try:
from aiohttp import web
import folder_paths
import server
instance = getattr(getattr(server, "PromptServer", None), "instance", None)
if instance and hasattr(instance, "routes"):
@instance.routes.get("/deadline/session")
async def deadline_session(_request):
return web.json_response({
"product": "ComfyUI-Deadline-Plugin",
"protocol": 1,
"session_id": DEADLINE_ENDPOINT_SESSION_ID,
"pid": os.getpid(),
"comfyui_root": os.path.abspath(folder_paths.base_path),
})
print(f"Deadline Submission: Registered endpoint identity {DEADLINE_ENDPOINT_SESSION_ID}.")
except Exception as exc:
print(f"Deadline Submission: Could not register endpoint identity route: {exc}")
NODE_CLASS_MAPPINGS = {
"DeadlineSubmit": DeadlineSubmitNode,
"DeadlineSeed": DeadlineSeed,
"DeadlineDistributedSeed": LegacyDeadlineSeedAlias,
"DistributedSeed": LegacyDeadlineSeedAlias,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DeadlineSubmit": "Submit to Deadline",
"DeadlineSeed": "Deadline Seed",
"DeadlineDistributedSeed": "Deadline Seed",
"DistributedSeed": "Deadline Seed",
}