565 lines
22 KiB
Python
565 lines
22 KiB
Python
"""
|
|
Advanced Camera Control Prompt Generator
|
|
Reads camera information from Load 3D nodes and generates precise camera control prompts.
|
|
|
|
Author: Jianghong Zhu
|
|
"""
|
|
|
|
import math
|
|
|
|
|
|
class AdvancedCameraControlNode:
|
|
"""Advanced camera control prompt generator for ComfyUI."""
|
|
|
|
EYE_LEVEL_HEIGHT = 0.425
|
|
DEFAULT_POSITION = {"x": 0.0, "y": EYE_LEVEL_HEIGHT, "z": 0.5}
|
|
DEFAULT_TARGET = {"x": 0.0, "y": EYE_LEVEL_HEIGHT, "z": 0.0}
|
|
GRID_TO_METERS = 4.0
|
|
|
|
SENSOR_WIDTH_MM = 36.0
|
|
SENSOR_HEIGHT_MM = 24.0
|
|
|
|
FRAMING_THRESHOLDS = {
|
|
"extreme close-up": (90, 1000),
|
|
"close-up": (60, 90),
|
|
"medium close-up": (45, 60),
|
|
"medium shot": (30, 60),
|
|
"medium long shot": (20, 30),
|
|
"full shot": (15, 30),
|
|
"wide shot": (5, 15),
|
|
"extreme wide shot": (0, 5),
|
|
}
|
|
|
|
CAMERA_SHOTS = [
|
|
{"name": "Extreme Close-Up", "distance_m": [0.3, 0.6], "focal_length_mm": [85, 135], "fov_deg": [10, 20]},
|
|
{"name": "Close-Up", "distance_m": [0.6, 1.2], "focal_length_mm": [50, 85], "fov_deg": [20, 30]},
|
|
{"name": "Medium Close-Up", "distance_m": [1.0, 1.8], "focal_length_mm": [35, 50], "fov_deg": [30, 40]},
|
|
{"name": "Medium Shot", "distance_m": [1.5, 3.0], "focal_length_mm": [28, 50], "fov_deg": [35, 45]},
|
|
{"name": "Medium Long Shot", "distance_m": [2.5, 4.0], "focal_length_mm": [24, 35], "fov_deg": [45, 55]},
|
|
{"name": "Full Shot", "distance_m": [3.0, 5.0], "focal_length_mm": [24, 35], "fov_deg": [50, 60]},
|
|
{"name": "Wide Shot", "distance_m": [5.0, 10.0], "focal_length_mm": [18, 24], "fov_deg": [60, 90]},
|
|
{"name": "Extreme Wide Shot", "distance_m": [10, 50], "focal_length_mm": [14, 20], "fov_deg": [90, 120]},
|
|
]
|
|
|
|
CAMERA_ANGLES = [
|
|
{"name": "Eye Level", "tilt_deg": [-5, 5]},
|
|
{"name": "High Angle", "tilt_deg": [-20, -45]},
|
|
{"name": "Slight Low Angle", "tilt_deg": [5, 15]},
|
|
{"name": "Standard Low Angle", "tilt_deg": [15, 30]},
|
|
{"name": "Deep Low Angle", "tilt_deg": [30, 45]},
|
|
{"name": "Extreme Low Angle", "tilt_deg": [45, 90]},
|
|
{"name": "Bird's Eye", "tilt_deg": [-80, -90]},
|
|
{"name": "Dutch Angle", "roll_deg": [5, 30]},
|
|
{"name": "Dutch Low Angle", "roll_deg": [10, 45]},
|
|
]
|
|
|
|
EYE_LEVEL_TOLERANCE = 5.0
|
|
DUTCH_ANGLE_ROLL_MIN = 5.0
|
|
HIGH_ANGLE_MIN = 15.0
|
|
BIRD_EYE_MIN = 75.0
|
|
LOW_ANGLE_MAX = -15.0
|
|
WORM_EYE_MAX = -75.0
|
|
|
|
EXTREME_CLOSE_UP_MAX = 0.6
|
|
CLOSE_UP_MAX = 1.2
|
|
MEDIUM_SHOT_MAX = 3.0
|
|
FULL_SHOT_MAX = 5.0
|
|
WIDE_SHOT_MAX = 10.0
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"camera_info": ("LOAD3D_CAMERA", {"label": "Camera Info (from Load 3D)"}),
|
|
},
|
|
"optional": {
|
|
"focal_length_mm": ("FLOAT", {
|
|
"default": 50.0,
|
|
"min": 1.0,
|
|
"max": 1000.0,
|
|
"step": 1.0,
|
|
"label": "Focal Length (mm)"
|
|
}),
|
|
"object_scale_meters": ("FLOAT", {
|
|
"default": 1.0,
|
|
"min": 0.01,
|
|
"max": 100.0,
|
|
"step": 0.01,
|
|
"label": "Object Scale (meters)"
|
|
}),
|
|
"custom_description": ("STRING", {
|
|
"default": "",
|
|
"multiline": True,
|
|
"label": "Custom Description (optional)"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("prompt", "camera_json")
|
|
FUNCTION = "generate_prompt"
|
|
CATEGORY = "prompt/camera"
|
|
|
|
def generate_prompt(self, camera_info, focal_length_mm=50.0, object_scale_meters=None, custom_description=""):
|
|
"""Generate camera control prompt from camera_info"""
|
|
if not camera_info:
|
|
return ("", "{}")
|
|
|
|
position = camera_info.get("position", {})
|
|
target = camera_info.get("target", {})
|
|
zoom = camera_info.get("zoom", 1)
|
|
|
|
distance = self._calculate_distance(position, target)
|
|
distance_meters = distance * self.GRID_TO_METERS
|
|
pitch, yaw, roll = self._calculate_camera_angles(position, target)
|
|
|
|
fov = self._calculate_fov_from_focal_length(focal_length_mm, use_horizontal=True)
|
|
|
|
angle_type = None
|
|
if abs(pitch) > self.EYE_LEVEL_TOLERANCE:
|
|
angle_type = self._get_angle_type_name(pitch, roll)
|
|
|
|
shot_prompt = self._get_shot_type_prompt(distance, zoom, fov, focal_length_mm, object_scale_meters)
|
|
position_prompt = self._get_camera_position_prompt(position, target, pitch, yaw)
|
|
|
|
prompt_parts = []
|
|
description_parts = []
|
|
|
|
camera_position_text = position_prompt if position_prompt else None
|
|
|
|
vertical_angle_text = None
|
|
if abs(pitch) > self.EYE_LEVEL_TOLERANCE and angle_type:
|
|
pitch_deg = int(abs(pitch))
|
|
vertical_angle_text = f"tilt down at {pitch_deg} degree" if pitch > 0 else f"tilt up at {pitch_deg} degree"
|
|
|
|
shot_type_name = None
|
|
camera_info_text = None
|
|
if shot_prompt:
|
|
if "(" in shot_prompt:
|
|
shot_type_name = shot_prompt.split("(")[0].strip()
|
|
camera_info_text = "(" + shot_prompt.split("(")[1].strip()
|
|
else:
|
|
shot_type_name = shot_prompt.strip()
|
|
|
|
movement_parts = []
|
|
if camera_position_text:
|
|
movement_parts.append(camera_position_text)
|
|
if vertical_angle_text:
|
|
movement_parts.append(vertical_angle_text)
|
|
|
|
angle_shot_parts = []
|
|
if angle_type:
|
|
angle_shot_parts.append(angle_type)
|
|
if shot_type_name:
|
|
angle_shot_parts.append(shot_type_name)
|
|
|
|
if movement_parts:
|
|
movement_str = " and ".join(movement_parts)
|
|
if angle_shot_parts:
|
|
angle_shot_str = " ".join(angle_shot_parts)
|
|
description_parts.append(f"{movement_str}, {angle_shot_str}")
|
|
else:
|
|
description_parts.append(f"{movement_str}.")
|
|
elif angle_shot_parts:
|
|
angle_shot_str = " ".join(angle_shot_parts)
|
|
description_parts.append(f"{angle_shot_str}.")
|
|
|
|
if description_parts:
|
|
prompt_parts.append(" ".join(description_parts))
|
|
|
|
if camera_info_text:
|
|
prompt_parts.append(camera_info_text)
|
|
|
|
if custom_description and custom_description.strip():
|
|
prompt_parts.append(custom_description.strip())
|
|
|
|
final_prompt = " ".join(prompt_parts)
|
|
|
|
camera_json = self._generate_camera_json(
|
|
position, target, pitch, yaw, roll, distance_meters,
|
|
focal_length_mm, fov, shot_type_name
|
|
)
|
|
|
|
return (final_prompt, camera_json)
|
|
|
|
def _generate_camera_json(self, position, target, pitch, yaw, roll, distance_meters,
|
|
focal_length_mm, fov, shot_type_name):
|
|
"""Generate JSON format for camera information"""
|
|
import json
|
|
|
|
shot_type_json = None
|
|
if shot_type_name:
|
|
shot_type_json = shot_type_name.replace(" ", "_").replace("-", "_").lower()
|
|
|
|
tilt_deg_value = round(pitch, 1)
|
|
if pitch > 0:
|
|
tilt_deg_str = f"tilt down {abs(tilt_deg_value)}"
|
|
elif pitch < 0:
|
|
tilt_deg_str = f"tilt up {abs(tilt_deg_value)}"
|
|
else:
|
|
tilt_deg_str = f"tilt {tilt_deg_value}"
|
|
|
|
pan_deg_value = round(yaw, 1)
|
|
if yaw > 0:
|
|
pan_deg_str = f"pan to right {abs(pan_deg_value)}"
|
|
elif yaw < 0:
|
|
pan_deg_str = f"pan to left {abs(pan_deg_value)}"
|
|
else:
|
|
pan_deg_str = f"pan {pan_deg_value}"
|
|
|
|
camera_data = {
|
|
"focal_length_mm": int(focal_length_mm) if focal_length_mm else None,
|
|
"sensor_width_mm": int(self.SENSOR_WIDTH_MM),
|
|
"sensor_height_mm": int(self.SENSOR_HEIGHT_MM),
|
|
"distance_m": round(distance_meters, 2),
|
|
"tilt_deg": tilt_deg_str,
|
|
"pan_deg": pan_deg_str,
|
|
"roll_deg": round(roll, 1),
|
|
"shot_type": shot_type_json
|
|
}
|
|
|
|
return json.dumps({"camera": camera_data}, indent=4)
|
|
|
|
def _calculate_distance(self, position, target):
|
|
"""Calculate 3D distance from camera position to target"""
|
|
if not position or not target:
|
|
return 0
|
|
|
|
dx = position.get("x", 0) - target.get("x", 0)
|
|
dy = position.get("y", 0) - target.get("y", 0)
|
|
dz = position.get("z", 0) - target.get("z", 0)
|
|
|
|
return math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
|
|
def _calculate_camera_angles(self, position, target):
|
|
"""Calculate camera angles: pitch (vertical), yaw (horizontal), roll (rotation)"""
|
|
if not position or not target:
|
|
return (0, 0, 0)
|
|
|
|
pos_x = position.get("x", 0)
|
|
pos_y = position.get("y", 0)
|
|
pos_z = position.get("z", 0)
|
|
|
|
tgt_x = target.get("x", 0)
|
|
tgt_y = target.get("y", 0)
|
|
tgt_z = target.get("z", 0)
|
|
|
|
dx = pos_x - tgt_x
|
|
dy = pos_y - tgt_y
|
|
dz = pos_z - tgt_z
|
|
|
|
horizontal_dist = math.sqrt(dx * dx + dz * dz)
|
|
|
|
if horizontal_dist > 0.001:
|
|
pitch_rad = math.atan2(dy, horizontal_dist)
|
|
pitch = math.degrees(pitch_rad)
|
|
else:
|
|
pitch = 90.0 if dy > 0 else -90.0
|
|
|
|
if abs(dz) > 0.001:
|
|
yaw_rad = math.atan2(dx, -dz)
|
|
yaw = math.degrees(yaw_rad)
|
|
else:
|
|
yaw = 0.0
|
|
|
|
roll = 0.0
|
|
|
|
return (pitch, yaw, roll)
|
|
|
|
def _get_angle_type_name(self, pitch, roll):
|
|
"""Get angle type name"""
|
|
if abs(roll) >= 10 and pitch < 0:
|
|
for angle in self.CAMERA_ANGLES:
|
|
if angle["name"] == "Dutch Low Angle" and "roll_deg" in angle:
|
|
roll_range = angle["roll_deg"]
|
|
if roll_range[0] <= abs(roll) <= roll_range[1]:
|
|
return "dutch low angle"
|
|
|
|
if abs(roll) >= self.DUTCH_ANGLE_ROLL_MIN:
|
|
return "dutch angle"
|
|
|
|
for angle in self.CAMERA_ANGLES:
|
|
if "tilt_deg" in angle:
|
|
tilt_range = angle["tilt_deg"]
|
|
tilt_value = -pitch
|
|
|
|
if tilt_range[0] <= tilt_value <= tilt_range[1]:
|
|
angle_name = angle["name"]
|
|
if angle_name == "High Angle":
|
|
return "high angle"
|
|
elif angle_name == "Slight Low Angle":
|
|
return "slight low angle"
|
|
elif angle_name == "Standard Low Angle":
|
|
return "standard low angle"
|
|
elif angle_name == "Deep Low Angle":
|
|
return "deep low angle"
|
|
elif angle_name == "Extreme Low Angle":
|
|
return "extreme low angle"
|
|
elif angle_name == "Bird's Eye":
|
|
return "bird's eye view"
|
|
|
|
if pitch >= self.BIRD_EYE_MIN:
|
|
return "bird's eye view"
|
|
elif pitch >= self.HIGH_ANGLE_MIN:
|
|
return "high angle"
|
|
elif pitch <= self.WORM_EYE_MAX:
|
|
return "extreme low angle"
|
|
elif pitch <= -45:
|
|
return "deep low angle"
|
|
elif pitch <= -30:
|
|
return "standard low angle"
|
|
elif pitch <= -15:
|
|
return "slight low angle"
|
|
elif pitch > 0:
|
|
return "high angle"
|
|
else:
|
|
return "low angle"
|
|
|
|
def _estimate_fov_from_distance(self, distance):
|
|
"""Estimate FOV based on distance"""
|
|
distance_meters = distance * self.GRID_TO_METERS
|
|
|
|
if distance_meters < self.EXTREME_CLOSE_UP_MAX:
|
|
return 15.0
|
|
elif distance_meters < self.CLOSE_UP_MAX:
|
|
return 25.0
|
|
elif distance_meters < self.MEDIUM_SHOT_MAX:
|
|
return 37.5
|
|
elif distance_meters < self.FULL_SHOT_MAX:
|
|
return 52.5
|
|
elif distance_meters < self.WIDE_SHOT_MAX:
|
|
return 70.0
|
|
else:
|
|
return 100.0
|
|
|
|
def _calculate_fov_from_focal_length(self, focal_length_mm, use_horizontal=True):
|
|
"""Calculate FOV from focal length"""
|
|
if focal_length_mm <= 0:
|
|
return None
|
|
|
|
sensor_dim = self.SENSOR_WIDTH_MM if use_horizontal else self.SENSOR_HEIGHT_MM
|
|
fov_rad = 2.0 * math.atan(sensor_dim / (2.0 * focal_length_mm))
|
|
return math.degrees(fov_rad)
|
|
|
|
def _estimate_focal_length(self, fov):
|
|
"""Estimate focal length from FOV"""
|
|
if fov and fov > 0:
|
|
fov_rad = math.radians(fov)
|
|
focal_length = 18.0 / math.tan(fov_rad / 2.0)
|
|
return max(14.0, min(200.0, focal_length))
|
|
return None
|
|
|
|
def _calculate_framing_shot_type(self, object_scale_meters, distance_meters, focal_length_mm, use_horizontal=False):
|
|
"""Calculate shot type based on framing"""
|
|
if object_scale_meters is None or object_scale_meters <= 0:
|
|
return None
|
|
if distance_meters <= 0 or focal_length_mm <= 0:
|
|
return None
|
|
|
|
projected_mm = focal_length_mm * (object_scale_meters / distance_meters)
|
|
sensor_dim_mm = self.SENSOR_WIDTH_MM if use_horizontal else self.SENSOR_HEIGHT_MM
|
|
percent_of_frame = 100.0 * projected_mm / sensor_dim_mm
|
|
|
|
for shot_name, (min_pct, max_pct) in self.FRAMING_THRESHOLDS.items():
|
|
if min_pct <= percent_of_frame < max_pct or (max_pct >= 1000 and percent_of_frame >= min_pct):
|
|
return shot_name
|
|
|
|
if percent_of_frame >= 100:
|
|
return "extreme close-up"
|
|
elif percent_of_frame < 0.1:
|
|
return "extreme wide shot"
|
|
|
|
return None
|
|
|
|
def _get_shot_type_prompt(self, distance, zoom, fov=None, focal_length_mm=None, object_scale_meters=None):
|
|
"""Generate shot type prompt"""
|
|
distance_meters = distance * self.GRID_TO_METERS
|
|
distance_str = f"{distance_meters:.1f} m"
|
|
|
|
estimated_focal = focal_length_mm
|
|
if estimated_focal is None and fov is not None:
|
|
estimated_focal = self._estimate_focal_length(fov)
|
|
|
|
shot_type = None
|
|
if object_scale_meters is not None and estimated_focal is not None:
|
|
shot_type = self._calculate_framing_shot_type(
|
|
object_scale_meters=object_scale_meters,
|
|
distance_meters=distance_meters,
|
|
focal_length_mm=estimated_focal,
|
|
use_horizontal=False
|
|
)
|
|
|
|
if shot_type is None:
|
|
for shot in self.CAMERA_SHOTS:
|
|
dist_range = shot["distance_m"]
|
|
if dist_range[0] <= distance_meters <= dist_range[1]:
|
|
shot_name = shot["name"]
|
|
if shot_name == "Extreme Close-Up":
|
|
shot_type = "extreme close-up"
|
|
elif shot_name == "Close-Up":
|
|
shot_type = "close-up"
|
|
elif shot_name == "Medium Close-Up":
|
|
shot_type = "medium close-up"
|
|
elif shot_name == "Medium Shot":
|
|
shot_type = "medium shot"
|
|
elif shot_name == "Medium Long Shot":
|
|
shot_type = "medium long shot"
|
|
elif shot_name == "Full Shot":
|
|
shot_type = "full shot"
|
|
elif shot_name == "Wide Shot":
|
|
shot_type = "wide shot"
|
|
elif shot_name == "Extreme Wide Shot":
|
|
shot_type = "extreme wide shot"
|
|
break
|
|
|
|
if shot_type is None and fov is not None:
|
|
try:
|
|
fov_deg = float(fov)
|
|
for shot in self.CAMERA_SHOTS:
|
|
fov_range = shot["fov_deg"]
|
|
if fov_range[0] <= fov_deg <= fov_range[1]:
|
|
shot_name = shot["name"]
|
|
if shot_name == "Extreme Close-Up":
|
|
shot_type = "extreme close-up"
|
|
elif shot_name == "Close-Up":
|
|
shot_type = "close-up"
|
|
elif shot_name == "Medium Close-Up":
|
|
shot_type = "medium close-up"
|
|
elif shot_name == "Medium Shot":
|
|
shot_type = "medium shot"
|
|
elif shot_name == "Medium Long Shot":
|
|
shot_type = "medium long shot"
|
|
elif shot_name == "Full Shot":
|
|
shot_type = "full shot"
|
|
elif shot_name == "Wide Shot":
|
|
shot_type = "wide shot"
|
|
elif shot_name == "Extreme Wide Shot":
|
|
shot_type = "extreme wide shot"
|
|
break
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if shot_type is None and estimated_focal is not None:
|
|
for shot in self.CAMERA_SHOTS:
|
|
focal_range = shot["focal_length_mm"]
|
|
if focal_range[0] <= estimated_focal <= focal_range[1]:
|
|
shot_name = shot["name"]
|
|
if shot_name == "Extreme Close-Up":
|
|
shot_type = "extreme close-up"
|
|
elif shot_name == "Close-Up":
|
|
shot_type = "close-up"
|
|
elif shot_name == "Medium Close-Up":
|
|
shot_type = "medium close-up"
|
|
elif shot_name == "Medium Shot":
|
|
shot_type = "medium shot"
|
|
elif shot_name == "Medium Long Shot":
|
|
shot_type = "medium long shot"
|
|
elif shot_name == "Full Shot":
|
|
shot_type = "full shot"
|
|
elif shot_name == "Wide Shot":
|
|
shot_type = "wide shot"
|
|
elif shot_name == "Extreme Wide Shot":
|
|
shot_type = "extreme wide shot"
|
|
break
|
|
|
|
if shot_type is None:
|
|
if distance_meters < self.EXTREME_CLOSE_UP_MAX:
|
|
shot_type = "extreme close-up"
|
|
elif distance_meters < self.CLOSE_UP_MAX:
|
|
shot_type = "close-up"
|
|
elif distance_meters < self.MEDIUM_SHOT_MAX:
|
|
shot_type = "medium shot"
|
|
elif distance_meters < self.FULL_SHOT_MAX:
|
|
shot_type = "full shot"
|
|
elif distance_meters < self.WIDE_SHOT_MAX:
|
|
shot_type = "wide shot"
|
|
else:
|
|
shot_type = "extreme wide shot"
|
|
|
|
camera_info_parts = [f"camera distance {distance_str}"]
|
|
|
|
focal_to_display = focal_length_mm if focal_length_mm is not None else estimated_focal
|
|
if focal_to_display is not None:
|
|
camera_info_parts.append(f"{int(focal_to_display)}mm")
|
|
|
|
if fov is not None:
|
|
fov_deg = int(fov) if isinstance(fov, (int, float)) else None
|
|
if fov_deg:
|
|
camera_info_parts.append(f"FOV {fov_deg}°")
|
|
|
|
camera_info = " ".join(camera_info_parts)
|
|
return f"{shot_type} ({camera_info})"
|
|
|
|
def _get_camera_position_prompt(self, position, target, pitch, yaw):
|
|
"""Get camera position prompt"""
|
|
if not position or not target:
|
|
return ""
|
|
|
|
def safe_float(value, default=0.0):
|
|
if isinstance(value, (int, float)):
|
|
return float(value)
|
|
try:
|
|
return float(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
pos_x = safe_float(position.get("x", 0)) - safe_float(target.get("x", 0))
|
|
pos_y = safe_float(position.get("y", 0)) - safe_float(target.get("y", 0))
|
|
pos_z = safe_float(position.get("z", 0)) - safe_float(target.get("z", 0))
|
|
|
|
horizontal_dist = math.sqrt(pos_x * pos_x + pos_z * pos_z)
|
|
|
|
if horizontal_dist < 0.001:
|
|
return "above object" if pos_y > 0 else "below object"
|
|
|
|
angle_deg = math.degrees(math.atan2(pos_x, pos_z))
|
|
if angle_deg < 0:
|
|
angle_deg += 360
|
|
|
|
parts = []
|
|
angle_int = int(angle_deg)
|
|
is_directly_front = (abs(angle_int) < 2 or abs(angle_int - 360) < 2) and pos_z > 0.1
|
|
|
|
if is_directly_front:
|
|
return ""
|
|
|
|
if pos_z < -0.1:
|
|
if abs(pos_x) < 0.1:
|
|
parts.append("looking from behind")
|
|
else:
|
|
if 90 <= angle_int <= 180:
|
|
parts.append(f"Pan the camera {angle_int} degrees to the right-back side")
|
|
elif 180 < angle_int <= 270:
|
|
parts.append(f"Pan the camera {angle_int} degrees to the left-back side")
|
|
else:
|
|
parts.append(f"looking from behind at {angle_int} degree")
|
|
else:
|
|
if abs(pos_x) < 0.1:
|
|
if pos_z > 0.1:
|
|
return ""
|
|
else:
|
|
parts.append("looking from behind")
|
|
else:
|
|
if pos_x > 0.1:
|
|
parts.append(f"Pan the camera {angle_int} degrees to the right")
|
|
elif pos_x < -0.1:
|
|
if angle_int >= 270:
|
|
left_angle = 360 - angle_int
|
|
parts.append(f"Pan the camera {left_angle} degrees to the left")
|
|
elif angle_int > 180:
|
|
left_angle = angle_int
|
|
parts.append(f"Pan the camera {left_angle} degrees to the left")
|
|
else:
|
|
parts.append(f"Pan the camera {angle_int} degrees to the left")
|
|
|
|
return ", ".join(parts) if parts else ""
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AdvancedCameraControlNode": AdvancedCameraControlNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AdvancedCameraControlNode": "Advanced Camera Control Prompt Generator"
|
|
}
|