257 lines
9.5 KiB
Python
257 lines
9.5 KiB
Python
import asyncio
|
|
import hashlib
|
|
import mimetypes
|
|
import os
|
|
import re
|
|
|
|
import aiohttp
|
|
|
|
from ...utils.logging import debug_log, log
|
|
from ...utils.network import build_worker_url, get_client_session
|
|
from ...utils.trace_logger import trace_debug, trace_info
|
|
|
|
|
|
LIKELY_FILENAME_RE = re.compile(
|
|
r"\.(ckpt|safetensors|pt|pth|bin|yaml|json|png|jpg|jpeg|webp|gif|bmp|mp4|avi|mov|mkv|webm|"
|
|
r"wav|mp3|flac|m4a|aac|ogg|opus|aiff|aif|wma|latent|txt|vae|lora|embedding)"
|
|
r"(\s*\[\w+\])?$",
|
|
re.IGNORECASE,
|
|
)
|
|
MEDIA_FILE_RE = re.compile(
|
|
r"\.(png|jpg|jpeg|webp|gif|bmp|mp4|avi|mov|mkv|webm|wav|mp3|flac|m4a|aac|ogg|opus|aiff|aif|wma)(\s*\[\w+\])?$",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def _normalize_media_reference(value):
|
|
"""Normalize one media string value to a path-like reference or None."""
|
|
if not isinstance(value, str):
|
|
return None
|
|
cleaned = re.sub(r"\s*\[\w+\]$", "", value).strip().replace("\\", "/")
|
|
if MEDIA_FILE_RE.search(cleaned):
|
|
return cleaned
|
|
return None
|
|
|
|
|
|
def convert_paths_for_platform(obj, target_separator):
|
|
"""Recursively normalize likely file paths for the worker platform separator."""
|
|
if target_separator not in ("/", "\\"):
|
|
return obj
|
|
|
|
def _convert(value):
|
|
if isinstance(value, str):
|
|
if ("/" in value or "\\" in value) and LIKELY_FILENAME_RE.search(value):
|
|
trimmed = value.strip()
|
|
has_drive = bool(re.match(r"^[A-Za-z]:(\\\\|/)", trimmed))
|
|
is_absolute = trimmed.startswith("/") or trimmed.startswith("\\\\")
|
|
has_protocol = bool(re.match(r"^\w+://", trimmed))
|
|
|
|
# URLs are not local paths and should never be separator-normalized.
|
|
if has_protocol:
|
|
return trimmed
|
|
|
|
# Keep relative media-style paths in forward-slash form (Comfy-style annotated paths).
|
|
if not has_drive and not is_absolute and not has_protocol and MEDIA_FILE_RE.search(trimmed):
|
|
return re.sub(r"[\\]+", "/", trimmed)
|
|
|
|
if target_separator == "\\":
|
|
return re.sub(r"[\\/]+", r"\\", trimmed)
|
|
return re.sub(r"[\\/]+", "/", trimmed)
|
|
return value
|
|
if isinstance(value, list):
|
|
return [_convert(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {key: _convert(item) for key, item in value.items()}
|
|
return value
|
|
|
|
return _convert(obj)
|
|
|
|
|
|
def _find_media_references(prompt_obj):
|
|
"""Find media file references in image/video/audio/file inputs used by worker prompts."""
|
|
media_refs = set()
|
|
for node in prompt_obj.values():
|
|
if not isinstance(node, dict):
|
|
continue
|
|
inputs = node.get("inputs", {})
|
|
for key in ("image", "video", "audio", "file"):
|
|
cleaned = _normalize_media_reference(inputs.get(key))
|
|
if cleaned:
|
|
media_refs.add(cleaned)
|
|
return sorted(media_refs)
|
|
|
|
|
|
def _rewrite_prompt_media_inputs(prompt_obj, worker_media_paths):
|
|
"""Rewrite media string inputs to worker-local uploaded paths."""
|
|
if not isinstance(worker_media_paths, dict) or not worker_media_paths:
|
|
return
|
|
|
|
for node in prompt_obj.values():
|
|
if not isinstance(node, dict):
|
|
continue
|
|
inputs = node.get("inputs", {})
|
|
if not isinstance(inputs, dict):
|
|
continue
|
|
for key in ("image", "video", "audio", "file"):
|
|
value = inputs.get(key)
|
|
cleaned = _normalize_media_reference(value)
|
|
if not cleaned:
|
|
continue
|
|
worker_path = worker_media_paths.get(cleaned)
|
|
if worker_path:
|
|
inputs[key] = worker_path
|
|
|
|
|
|
def _load_media_file_sync(filename):
|
|
"""Load local media bytes and hash for worker upload sync."""
|
|
import folder_paths
|
|
|
|
full_path = folder_paths.get_annotated_filepath(filename)
|
|
if not os.path.exists(full_path):
|
|
raise FileNotFoundError(filename)
|
|
|
|
with open(full_path, "rb") as f:
|
|
file_bytes = f.read()
|
|
|
|
file_hash = hashlib.md5(file_bytes).hexdigest()
|
|
mime_type = mimetypes.guess_type(full_path)[0]
|
|
if not mime_type:
|
|
ext = os.path.splitext(full_path)[1].lower()
|
|
if ext in {".mp4", ".avi", ".mov", ".mkv", ".webm"}:
|
|
mime_type = "video/mp4"
|
|
else:
|
|
mime_type = "image/png"
|
|
return file_bytes, file_hash, mime_type
|
|
|
|
|
|
async def fetch_worker_path_separator(worker, trace_execution_id=None):
|
|
"""Best-effort fetch of a worker's path separator from /distributed/system_info."""
|
|
url = build_worker_url(worker, "/distributed/system_info")
|
|
session = await get_client_session()
|
|
try:
|
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
|
if resp.status != 200:
|
|
return None
|
|
payload = await resp.json()
|
|
separator = ((payload or {}).get("platform") or {}).get("path_separator")
|
|
return separator if separator in ("/", "\\") else None
|
|
except Exception as exc:
|
|
if trace_execution_id:
|
|
trace_debug(trace_execution_id, f"Failed to fetch worker system info ({worker.get('id')}): {exc}")
|
|
else:
|
|
debug_log(f"[Distributed] Failed to fetch worker system info ({worker.get('id')}): {exc}")
|
|
return None
|
|
|
|
|
|
async def _upload_media_to_worker(worker, filename, file_bytes, file_hash, mime_type, trace_execution_id=None):
|
|
"""Upload one media file to worker iff missing or hash-mismatched."""
|
|
session = await get_client_session()
|
|
normalized = filename.replace("\\", "/")
|
|
|
|
check_url = build_worker_url(worker, "/distributed/check_file")
|
|
try:
|
|
async with session.post(
|
|
check_url,
|
|
json={"filename": normalized, "hash": file_hash},
|
|
timeout=aiohttp.ClientTimeout(total=6),
|
|
) as resp:
|
|
if resp.status == 200:
|
|
payload = await resp.json()
|
|
if payload.get("exists") and payload.get("hash_matches"):
|
|
return False, normalized
|
|
except Exception as exc:
|
|
if trace_execution_id:
|
|
trace_debug(trace_execution_id, f"Media check failed for '{normalized}' on worker {worker.get('id')}: {exc}")
|
|
else:
|
|
debug_log(f"[Distributed] Media check failed for '{normalized}' on worker {worker.get('id')}: {exc}")
|
|
|
|
parts = normalized.split("/")
|
|
clean_name = parts[-1]
|
|
subfolder = "/".join(parts[:-1])
|
|
|
|
form = aiohttp.FormData()
|
|
form.add_field("image", file_bytes, filename=clean_name, content_type=mime_type)
|
|
form.add_field("type", "input")
|
|
form.add_field("subfolder", subfolder)
|
|
form.add_field("overwrite", "true")
|
|
|
|
upload_url = build_worker_url(worker, "/upload/image")
|
|
async with session.post(
|
|
upload_url,
|
|
data=form,
|
|
timeout=aiohttp.ClientTimeout(total=30),
|
|
) as resp:
|
|
resp.raise_for_status()
|
|
try:
|
|
payload = await resp.json()
|
|
except Exception:
|
|
payload = {}
|
|
|
|
name = str((payload or {}).get("name") or clean_name).strip()
|
|
subfolder = str((payload or {}).get("subfolder") or "").strip().replace("\\", "/").strip("/")
|
|
worker_path = f"{subfolder}/{name}" if subfolder else name
|
|
return True, worker_path
|
|
|
|
|
|
async def sync_worker_media(worker, prompt_obj, trace_execution_id=None):
|
|
"""Sync referenced media files from master to a remote worker before dispatch."""
|
|
media_refs = _find_media_references(prompt_obj)
|
|
if not media_refs:
|
|
return
|
|
|
|
loop = asyncio.get_running_loop()
|
|
uploaded = 0
|
|
skipped = 0
|
|
missing = 0
|
|
worker_media_paths = {}
|
|
for filename in media_refs:
|
|
try:
|
|
file_bytes, file_hash, mime_type = await loop.run_in_executor(
|
|
None, _load_media_file_sync, filename
|
|
)
|
|
except FileNotFoundError:
|
|
missing += 1
|
|
if trace_execution_id:
|
|
trace_info(trace_execution_id, f"Media file '{filename}' not found on master; worker may fail to load it.")
|
|
else:
|
|
log(f"[Distributed] Media file '{filename}' not found on master; worker may fail to load it.")
|
|
continue
|
|
except Exception as exc:
|
|
if trace_execution_id:
|
|
trace_info(trace_execution_id, f"Failed to load media '{filename}' for worker sync: {exc}")
|
|
else:
|
|
log(f"[Distributed] Failed to load media '{filename}' for worker sync: {exc}")
|
|
continue
|
|
|
|
try:
|
|
changed, worker_path = await _upload_media_to_worker(
|
|
worker,
|
|
filename,
|
|
file_bytes,
|
|
file_hash,
|
|
mime_type,
|
|
trace_execution_id=trace_execution_id,
|
|
)
|
|
if worker_path:
|
|
worker_media_paths[filename] = worker_path
|
|
if changed:
|
|
uploaded += 1
|
|
else:
|
|
skipped += 1
|
|
except Exception as exc:
|
|
if trace_execution_id:
|
|
trace_info(trace_execution_id, f"Failed to upload media '{filename}' to worker {worker.get('id')}: {exc}")
|
|
else:
|
|
log(f"[Distributed] Failed to upload media '{filename}' to worker {worker.get('id')}: {exc}")
|
|
|
|
_rewrite_prompt_media_inputs(prompt_obj, worker_media_paths)
|
|
|
|
summary = (
|
|
f"Media sync for worker {worker.get('id')}: "
|
|
f"uploaded={uploaded}, skipped={skipped}, missing={missing}, referenced={len(media_refs)}"
|
|
)
|
|
if trace_execution_id:
|
|
trace_debug(trace_execution_id, summary)
|
|
else:
|
|
debug_log(f"[Distributed] {summary}")
|