commit 2191efcfcd221a35e9b45a9e953964fb654155ab Author: Jianghong Zhu Date: Wed Nov 12 10:59:47 2025 -0800 Initial commit: Advanced Camera Control Prompt Generator diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6ce04a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4be0417 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# ComfyUI Advanced Camera Prompts + +Advanced camera control prompt generator for ComfyUI that reads camera information from 3D nodes and generates precise, professional camera control prompts. + +## Features + +- **Automatic Camera Detection**: Reads camera position, target, and angles from Load 3D nodes +- **Professional Shot Types**: Automatically classifies shots (extreme close-up, close-up, medium shot, wide shot, etc.) +- **Camera Angle Recognition**: Detects high angle, low angle, bird's eye, dutch angle, and more +- **Focal Length Support**: Calculate FOV from focal length (1-1000mm) +- **Framing-Based Classification**: Optional object scale input for accurate shot type detection +- **Dual Output**: Generates both human-readable prompts and structured JSON data +- **Industry Standard**: Based on cinematography standards for shot types and camera angles + +## Installation + +1. Clone this repository into your `ComfyUI/custom_nodes/` folder: + ```bash + cd ComfyUI/custom_nodes + git clone https://github.com/jandan520/ComfyUI-AdvancedCameraPrompts.git + ``` + +2. Restart ComfyUI + +## Usage + +### Node Inputs + +**Required:** +- **Camera Info** (`LOAD3D_CAMERA`): Camera information from Load 3D or Load 3D - Animation nodes + +**Optional:** +- **Focal Length (mm)** (`FLOAT`, default: 50.0): Camera lens focal length (1-1000mm) +- **Object Scale (meters)** (`FLOAT`, default: 1.0): Object size in meters for framing-based shot classification +- **Custom Description** (`STRING`): Additional text to append to the prompt + +### Node Outputs + +1. **Prompt** (`STRING`): Human-readable camera control prompt +2. **Camera JSON** (`STRING`): Structured JSON with all camera parameters + +## Example Output + +**Prompt:** +``` +Pan the camera 45 degrees to the right, high angle medium shot (camera distance 2.5 m 50mm FOV 40°) +``` + +**Camera JSON:** +```json +{ + "camera": { + "focal_length_mm": 50, + "sensor_width_mm": 36, + "sensor_height_mm": 24, + "distance_m": 2.5, + "tilt_deg": "tilt down 15.0", + "pan_deg": "pan to right 45.0", + "roll_deg": 0.0, + "shot_type": "medium_shot" + } +} +``` + +## Shot Types + +The node automatically classifies shots based on distance, focal length, FOV, or framing: + +- Extreme Close-Up +- Close-Up +- Medium Close-Up +- Medium Shot +- Medium Long Shot +- Full Shot +- Wide Shot +- Extreme Wide Shot + +## Camera Angles + +Recognized camera angles: + +- Eye Level +- High Angle +- Slight Low Angle +- Standard Low Angle +- Deep Low Angle +- Extreme Low Angle (Worm's Eye) +- Bird's Eye +- Dutch Angle +- Dutch Low Angle + +## Technical Details + +- **Coordinate System**: Z-axis points at camera, X-axis is horizontal, Y-axis is vertical +- **Sensor Format**: 35mm full-frame equivalent (36mm × 24mm) +- **Distance Scale**: 1 grid unit = 4 meters +- **FOV Calculation**: Uses standard formula: `FOV = 2 × arctan(sensor_dim / (2 × focal_length))` + +## Requirements + +- ComfyUI (latest versions) +- Python 3.8+ +- Load 3D nodes (for camera input) + +## License + +MIT + +## Author + +Jianghong Zhu + +## Credits + +Optimized for use with dx8152's MultiAngle LoRA and similar camera control workflows. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..6b78209 --- /dev/null +++ b/__init__.py @@ -0,0 +1,18 @@ +""" +Advanced Camera Control Prompt Generator for ComfyUI + +Author: Jianghong Zhu +License: MIT +""" + +from .advanced_camera_control_node import AdvancedCameraControlNode + +NODE_CLASS_MAPPINGS = { + "AdvancedCameraControlNode": AdvancedCameraControlNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AdvancedCameraControlNode": "Advanced Camera Control Prompt Generator" +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/advanced_camera_control_node.py b/advanced_camera_control_node.py new file mode 100644 index 0000000..b779a11 --- /dev/null +++ b/advanced_camera_control_node.py @@ -0,0 +1,564 @@ +""" +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" +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8aa4498 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "comfyui-advanced-camera-prompts" +description = "Advanced camera control prompt generator for ComfyUI that reads camera information from 3D nodes and generates precise, professional camera control prompts." +version = "1.0.0" +license = {file = "LICENSE"} +authors = [ + {name = "Jianghong Zhu"} +] +# classifiers = [ +# # For OS-independent nodes (works on all operating systems) +# "Operating System :: OS Independent", +# +# # OR for OS-specific nodes, specify the supported systems: +# "Operating System :: Microsoft :: Windows", # Windows specific +# "Operating System :: POSIX :: Linux", # Linux specific +# "Operating System :: MacOS", # macOS specific +# +# # GPU Accelerator support. Pick the ones that are supported by your extension. +# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support +# "Environment :: GPU :: AMD ROCm", # AMD ROCm support +# "Environment :: GPU :: Intel Arc", # Intel Arc support +# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support +# "Environment :: GPU :: Apple Metal", # Apple Metal support +# ] + +[project.urls] +Repository = "https://github.com/jandan520/ComfyUI-AdvancedCameraPrompts" +Documentation = "https://github.com/jandan520/ComfyUI-AdvancedCameraPrompts" +"Bug Tracker" = "https://github.com/jandan520/ComfyUI-AdvancedCameraPrompts/issues" + +[tool.comfy] +PublisherId = "jianghong-zhu" +DisplayName = "ComfyUI-AdvancedCameraPrompts" +Icon = "" +includes = [] +# "requires-comfyui" = ">=1.0.0"