diff --git a/__init__.py b/__init__.py index 9a726c3..d0de23b 100644 --- a/__init__.py +++ b/__init__.py @@ -12,6 +12,7 @@ from .nodes.power_spline_editor import * from .nodes.lora_extract_v2 import * from .nodes.lora_change import * from .nodes.lora_combine import * +from .nodes.math_nodes import * from .nodes.draw_shapes import * from .nodes.draw_joints import * diff --git a/nodes/math_nodes.py b/nodes/math_nodes.py new file mode 100644 index 0000000..8bc64ab --- /dev/null +++ b/nodes/math_nodes.py @@ -0,0 +1,136 @@ +class HolocineFrames: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "video_length": ("INT", {"default": 161, "min": 41, "max": 541, "step": 4}), + "shots": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), + "distribution": ("FLOAT", {"default": 0.50, "min": 0.01, "max": 1.00, "step": 0.01}), + }, + "optional": { + "shot_list": ("WANVID_HOLOCINE_SHOT_LIST",), + } + } + + RETURN_TYPES = ("INT", "STRING") + RETURN_NAMES = ("video_length", "shots_string") + FUNCTION = "calculate_holocine_frames" + CATEGORY = "WanVideoWrapper_QQ" + DESCRIPTION = "Calculates video frame distribution for Holocine video generation" + + def calculate_holocine_frames(self, video_length, shots, distribution, shot_list=None): + """ + Compute cut positions using exponent-based easing: + - distribution < 0.5 → front-loaded (gamma > 1) + - distribution = 0.5 → linear (gamma = 1) + - distribution > 0.5 → back-loaded (gamma < 1) + Guarantees strictly increasing intermediate cuts and reserves final frame. + """ + + # Use shot_list length if provided and valid, otherwise use shots parameter + if shot_list is not None and hasattr(shot_list, '__len__') and len(shot_list) >= 2: + actual_shots = len(shot_list) + else: + actual_shots = shots + + # Number of intermediate cuts between start (0) and final (video_length) + num_cuts = max(1, actual_shots - 1) + + d = float(distribution) + # Map distribution to exponent gamma + gamma_min = 0.35 + gamma_max = 4.0 + if d < 0.5: + # Front-load: push more cuts earlier + gamma = 1.0 + (1.0 - (d / 0.5)) * (gamma_max - 1.0) + elif d > 0.5: + # Back-load: push more cuts later + gamma = 1.0 - (((d - 0.5) / 0.5) * (1.0 - gamma_min)) + else: + gamma = 1.0 + + shot_positions = [] + prev = 0 + for i in range(1, num_cuts + 1): + # Uniform fraction in (0,1) + t = i / float(actual_shots) + # Exponent-based easing + f = t ** gamma + pos = int(round(f * video_length)) + + # Reserve space for remaining cuts and final frame + remaining = num_cuts - i + max_allowed = (video_length - 1) - remaining + # Clamp and enforce strict monotonic increase + pos = max(prev + 1, min(max_allowed, pos)) + shot_positions.append(pos) + prev = pos + + # Always end with video_length + shot_positions.append(video_length) + + # Post-processing: enforce first cut >= 9 and min gap >= 8 + min_first = 9 + min_gap = 8 + cuts = shot_positions[:-1] + L = video_length + n = len(cuts) + + if n > 0: + # Compute feasible gap given first cut constraint + gap_eff = min_gap + if n > 1: + max_gap_for_min_first = (L - 1 - min_first) // (n - 1) + if max_gap_for_min_first < gap_eff: + gap_eff = max(1, max_gap_for_min_first) + + # Clamp first cut within feasible bounds + max_first_allowed = L - 1 - (n - 1) * gap_eff + if max_first_allowed < 1: + max_first_allowed = 1 + cuts[0] = max(cuts[0], min_first) + if cuts[0] > max_first_allowed: + cuts[0] = max_first_allowed + + # Forward pass: enforce min gaps and feasibility + for i in range(1, n): + min_i = cuts[i - 1] + gap_eff + max_i = L - 1 - (n - 1 - i) * gap_eff + if cuts[i] < min_i: + cuts[i] = min_i + if cuts[i] > max_i: + cuts[i] = max_i + + # Backward pass: tighten from the end while keeping feasibility + for i in range(n - 2, -1, -1): + max_from_next = cuts[i + 1] - gap_eff + bound = L - 1 - (n - 1 - i) * gap_eff + max_allowed = min(max_from_next, bound) + if cuts[i] > max_allowed: + cuts[i] = max_allowed + + # Final forward pass to ensure min gaps after backward tightening + for i in range(1, n): + min_i = cuts[i - 1] + gap_eff + if cuts[i] < min_i: + cuts[i] = min_i + + # Ensure last intermediate cut doesn't exceed L - 1 + if cuts[-1] > L - 1: + cuts[-1] = L - 1 + + # Recombine with final frame + shot_positions = cuts + [L] + + # Convert to comma-separated string + shots_string = ",".join(map(str, shot_positions)) + + return (video_length, shots_string) + +NODE_CLASS_MAPPINGS = { + "HolocineFrames": HolocineFrames +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "HolocineFrames": "Holocine Frames" +}