Add files via upload
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
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",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user