mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-06 22:51:18 +08:00
Compare commits
9 Commits
b70ac6d8d8
...
9bdd31d35f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bdd31d35f | ||
|
|
b08debceca | ||
|
|
000c6b784e | ||
|
|
985fb9d6ad | ||
|
|
7f287b705e | ||
|
|
b7ba504e06 | ||
|
|
6c62ca0b6b | ||
|
|
9a5e6233f4 | ||
|
|
abfea891ef |
@ -4,12 +4,12 @@ early_access: false
|
||||
tone_instructions: "Only comment on issues introduced by this PR's changes. Do not flag pre-existing problems in moved, re-indented, or reformatted code."
|
||||
|
||||
reviews:
|
||||
profile: "chill"
|
||||
request_changes_workflow: false
|
||||
profile: "assertive"
|
||||
request_changes_workflow: true
|
||||
high_level_summary: false
|
||||
poem: false
|
||||
review_status: false
|
||||
review_details: false
|
||||
review_details: true
|
||||
commit_status: true
|
||||
collapse_walkthrough: true
|
||||
changed_files_summary: false
|
||||
@ -39,6 +39,14 @@ reviews:
|
||||
- path: "**"
|
||||
instructions: |
|
||||
IMPORTANT: Only comment on issues directly introduced by this PR's code changes.
|
||||
Treat AGENTS.md as mandatory repository policy, not optional style guidance.
|
||||
Flag PR changes that violate AGENTS.md even when the code is otherwise functional.
|
||||
In particular, enforce architecture boundaries, dtype/device/memory rules,
|
||||
interface contracts, import style, no unnecessary try/except blocks, no inline
|
||||
imports, no outbound internet paths in core ComfyUI, and narrow scoped fixes.
|
||||
Prefer direct findings over suggestions when a rule is violated. Only ignore
|
||||
AGENTS.md when it clearly conflicts with a newer explicit maintainer instruction
|
||||
in the PR.
|
||||
Do NOT flag pre-existing issues in code that was merely moved, re-indented,
|
||||
de-indented, or reformatted without logic changes. If code appears in the diff
|
||||
only due to whitespace or structural reformatting (e.g., removing a `with:` block),
|
||||
@ -123,5 +131,10 @@ chat:
|
||||
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns:
|
||||
- files: "AGENTS.md"
|
||||
applyTo: "**"
|
||||
learnings:
|
||||
scope: "auto"
|
||||
|
||||
@ -782,15 +782,23 @@ def resolve_areas_and_cond_masks_multidim(conditions, dims, device):
|
||||
mask = c['mask']
|
||||
mask = mask.to(device=device)
|
||||
modified = c.copy()
|
||||
if len(mask.shape) == len(dims):
|
||||
# Normalize mask to [batch, *spatial_dims]
|
||||
target_ndim = len(dims) + 1
|
||||
while mask.ndim > target_ndim and mask.shape[0] == 1:
|
||||
mask = mask.squeeze(0)
|
||||
while mask.ndim < target_ndim:
|
||||
mask = mask.unsqueeze(0)
|
||||
if mask.shape[1:] != dims:
|
||||
if mask.ndim < 4:
|
||||
if len(dims) == 1:
|
||||
mask = torch.nn.functional.interpolate(
|
||||
mask.unsqueeze(1), size=dims[0],
|
||||
mode='linear', align_corners=False).squeeze(1)
|
||||
elif mask.ndim < 4:
|
||||
mask = comfy.utils.common_upscale(mask.unsqueeze(1), dims[-1], dims[-2], 'bilinear', 'none').squeeze(1)
|
||||
else:
|
||||
mask = comfy.utils.common_upscale(mask, dims[-1], dims[-2], 'bilinear', 'none')
|
||||
|
||||
if modified.get("set_area_to_bounds", False): #TODO: handle dim != 2
|
||||
if modified.get("set_area_to_bounds", False) and len(dims) == 2:
|
||||
bounds = torch.max(torch.abs(mask),dim=0).values.unsqueeze(0)
|
||||
boxes, is_empty = get_mask_aabb(bounds)
|
||||
if is_empty[0]:
|
||||
|
||||
@ -543,18 +543,24 @@ class SDTokenizer:
|
||||
def _try_get_embedding(self, embedding_name:str):
|
||||
'''
|
||||
Takes a potential embedding name and tries to retrieve it.
|
||||
Returns a Tuple consisting of the embedding and any leftover string, embedding can be None.
|
||||
Returns a Tuple consisting of the embedding, the cleaned embedding name, and any leftover string, embedding can be None.
|
||||
'''
|
||||
split_embed = embedding_name.split()
|
||||
embedding_name = split_embed[0]
|
||||
leftover = ' '.join(split_embed[1:])
|
||||
|
||||
match = re.search(r'[<\[]', embedding_name)
|
||||
if match is not None:
|
||||
leftover = embedding_name[match.start():] + (" " + leftover if leftover else "")
|
||||
embedding_name = embedding_name[:match.start()]
|
||||
|
||||
embed = load_embed(embedding_name, self.embedding_directory, self.embedding_size, self.embedding_key)
|
||||
if embed is None:
|
||||
stripped = embedding_name.strip(',')
|
||||
if len(stripped) < len(embedding_name):
|
||||
embed = load_embed(stripped, self.embedding_directory, self.embedding_size, self.embedding_key)
|
||||
return (embed, "{} {}".format(embedding_name[len(stripped):], leftover))
|
||||
return (embed, leftover)
|
||||
return (embed, embedding_name, "{} {}".format(embedding_name[len(stripped):], leftover))
|
||||
return (embed, embedding_name, leftover)
|
||||
|
||||
def pad_tokens(self, tokens, amount):
|
||||
if self.pad_left:
|
||||
@ -585,7 +591,7 @@ class SDTokenizer:
|
||||
tokens = []
|
||||
for weighted_segment, weight in parsed_weights:
|
||||
to_tokenize = unescape_important(weighted_segment)
|
||||
split = re.split(' {0}|\n{0}'.format(self.embedding_identifier), to_tokenize)
|
||||
split = re.split(r'(?<=\s){}'.format(re.escape(self.embedding_identifier)), to_tokenize)
|
||||
to_tokenize = [split[0]]
|
||||
for i in range(1, len(split)):
|
||||
to_tokenize.append("{}{}".format(self.embedding_identifier, split[i]))
|
||||
@ -595,7 +601,7 @@ class SDTokenizer:
|
||||
# if we find an embedding, deal with the embedding
|
||||
if word.startswith(self.embedding_identifier) and self.embedding_directory is not None:
|
||||
embedding_name = word[len(self.embedding_identifier):].strip('\n')
|
||||
embed, leftover = self._try_get_embedding(embedding_name)
|
||||
embed, embedding_name, leftover = self._try_get_embedding(embedding_name)
|
||||
if embed is None:
|
||||
logging.warning(f"warning, embedding:{embedding_name} does not exist, ignoring")
|
||||
else:
|
||||
|
||||
@ -937,22 +937,41 @@ class BaseGenerate:
|
||||
return torch.argmax(logits, dim=-1, keepdim=True)
|
||||
|
||||
# Sampling mode
|
||||
if repetition_penalty != 1.0:
|
||||
for i in range(logits.shape[0]):
|
||||
for token_id in set(token_history):
|
||||
logits[i, token_id] *= repetition_penalty if logits[i, token_id] < 0 else 1/repetition_penalty
|
||||
|
||||
if presence_penalty is not None and presence_penalty != 0.0:
|
||||
for i in range(logits.shape[0]):
|
||||
for token_id in set(token_history):
|
||||
logits[i, token_id] -= presence_penalty
|
||||
if len(token_history) > 0 and (repetition_penalty != 1.0 or (presence_penalty is not None and presence_penalty != 0.0)):
|
||||
token_ids = torch.tensor(list(set(token_history)), device=logits.device)
|
||||
token_logits = logits[:, token_ids]
|
||||
if repetition_penalty != 1.0:
|
||||
token_logits = torch.where(token_logits < 0, token_logits * repetition_penalty, token_logits / repetition_penalty)
|
||||
if presence_penalty is not None and presence_penalty != 0.0:
|
||||
token_logits = token_logits - presence_penalty
|
||||
logits[:, token_ids] = token_logits
|
||||
|
||||
if temperature != 1.0:
|
||||
logits = logits / temperature
|
||||
|
||||
if top_k > 0:
|
||||
indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
|
||||
logits[indices_to_remove] = torch.finfo(logits.dtype).min
|
||||
top_k = min(top_k, logits.shape[-1])
|
||||
logits, top_indices = torch.topk(logits, top_k)
|
||||
|
||||
if min_p > 0.0:
|
||||
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)
|
||||
top_probs, _ = probs_before_filter.max(dim=-1, keepdim=True)
|
||||
min_threshold = min_p * top_probs
|
||||
indices_to_remove = probs_before_filter < min_threshold
|
||||
logits[indices_to_remove] = torch.finfo(logits.dtype).min
|
||||
|
||||
if top_p < 1.0:
|
||||
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
|
||||
cumulative_probs = torch.cumsum(torch.nn.functional.softmax(sorted_logits, dim=-1), dim=-1)
|
||||
sorted_indices_to_remove = cumulative_probs > top_p
|
||||
sorted_indices_to_remove[..., 0] = False
|
||||
indices_to_remove = torch.zeros_like(logits, dtype=torch.bool)
|
||||
indices_to_remove.scatter_(1, sorted_indices, sorted_indices_to_remove)
|
||||
logits[indices_to_remove] = torch.finfo(logits.dtype).min
|
||||
|
||||
probs = torch.nn.functional.softmax(logits, dim=-1)
|
||||
next_token = torch.multinomial(probs, num_samples=1, generator=generator)
|
||||
return top_indices.gather(1, next_token)
|
||||
|
||||
if min_p > 0.0:
|
||||
probs_before_filter = torch.nn.functional.softmax(logits, dim=-1)
|
||||
|
||||
@ -1313,6 +1313,7 @@ def reshape_mask(input_mask, output_shape):
|
||||
dims = len(output_shape) - 2
|
||||
|
||||
if dims == 1:
|
||||
input_mask = input_mask.reshape((-1, 1, input_mask.shape[-1]))
|
||||
scale_mode = "linear"
|
||||
|
||||
if dims == 2:
|
||||
|
||||
@ -9,6 +9,7 @@ from typing import Any
|
||||
import folder_paths
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_SENSITIVE_HEADERS = {"authorization", "x-api-key"}
|
||||
|
||||
|
||||
def get_log_directory():
|
||||
@ -73,6 +74,10 @@ def _format_data_for_logging(data: Any) -> str:
|
||||
return str(data)
|
||||
|
||||
|
||||
def _redact_headers(headers: dict) -> dict:
|
||||
return {k: ("***" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()}
|
||||
|
||||
|
||||
def log_request_response(
|
||||
operation_id: str,
|
||||
request_method: str,
|
||||
@ -101,7 +106,7 @@ def log_request_response(
|
||||
log_content.append(f"Method: {request_method}")
|
||||
log_content.append(f"URL: {request_url}")
|
||||
if request_headers:
|
||||
log_content.append(f"Headers:\n{_format_data_for_logging(request_headers)}")
|
||||
log_content.append(f"Headers:\n{_format_data_for_logging(_redact_headers(request_headers))}")
|
||||
if request_params:
|
||||
log_content.append(f"Params:\n{_format_data_for_logging(request_params)}")
|
||||
if request_data is not None:
|
||||
|
||||
@ -16,23 +16,30 @@ class ColorToRGBInt(io.ComfyNode):
|
||||
],
|
||||
outputs=[
|
||||
io.Int.Output(display_name="rgb_int"),
|
||||
io.Color.Output(display_name="hex")
|
||||
io.Color.Output(display_name="hex"),
|
||||
io.Float.Output(display_name="alpha"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, color: str) -> io.NodeOutput:
|
||||
# expect format #RRGGBB
|
||||
if len(color) != 7 or color[0] != "#":
|
||||
raise ValueError("Color must be in format #RRGGBB")
|
||||
# expect format #RRGGBB or #RRGGBBAA
|
||||
if len(color) not in (7, 9) or color[0] != "#":
|
||||
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA")
|
||||
try:
|
||||
int(color[1:], 16)
|
||||
except ValueError:
|
||||
raise ValueError("Color must be in format #RRGGBB") from None
|
||||
raise ValueError("Color must be in format #RRGGBB or #RRGGBBAA") from None
|
||||
|
||||
alpha = 1.0
|
||||
if len(color) == 9:
|
||||
alpha = int(color[7:9], 16) / 255.0
|
||||
color = color[:7]
|
||||
|
||||
r, g, b = hex_to_rgb(color)
|
||||
|
||||
rgb_int = r * 256 * 256 + g * 256 + b
|
||||
return io.NodeOutput(rgb_int, color)
|
||||
return io.NodeOutput(rgb_int, color, alpha)
|
||||
|
||||
|
||||
class ColorExtension(ComfyExtension):
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
comfyui-frontend-package==1.45.20
|
||||
comfyui-workflow-templates==0.11.2
|
||||
comfyui-embedded-docs==0.5.6
|
||||
comfyui-embedded-docs==0.5.7
|
||||
torch
|
||||
torchsde
|
||||
torchvision
|
||||
|
||||
336
tests-unit/comfy_test/mask_test.py
Normal file
336
tests-unit/comfy_test/mask_test.py
Normal file
@ -0,0 +1,336 @@
|
||||
"""
|
||||
Tests for mask handling across arbitrary spatial dimensions.
|
||||
|
||||
Covers resolve_areas_and_cond_masks_multidim (conditioning masks) and
|
||||
reshape_mask (denoise masks) for 1D (audio), 2D (image), and 3D (video)
|
||||
spatial dims, including edge cases around batch size, spurious unsqueeze
|
||||
from upstream nodes, and size mismatches.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from comfy.samplers import resolve_areas_and_cond_masks_multidim
|
||||
from comfy.utils import reshape_mask
|
||||
|
||||
|
||||
def make_cond(mask):
|
||||
"""Create a minimal conditioning dict with a mask."""
|
||||
return {"mask": mask, "model_conds": {}}
|
||||
|
||||
|
||||
def run_resolve(mask, dims, device="cpu"):
|
||||
"""Run resolve on a single condition and return the resolved mask."""
|
||||
conds = [make_cond(mask)]
|
||||
resolve_areas_and_cond_masks_multidim(conds, dims, device)
|
||||
return conds[0]["mask"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1D spatial dims (audio models like AceStep v1.5)
|
||||
# dims = (length,), expected mask output: [batch, length]
|
||||
# ============================================================
|
||||
|
||||
class Test1DSpatial:
|
||||
"""Tests for 1D spatial models (e.g. audio with noise shape [B, C, L])."""
|
||||
|
||||
def test_correct_shape_same_length(self):
|
||||
"""[B, L] mask with matching length — should pass through unchanged."""
|
||||
mask = torch.ones(2, 100)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (2, 100)
|
||||
|
||||
def test_correct_shape_resize(self):
|
||||
"""[B, L] mask with different length — should resize via linear interp."""
|
||||
mask = torch.ones(1, 50)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (1, 100)
|
||||
|
||||
def test_bare_spatial_mask(self):
|
||||
"""[L] mask (no batch) — should get batch dim added."""
|
||||
mask = torch.ones(50)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (1, 100)
|
||||
|
||||
def test_spurious_unsqueeze_from_hooks(self):
|
||||
"""[1, B, L] mask (from set_mask_for_conditioning unsqueezing a [B, L] mask)
|
||||
— should squeeze back to [B, L]."""
|
||||
# Simulates: mask is [B, L], hooks.py does unsqueeze(0) -> [1, B, L]
|
||||
mask = torch.ones(1, 2, 100)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (2, 100)
|
||||
|
||||
def test_spurious_unsqueeze_batch1(self):
|
||||
"""[1, 1, L] mask (batch=1, hooks added extra dim) — should become [1, L]."""
|
||||
mask = torch.ones(1, 1, 50)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (1, 100)
|
||||
|
||||
def test_batch_gt1_same_length(self):
|
||||
"""[B, L] mask with batch=4 and matching length — no changes needed."""
|
||||
mask = torch.rand(4, 100)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (4, 100)
|
||||
torch.testing.assert_close(result, mask)
|
||||
|
||||
def test_batch_gt1_resize(self):
|
||||
"""[B, L] mask with batch=4 and different length — should resize each batch."""
|
||||
mask = torch.rand(4, 50)
|
||||
result = run_resolve(mask, dims=(100,))
|
||||
assert result.shape == (4, 100)
|
||||
|
||||
def test_values_preserved_no_resize(self):
|
||||
"""Mask values should be preserved when no resize is needed."""
|
||||
mask = torch.tensor([[0.0, 0.5, 1.0]])
|
||||
result = run_resolve(mask, dims=(3,))
|
||||
torch.testing.assert_close(result, mask)
|
||||
|
||||
def test_linear_interpolation_values(self):
|
||||
"""Check that linear interpolation produces sensible values."""
|
||||
mask = torch.tensor([[0.0, 1.0]]) # [1, 2]
|
||||
result = run_resolve(mask, dims=(5,))
|
||||
assert result.shape == (1, 5)
|
||||
# Should interpolate from 0 to 1
|
||||
assert result[0, 0].item() < result[0, -1].item()
|
||||
|
||||
def test_set_area_to_bounds_skipped_for_1d(self):
|
||||
"""set_area_to_bounds should be skipped for 1D (no crash)."""
|
||||
mask = torch.zeros(1, 100)
|
||||
mask[0, 10:50] = 1.0
|
||||
conds = [{"mask": mask, "model_conds": {}, "set_area_to_bounds": True}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (100,), "cpu")
|
||||
assert "area" not in conds[0]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2D spatial dims (image models) — regression tests
|
||||
# dims = (H, W), expected mask output: [batch, H, W]
|
||||
# ============================================================
|
||||
|
||||
class Test2DSpatial:
|
||||
"""Regression tests for standard 2D image models."""
|
||||
|
||||
def test_correct_shape_same_size(self):
|
||||
"""[B, H, W] mask matching dims — pass through."""
|
||||
mask = torch.ones(1, 64, 64)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (1, 64, 64)
|
||||
|
||||
def test_bare_spatial_mask(self):
|
||||
"""[H, W] mask — should get batch dim added."""
|
||||
mask = torch.ones(64, 64)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (1, 64, 64)
|
||||
|
||||
def test_resize_different_resolution(self):
|
||||
"""[B, H1, W1] mask with different size than dims — should bilinear resize."""
|
||||
mask = torch.ones(1, 32, 32)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (1, 64, 64)
|
||||
|
||||
def test_4d_mask(self):
|
||||
"""[B, C, H, W] mask (4D) — should resize via common_upscale 4D path."""
|
||||
mask = torch.ones(1, 1, 32, 32)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (1, 64, 64)
|
||||
|
||||
def test_batch_gt1(self):
|
||||
"""[B, H, W] mask with batch > 1."""
|
||||
mask = torch.rand(4, 64, 64)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (4, 64, 64)
|
||||
|
||||
def test_batch_gt1_resize(self):
|
||||
"""[B, H, W] mask with batch > 1 and different resolution."""
|
||||
mask = torch.rand(4, 32, 32)
|
||||
result = run_resolve(mask, dims=(64, 64))
|
||||
assert result.shape == (4, 64, 64)
|
||||
|
||||
def test_set_area_to_bounds(self):
|
||||
"""set_area_to_bounds should work for 2D masks."""
|
||||
mask = torch.zeros(1, 64, 64)
|
||||
mask[0, 10:20, 10:30] = 1.0
|
||||
conds = [{"mask": mask, "model_conds": {}, "set_area_to_bounds": True}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (64, 64), "cpu")
|
||||
assert "area" in conds[0]
|
||||
|
||||
def test_non_square_resize(self):
|
||||
"""[B, H1, W1] mask resized to non-square dims."""
|
||||
mask = torch.ones(1, 16, 32)
|
||||
result = run_resolve(mask, dims=(64, 128))
|
||||
assert result.shape == (1, 64, 128)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3D spatial dims (video models)
|
||||
# dims = (T, H, W), expected mask output: [batch, T, H, W]
|
||||
# ============================================================
|
||||
|
||||
class Test3DSpatial:
|
||||
"""Tests for 3D spatial models (e.g. video with noise shape [B, C, T, H, W])."""
|
||||
|
||||
def test_correct_shape_same_size(self):
|
||||
"""[B, T, H, W] mask matching dims — pass through."""
|
||||
mask = torch.ones(1, 8, 64, 64)
|
||||
result = run_resolve(mask, dims=(8, 64, 64))
|
||||
assert result.shape == (1, 8, 64, 64)
|
||||
|
||||
def test_bare_spatial_mask(self):
|
||||
"""[T, H, W] mask — should get batch dim added."""
|
||||
mask = torch.ones(8, 64, 64)
|
||||
result = run_resolve(mask, dims=(8, 64, 64))
|
||||
assert result.shape == (1, 8, 64, 64)
|
||||
|
||||
def test_resize_hw(self):
|
||||
"""[B, T, H1, W1] mask with different H, W — should resize last 2 dims."""
|
||||
mask = torch.ones(1, 8, 32, 32)
|
||||
result = run_resolve(mask, dims=(8, 64, 64))
|
||||
assert result.shape == (1, 8, 64, 64)
|
||||
|
||||
def test_set_area_to_bounds_skipped_for_3d(self):
|
||||
"""set_area_to_bounds should be skipped for 3D (no crash)."""
|
||||
mask = torch.zeros(1, 8, 64, 64)
|
||||
mask[0, :, 10:20, 10:30] = 1.0
|
||||
conds = [{"mask": mask, "model_conds": {}, "set_area_to_bounds": True}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (8, 64, 64), "cpu")
|
||||
assert "area" not in conds[0]
|
||||
|
||||
|
||||
class TestNoMask:
|
||||
"""Conditions without masks should pass through untouched."""
|
||||
|
||||
def test_no_mask_key(self):
|
||||
"""Condition with no mask key — untouched."""
|
||||
conds = [{"model_conds": {}}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (64, 64), "cpu")
|
||||
assert "mask" not in conds[0]
|
||||
|
||||
def test_empty_conditions(self):
|
||||
"""Empty conditions list — no crash."""
|
||||
conds = []
|
||||
resolve_areas_and_cond_masks_multidim(conds, (64, 64), "cpu")
|
||||
assert len(conds) == 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Area resolution (percentage-based)
|
||||
# ============================================================
|
||||
|
||||
class TestAreaResolution:
|
||||
"""Test that percentage-based area resolution works for different dims."""
|
||||
|
||||
def test_percentage_area_2d(self):
|
||||
"""Percentage area for 2D should resolve to pixel coords."""
|
||||
conds = [{"area": ("percentage", 0.5, 0.5, 0.25, 0.25), "model_conds": {}}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (64, 64), "cpu")
|
||||
area = conds[0]["area"]
|
||||
assert area == (32, 32, 16, 16)
|
||||
|
||||
def test_percentage_area_1d(self):
|
||||
"""Percentage area for 1D should resolve to frame coords."""
|
||||
conds = [{"area": ("percentage", 0.5, 0.25), "model_conds": {}}]
|
||||
resolve_areas_and_cond_masks_multidim(conds, (100,), "cpu")
|
||||
area = conds[0]["area"]
|
||||
assert area == (50, 25)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# reshape_mask — mask reshaping for F.interpolate
|
||||
# ============================================================
|
||||
|
||||
class TestReshapeMask1D:
|
||||
"""Tests for reshape_mask with 1D output (e.g. audio with noise shape [B, C, L])."""
|
||||
|
||||
def test_4d_input_same_length(self):
|
||||
"""[1, 1, 1, L] input (typical from pipeline) — should reshape and expand channels."""
|
||||
mask = torch.ones(1, 1, 1, 100)
|
||||
result = reshape_mask(mask, torch.Size([1, 64, 100]))
|
||||
assert result.shape == (1, 64, 100)
|
||||
|
||||
def test_4d_input_resize(self):
|
||||
"""[1, 1, 1, L1] input resized to different length."""
|
||||
mask = torch.ones(1, 1, 1, 50)
|
||||
result = reshape_mask(mask, torch.Size([1, 64, 100]))
|
||||
assert result.shape == (1, 64, 100)
|
||||
|
||||
def test_3d_input(self):
|
||||
"""[1, 1, L] input — should work directly."""
|
||||
mask = torch.ones(1, 1, 100)
|
||||
result = reshape_mask(mask, torch.Size([1, 64, 100]))
|
||||
assert result.shape == (1, 64, 100)
|
||||
|
||||
def test_2d_input(self):
|
||||
"""[B, L] input — should reshape to [B, 1, L]."""
|
||||
mask = torch.ones(1, 50)
|
||||
result = reshape_mask(mask, torch.Size([1, 64, 100]))
|
||||
assert result.shape == (1, 64, 100)
|
||||
|
||||
def test_1d_input(self):
|
||||
"""[L] input — should reshape to [1, 1, L]."""
|
||||
mask = torch.ones(50)
|
||||
result = reshape_mask(mask, torch.Size([1, 64, 100]))
|
||||
assert result.shape == (1, 64, 100)
|
||||
|
||||
def test_channel_repeat(self):
|
||||
"""Mask with 1 channel should repeat to match output channels."""
|
||||
mask = torch.full((1, 1, 1, 100), 0.5)
|
||||
result = reshape_mask(mask, torch.Size([1, 32, 100]))
|
||||
assert result.shape == (1, 32, 100)
|
||||
torch.testing.assert_close(result, torch.full_like(result, 0.5))
|
||||
|
||||
def test_batch_repeat(self):
|
||||
"""Single-batch mask should repeat to match output batch size."""
|
||||
mask = torch.full((1, 1, 1, 100), 0.7)
|
||||
result = reshape_mask(mask, torch.Size([4, 64, 100]))
|
||||
assert result.shape == (4, 64, 100)
|
||||
|
||||
def test_values_preserved_no_resize(self):
|
||||
"""Values should be preserved when no resize is needed."""
|
||||
values = torch.tensor([[[0.0, 0.5, 1.0]]]) # [1, 1, 3]
|
||||
result = reshape_mask(values, torch.Size([1, 1, 3]))
|
||||
torch.testing.assert_close(result, values)
|
||||
|
||||
def test_interpolation_values(self):
|
||||
"""Linear interpolation should produce sensible intermediate values."""
|
||||
mask = torch.tensor([[[[0.0, 1.0]]]]) # [1, 1, 1, 2]
|
||||
result = reshape_mask(mask, torch.Size([1, 1, 4]))
|
||||
assert result.shape == (1, 1, 4)
|
||||
# Should interpolate from 0 to 1
|
||||
assert result[0, 0, 0].item() < result[0, 0, -1].item()
|
||||
|
||||
|
||||
class TestReshapeMask2D:
|
||||
"""Regression tests for reshape_mask with 2D output (image models)."""
|
||||
|
||||
def test_standard_resize(self):
|
||||
"""[1, 1, H, W] mask resized to different resolution."""
|
||||
mask = torch.ones(1, 1, 32, 32)
|
||||
result = reshape_mask(mask, torch.Size([1, 4, 64, 64]))
|
||||
assert result.shape == (1, 4, 64, 64)
|
||||
|
||||
def test_same_size(self):
|
||||
"""[1, 1, H, W] mask with matching size — no resize needed."""
|
||||
mask = torch.rand(1, 1, 64, 64)
|
||||
result = reshape_mask(mask, torch.Size([1, 4, 64, 64]))
|
||||
assert result.shape == (1, 4, 64, 64)
|
||||
|
||||
def test_3d_input(self):
|
||||
"""[B, H, W] input — should reshape to [B, 1, H, W]."""
|
||||
mask = torch.ones(1, 32, 32)
|
||||
result = reshape_mask(mask, torch.Size([1, 4, 64, 64]))
|
||||
assert result.shape == (1, 4, 64, 64)
|
||||
|
||||
|
||||
class TestReshapeMask3D:
|
||||
"""Regression tests for reshape_mask with 3D output (video models)."""
|
||||
|
||||
def test_standard_resize(self):
|
||||
"""[1, 1, T, H, W] mask resized to different resolution."""
|
||||
mask = torch.ones(1, 1, 8, 32, 32)
|
||||
result = reshape_mask(mask, torch.Size([1, 4, 8, 64, 64]))
|
||||
assert result.shape == (1, 4, 8, 64, 64)
|
||||
|
||||
def test_4d_input(self):
|
||||
"""[B, T, H, W] input — should reshape to [1, 1, T, H, W]."""
|
||||
mask = torch.ones(1, 8, 32, 32)
|
||||
result = reshape_mask(mask, torch.Size([1, 4, 8, 64, 64]))
|
||||
assert result.shape == (1, 4, 8, 64, 64)
|
||||
Loading…
Reference in New Issue
Block a user