first
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -0,0 +1,135 @@
|
||||
# FL Path Animator
|
||||
|
||||
A standalone ComfyUI custom node for creating animated shapes that follow user-drawn paths.
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Path Editor** - Visual modal interface for drawing motion paths and static anchor points
|
||||
- **Two Path Types**:
|
||||
- **Motion Paths** - Draw continuous paths for shapes to follow over time
|
||||
- **Static Anchors** - Single-point paths for stationary shapes
|
||||
- **Background Image Support** - Load or paste reference images to draw paths on
|
||||
- **WAN ATI Compatible** - Outputs 121-point resampled coordinates for stable AI video generation
|
||||
- **Visual Effects** - Blur, trails, rotation, borders, and custom colors
|
||||
- **Multiple Shapes** - Circle, square, triangle, hexagon, and star
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download this repository into your ComfyUI custom_nodes folder:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
git clone https://github.com/yourusername/ComfyUI_FL-Path-Animator.git
|
||||
```
|
||||
|
||||
2. Restart ComfyUI
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. Add the "FL Path Animator V2" node to your workflow
|
||||
2. Click the **"Edit Paths"** button to open the path editor
|
||||
3. Use the toolbar to:
|
||||
- **✏️ Pencil** - Draw motion paths (hold SHIFT for straight lines)
|
||||
- **📍 Point** - Add static anchor points
|
||||
- **🗑️ Eraser** - Delete paths by clicking
|
||||
- **↖️ Select** - Select and inspect paths
|
||||
- **🔒 Lock Perimeter** - Auto-generate static points around border
|
||||
4. Optionally load a background image with **🖼️** or paste with **Ctrl+V**
|
||||
5. Press **ESC** to save and close
|
||||
6. Configure shape properties in the node
|
||||
7. Connect outputs to your workflow
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **ESC** - Save paths and close editor
|
||||
- **SHIFT (hold)** - Draw straight lines (horizontal/vertical/45° diagonal)
|
||||
- **Ctrl+V** - Paste background image from clipboard
|
||||
|
||||
### Node Parameters
|
||||
|
||||
#### Required
|
||||
- `frame_width` / `frame_height` - Output frame dimensions
|
||||
- `frame_count` - Number of frames to generate
|
||||
- `shape` - Shape type (circle, square, triangle, hexagon, star)
|
||||
- `shape_size` - Size in pixels
|
||||
- `shape_color` - Color as hex (#FFFFFF) or RGB (255,255,255)
|
||||
- `bg_color` - Background color
|
||||
|
||||
#### Optional
|
||||
- `blur_radius` - Gaussian blur strength
|
||||
- `trail_length` - Motion trail effect (0.0-1.0)
|
||||
- `rotation_speed` - Shape rotation over time
|
||||
- `border_width` - Border thickness
|
||||
- `border_color` - Border color
|
||||
- `paths_data` - JSON data from path editor (auto-managed)
|
||||
|
||||
### Outputs
|
||||
|
||||
1. **IMAGE** - Batch of rendered frames (shape following paths)
|
||||
2. **MASK** - Alpha masks extracted from red channel
|
||||
3. **STRING** - WAN ATI-compatible coordinate data (121 points per path)
|
||||
|
||||
## WAN ATI Integration
|
||||
|
||||
The coordinate output is specifically formatted for WAN (Warp and Noise) ATI video generation:
|
||||
|
||||
- Each path is resampled to exactly 121 points
|
||||
- Arc-length parameterization ensures smooth motion
|
||||
- Static points are repeated 121 times for stable anchors
|
||||
- Output format: `[[{x, y}, ...], [{x, y}, ...]]` (array of tracks)
|
||||
|
||||
This prevents jitter and warping in AI-generated video by providing consistent tracking data.
|
||||
|
||||
## Path Editor Tools
|
||||
|
||||
### ✏️ Pencil Tool
|
||||
Draw continuous motion paths by clicking and dragging. Shapes will smoothly follow these paths over the animation duration.
|
||||
|
||||
- Minimum 3px spacing between points for smoothing
|
||||
- Hold SHIFT to constrain to straight lines
|
||||
|
||||
### 📍 Point Tool
|
||||
Click once to create a static anchor point. Shapes at these positions won't move, useful for border stability in video generation.
|
||||
|
||||
### 🗑️ Eraser Tool
|
||||
Click on any path to delete it.
|
||||
|
||||
### ↖️ Select Tool
|
||||
Click paths to inspect details:
|
||||
- Neon green highlight
|
||||
- Point count and numbering
|
||||
- Path type indicator
|
||||
|
||||
### 🔒 Lock Perimeter
|
||||
Automatically distributes N static anchor points evenly around the canvas border. Useful for WAN video generation to stabilize frame edges.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Path Resampling**: Arc-length parameterization for constant-speed motion
|
||||
- **Canvas Scaling**: Paths automatically scale from editor coordinates to output frame size
|
||||
- **Background Cache**: Uploaded/pasted images persist across editor sessions
|
||||
- **Animated Preview**: Directional flow indicators show path direction in real-time
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- PIL (Pillow)
|
||||
- NumPy
|
||||
- PyTorch
|
||||
- ComfyUI
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## Credits
|
||||
|
||||
Created by Machine Delusions for the Fill-Nodes pack.
|
||||
Extracted as standalone node for easier distribution.
|
||||
|
||||
## Support
|
||||
|
||||
For issues, feature requests, or questions:
|
||||
- GitHub Issues: [Your repo issues page]
|
||||
- Patreon: https://www.patreon.com/c/Machinedelusions
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
FL Path Animator V2 - Standalone Node Pack
|
||||
Creates animated shapes that follow user-drawn paths with visual editor.
|
||||
"""
|
||||
|
||||
from .nodes.FL_PathAnimatorV2 import FL_PathAnimatorV2
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FL_PathAnimatorV2": FL_PathAnimatorV2,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FL_PathAnimatorV2": "FL Path Animator V2",
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("FL Path Animator V2 - Standalone Node Pack Loaded")
|
||||
print("="*60 + "\n")
|
||||
@@ -0,0 +1,374 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
import math
|
||||
import json
|
||||
|
||||
def pil2tensor(image):
|
||||
"""Convert PIL Image to tensor"""
|
||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
def tensor2pil(tensor):
|
||||
"""Convert tensor to PIL Image"""
|
||||
return Image.fromarray(np.clip(255. * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
def parse_color(color):
|
||||
"""Parse color string to RGB tuple"""
|
||||
if isinstance(color, str):
|
||||
if ',' in color:
|
||||
return tuple(int(c.strip()) for c in color.split(','))
|
||||
else:
|
||||
from PIL import ImageColor
|
||||
try:
|
||||
return ImageColor.getrgb(color)
|
||||
except:
|
||||
return (255, 255, 255)
|
||||
return color
|
||||
|
||||
class FL_PathAnimatorV2:
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING",)
|
||||
RETURN_NAMES = ("image", "mask", "coordinates",)
|
||||
FUNCTION = "animate_paths"
|
||||
CATEGORY = "🎨 FL Path Animator V2"
|
||||
DESCRIPTION = """
|
||||
Creates animated shapes that follow user-drawn paths.
|
||||
Open the path editor to draw trajectories on a reference image, then shapes will follow these paths over time.
|
||||
Outputs WAN ATI-compatible coordinate strings with proper 121-point resampling for stable video generation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"frame_width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 1}),
|
||||
"frame_height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 1}),
|
||||
"frame_count": ("INT", {"default": 30, "min": 1, "max": 500, "step": 1}),
|
||||
"shape": ([
|
||||
'circle',
|
||||
'square',
|
||||
'triangle',
|
||||
'hexagon',
|
||||
'star',
|
||||
], {"default": 'circle'}),
|
||||
"shape_size": ("INT", {"default": 20, "min": 2, "max": 500, "step": 1}),
|
||||
"shape_color": ("STRING", {"default": 'white'}),
|
||||
"bg_color": ("STRING", {"default": 'black'}),
|
||||
},
|
||||
"optional": {
|
||||
"blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 50.0, "step": 0.1}),
|
||||
"trail_length": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"rotation_speed": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 1.0}),
|
||||
"border_width": ("INT", {"default": 0, "min": 0, "max": 20, "step": 1}),
|
||||
"border_color": ("STRING", {"default": 'white'}),
|
||||
"paths_data": ("STRING", {"default": '{"paths": [], "canvas_size": {"width": 512, "height": 512}}', "multiline": True}),
|
||||
}
|
||||
}
|
||||
|
||||
def draw_shape(self, draw, shape, center_x, center_y, size, rotation, fill_color, border_width=0, border_color='white'):
|
||||
"""Draw a shape at the specified location"""
|
||||
half_size = size / 2
|
||||
|
||||
if shape == 'circle':
|
||||
bbox = [center_x - half_size, center_y - half_size,
|
||||
center_x + half_size, center_y + half_size]
|
||||
if border_width > 0:
|
||||
draw.ellipse(bbox, fill=fill_color, outline=border_color, width=border_width)
|
||||
else:
|
||||
draw.ellipse(bbox, fill=fill_color)
|
||||
|
||||
elif shape == 'square':
|
||||
bbox = [center_x - half_size, center_y - half_size,
|
||||
center_x + half_size, center_y + half_size]
|
||||
if border_width > 0:
|
||||
draw.rectangle(bbox, fill=fill_color, outline=border_color, width=border_width)
|
||||
else:
|
||||
draw.rectangle(bbox, fill=fill_color)
|
||||
|
||||
elif shape == 'triangle':
|
||||
points = [
|
||||
(center_x, center_y - half_size),
|
||||
(center_x - half_size, center_y + half_size),
|
||||
(center_x + half_size, center_y + half_size),
|
||||
]
|
||||
if rotation != 0:
|
||||
points = self.rotate_points(points, center_x, center_y, rotation)
|
||||
if border_width > 0:
|
||||
draw.polygon(points, fill=fill_color, outline=border_color, width=border_width)
|
||||
else:
|
||||
draw.polygon(points, fill=fill_color)
|
||||
|
||||
elif shape == 'hexagon':
|
||||
points = []
|
||||
for i in range(6):
|
||||
angle = math.radians(60 * i + rotation)
|
||||
x = center_x + half_size * math.cos(angle)
|
||||
y = center_y + half_size * math.sin(angle)
|
||||
points.append((x, y))
|
||||
if border_width > 0:
|
||||
draw.polygon(points, fill=fill_color, outline=border_color, width=border_width)
|
||||
else:
|
||||
draw.polygon(points, fill=fill_color)
|
||||
|
||||
elif shape == 'star':
|
||||
points = []
|
||||
for i in range(10):
|
||||
angle = math.radians(36 * i + rotation)
|
||||
r = half_size if i % 2 == 0 else half_size * 0.4
|
||||
x = center_x + r * math.cos(angle - math.pi / 2)
|
||||
y = center_y + r * math.sin(angle - math.pi / 2)
|
||||
points.append((x, y))
|
||||
if border_width > 0:
|
||||
draw.polygon(points, fill=fill_color, outline=border_color, width=border_width)
|
||||
else:
|
||||
draw.polygon(points, fill=fill_color)
|
||||
|
||||
def rotate_points(self, points, cx, cy, angle):
|
||||
"""Rotate points around a center"""
|
||||
rad = math.radians(angle)
|
||||
cos_a = math.cos(rad)
|
||||
sin_a = math.sin(rad)
|
||||
rotated = []
|
||||
for x, y in points:
|
||||
x -= cx
|
||||
y -= cy
|
||||
new_x = x * cos_a - y * sin_a + cx
|
||||
new_y = x * sin_a + y * cos_a + cy
|
||||
rotated.append((new_x, new_y))
|
||||
return rotated
|
||||
|
||||
def resample_path_uniform(self, points, num_samples=121):
|
||||
"""
|
||||
Resample path to exactly num_samples points with even arc-length spacing.
|
||||
This matches KJNodes "path" sampling method and is CRITICAL for WAN ATI stability.
|
||||
|
||||
Args:
|
||||
points: List of {x, y} dicts representing the path
|
||||
num_samples: Number of points to resample to (default 121 for WAN ATI)
|
||||
|
||||
Returns:
|
||||
List of {x, y} dicts with exactly num_samples points evenly distributed along the arc
|
||||
"""
|
||||
if len(points) == 0:
|
||||
return []
|
||||
|
||||
# SOLUTION 1: Support static single points
|
||||
if len(points) == 1:
|
||||
# Single point - repeat for all samples (creates static anchor)
|
||||
return [{'x': points[0]['x'], 'y': points[0]['y']} for _ in range(num_samples)]
|
||||
|
||||
# Calculate cumulative arc lengths along the path
|
||||
cumulative_lengths = [0.0]
|
||||
for i in range(len(points) - 1):
|
||||
dx = points[i + 1]['x'] - points[i]['x']
|
||||
dy = points[i + 1]['y'] - points[i]['y']
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
cumulative_lengths.append(cumulative_lengths[-1] + length)
|
||||
|
||||
total_length = cumulative_lengths[-1]
|
||||
|
||||
# Handle zero-length path (all points are the same)
|
||||
if total_length == 0:
|
||||
return [{'x': points[0]['x'], 'y': points[0]['y']} for _ in range(num_samples)]
|
||||
|
||||
# Resample at even intervals along the arc
|
||||
resampled = []
|
||||
for i in range(num_samples):
|
||||
# Calculate target distance along path
|
||||
if num_samples == 1:
|
||||
target_length = 0
|
||||
else:
|
||||
target_length = (i / (num_samples - 1)) * total_length
|
||||
|
||||
# Find segment containing target length
|
||||
for j in range(len(cumulative_lengths) - 1):
|
||||
if cumulative_lengths[j] <= target_length <= cumulative_lengths[j + 1]:
|
||||
# Interpolate within this segment
|
||||
seg_length = cumulative_lengths[j + 1] - cumulative_lengths[j]
|
||||
if seg_length > 0:
|
||||
t = (target_length - cumulative_lengths[j]) / seg_length
|
||||
else:
|
||||
t = 0
|
||||
|
||||
x = points[j]['x'] + t * (points[j + 1]['x'] - points[j]['x'])
|
||||
y = points[j]['y'] + t * (points[j + 1]['y'] - points[j]['y'])
|
||||
resampled.append({'x': x, 'y': y})
|
||||
break
|
||||
else:
|
||||
# Fallback to last point (shouldn't happen with correct logic)
|
||||
resampled.append({'x': points[-1]['x'], 'y': points[-1]['y']})
|
||||
|
||||
return resampled
|
||||
|
||||
def interpolate_path(self, points, t):
|
||||
"""
|
||||
Interpolate position along a path at time t (0.0 to 1.0)
|
||||
Returns (x, y) coordinates
|
||||
|
||||
NOTE: This is used for visualization/animation only.
|
||||
For WAN ATI output, use resample_path_uniform() instead.
|
||||
"""
|
||||
if len(points) == 0:
|
||||
return (0, 0)
|
||||
|
||||
# Support static single points
|
||||
if len(points) == 1:
|
||||
return (points[0]['x'], points[0]['y'])
|
||||
|
||||
# Calculate total path length
|
||||
total_length = 0
|
||||
segment_lengths = []
|
||||
for i in range(len(points) - 1):
|
||||
dx = points[i + 1]['x'] - points[i]['x']
|
||||
dy = points[i + 1]['y'] - points[i]['y']
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
segment_lengths.append(length)
|
||||
total_length += length
|
||||
|
||||
if total_length == 0:
|
||||
return (points[0]['x'], points[0]['y'])
|
||||
|
||||
# Find target distance along path
|
||||
target_distance = t * total_length
|
||||
|
||||
# Find which segment contains target distance
|
||||
current_distance = 0
|
||||
for i, seg_length in enumerate(segment_lengths):
|
||||
if current_distance + seg_length >= target_distance:
|
||||
# Interpolate within this segment
|
||||
segment_t = (target_distance - current_distance) / seg_length if seg_length > 0 else 0
|
||||
x = points[i]['x'] + (points[i + 1]['x'] - points[i]['x']) * segment_t
|
||||
y = points[i]['y'] + (points[i + 1]['y'] - points[i]['y']) * segment_t
|
||||
return (x, y)
|
||||
current_distance += seg_length
|
||||
|
||||
# Return last point if we've gone past the end
|
||||
return (points[-1]['x'], points[-1]['y'])
|
||||
|
||||
def animate_paths(self, frame_width, frame_height, frame_count, shape, shape_size,
|
||||
shape_color, bg_color, blur_radius=0.0, trail_length=0.0,
|
||||
rotation_speed=0.0, border_width=0, border_color='white',
|
||||
paths_data='{"paths": [], "canvas_size": {"width": 512, "height": 512}}'):
|
||||
|
||||
# Parse colors
|
||||
shape_color = parse_color(shape_color)
|
||||
bg_color = parse_color(bg_color)
|
||||
border_color = parse_color(border_color)
|
||||
|
||||
# Parse paths data
|
||||
try:
|
||||
paths_obj = json.loads(paths_data)
|
||||
paths = paths_obj.get('paths', [])
|
||||
canvas_size = paths_obj.get('canvas_size', {'width': frame_width, 'height': frame_height})
|
||||
except json.JSONDecodeError:
|
||||
print("FL_PathAnimatorV2: Invalid JSON in paths_data, using empty paths")
|
||||
paths = []
|
||||
canvas_size = {'width': frame_width, 'height': frame_height}
|
||||
|
||||
# Calculate scaling factors to transform from canvas coordinates to frame coordinates
|
||||
canvas_width = canvas_size.get('width', frame_width)
|
||||
canvas_height = canvas_size.get('height', frame_height)
|
||||
scale_x = frame_width / canvas_width if canvas_width > 0 else 1.0
|
||||
scale_y = frame_height / canvas_height if canvas_height > 0 else 1.0
|
||||
|
||||
# Scale all path coordinates
|
||||
scaled_paths = []
|
||||
for path in paths:
|
||||
scaled_path = path.copy()
|
||||
scaled_points = []
|
||||
for point in path.get('points', []):
|
||||
scaled_points.append({
|
||||
'x': point['x'] * scale_x,
|
||||
'y': point['y'] * scale_y
|
||||
})
|
||||
scaled_path['points'] = scaled_points
|
||||
|
||||
# Preserve isSinglePoint flag if it exists
|
||||
if 'isSinglePoint' in path:
|
||||
scaled_path['isSinglePoint'] = path['isSinglePoint']
|
||||
|
||||
scaled_paths.append(scaled_path)
|
||||
|
||||
images_list = []
|
||||
masks_list = []
|
||||
previous_output = None
|
||||
|
||||
for frame in range(frame_count):
|
||||
# Create blank image with bg_color
|
||||
image = Image.new("RGB", (frame_width, frame_height), bg_color)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Calculate time along path (0.0 to 1.0)
|
||||
t = frame / max(frame_count - 1, 1)
|
||||
|
||||
# Draw each path's shape
|
||||
for path_idx, path in enumerate(scaled_paths):
|
||||
points = path.get('points', [])
|
||||
if len(points) == 0:
|
||||
continue
|
||||
|
||||
# Get position along this path
|
||||
x, y = self.interpolate_path(points, t)
|
||||
|
||||
# Calculate rotation
|
||||
current_rotation = rotation_speed * t * 360.0
|
||||
|
||||
# Draw the shape
|
||||
self.draw_shape(draw, shape, x, y, shape_size, current_rotation,
|
||||
shape_color, border_width, border_color)
|
||||
|
||||
# Apply blur
|
||||
if blur_radius > 0:
|
||||
image = image.filter(ImageFilter.GaussianBlur(blur_radius))
|
||||
|
||||
# Convert to tensor
|
||||
image_tensor = pil2tensor(image)
|
||||
|
||||
# Apply trailing effect
|
||||
if trail_length > 0 and previous_output is not None:
|
||||
image_tensor = image_tensor + trail_length * previous_output
|
||||
image_tensor = image_tensor / image_tensor.max()
|
||||
|
||||
previous_output = image_tensor.clone()
|
||||
|
||||
# Clamp values
|
||||
image_tensor = torch.clamp(image_tensor, 0.0, 1.0)
|
||||
|
||||
# Extract mask from red channel
|
||||
mask = image_tensor[:, :, :, 0]
|
||||
|
||||
images_list.append(image_tensor)
|
||||
masks_list.append(mask)
|
||||
|
||||
# Concatenate all frames
|
||||
out_images = torch.cat(images_list, dim=0)
|
||||
out_masks = torch.cat(masks_list, dim=0)
|
||||
|
||||
# SOLUTION 2 & 3: Generate WAN ATI-compatible coordinate string
|
||||
# Resample each path to exactly 121 points with visibility flags
|
||||
coord_tracks = []
|
||||
for path in scaled_paths:
|
||||
points = path.get('points', [])
|
||||
|
||||
# Check if this is a single-point path (static anchor)
|
||||
is_single_point = path.get('isSinglePoint', False) or len(points) == 1
|
||||
|
||||
# Resample to exactly 121 points for WAN ATI compatibility
|
||||
resampled_points = self.resample_path_uniform(points, num_samples=121)
|
||||
|
||||
# Add visibility flag (1.0 = visible, required by WAN ATI)
|
||||
# Format: [{"x": x, "y": y}, {"x": x, "y": y}, ...]
|
||||
# The visibility will be added as a third coordinate when processed by ATI
|
||||
track_coords = [
|
||||
{"x": int(round(p["x"])), "y": int(round(p["y"]))}
|
||||
for p in resampled_points
|
||||
]
|
||||
|
||||
coord_tracks.append(track_coords)
|
||||
|
||||
# Output as list of tracks (each track is a list of 121 {x, y} points)
|
||||
coord_string = json.dumps(coord_tracks)
|
||||
|
||||
print(f"FL_PathAnimatorV2: Generated {len(coord_tracks)} tracks with 121 points each for WAN ATI")
|
||||
|
||||
return (out_images, out_masks, coord_string)
|
||||
@@ -0,0 +1,2 @@
|
||||
Pillow>=9.0.0
|
||||
numpy>=1.20.0
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user