159 lines
5.7 KiB
Python
159 lines
5.7 KiB
Python
"""
|
|
SynvowMultiangle Node for ComfyUI
|
|
A 3D camera control node that outputs angle prompts
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import numpy as np
|
|
from PIL import Image
|
|
import torch
|
|
import folder_paths
|
|
|
|
|
|
class SynvowMultiangleCameraNode:
|
|
"""
|
|
3D Camera Angle Control Node
|
|
Provides a 3D scene to adjust camera angles and outputs a formatted prompt string
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"horizontal_angle": ("INT", {
|
|
"default": 0, "min": 0, "max": 360, "step": 1, "display": "slider"
|
|
}),
|
|
"vertical_angle": ("INT", {
|
|
"default": 0, "min": -60, "max": 60, "step": 1, "display": "slider"
|
|
}),
|
|
"zoom": ("FLOAT", {
|
|
"default": 5.0, "min": 0.0, "max": 10.0, "step": 0.1, "display": "slider"
|
|
}),
|
|
"size_mode": (["custom", "original"], {"default": "custom"}),
|
|
"max_long_edge": ("INT", {
|
|
"default": 1024, "min": 64, "max": 8192, "step": 8
|
|
}),
|
|
},
|
|
"optional": {
|
|
"image": ("IMAGE",),
|
|
},
|
|
"hidden": {
|
|
"uploaded_image": "STRING",
|
|
"prompt": "PROMPT",
|
|
"extra_pnginfo": "EXTRA_PNGINFO",
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "IMAGE")
|
|
RETURN_NAMES = ("prompt", "image")
|
|
FUNCTION = "generate_prompt"
|
|
CATEGORY = "💫SynVow/Image"
|
|
DESCRIPTION = "3D camera angle control node for multi-angle prompt generation"
|
|
|
|
def generate_prompt(self, horizontal_angle, vertical_angle, zoom,
|
|
size_mode="custom", max_long_edge=1024,
|
|
image=None, uploaded_image="", prompt=None, extra_pnginfo=None):
|
|
# Validate input ranges
|
|
horizontal_angle = max(0, min(360, int(horizontal_angle)))
|
|
vertical_angle = max(-60, min(60, int(vertical_angle)))
|
|
zoom = max(0.0, min(10.0, float(zoom)))
|
|
|
|
h_angle = horizontal_angle % 360
|
|
|
|
# Azimuth (horizontal) - 8 directions
|
|
h_boundaries = [22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]
|
|
h_directions = [
|
|
"front view", "front-right quarter view", "right side view",
|
|
"back-right quarter view", "back view", "back-left quarter view",
|
|
"left side view", "front-left quarter view"
|
|
]
|
|
h_idx = 0
|
|
for i, b in enumerate(h_boundaries):
|
|
if h_angle < b:
|
|
h_idx = i
|
|
break
|
|
else:
|
|
h_idx = 0
|
|
h_direction = h_directions[h_idx]
|
|
|
|
# Elevation (vertical) - 5 levels
|
|
if vertical_angle < -30:
|
|
v_direction = "extreme low-angle shot"
|
|
elif vertical_angle < -10:
|
|
v_direction = "low-angle shot"
|
|
elif vertical_angle < 10:
|
|
v_direction = "eye-level shot"
|
|
elif vertical_angle < 40:
|
|
v_direction = "elevated shot"
|
|
else:
|
|
v_direction = "high-angle shot"
|
|
|
|
# Distance - 3 levels
|
|
if zoom < 2:
|
|
distance = "wide shot"
|
|
elif zoom < 6:
|
|
distance = "medium shot"
|
|
else:
|
|
distance = "close-up"
|
|
|
|
prompt = f"<sks> {h_direction} {v_direction} {distance}"
|
|
|
|
# Get image: prioritize connected IMAGE input, fallback to uploaded file
|
|
if image is not None:
|
|
img_tensor = image[0] if len(image.shape) == 4 else image
|
|
elif uploaded_image:
|
|
image_path = folder_paths.get_annotated_filepath(uploaded_image)
|
|
pil_img = Image.open(image_path).convert('RGB')
|
|
img_np = np.array(pil_img).astype(np.float32) / 255.0
|
|
img_tensor = torch.from_numpy(img_np)
|
|
else:
|
|
raise ValueError("Please provide an image: connect image input or use the load button")
|
|
|
|
# Process image based on size mode
|
|
if size_mode == "custom":
|
|
h, w = img_tensor.shape[:2]
|
|
long_edge = max(h, w)
|
|
if long_edge != max_long_edge:
|
|
scale = max_long_edge / long_edge
|
|
new_h, new_w = int(h * scale), int(w * scale)
|
|
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
|
|
pil_image = Image.fromarray(img_np).resize((new_w, new_h), Image.BILINEAR)
|
|
img_np = np.array(pil_image).astype(np.float32) / 255.0
|
|
output_image = torch.from_numpy(img_np).unsqueeze(0)
|
|
else:
|
|
output_image = img_tensor.unsqueeze(0)
|
|
else:
|
|
output_image = img_tensor.unsqueeze(0)
|
|
|
|
# Save preview image to temp directory for frontend 3D preview
|
|
preview_image = None
|
|
try:
|
|
temp_dir = folder_paths.get_temp_directory()
|
|
timestamp = int(time.time() * 1000)
|
|
filename = f"synvow_preview_{timestamp}.png"
|
|
filepath = os.path.join(temp_dir, filename)
|
|
|
|
img_np = (output_image[0].cpu().numpy() * 255).astype(np.uint8)
|
|
pil_img = Image.fromarray(img_np)
|
|
pil_img.save(filepath, compress_level=1)
|
|
|
|
preview_image = {"filename": filename, "subfolder": "", "type": "temp"}
|
|
except Exception as e:
|
|
print(f"[SynvowMultiangle] Error saving preview: {e}")
|
|
|
|
return {
|
|
"ui": {"preview_image": [preview_image] if preview_image else []},
|
|
"result": (prompt, output_image)
|
|
}
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SynvowMultiangleCameraNode": SynvowMultiangleCameraNode,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SynvowMultiangleCameraNode": "Synvow Multiangle Camera",
|
|
}
|
|
|