Compare commits

...

9 Commits

Author SHA1 Message Date
RyanOnTheInside
9bdd31d35f
Merge 9a5e6233f4 into b08debceca 2026-07-06 17:34:07 +08:00
Daxiong (Lin)
b08debceca
chore: update embedded docs to v0.5.7 (#14783)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Build package / Build Test (3.10) (push) Waiting to run
Build package / Build Test (3.11) (push) Waiting to run
Build package / Build Test (3.12) (push) Waiting to run
Build package / Build Test (3.13) (push) Waiting to run
Build package / Build Test (3.14) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-06 09:56:09 +08:00
comfyanonymous
000c6b784e
Small speedup for text model sampling. (#14773) 2026-07-05 18:39:24 -07:00
Alexander Piskun
985fb9d6ad
[Partner Nodes] fix(logs-auth): mask authorization headers in logs (#14774)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Signed-off-by: bigcat88 <bigcat88@icloud.com>
2026-07-05 13:55:29 +03:00
Alexis Rolland
7f287b705e
fix: Bug when setting transparency in color picker (#14764)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 19:13:38 -04:00
comfyanonymous
b7ba504e06
Try to make coderabbit enforce AGENTS.md (#14759)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 14:25:24 -04:00
Silver
6c62ca0b6b
fix: error when embedding is loaded with models using llama_template (#14744)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
2026-07-04 17:06:09 +08:00
RyanOnTheInside
9a5e6233f4 Fix reshape_mask for 1D spatial dimensions.
reshape_mask sets scale_mode="linear" for dims==1 but is missing the input reshape to [N, C, W] that the 2D and 3D branches both perform. Add the missing reshape, matching the existing pattern.
2026-02-15 10:53:49 -05:00
RyanOnTheInside
abfea891ef Fix conditioning mask normalization for arbitrary spatial dimensions.
May also resolve #9784 — the mask normalization fixes a class of dimensionality mismatches that can cause the `y, x = torch.where(mask)` crash in `get_mask_aabb`, though the root cause in that report is unconfirmed.

## Summary

`resolve_areas_and_cond_masks_multidim` assumes 2D spatial masks. This breaks for 1D audio models (StableAudio1, ACEAudio15) because upstream code (`ConditioningSetMask`, `set_mask_for_conditioning`) unconditionally unsqueezes masks with `ndim < 3`, corrupting valid `[B, L]` masks into `[1, B, L]` before they reach the sampler.

This PR:
- Normalizes masks to `[batch, *spatial_dims]` using `dims` as the source of truth
- Adds a 1D resize path via `F.interpolate(mode='linear')`
- Guards `set_area_to_bounds` with `len(dims) == 2` to prevent crashes on non-2D masks (the existing `get_mask_aabb` and `H, W, Y, X` unpacking are 2D-only)

The root cause is the hardcoded `if len(mask.shape) < 3` in `nodes.py:242` and `hooks.py:725`. Fixing it there would require threading latent dimensionality into the conditioning nodes — a much larger change. Normalizing in `resolve_areas_and_cond_masks_multidim` where `dims` is already available is the minimal fix.

Fully backwards compatible for existing 2D image and 3D video workflows.

## Test plan

- [x] 26 unit tests covering 1D/2D/3D mask normalization, resize, and `set_area_to_bounds` guard (`tests-unit/comfy_test/samplers_test.py`)
- [x] 2D image regression with hook masking: [lorahookmasking.json](https://github.com/Kosinkadink/ComfyUI/blob/workflows/lorahookmasking.json)
- [x] 2D image with `set_area_to_bounds` ("mask bounds" mode) — no crash, correct area computation
- [x] 1D audio with conditioning mask: [acestep-1.5-prompt-lora-blending.json](https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/examples/ace1.5/acestep-1.5-prompt-lora-blending.json) (requires custom nodes that patch this function pending upstream)
2026-02-15 09:45:14 -05:00
9 changed files with 425 additions and 30 deletions

View File

@ -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"

View File

@ -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]:

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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):

View File

@ -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

View 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)