Files
2025-11-24 14:23:45 +08:00

766 lines
29 KiB
Python

import requests
import json
import base64
import io
import os
import time
import numpy as np
from PIL import Image
import torch
import folder_paths
class RemoteWorkflowExecutor:
def __init__(self):
self.client_id = str(time.time())
self.workflow_cache = {}
self.hide_ip = True
def mask_ip(self, server_address):
if not self.hide_ip:
return server_address
if ':' in server_address:
ip, port = server_address.rsplit(':', 1)
else:
ip = server_address
port = None
parts = ip.split('.')
if len(parts) == 4:
masked_ip = f"{parts[0]}.***.***.{parts[3]}"
else:
masked_ip = "***.***.***"
if port:
return f"{masked_ip}:****"
return masked_ip
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"remote_ip": ("STRING", {"default": "192.168.1.100"}),
"remote_port": ("INT", {"default": 8188, "min": 1, "max": 65535}),
"workflow_file": ("STRING", {"default": "", "multiline": True}),
"selected_nodes": ("STRING", {"default": "{}", "multiline": True}),
"saved_state": ("STRING", {"default": "{}", "multiline": True}),
},
"optional": {
"image_1": ("IMAGE",),
"text_1": ("STRING", {"forceInput": True}),
"audio_1": ("AUDIO",),
"video_1": ("IMAGE",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("IMAGE", "STRING", "AUDIO", "IMAGE")
RETURN_NAMES = ("output_image", "output_text", "output_audio", "output_video")
FUNCTION = "execute_remote"
CATEGORY = "🐳Pond_Owner/IP"
def load_workflow(self, workflow_json_str):
if workflow_json_str in self.workflow_cache:
return self.workflow_cache[workflow_json_str]
try:
workflow = json.loads(workflow_json_str)
self.workflow_cache[workflow_json_str] = workflow
return workflow
except Exception as e:
return None
def get_workflow_nodes(self, workflow):
nodes_info = []
if workflow and isinstance(workflow, dict):
for node_id, node_data in workflow.items():
if isinstance(node_data, dict) and "class_type" in node_data:
nodes_info.append({
"id": node_id,
"class_type": node_data.get("class_type", "Unknown"),
"inputs": node_data.get("inputs", {})
})
return nodes_info
def create_empty_audio(self):
return {
"waveform": torch.zeros((1, 2, 44100)),
"sample_rate": 44100
}
def test_remote_connection(self, server_address):
try:
url = f"http://{server_address}/system_stats"
response = requests.get(url, timeout=5)
return response.status_code == 200
except:
return False
def upload_audio_to_remote(self, server_address, audio_data):
try:
waveform = audio_data.get('waveform')
sample_rate = audio_data.get('sample_rate', 44100)
if waveform is None:
return None
if waveform.dim() == 3:
waveform = waveform.squeeze(0)
elif waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
elif waveform.dim() != 2:
return None
if waveform.dim() != 2:
return None
import tempfile
import uuid
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
tmp_path = tmp_file.name
import torchaudio
torchaudio.save(tmp_path, waveform.cpu(), sample_rate)
unique_filename = f"audio_{uuid.uuid4().hex[:8]}.wav"
try:
with open(tmp_path, 'rb') as f:
files = {'image': (unique_filename, f, 'audio/wav')}
data = {'overwrite': 'true', 'type': 'input'}
url = f"http://{server_address}/upload/image"
response = requests.post(url, files=files, data=data, timeout=30)
if response.status_code == 200:
result = response.json()
returned_name = result.get('name', unique_filename)
subfolder = result.get('subfolder', '')
try:
os.unlink(tmp_path)
except:
pass
if subfolder:
return f"{subfolder}/{returned_name}"
else:
return returned_name
except:
pass
try:
with open(tmp_path, 'rb') as f:
files = {'audio': (unique_filename, f, 'audio/wav')}
data = {'overwrite': 'true'}
url = f"http://{server_address}/upload/audio"
response = requests.post(url, files=files, data=data, timeout=30)
if response.status_code == 200:
try:
os.unlink(tmp_path)
except:
pass
return unique_filename
except:
pass
try:
os.unlink(tmp_path)
except:
pass
return None
except Exception as e:
return None
def upload_image_to_remote(self, server_address, image_tensor):
try:
if image_tensor.dim() == 4:
img_array = image_tensor[0]
elif image_tensor.dim() == 3:
img_array = image_tensor
else:
return None
img_np = (img_array.cpu().numpy() * 255).astype(np.uint8)
img_pil = Image.fromarray(img_np, mode='RGB')
img_buffer = io.BytesIO()
img_pil.save(img_buffer, format='PNG')
img_bytes = img_buffer.getvalue()
import uuid
unique_filename = f"input.png"
files = {'image': (unique_filename, img_bytes, 'image/png')}
data = {'overwrite': 'true', 'type': 'input'}
url = f"http://{server_address}/upload/image"
response = requests.post(url, files=files, data=data, timeout=30)
if response.status_code == 200:
result = response.json()
uploaded_name = result.get('name', unique_filename)
subfolder = result.get('subfolder', '')
if subfolder:
return f"{subfolder}/{uploaded_name}"
else:
return uploaded_name
else:
return None
except Exception as e:
return None
def modify_workflow_input(self, workflow, node_id, input_type, input_value, server_address):
if node_id not in workflow:
return workflow
node = workflow[node_id]
if input_type == "image" or input_type == "video":
if node.get("class_type") == "LoadImage":
uploaded_filename = self.upload_image_to_remote(server_address, input_value)
if uploaded_filename:
if "inputs" not in node:
node["inputs"] = {}
node["inputs"]["image"] = uploaded_filename
elif input_type == "text":
if "inputs" not in node:
node["inputs"] = {}
text_fields = ["prompt", "text", "string", "value"]
found = False
for field in text_fields:
if field in node["inputs"]:
node["inputs"][field] = str(input_value)
found = True
break
if not found:
node["inputs"]["prompt"] = str(input_value)
elif input_type == "audio":
if node.get("class_type") == "LoadAudio":
uploaded_filename = self.upload_audio_to_remote(server_address, input_value)
if uploaded_filename:
if "inputs" not in node:
node["inputs"] = {}
node["inputs"]["audio"] = uploaded_filename
return workflow
def queue_prompt(self, server_address, workflow):
try:
url = f"http://{server_address}/prompt"
payload = {
"prompt": workflow,
"client_id": self.client_id
}
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 200:
result = response.json()
return result.get("prompt_id")
else:
return None
except Exception as e:
return None
def wait_for_completion(self, server_address, prompt_id, timeout=600):
import websocket
import threading
result_data = {"status": None, "outputs": None}
ws_url = f"ws://{server_address}/ws?clientId={self.client_id}"
start_time = time.time()
def on_message(ws, message):
try:
if isinstance(message, bytes):
return
data = json.loads(message)
msg_type = data.get("type")
if msg_type == "executing":
node_id = data.get("data", {}).get("node")
if node_id is None:
result_data["status"] = {"status_str": "success"}
elif msg_type == "executed":
node_id = data.get("data", {}).get("node")
output = data.get("data", {}).get("output", {})
if result_data["outputs"] is None:
result_data["outputs"] = {}
result_data["outputs"][node_id] = output
elif msg_type == "execution_error":
error_data = data.get("data", {})
result_data["status"] = {"status_str": "error", "error": error_data}
except Exception as e:
pass
def on_error(ws, error):
pass
def on_close(ws, close_status_code, close_msg):
pass
def on_open(ws):
pass
ws = websocket.WebSocketApp(
ws_url,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
ws_thread = threading.Thread(target=ws.run_forever)
ws_thread.daemon = True
ws_thread.start()
elapsed = 0
while elapsed < timeout:
if result_data["status"] is not None:
break
time.sleep(0.5)
elapsed = time.time() - start_time
ws.close()
if result_data["status"] is None:
return None
if result_data["status"].get("status_str") == "error":
return None
return result_data["outputs"]
def download_output_file(self, server_address, filename, subfolder="", folder_type="output"):
try:
url = f"http://{server_address}/view"
params = {
"filename": filename,
"subfolder": subfolder,
"type": folder_type
}
response = requests.get(url, params=params, timeout=30)
if response.status_code == 200:
file_ext = os.path.splitext(filename)[1].lower()
if file_ext in ['.mp4', '.avi', '.mov', '.webm', '.mkv', '.gif']:
import tempfile
with tempfile.NamedTemporaryFile(suffix=file_ext, delete=False) as tmp_file:
tmp_file.write(response.content)
tmp_path = tmp_file.name
try:
import cv2
cap = cv2.VideoCapture(tmp_path)
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frame_tensor = torch.from_numpy(frame_rgb).float() / 255.0
frames.append(frame_tensor)
cap.release()
if frames:
video_tensor = torch.stack(frames, dim=0)
audio_data = None
try:
import subprocess
audio_tmp_path = tmp_path.replace(file_ext, '.wav')
cmd = [
'ffmpeg',
'-i', tmp_path,
'-vn',
'-acodec', 'pcm_s16le',
'-ar', '44100',
'-ac', '2',
'-y',
audio_tmp_path
]
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=60
)
if result.returncode == 0 and os.path.exists(audio_tmp_path):
import torchaudio
waveform, sample_rate = torchaudio.load(audio_tmp_path)
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0)
audio_data = {
"waveform": waveform,
"sample_rate": sample_rate
}
try:
os.unlink(audio_tmp_path)
except:
pass
except:
try:
import torchaudio
waveform, sample_rate = torchaudio.load(tmp_path)
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0)
audio_data = {
"waveform": waveform,
"sample_rate": sample_rate
}
except:
pass
try:
os.unlink(tmp_path)
except:
pass
return video_tensor, audio_data
else:
try:
os.unlink(tmp_path)
except:
pass
return None, None
except Exception as video_error:
try:
os.unlink(tmp_path)
except:
pass
return None, None
else:
img = Image.open(io.BytesIO(response.content))
img = img.convert("RGB")
img_array = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_array).unsqueeze(0)
return img_tensor, None
else:
return None, None
except Exception as e:
return None, None
def download_output_audio(self, server_address, filename, subfolder="", folder_type="output"):
try:
url = f"http://{server_address}/view"
params = {
"filename": filename,
"subfolder": subfolder,
"type": folder_type
}
response = requests.get(url, params=params, timeout=30)
if response.status_code == 200:
import tempfile
file_ext = os.path.splitext(filename)[1].lower()
with tempfile.NamedTemporaryFile(suffix=file_ext, delete=False) as tmp_file:
tmp_file.write(response.content)
tmp_path = tmp_file.name
try:
import torchaudio
waveform, sample_rate = torchaudio.load(tmp_path)
if waveform.dim() == 2:
waveform = waveform.unsqueeze(0)
audio_data = {
"waveform": waveform,
"sample_rate": sample_rate
}
try:
os.unlink(tmp_path)
except:
pass
return audio_data
except Exception as load_error:
try:
os.unlink(tmp_path)
except:
pass
return None
else:
return None
except Exception as e:
return None
def execute_remote(self, remote_ip, remote_port, workflow_file, selected_nodes, saved_state="{}", **kwargs):
server_address = f"{remote_ip}:{remote_port}"
if not self.test_remote_connection(server_address):
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "无法连接到远程服务器", self.create_empty_audio(), error_img)
workflow = self.load_workflow(workflow_file)
if workflow is None:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "工作流加载失败", self.create_empty_audio(), error_img)
is_api_format = False
if isinstance(workflow, dict):
first_key = next(iter(workflow.keys()), None)
if first_key and first_key.isdigit():
is_api_format = True
if not is_api_format:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "请使用API格式的工作流文件", self.create_empty_audio(), error_img)
try:
selected_map = json.loads(selected_nodes)
except:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "选中节点数据格式错误", self.create_empty_audio(), error_img)
if not selected_map:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "未选中任何节点", self.create_empty_audio(), error_img)
sorted_nodes = sorted(selected_map.items(), key=lambda x: int(x[0]))
type_counters = {"image": 0, "text": 0, "audio": 0, "video": 0}
for node_id, input_type in sorted_nodes:
type_counters[input_type] += 1
counter = type_counters[input_type]
input_key = f"{input_type}_{counter}"
input_value = kwargs.get(input_key)
if input_value is None:
continue
workflow = self.modify_workflow_input(workflow, node_id, input_type, input_value, server_address)
prompt_id = self.queue_prompt(server_address, workflow)
if prompt_id is None:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "提交工作流失败", self.create_empty_audio(), error_img)
all_outputs = self.wait_for_completion(server_address, prompt_id)
if all_outputs is None:
error_img = torch.zeros((1, 64, 64, 3))
return (error_img, "执行失败或超时", self.create_empty_audio(), error_img)
output_images = []
output_texts = []
output_audios = []
output_videos = []
if all_outputs:
save_image_nodes = []
preview_image_nodes = []
text_nodes = []
video_nodes = []
audio_nodes = []
for node_id, node_output in all_outputs.items():
if not node_output:
continue
node_class = workflow.get(node_id, {}).get("class_type", "")
if "images" in node_output:
if node_class == "SaveImage":
save_image_nodes.append(node_id)
elif node_class == "PreviewImage":
preview_image_nodes.append(node_id)
if "text" in node_output or "string" in node_output:
text_nodes.append(node_id)
if "gifs" in node_output:
video_nodes.append(node_id)
if "audio" in node_output or "audios" in node_output:
audio_nodes.append(node_id)
final_image_node = None
if save_image_nodes:
final_image_node = max(save_image_nodes, key=lambda x: int(x))
elif preview_image_nodes:
final_image_node = max(preview_image_nodes, key=lambda x: int(x))
else:
for node_id, node_output in all_outputs.items():
if node_output and "images" in node_output:
final_image_node = node_id
final_text_node = None
if text_nodes:
final_text_node = max(text_nodes, key=lambda x: int(x))
final_video_node = None
if video_nodes:
final_video_node = max(video_nodes, key=lambda x: int(x))
final_audio_node = None
if audio_nodes:
final_audio_node = max(audio_nodes, key=lambda x: int(x))
if final_image_node and final_image_node in all_outputs:
node_output = all_outputs[final_image_node]
if node_output:
if "images" in node_output:
for img_info in node_output["images"]:
img_tensor, _ = self.download_output_file(
server_address,
img_info["filename"],
img_info.get("subfolder", ""),
img_info.get("type", "output")
)
if img_tensor is not None:
output_images.append(img_tensor)
if final_text_node and final_text_node in all_outputs:
node_output = all_outputs[final_text_node]
if node_output:
if "text" in node_output:
if isinstance(node_output["text"], list):
for text_item in node_output["text"]:
output_texts.append(str(text_item))
else:
output_texts.append(str(node_output["text"]))
if "string" in node_output:
if isinstance(node_output["string"], list):
for string_item in node_output["string"]:
output_texts.append(str(string_item))
else:
output_texts.append(str(node_output["string"]))
if final_video_node and final_video_node in all_outputs:
node_output = all_outputs[final_video_node]
if node_output:
if "gifs" in node_output:
for gif_info in node_output["gifs"]:
video_filename = gif_info.get("filename", "")
video_tensor, video_audio = self.download_output_file(
server_address,
video_filename,
gif_info.get("subfolder", ""),
gif_info.get("type", "output")
)
if video_tensor is not None:
output_videos.append(video_tensor)
if video_audio is not None:
output_audios.append(video_audio)
if final_audio_node and final_audio_node in all_outputs:
node_output = all_outputs[final_audio_node]
if node_output:
if "audio" in node_output:
audio_list = node_output["audio"]
if not isinstance(audio_list, list):
audio_list = [audio_list]
for audio_item in audio_list:
if isinstance(audio_item, dict) and "filename" in audio_item:
audio_file = audio_item.get("filename", "")
audio_data = self.download_output_audio(
server_address,
audio_file,
audio_item.get("subfolder", ""),
audio_item.get("type", "output")
)
if audio_data is not None:
output_audios.append(audio_data)
elif "audios" in node_output:
audios_list = node_output["audios"]
if not isinstance(audios_list, list):
audios_list = [audios_list]
for audio_item in audios_list:
if isinstance(audio_item, dict) and "filename" in audio_item:
audio_file = audio_item.get("filename", "")
audio_data = self.download_output_audio(
server_address,
audio_file,
audio_item.get("subfolder", ""),
audio_item.get("type", "output")
)
if audio_data is not None:
output_audios.append(audio_data)
final_image = output_images[-1] if output_images else torch.zeros((1, 64, 64, 3))
final_text = output_texts[-1] if output_texts else "执行成功"
if output_audios:
final_audio = output_audios[-1]
else:
final_audio = self.create_empty_audio()
final_video = output_videos[-1] if output_videos else torch.zeros((1, 64, 64, 3))
return (final_image, final_text, final_audio, final_video)
NODE_CLASS_MAPPINGS = {
"RemoteWorkflowExecutor": RemoteWorkflowExecutor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RemoteWorkflowExecutor": "🐳IP Workflow"
}
WEB_DIRECTORY = "./js"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']