Merge pull request #4 from mcDandy/review-fixes

Review fixes
This commit is contained in:
mcDandy
2026-10-18 14:40:35 +02:00
committed by GitHub
36 changed files with 157 additions and 26195 deletions
-212
View File
@@ -1,212 +0,0 @@
# Function Consistency Check: README.md vs MathExpr.g4
## Executive Summary
✅ **All documented functions have grammar rules defined**
✅ **All grammar functions have documentation**
⚠️ **Minor discrepancies in aliases and function naming**
## Functions in README but Need Verification in Grammar
### ✅ Verified - All Present
#### Basic Math (All Present)
- ✅ abs (AbsFunc)
- ✅ sqrt (SqrtFunc)
- ✅ ln (LnFunc)
- ✅ log (LogFunc)
- ✅ exp (ExpFunc)
- ✅ pow (PowFunc as POWE)
- ✅ floor (FloorFunc)
- ✅ ceil (CeilFunc)
- ✅ round (RoundFunc)
- ✅ fract (FractFunc)
- ✅ sign (SignFunc)
- ✅ gamma (GammaFunc)
- ✅ dist/distance (DistFunc)
- ✅ clamp (ClampFunc)
- ✅ step (StepFunc)
#### Trigonometric (All Present)
- ✅ sin, cos, tan, asin, acos, atan, atan2 (all present)
#### Hyperbolic (All Present)
- ✅ sinh, cosh, tanh, asinh, acosh, atanh (all present)
#### Machine Learning / Activation (All Present)
- ✅ relu (ReluFunc)
- ✅ gelu (GeluFunc)
- ✅ softplus (SoftplusFunc)
- ✅ sigm (SigmoidFunc as SIGM)
- ✅ softmax (SoftmaxFunc)
- ✅ softmin (SoftminFunc)
#### Interpolation (All Present)
- ✅ smoothstep (SmoothstepFunc)
- ✅ smootherstep (SmootherstepFunc)
- ✅ cubic_ease/cubic (CubicEaseFunc)
- ✅ sine_ease/sine (SineEaseFunc)
- ✅ elastic_ease/elastic (ElasticEaseFunc)
- ✅ lerp (LerpFunc)
#### Aggregates & Tensor Operations (All Present)
- ✅ tmin (TMinFunc)
- ✅ tmax (TMaxFunc)
- ✅ smin (SMinFunc)
- ✅ smax (SMaxFunc)
- ✅ sum (SumFunc)
- ✅ mean (MeanFunc)
- ✅ std (StdFunc)
- ✅ var (VarFunc)
- ✅ median (MedianFunc)
- ✅ mode (ModeFunc)
- ✅ quartile (QuartileFunc)
- ✅ percentile (PercentileFunc)
- ✅ quantile (QuantileFunc)
- ✅ dot (DotFunc)
- ✅ moment (MomentFunc)
- ✅ topk (TopkFunc)
- ✅ botk (BotkFunc)
- ✅ topk_ind/topk_indices (TopkIndFunc)
- ✅ botk_ind/botk_indices (BotkIndFunc)
- ✅ sort (SortFunc)
- ✅ argsort (ArgsortFunc)
- ✅ argmin (ArgminFunc)
- ✅ argmax (ArgmaxFunc)
- ✅ unique (UniqueFunc)
- ✅ tnorm (TNormFunc)
- ✅ snorm (SNormFunc)
- ✅ swap (SwapFunc)
- ✅ cossim (CossimFunc)
- ✅ flip (FlipFunc)
- ✅ cov (CovFunc)
- ✅ append (AppendFunc)
- ✅ any (AnyFunc)
- ✅ all (AllFunc)
- ✅ cumsum (CumsumFunc)
- ✅ cumprod (CumprodFunc)
- ✅ tensor (EmptyTensorFunc)
- ✅ flatten (FlattenFunc)
#### Advanced Tensor Operations (All Present)
- ✅ map (MapFunc)
- ✅ ezconvolution/ezconv (EzConvFunc)
- ✅ convolution/conv (ConvFunc)
- ✅ get_value (GetValueFunc)
- ✅ crop (CropFunc)
- ✅ permute/perm (PermuteFunc)
- ✅ reshape/rshp (ReshapeFunc)
- ✅ blur/gaussian (GaussianFunc)
- ✅ edge (EdgeFunc)
- ✅ batch_shuffle/shuffle/select (BatchShuffleFunc)
- ✅ matmul (MatmulFunc)
- ✅ cross (CrossFunc)
#### Optical Flow (All Present)
- ✅ rife (RifeFunc)
- ✅ motion_mask (MotionMaskFunc)
- ✅ flow_to_image (FlowToImageFunc)
- ✅ flow_apply (FlowApplyFunc)
#### FFT (All Present)
- ✅ fft (SFFT)
- ✅ ifft (SIFFT)
- ✅ angle (ANGL)
#### Utility (All Present)
- ✅ print (PrintFunc as PRNT)
- ✅ print_shape/pshp (PrintShapeFunc)
- ✅ pinv (PinvFunc)
- ✅ range (RangeFunc)
- ✅ nan_to_num/nvl (NvlFunc)
- ✅ remap (RemapFunc)
- ✅ timestamp (TimestampFunc as TIMESTAMP)
- ✅ count/length/cnt (CountFunc)
#### Random Distributions (All Present)
- ✅ random_normal/randn/noise (NoiseFunc)
- ✅ random_uniform/rand (RandFunc)
- ✅ random_exponential/rande (ExponentialFunc)
- ✅ random_cauchy/randc (CauchyFunc)
- ✅ random_log_normal/randln (LogNormalFunc)
- ✅ random_bernoulli/randb (BernoulliFunc)
- ✅ random_poisson/randp (PoissonFunc)
- ✅ random_gamma/randg (GammaDistFunc)
- ✅ random_beta/randbeta (BetaDistFunc)
- ✅ random_laplace/randl (LaplaceDistFunc)
- ✅ random_gumbel/randgumbel (GumbelDistFunc)
- ✅ random_weibull/randw (WeibullDistFunc)
- ✅ random_chi2/randchi2 (Chi2DistFunc)
- ✅ random_studentt/randt (StudentTDistFunc)
#### Bitwise Operations (All Present)
- ✅ band/bitwise_and (BitAndFunc)
- ✅ bor/bitwise_or (BitOrFunc)
- ✅ xor/bitwise_xor (BitXorFunc)
- ✅ bnot/bitwise_not (BitNotFunc)
- ✅ bitcount/popcount/popcnt (BitCountFunc)
- ✅ << (LSHIFT)
- ✅ >> (RSHIFT)
#### Stack (All Present)
- ✅ stack_push (PushFunc)
- ✅ stack_pop (PopFunc)
- ✅ stack_get (GetFunc)
- ✅ stack_clear (ClearFunc)
- ✅ stack_has (HasFunc)
## Minor Naming Discrepancies
### README Mentions These Aliases Not Explicitly in Grammar Lexer
1. `now` - README mentions as alias for `timestamp`, but only TIMESTAMP: 'timestamp' is in grammar
- **Fix Needed**: Add `TIMESTAMP: 'timestamp' | 'now';`
2. `flow_view` - README mentions as alias for `flow_to_image`
- **Fix Needed**: Add to `FLOW_TO_IMAGE: 'flow_to_image' | 'flow_view';`
3. `apply_flow` - README mentions as alias for `flow_apply`
- **Fix Needed**: Add to `FLOW_APPLY: 'flow_apply' | 'apply_flow';`
4. Bitwise aliases - Should update for consistency:
- `BAND: 'band' | 'bitwise_and' | 'bitand';` (currently has 'bitand' as alias)
- `BOR: 'bor' | 'bitwise_or' | 'bitor';` (currently has 'bitor' as alias)
- `XOR: 'xor' | 'bitwise_xor' | 'xor';` (should add 'bitwise_xor')
- `BNOT: 'bnot' | 'bitnot' | 'bitwise_not';` (should add 'bitwise_not')
## Summary of Issues
### Critical (Must Fix)
None - all documented functions work
### High Priority (Should Fix)
1. Add missing aliases for consistency:
- `now` for `timestamp`
- `flow_view` for `flow_to_image`
- `apply_flow` for `flow_apply`
- `bitwise_*` names for bitwise operators
### Low Priority (Nice to Have)
- Documentation in README could list all aliases explicitly
## Recommended Grammar Updates
```antlr
// Update these lexer rules:
TIMESTAMP: 'timestamp' | 'now';
FLOW_APPLY: 'flow_apply' | 'apply_flow';
FLOW_TO_IMAGE: 'flow_to_image' | 'flow_view';
BAND: 'band' | 'bitand' | 'bitwise_and';
BOR: 'bor' | 'bitor' | 'bitwise_or';
XOR: 'xor' | 'bitwise_xor';
BNOT: 'bnot' | 'bitnot' | 'bitwise_not';
BITCOUNT: 'bitcount' | 'popcount' | 'popcnt' | 'bitcount';
```
## Conclusion
✅ **Status: EXCELLENT CONSISTENCY**
- 100% of documented functions have grammar rules
- 100% of grammar functions have documentation
- Only minor alias discrepancies that don't affect functionality
- All core functionality is properly defined and documented
**Recommendation**: Regenerate parser after applying the minor grammar updates above to ensure all documented aliases work correctly.
+2 -8
View File
@@ -1,10 +1,4 @@
import traceback
try:
from .more_math.nodes import comfy_entrypoint as comfy_entrypoint
except ImportError:
print("Something is seriously wrong")
traceback.print_exc()
from .more_math.nodes import comfy_entrypoint as comfy_entrypoint
WEB_DIRECTORY = "./web"
__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"]
__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"]
+1 -2
View File
@@ -135,7 +135,7 @@ class AudioMathNode(io.ComfyNode):
"batch_count": float(a_w.shape[0]),
} | generate_dim_variables(a_w) | V_norm_waveforms | sample_rates
v_stacked, v_cnt = get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm_waveforms)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -167,6 +167,5 @@ class AudioMathNode(io.ComfyNode):
res_list.append({"waveform": result_chunk, "sample_rate": sample_rate})
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([{"waveform": result, "sample_rate": sample_rate}], stack)
+2 -3
View File
@@ -147,7 +147,7 @@ class ConditioningMathNode(io.ComfyNode):
"batch_count": a.shape[0],
} | generate_dim_variables(a) | V_norm_tensors
v_stacked, v_cnt = get_v_variable(V_norm_tensors, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm_tensors)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -193,7 +193,7 @@ class ConditioningMathNode(io.ComfyNode):
"batch_count": a_p.shape[0] if a_p.numel() > 0 else 0,
} | generate_dim_variables(a_p) | V_norm_pooled
v_stacked, v_cnt = get_v_variable(V_norm_pooled, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm_pooled)
if v_stacked is not None:
variables_pi["V"] = v_stacked
variables_pi["Vcnt"] = float(v_cnt)
@@ -251,5 +251,4 @@ class ConditioningMathNode(io.ComfyNode):
base[0] = (rtensor, new_dict)
res_list = [base]
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(res_list, original_device),stack)
+10 -5
View File
@@ -92,7 +92,6 @@ class MathGuider:
self.tree1 = expression1
self.inner_model = None # Will be set during sample
self.sigmas = None
self.current_step = 0
self.steps = 0
self.stck = stack
@@ -142,7 +141,6 @@ class MathGuider:
eval_samples, variables = self.setVars(x, sigma, seed, lazy_g_results)
visitor = UnifiedMathVisitor(variables, eval_samples.shape,eval_samples.device,state_storage=self.stck)
result_tensor = visitor.visit(self.tree)
self.current_step = self.current_step + 1;
return as_tensor(result_tensor, eval_samples.shape).to(x.device)
@@ -164,6 +162,13 @@ class MathGuider:
frame_count = eval_samples.shape[time_dim] if time_dim is not None else 1
# sigma has one entry per batch item, all equal
sigma = float(sigma.flatten()[0]) if isinstance(sigma, torch.Tensor) else sigma
current_step = 0
if self.sigmas is not None:
# Index of the step whose interval contains sigma, so extra model calls within a step (heun, dpmpp_2s, ...) keep the same index
current_step = max((i for i, s in enumerate(self.sigmas.tolist()) if s >= sigma), default=0)
def make_lazy_var(value_fn):
value_fn.is_lazy_var = True
return value_fn
@@ -184,10 +189,10 @@ class MathGuider:
"batch_count": float(eval_samples.shape[0]),
"N": float(eval_samples.shape[channel_dim]) if channel_dim < ndim else 0.0,
"channel_count": float(eval_samples.shape[channel_dim]) if channel_dim < ndim else 0.0,
"sigma": sigma.item() if isinstance(sigma,torch.Tensor) else sigma,
"sigma": sigma,
"seed": seed if seed is not None else 0,
"steps": self.steps,
"current_step": self.current_step,
"current_step": current_step,
"sample": x
})
if g_results is not None:
@@ -226,7 +231,7 @@ class MathGuider:
def sample(self, noise, latent_image, sampler, sigmas, denoise_mask=None, callback=None, disable_pbar=False, seed=None):
self.sigmas = sigmas
self.steps = len(sigmas)
self.steps = len(sigmas) - 1
if sigmas.shape[-1] == 0:
return latent_image
active_guiders = [g for g in self.V.values() if g is not None]
+1 -3
View File
@@ -129,7 +129,7 @@ class ImageMathNode(io.ComfyNode):
# Add all dynamic inputs
variables.update(V_norm)
v_stacked, v_cnt = get_v_variable(V_norm, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -159,8 +159,6 @@ class ImageMathNode(io.ComfyNode):
res_list = []
for result_chunk in res:
res_list.append(result_chunk)
stack = stack if remember_stack else copy.deepcopy(stack)
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+1 -5
View File
@@ -215,7 +215,7 @@ class LatentMathNode(io.ComfyNode):
# Add all dynamic inputs
variables.update(V_norm_samples)
v_stacked, v_cnt = get_v_variable(V_norm_samples, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm_samples)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -242,7 +242,6 @@ class LatentMathNode(io.ComfyNode):
if getattr(result_t, "is_nested", False):
rl = result_latent.copy()
rl["samples"] = result_t
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device([rl], original_device), stack)
# If the visitor produced a list/tuple, emit each element as a separate latent.
@@ -252,7 +251,6 @@ class LatentMathNode(io.ComfyNode):
rl = result_latent.copy()
rl["samples"] = comp
results.append(rl)
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(results, original_device), stack)
if batching > 0:
@@ -262,9 +260,7 @@ class LatentMathNode(io.ComfyNode):
rl = result_latent.copy()
rl["samples"] = result_tensor
results.append(rl)
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device(results, original_device), stack)
rl = result_latent.copy()
rl["samples"] = result_t
stack = stack if remember_stack else copy.deepcopy(stack)
return (move_to_device([rl], original_device), stack)
+1 -3
View File
@@ -108,7 +108,7 @@ class MaskMathNode(io.ComfyNode):
"batch_count": float(ae.shape[0]),
} | generate_dim_variables(ae)
v_stacked, v_cnt = get_v_variable(V_norm, length_mismatch=length_mismatch)
v_stacked, v_cnt = get_v_variable(V_norm)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -140,8 +140,6 @@ class MaskMathNode(io.ComfyNode):
res_list = []
for result_chunk in res:
res_list.append(result_chunk)
stack = stack if remember_stack else copy.deepcopy(stack)
return (res_list, stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+2 -2
View File
@@ -73,8 +73,8 @@ class NoiseExecutor:
else:
self.tree = expr
self.stack = stack
seed = -1
# Samplers seed their own noise (ancestral/SDE) from noise.seed, so pass the first input's seed through
self.seed = next((v.seed for v in V.values() if v is not None), 0)
def generate_noise(self, input_latent: torch.Tensor) -> torch.Tensor:
samples = input_latent["samples"]
+1 -1
View File
@@ -1383,7 +1383,7 @@ func3:
/**
text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline]) - renders text to a 2D tensor with values 0-1
- text: text to render; line breaks start new lines
- font: font name to look up in the system fonts, or a path to a font file
- font: font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder
- size: font size in pixels
- max_width: wrap lines wider than this many pixels (default: 0, no wrapping)
- weight: font weight 100-900 as in CSS (default: 400)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-423
View File
@@ -1,423 +0,0 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
TRANSPOSE=50
PERM=51
RESHAPE=52
RANGE=53
LINSPACE=54
LOGSPACE=55
TOPK=56
BOTK=57
PINV=58
SUM=59
MEAN=60
STD=61
VAR=62
QUARTILE=63
PERCENTILE=64
QUANTILE=65
DOT=66
MOMENT=67
ERF=68
ERFINV=69
ANY=70
ALL=71
EDGE=72
GAUSSIAN=73
MEDIAN=74
MODE=75
CUMSUM=76
CUMPROD=77
TOPK_IND=78
BOTK_IND=79
CUBIC_EASE=80
ELASTIC_EASE=81
SINE_EASE=82
SMOOTHERSTEP=83
DIST=84
REMAP=85
COSSIM=86
COUNT=87
KEYS=88
ENABLE_TRACKING=89
DISABLE_TRACKING=90
RESET_TRACKING=91
GET_CHANGED_KEYS=92
GET_ADDED_KEYS=93
GET_REMOVED_KEYS=94
GET_UNCHANGED_KEYS=95
REPEAT=96
FLATTEN=97
APPEND=98
GET_VALUE=99
FLOW_APPLY=100
BATCH_SHUFFLE=101
MOTION_MASK=102
FLOW_TO_IMAGE=103
FLOW_MAG=104
FLOW_ANG=105
WHERE=106
HISTOGRAM=107
OVERLAY=108
PAD=109
CROSS=110
MATMUL=111
RIFE=112
BNOT=113
BITCOUNT=114
SHAPE=115
BAND=116
XOR=117
BOR=118
TENSOR=119
ADD_KEY=120
REMOVE_KEY=121
PUSH=122
POP=123
CLEAR=124
HAS=125
GET=126
CONCAT=127
STACK=128
NONZERO=129
INT=130
FLOAT=131
UPPER=132
LOWER=133
TRIM=134
SPLIT=135
JOIN=136
SUBSTRING=137
FIND=138
REPLACE=139
DILATE=140
ERODE=141
MORPH_OPEN=142
MORPH_CLOSE=143
RGB_TO_OKLAB=144
RGB_TO_CIELAB=145
OKLAB_TO_RGB=146
CIELAB_TO_RGB=147
RGB_TO_HSV=148
HSV_TO_RGB=149
INT_TO_RGB=150
RGB_TO_INT=151
INTERPOLATE_LINEAR=152
INTERPOLATE_AREA=153
INTERPOLATE_NEAREST=154
TEXT_IMAGE=155
AS_NESTED=156
SVD=157
DIAG=158
IF=159
ELSE=160
WHILE=161
FOR=162
IN=163
BREAK=164
CONTINUE=165
RETURN=166
TIMESTAMP=167
SORT=168
ARGSORT=169
ARGMIN=170
ARGMAX=171
SOFTMAX=172
SOFTMIN=173
UNIQUE=174
FLIP=175
STARTSWITH=176
ENDSWITH=177
SQUEEZE=178
UNSQUEEZE=179
ROLL=180
COV=181
CORR=182
ENTROPY=183
CROP=184
NONE=185
COORDS=186
GATHER=187
SCATTER=188
SCATTER_ADD=189
NOISE=190
RAND=191
CAUCHY=192
EXPONENTIAL=193
LOGNORMAL=194
BERNOULLI=195
POISSON=196
GAMMADIST=197
BETADIST=198
LAPLACEDIST=199
GUMBELDIST=200
WEIBULLDIST=201
CHI2DIST=202
STUDENTTDIST=203
PERLIN=204
CELLULAR=205
PLASMA=206
RIDGED=207
DOMAIN_WARP=208
PLUS=209
MINUS=210
MULT=211
DIV=212
MOD=213
POW=214
LSHIFT=215
RSHIFT=216
GE=217
GT=218
LE=219
LT=220
EQ=221
EQUEALS=222
PLUS_EQ=223
MINUS_EQ=224
MULT_EQ=225
DIV_EQ=226
MOD_EQ=227
NE=228
PIPE=229
LPAREN=230
RPAREN=231
COMMA=232
SEMICOLON=233
ARROW=234
LBRACKET=235
RBRACKET=236
QUESTION=237
COLON=238
LBRACE=239
RBRACE=240
NUMBER=241
CONSTANT=242
STRING=243
VARIABLE=244
SL_COMMENT=245
ML_COMMENT=246
WS=247
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=53
'linspace'=54
'logspace'=55
'topk'=56
'botk'=57
'pinv'=58
'sum'=59
'mean'=60
'std'=61
'var'=62
'quantile'=65
'dot'=66
'moment'=67
'erf'=68
'erfinv'=69
'any'=70
'all'=71
'edge'=72
'median'=74
'mode'=75
'cumsum'=76
'cumprod'=77
'smootherstep'=83
'remap'=85
'keys'=88
'repeat'=96
'flatten'=97
'append'=98
'get_value'=99
'flow_apply'=100
'motion_mask'=102
'flow_to_image'=103
'where'=106
'overlay'=108
'pad'=109
'cross'=110
'matmul'=111
'rife'=112
'shape'=115
'tensor'=119
'add_key'=120
'stack_push'=122
'stack_pop'=123
'stack_clear'=124
'stack_has'=125
'stack_get'=126
'stack'=128
'nonzero'=129
'int'=130
'float'=131
'upper'=132
'lower'=133
'trim'=134
'split'=135
'join'=136
'find'=138
'replace'=139
'dilate'=140
'erode'=141
'morph_open'=142
'morph_close'=143
'rgb_to_oklab'=144
'rgb_to_cielab'=145
'oklab_to_rgb'=146
'cielab_to_rgb'=147
'rgb_to_hsv'=148
'hsv_to_rgb'=149
'int_to_rgb'=150
'rgb_to_int'=151
'interpolate_linear'=152
'interpolate_area'=153
'text_image'=155
'if'=159
'else'=160
'while'=161
'for'=162
'in'=163
'break'=164
'continue'=165
'return'=166
'sort'=168
'argsort'=169
'argmin'=170
'argmax'=171
'softmax'=172
'softmin'=173
'unique'=174
'flip'=175
'startswith'=176
'endswith'=177
'squeeze'=178
'unsqueeze'=179
'roll'=180
'cov'=181
'entropy'=183
'crop'=184
'gather'=187
'scatter'=188
'scatter_add'=189
'+'=209
'-'=210
'*'=211
'/'=212
'%'=213
'^'=214
'<<'=215
'>>'=216
'>='=217
'>'=218
'<='=219
'<'=220
'=='=221
'='=222
'+='=223
'-='=224
'*='=225
'/='=226
'%='=227
'!='=228
'|'=229
'('=230
')'=231
','=232
';'=233
'->'=234
'['=235
']'=236
'?'=237
':'=238
'{'=239
'}'=240
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-423
View File
@@ -1,423 +0,0 @@
SIN=1
COS=2
TAN=3
ASIN=4
ACOS=5
ATAN=6
ATAN2=7
SINH=8
COSH=9
TANH=10
ASINH=11
ACOSH=12
ATANH=13
ABS=14
SQRT=15
LN=16
LOG=17
EXP=18
SMIN=19
SMAX=20
TMIN=21
TMAX=22
TNORM=23
SNORM=24
FLOOR=25
CEIL=26
ROUND=27
GAMMA=28
POWE=29
SIGM=30
CLAMP=31
SFFT=32
SIFFT=33
ANGL=34
PRNT=35
PRINT_SHAPE=36
NVL=37
LERP=38
STEP=39
SMOOTHSTEP=40
FRACT=41
RELU=42
SOFTPLUS=43
GELU=44
SIGN=45
MAP=46
EZCONV=47
CONV=48
SWAP=49
TRANSPOSE=50
PERM=51
RESHAPE=52
RANGE=53
LINSPACE=54
LOGSPACE=55
TOPK=56
BOTK=57
PINV=58
SUM=59
MEAN=60
STD=61
VAR=62
QUARTILE=63
PERCENTILE=64
QUANTILE=65
DOT=66
MOMENT=67
ERF=68
ERFINV=69
ANY=70
ALL=71
EDGE=72
GAUSSIAN=73
MEDIAN=74
MODE=75
CUMSUM=76
CUMPROD=77
TOPK_IND=78
BOTK_IND=79
CUBIC_EASE=80
ELASTIC_EASE=81
SINE_EASE=82
SMOOTHERSTEP=83
DIST=84
REMAP=85
COSSIM=86
COUNT=87
KEYS=88
ENABLE_TRACKING=89
DISABLE_TRACKING=90
RESET_TRACKING=91
GET_CHANGED_KEYS=92
GET_ADDED_KEYS=93
GET_REMOVED_KEYS=94
GET_UNCHANGED_KEYS=95
REPEAT=96
FLATTEN=97
APPEND=98
GET_VALUE=99
FLOW_APPLY=100
BATCH_SHUFFLE=101
MOTION_MASK=102
FLOW_TO_IMAGE=103
FLOW_MAG=104
FLOW_ANG=105
WHERE=106
HISTOGRAM=107
OVERLAY=108
PAD=109
CROSS=110
MATMUL=111
RIFE=112
BNOT=113
BITCOUNT=114
SHAPE=115
BAND=116
XOR=117
BOR=118
TENSOR=119
ADD_KEY=120
REMOVE_KEY=121
PUSH=122
POP=123
CLEAR=124
HAS=125
GET=126
CONCAT=127
STACK=128
NONZERO=129
INT=130
FLOAT=131
UPPER=132
LOWER=133
TRIM=134
SPLIT=135
JOIN=136
SUBSTRING=137
FIND=138
REPLACE=139
DILATE=140
ERODE=141
MORPH_OPEN=142
MORPH_CLOSE=143
RGB_TO_OKLAB=144
RGB_TO_CIELAB=145
OKLAB_TO_RGB=146
CIELAB_TO_RGB=147
RGB_TO_HSV=148
HSV_TO_RGB=149
INT_TO_RGB=150
RGB_TO_INT=151
INTERPOLATE_LINEAR=152
INTERPOLATE_AREA=153
INTERPOLATE_NEAREST=154
TEXT_IMAGE=155
AS_NESTED=156
SVD=157
DIAG=158
IF=159
ELSE=160
WHILE=161
FOR=162
IN=163
BREAK=164
CONTINUE=165
RETURN=166
TIMESTAMP=167
SORT=168
ARGSORT=169
ARGMIN=170
ARGMAX=171
SOFTMAX=172
SOFTMIN=173
UNIQUE=174
FLIP=175
STARTSWITH=176
ENDSWITH=177
SQUEEZE=178
UNSQUEEZE=179
ROLL=180
COV=181
CORR=182
ENTROPY=183
CROP=184
NONE=185
COORDS=186
GATHER=187
SCATTER=188
SCATTER_ADD=189
NOISE=190
RAND=191
CAUCHY=192
EXPONENTIAL=193
LOGNORMAL=194
BERNOULLI=195
POISSON=196
GAMMADIST=197
BETADIST=198
LAPLACEDIST=199
GUMBELDIST=200
WEIBULLDIST=201
CHI2DIST=202
STUDENTTDIST=203
PERLIN=204
CELLULAR=205
PLASMA=206
RIDGED=207
DOMAIN_WARP=208
PLUS=209
MINUS=210
MULT=211
DIV=212
MOD=213
POW=214
LSHIFT=215
RSHIFT=216
GE=217
GT=218
LE=219
LT=220
EQ=221
EQUEALS=222
PLUS_EQ=223
MINUS_EQ=224
MULT_EQ=225
DIV_EQ=226
MOD_EQ=227
NE=228
PIPE=229
LPAREN=230
RPAREN=231
COMMA=232
SEMICOLON=233
ARROW=234
LBRACKET=235
RBRACKET=236
QUESTION=237
COLON=238
LBRACE=239
RBRACE=240
NUMBER=241
CONSTANT=242
STRING=243
VARIABLE=244
SL_COMMENT=245
ML_COMMENT=246
WS=247
'sin'=1
'cos'=2
'tan'=3
'asin'=4
'acos'=5
'atan'=6
'atan2'=7
'sinh'=8
'cosh'=9
'tanh'=10
'asinh'=11
'acosh'=12
'atanh'=13
'abs'=14
'sqrt'=15
'ln'=16
'log'=17
'exp'=18
'smin'=19
'smax'=20
'tmin'=21
'tmax'=22
'tnorm'=23
'snorm'=24
'floor'=25
'ceil'=26
'round'=27
'gamma'=28
'pow'=29
'sigm'=30
'clamp'=31
'fft'=32
'ifft'=33
'angle'=34
'print'=35
'lerp'=38
'step'=39
'smoothstep'=40
'fract'=41
'relu'=42
'softplus'=43
'gelu'=44
'sign'=45
'map'=46
'swap'=49
'range'=53
'linspace'=54
'logspace'=55
'topk'=56
'botk'=57
'pinv'=58
'sum'=59
'mean'=60
'std'=61
'var'=62
'quantile'=65
'dot'=66
'moment'=67
'erf'=68
'erfinv'=69
'any'=70
'all'=71
'edge'=72
'median'=74
'mode'=75
'cumsum'=76
'cumprod'=77
'smootherstep'=83
'remap'=85
'keys'=88
'repeat'=96
'flatten'=97
'append'=98
'get_value'=99
'flow_apply'=100
'motion_mask'=102
'flow_to_image'=103
'where'=106
'overlay'=108
'pad'=109
'cross'=110
'matmul'=111
'rife'=112
'shape'=115
'tensor'=119
'add_key'=120
'stack_push'=122
'stack_pop'=123
'stack_clear'=124
'stack_has'=125
'stack_get'=126
'stack'=128
'nonzero'=129
'int'=130
'float'=131
'upper'=132
'lower'=133
'trim'=134
'split'=135
'join'=136
'find'=138
'replace'=139
'dilate'=140
'erode'=141
'morph_open'=142
'morph_close'=143
'rgb_to_oklab'=144
'rgb_to_cielab'=145
'oklab_to_rgb'=146
'cielab_to_rgb'=147
'rgb_to_hsv'=148
'hsv_to_rgb'=149
'int_to_rgb'=150
'rgb_to_int'=151
'interpolate_linear'=152
'interpolate_area'=153
'text_image'=155
'if'=159
'else'=160
'while'=161
'for'=162
'in'=163
'break'=164
'continue'=165
'return'=166
'sort'=168
'argsort'=169
'argmin'=170
'argmax'=171
'softmax'=172
'softmin'=173
'unique'=174
'flip'=175
'startswith'=176
'endswith'=177
'squeeze'=178
'unsqueeze'=179
'roll'=180
'cov'=181
'entropy'=183
'crop'=184
'gather'=187
'scatter'=188
'scatter_add'=189
'+'=209
'-'=210
'*'=211
'/'=212
'%'=213
'^'=214
'<<'=215
'>>'=216
'>='=217
'>'=218
'<='=219
'<'=220
'=='=221
'='=222
'+='=223
'-='=224
'*='=225
'/='=226
'%='=227
'!='=228
'|'=229
'('=230
')'=231
','=232
';'=233
'->'=234
'['=235
']'=236
'?'=237
':'=238
'{'=239
'}'=240
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+50 -38
View File
@@ -10,6 +10,7 @@ import math
import inspect
import weakref
import torch.nn.functional as F
import folder_paths
from comfy.nested_tensor import NestedTensor
from comfy.model_management import throw_exception_if_processing_interrupted
from comfy.k_diffusion import sampling as k_sampling
@@ -156,6 +157,17 @@ _BLEND_MODES = {
}
_FLOAT8_DTYPES = {
dtype for dtype in (
getattr(torch, "float8_e4m3fn", None),
getattr(torch, "float8_e4m3fnuz", None),
getattr(torch, "float8_e5m2", None),
getattr(torch, "float8_e5m2fnuz", None),
)
if dtype is not None
}
def _rec709_luma(r, g, b):
return 0.2126 * r + 0.7152 * g + 0.0722 * b
@@ -190,9 +202,7 @@ def _font_axis_name(axis):
return str(name).lower()
@functools.lru_cache(maxsize=1)
def _system_fonts():
"""Index of installed fonts, scanned once per process (fonts installed later need a restart)."""
def _font_dirs():
if sys.platform == "win32":
search_dirs = [os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts"), os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts")]
elif sys.platform == "darwin":
@@ -205,9 +215,23 @@ def _system_fonts():
os.path.expanduser("~/.local/share/fonts"),
os.path.expanduser("~/.fonts"),
]
return list(dict.fromkeys(search_dirs))
def _is_allowed_font_file(path):
"""Font files given by path must be in a system font folder or in ComfyUI's input folder."""
if not os.path.isfile(path):
return False
path = os.path.normcase(os.path.realpath(path))
roots = _font_dirs() + [folder_paths.get_input_directory()]
return any(path.startswith(os.path.normcase(os.path.realpath(root)) + os.sep) for root in roots)
@functools.lru_cache(maxsize=1)
def _system_fonts():
"""Index of installed fonts, scanned once per process (fonts installed later need a restart)."""
fonts = []
for directory in dict.fromkeys(search_dirs):
for directory in _font_dirs():
for root, _, files in os.walk(directory):
for fname in files:
if not fname.lower().endswith((".ttf", ".otf", ".ttc", ".otc")):
@@ -419,20 +443,8 @@ class UnifiedMathVisitor(MathExprVisitor):
return torch.full(t,val,device=self.device)
return torch.tensor(val, device=self.device)
@staticmethod
def _float8_dtypes():
return {
dtype for dtype in (
getattr(torch, "float8_e4m3fn", None),
getattr(torch, "float8_e4m3fnuz", None),
getattr(torch, "float8_e5m2", None),
getattr(torch, "float8_e5m2fnuz", None),
)
if dtype is not None
}
def _apply_binary_operation(self, a, b, torch_op, scalar_op):
float8_dtypes = self._float8_dtypes()
float8_dtypes = _FLOAT8_DTYPES
original_dtype = None
if self._is_plain_tensor(a) and a.dtype in float8_dtypes:
original_dtype = a.dtype
@@ -449,7 +461,7 @@ class UnifiedMathVisitor(MathExprVisitor):
def _apply_unary_operation(self, value, torch_op, scalar_op):
original_dtype = None
if self._is_plain_tensor(value) and value.dtype in self._float8_dtypes():
if self._is_plain_tensor(value) and value.dtype in _FLOAT8_DTYPES:
original_dtype = value.dtype
value = value.to(torch.float16)
@@ -1342,10 +1354,10 @@ class UnifiedMathVisitor(MathExprVisitor):
dims = self._normalize_dims((yield ctx.expr(2)), val.ndim, ctx, "norm dimension")
dim = tuple(dims)
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.vector_norm(val, ord=p, dim=dim)
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
@@ -1359,13 +1371,13 @@ class UnifiedMathVisitor(MathExprVisitor):
if eps < 0:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: normalize eps must be non-negative")
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
if val.ndim == 0:
result = val / torch.linalg.vector_norm(val, ord=p).clamp_min(eps)
else:
result = F.normalize(val, p=p, dim=dim, eps=eps)
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.tolist() if return_list else result
@@ -1380,7 +1392,7 @@ class UnifiedMathVisitor(MathExprVisitor):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: group_norm groups must be positive and divide the channel count {channels}")
original_dtype = val.dtype
float8_dtypes = self._float8_dtypes()
float8_dtypes = _FLOAT8_DTYPES
if original_dtype in float8_dtypes:
val = val.to(torch.float32)
@@ -1852,7 +1864,7 @@ class UnifiedMathVisitor(MathExprVisitor):
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: slerp expects one vector dimension")
dim = dims[0]
float8_dtypes = self._float8_dtypes()
float8_dtypes = _FLOAT8_DTYPES
input_dtypes = (a.dtype, b.dtype)
output_dtype = next((dtype for dtype in input_dtypes if dtype in float8_dtypes), None)
if output_dtype is None:
@@ -2313,7 +2325,7 @@ class UnifiedMathVisitor(MathExprVisitor):
def visitRfftFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr(0)))
dim = self._to_int((yield ctx.expr(1)), ctx, "rfft dimension", strict=True) if len(ctx.expr()) > 1 else -1
if val.dtype in self._float8_dtypes() or not (val.is_floating_point() or val.is_complex()):
if val.dtype in _FLOAT8_DTYPES or not (val.is_floating_point() or val.is_complex()):
val = val.to(torch.float32)
return torch.fft.rfft(val, dim=dim)
@@ -2321,7 +2333,7 @@ class UnifiedMathVisitor(MathExprVisitor):
val = self._promote_to_tensor((yield ctx.expr(0)))
n = self._to_int((yield ctx.expr(1)), ctx, "irfft length", strict=True) if len(ctx.expr()) > 1 else None
dim = self._to_int((yield ctx.expr(2)), ctx, "irfft dimension", strict=True) if len(ctx.expr()) > 2 else -1
if val.dtype in self._float8_dtypes():
if val.dtype in _FLOAT8_DTYPES:
val = val.to(torch.complex64)
return torch.fft.irfft(val, n=n, dim=dim)
@@ -2420,10 +2432,10 @@ class UnifiedMathVisitor(MathExprVisitor):
elif val.ndim > 0:
dims = tuple(range(val.ndim))
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = val if dims is None else torch.logsumexp(val, dim=dims)
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
@@ -2672,7 +2684,7 @@ class UnifiedMathVisitor(MathExprVisitor):
def visitOuterFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0))).flatten()
b = self._promote_to_tensor((yield ctx.expr(1))).flatten()
float8_dtypes = self._float8_dtypes()
float8_dtypes = _FLOAT8_DTYPES
output_dtype = a.dtype if a.dtype in float8_dtypes else b.dtype if b.dtype in float8_dtypes else None
if output_dtype is not None:
a = a.to(torch.float32)
@@ -2685,20 +2697,20 @@ class UnifiedMathVisitor(MathExprVisitor):
if val.ndim < 2 or val.shape[-1] != val.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: trace expects square matrices")
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.diagonal(val, dim1=-2, dim2=-1).sum(dim=-1)
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
def visitDetFunc(self, ctx):
val = self._promote_to_tensor((yield ctx.expr()))
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.det(val)
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
result = result.to(original_dtype)
return result.item() if result.numel() == 1 else result
@@ -2710,10 +2722,10 @@ class UnifiedMathVisitor(MathExprVisitor):
if not pseudo and val.shape[-1] != val.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: inv without pseudo requires square matrices")
original_dtype = val.dtype
if original_dtype in self._float8_dtypes():
if original_dtype in _FLOAT8_DTYPES:
val = val.to(torch.float32)
result = torch.linalg.pinv(val) if pseudo else torch.linalg.inv(val)
return result.to(original_dtype) if original_dtype in self._float8_dtypes() else result
return result.to(original_dtype) if original_dtype in _FLOAT8_DTYPES else result
def visitSolveFunc(self, ctx):
a = self._promote_to_tensor((yield ctx.expr(0)))
@@ -2721,7 +2733,7 @@ class UnifiedMathVisitor(MathExprVisitor):
if a.ndim < 2 or a.shape[-1] != a.shape[-2]:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: solve expects square coefficient matrices")
float8_dtypes = self._float8_dtypes()
float8_dtypes = _FLOAT8_DTYPES
output_dtype = a.dtype if a.dtype in float8_dtypes else b.dtype if b.dtype in float8_dtypes else None
if a.dtype in float8_dtypes or not (a.is_floating_point() or a.is_complex()):
a = a.to(torch.float32)
@@ -2736,7 +2748,7 @@ class UnifiedMathVisitor(MathExprVisitor):
if val.ndim < 2:
raise ValueError(f"{ctx.start.line}:{ctx.start.column}: matrix_rank expects matrices")
original_dtype = val.dtype
if original_dtype in self._float8_dtypes() or not (val.is_floating_point() or val.is_complex()):
if original_dtype in _FLOAT8_DTYPES or not (val.is_floating_point() or val.is_complex()):
val = val.to(torch.float32)
tol = (yield ctx.expr(1)) if len(ctx.expr()) > 1 else None
if self._is_tensor(tol):
@@ -3536,7 +3548,7 @@ class UnifiedMathVisitor(MathExprVisitor):
tier = _FONT_WEIGHT_TIERS[bisect.bisect_left((150, 250, 350, 450, 550, 650, 750, 850), weight_val)]
synthetic_italic = synthetic_bold = False
variation_weight, variation_italic = weight_val, is_italic
if os.path.isfile(font_name):
if _is_allowed_font_file(font_name):
font_path = font_name
else:
font_path = _find_font(font_name, tier, weight_val, is_italic)
+1 -1
View File
@@ -264,7 +264,7 @@ INBUILT_FUNCTION_META = {
'interpolate_area': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_area(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': []},
'interpolate_nearest': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest_exact']},
'interpolate_nearest_exact': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest_exact(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest']},
'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []},
'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []},
'as_nested_tensor': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested_tensor(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested']},
'as_nested': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested_tensor']},
'svd': {'min_args': 1, 'max_args': 2, 'signature': 'svd(x, [full_matrices])', 'forms': [['x', '[full_matrices]']], 'description': 'computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)', 'details': '', 'params': {'x': 'matrix, or a batch of matrices in the last two dimensions', 'full_matrices': 'truthy to return full-size U and Vh (default: false)'}, 'aliases': ['singular_value_decomposition']},
+17 -13
View File
@@ -6,25 +6,29 @@ from torchvision.utils import flow_to_image as torchvision_flow_to_image
import numpy as np
import folder_paths
import comfy.utils
import comfy.model_management
import comfy.model_patcher
_raft_model = None
folder_paths.folder_names_and_paths.setdefault("optical_flow", ([os.path.join(folder_paths.models_dir, "optical_flow")], folder_paths.supported_pt_extensions))
def get_raft_model(device):
global _raft_model
if _raft_model is None:
# Small (~20MB) and offloaded by model management like any other model, so it is loaded once per process
_raft_patcher = None
def get_raft_model():
global _raft_patcher
if _raft_patcher is None:
names = folder_paths.get_filename_list("optical_flow")
model = raft_large(weights=None, progress=False)
model.load_state_dict(comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("optical_flow", names[0]), safe_load=True))
_raft_patcher = comfy.model_patcher.ModelPatcher(model.eval(), load_device=comfy.model_management.get_torch_device(), offload_device=comfy.model_management.unet_offload_device())
comfy.model_management.load_model_gpu(_raft_patcher)
return _raft_patcher.model
if names:
model = raft_large(weights=None, progress=False)
model.load_state_dict(comfy.utils.load_torch_file(folder_paths.get_full_path_or_raise("optical_flow", names[0]), safe_load=True))
else:
# No local checkpoint: download torchvision's RAFT weights once and keep a copy in models/optical_flow
weights = Raft_Large_Weights.DEFAULT
model = raft_large(weights=weights, progress=True)
save_dir = folder_paths.get_folder_paths("optical_flow")[0]
os.makedirs(save_dir, exist_ok=True)
torch.save(model.state_dict(), os.path.join(save_dir, os.path.basename(weights.url)))
_raft_model = model.to(device).eval()
return _raft_model
def warp(x, flow, padding_mode='reflection'):
"""
@@ -90,8 +94,8 @@ def get_optical_flow(img1, img2, tiling_size=0, iterations=12, multi_scale=False
img1, img2: [H, W, C] or [B, H, W, C]
Returns: [B, H, W, 2] flow vectors
"""
device = img1.device
model = get_raft_model(device)
model = get_raft_model()
device = comfy.model_management.get_torch_device()
img1_proc = preprocess_image(img1, device)
img2_proc = preprocess_image(img2, device)
@@ -176,7 +180,7 @@ def get_optical_flow(img1, img2, tiling_size=0, iterations=12, multi_scale=False
flow[:, 1] *= float(h) / h_new
# [B, 2, H, W] -> [B, H, W, 2]
return flow.movedim(1, -1)
return flow.movedim(1, -1).to(img1.device)
def calculate_occlusion_mask(flow):
"""
+1 -1
View File
@@ -22,4 +22,4 @@ class ScriptTextInput(io.ComfyNode):
@classmethod
def execute(cls, script):
return (remove_dead_code(parse_expr(script)),)
return (remove_dead_code(parse_expr.__wrapped__(script)),)
+1 -3
View File
@@ -69,7 +69,7 @@ class StringMathNode(io.ComfyNode):
"z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0
}
v_stacked, v_cnt = get_v_variable(V, length_mismatch="do nothing")
v_stacked, v_cnt = get_v_variable(V)
if v_stacked is not None:
variables["V"] = v_stacked
variables["Vcnt"] = float(v_cnt)
@@ -105,9 +105,7 @@ class StringMathNode(io.ComfyNode):
for start in range(0, len(s), n):
yield s[start:start+n]
stack = stack if remember_stack else copy.deepcopy(stack)
return (chunks(result,batching), stack)
else:
stack = stack if remember_stack else copy.deepcopy(stack)
return ([result], stack)
+4 -4
View File
@@ -169,10 +169,10 @@ class VideoMathNode(io.ComfyNode):
variables[v_name].is_lazy_var = True
def lazy_v_stack():
return get_v_variable(V_norm, length_mismatch=length_mismatch)[0]
return get_v_variable(V_norm)[0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
return float(get_v_variable(V_norm, length_mismatch=length_mismatch)[1])
return float(get_v_variable(V_norm)[1])
lazy_v_count.is_lazy_var = True
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
@@ -251,10 +251,10 @@ class VideoMathNode(io.ComfyNode):
variables[v_name].is_lazy_var = True
def lazy_v_stack():
return get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)[0]
return get_v_variable(V_norm_waveforms)[0]
lazy_v_stack.is_lazy_var = True
def lazy_v_count():
return float(get_v_variable(V_norm_waveforms, length_mismatch=length_mismatch)[1])
return float(get_v_variable(V_norm_waveforms)[1])
lazy_v_count.is_lazy_var = True
variables["V"] = lazy_v_stack
variables["Vcnt"] = lazy_v_count
+1 -2
View File
@@ -100,8 +100,7 @@ class NoiseExecutor:
self.z = z
self.expr = Noise
self.tree = parse_expr(Noise)
seed = -1
self.seed = next((n.seed for n in (a, b, c, d) if n is not None), 0)
def generate_noise(self, input_latent: torch.Tensor) -> torch.Tensor:
samples = input_latent["samples"]
+6 -2
View File
@@ -1,3 +1,5 @@
import functools
from antlr4.error.ErrorListener import ErrorListener
from antlr4 import InputStream, CommonTokenStream
from antlr4.atn.PredictionMode import PredictionMode
@@ -66,8 +68,10 @@ def as_tensor(value, shape):
return torch.cat(value)
# check_lazy_status and execute both parse the same expression; trees are only read, so they can be shared.
@functools.lru_cache(maxsize=64)
def parse_expr(expr: str):
"""Parse a math expression and return the parse tree."""
"""Parse a math expression and return the parse tree. The tree is shared, use parse_expr.__wrapped__ for one you modify."""
input_stream = InputStream(expr)
lexer = MathExprLexer(input_stream)
stream = CommonTokenStream(lexer)
@@ -299,7 +303,7 @@ def normalize_to_common_shape(*tensors, mode="pad"):
result.append(t.contiguous())
return tuple(result)
def get_v_variable(v_norm_dict, length_mismatch="error"):
def get_v_variable(v_norm_dict):
"""
Collects V0, V1, ... from the dict (ignoring NestedTensor component keys
like V0_0, V1_1), and returns them as a list and count.
+2 -2
View File
@@ -100,7 +100,7 @@ class BoolToIntNode(io.ComfyNode):
display_name="Bool -> Int",
category="More math/converters",
inputs=[
io.Boolean.Input(id="value", default=0),
io.Boolean.Input(id="value", default=False),
],
outputs=[
io.Int.Output(),
@@ -125,7 +125,7 @@ class IntToBoolNode(io.ComfyNode):
category="More math/converters",
display_name="Int -> bool",
inputs=[
io.Int.Input(id="value", default=0.0),
io.Int.Input(id="value", default=0),
],
outputs=[
io.Boolean.Output(),
+3 -1
View File
@@ -1,3 +1,5 @@
antlr4-python3-runtime
torch
pillow
pillow
numpy
scipy
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env python3
"""Quick test to verify bitwise operations work with tensors"""
import sys
sys.path.insert(0, r'D:\stability\Data\Packages\ComfyUI')
import torch
from custom_nodes.more_math.more_math.Parser.UnifiedMathVisitor import UnifiedMathVisitor
def test_bitwise_xor_float():
"""Test XOR with float tensors (the reported bug)"""
print("Testing bitwise XOR with float tensors...")
# Create float tensors (this was causing the error)
a = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
b = torch.tensor([4.0, 5.0, 6.0], dtype=torch.float32)
visitor = UnifiedMathVisitor({"a": a, "b": b})
try:
# This should convert to int64, perform XOR, then convert back
result = visitor._bitwise_op(a, b, torch.bitwise_xor, lambda x, y: x ^ y)
print("✓ XOR succeeded!")
print(f" Input a (float32): {a}")
print(f" Input b (float32): {b}")
print(f" Result: {result}")
print(f" Result dtype: {result.dtype}")
return True
except Exception as e:
print(f"✗ XOR failed: {e}")
return False
def test_bitwise_and_float():
"""Test AND with float tensors"""
print("\nTesting bitwise AND with float tensors...")
a = torch.tensor([15.0, 14.0, 13.0], dtype=torch.float32)
b = torch.tensor([7.0, 3.0, 1.0], dtype=torch.float32)
visitor = UnifiedMathVisitor({"a": a, "b": b})
try:
result = visitor._bitwise_op(a, b, torch.bitwise_and, lambda x, y: x & y)
print("✓ AND succeeded!")
print(f" Input a (float32): {a}")
print(f" Input b (float32): {b}")
print(f" Result: {result}")
print(f" Result dtype: {result.dtype}")
return True
except Exception as e:
print(f"✗ AND failed: {e}")
return False
def test_bitwise_or_float():
"""Test OR with float tensors"""
print("\nTesting bitwise OR with float tensors...")
a = torch.tensor([15.0, 14.0, 13.0], dtype=torch.float32)
b = torch.tensor([7.0, 3.0, 1.0], dtype=torch.float32)
visitor = UnifiedMathVisitor({"a": a, "b": b})
try:
result = visitor._bitwise_op(a, b, torch.bitwise_or, lambda x, y: x | y)
print("✓ OR succeeded!")
print(f" Input a (float32): {a}")
print(f" Input b (float32): {b}")
print(f" Result: {result}")
print(f" Result dtype: {result.dtype}")
return True
except Exception as e:
print(f"✗ OR failed: {e}")
return False
def test_bitwise_not_float():
"""Test NOT with float tensors"""
print("\nTesting bitwise NOT with float tensors...")
a = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
visitor = UnifiedMathVisitor({"a": a})
try:
result = visitor._bitwise_not(a)
print("✓ NOT succeeded!")
print(f" Input a (float32): {a}")
print(f" Result: {result}")
print(f" Result dtype: {result.dtype}")
return True
except Exception as e:
print(f"✗ NOT failed: {e}")
return False
def test_int_tensors_preserved():
"""Ensure int tensors still work as before"""
print("\nTesting that int tensor dtypes are preserved...")
a = torch.tensor([15, 14, 13], dtype=torch.int16)
b = torch.tensor([7, 3, 1], dtype=torch.int16)
visitor = UnifiedMathVisitor({"a": a, "b": b})
try:
result = visitor._bitwise_op(a, b, torch.bitwise_and, lambda x, y: x & y)
assert result.dtype == torch.int16, f"Expected int16, got {result.dtype}"
print("✓ Int16 dtype preserved!")
print(f" Input a (int16): {a}")
print(f" Input b (int16): {b}")
print(f" Result (int16): {result}")
return True
except Exception as e:
print(f"✗ Int16 test failed: {e}")
return False
if __name__ == "__main__":
print("=" * 70)
print("Bitwise Operations Fix Verification")
print("=" * 70)
results = [
test_bitwise_xor_float(),
test_bitwise_and_float(),
test_bitwise_or_float(),
test_bitwise_not_float(),
test_int_tensors_preserved(),
]
print("\n" + "=" * 70)
if all(results):
print("✓ All tests passed!")
else:
print("✗ Some tests failed")
sys.exit(1)
print("=" * 70)
-170
View File
@@ -1,170 +0,0 @@
#!/usr/bin/env python3
"""
Test bitwise shift operators and bit count function
"""
import torch
import sys
sys.path.insert(0, 'custom_nodes/more_math')
from more_math.Parser.MathExprParser import MathExprParser
from more_math.Parser.UnifiedMathVisitor import UnifiedMathVisitor
from antlr4 import InputStream, CommonTokenFactory
def parse_and_evaluate(expression, variables=None):
"""Parse and evaluate a math expression"""
if variables is None:
variables = {}
input_stream = InputStream(expression)
lexer = MathExprParser(input_stream).lexer
stream = CommonTokenFactory()
parser = MathExprParser(input_stream)
tree = parser.start()
visitor = UnifiedMathVisitor(variables, device='cpu')
result = visitor.visit(tree)
return result
def test_bit_shifts():
"""Test bitwise shift operators"""
print("=" * 60)
print("Testing Bitwise Shift Operators")
print("=" * 60)
test_cases = [
# Left shift: 5 << 2 = 20 (0101 << 2 = 10100)
("5 << 2", {}, 20),
# Right shift: 20 >> 2 = 5 (10100 >> 2 = 0101)
("20 >> 2", {}, 5),
# Left shift with variable
("x << 3", {"x": 4}, 32), # 4 << 3 = 32
# Right shift with variable
("x >> 2", {"x": 16}, 4), # 16 >> 2 = 4
# Chained shifts
("(8 << 2) >> 3", {}, 4), # (32) >> 3 = 4
]
for expr, vars, expected in test_cases:
try:
result = parse_and_evaluate(expr, vars)
status = "✓" if result == expected else "✗"
print(f"{status} {expr:30} = {result:10} (expected {expected})")
except Exception as e:
print(f"✗ {expr:30} ERROR: {e}")
def test_bit_count():
"""Test bit count function"""
print("\n" + "=" * 60)
print("Testing Bitwise Bit Count Function")
print("=" * 60)
test_cases = [
# bitcount(5) = 2 (0101 has 2 set bits)
("bitcount(5)", {}, 2),
# bitcount(15) = 4 (1111 has 4 set bits)
("bitcount(15)", {}, 4),
# bitcount(7) = 3 (111 has 3 set bits)
("bitcount(7)", {}, 3),
# bitcount(255) = 8 (11111111 has 8 set bits)
("bitcount(255)", {}, 8),
# bitcount(0) = 0
("bitcount(0)", {}, 0),
# With variable
("bitcount(x)", {"x": 31}, 5), # 31 = 11111 = 5 bits set
]
for expr, vars, expected in test_cases:
try:
result = parse_and_evaluate(expr, vars)
status = "✓" if result == expected else "✗"
print(f"{status} {expr:30} = {result:10} (expected {expected})")
except Exception as e:
print(f"✗ {expr:30} ERROR: {e}")
def test_bit_shifts_with_tensors():
"""Test bitwise shifts with tensors"""
print("\n" + "=" * 60)
print("Testing Bitwise Shifts with Tensors")
print("=" * 60)
vars = {
"a": torch.tensor([1, 2, 4, 8], dtype=torch.int32),
"shift": 2,
}
try:
result = parse_and_evaluate("a << shift", vars)
expected = torch.tensor([4, 8, 16, 32], dtype=torch.int32)
match = torch.equal(result, expected)
status = "✓" if match else "✗"
print(f"{status} tensor_shift_left: [1,2,4,8] << 2 = {result.tolist()}")
except Exception as e:
print(f"✗ tensor_shift_left ERROR: {e}")
try:
result = parse_and_evaluate("a >> shift", vars)
expected = torch.tensor([0, 0, 1, 2], dtype=torch.int32)
match = torch.equal(result, expected)
status = "✓" if match else "✗"
print(f"{status} tensor_shift_right: [1,2,4,8] >> 2 = {result.tolist()}")
except Exception as e:
print(f"✗ tensor_shift_right ERROR: {e}")
def test_bit_count_with_tensors():
"""Test bit count with tensors"""
print("\n" + "=" * 60)
print("Testing Bit Count with Tensors")
print("=" * 60)
vars = {
"nums": torch.tensor([5, 15, 7, 255], dtype=torch.int32),
}
try:
result = parse_and_evaluate("bitcount(nums)", vars)
# Expected: [2, 4, 3, 8] set bits
print(f"✓ bitcount([5,15,7,255]): {result}")
except Exception as e:
print(f"✗ bitcount_tensor ERROR: {e}")
def test_combinations():
"""Test combinations of shift and bit count"""
print("\n" + "=" * 60)
print("Testing Combinations")
print("=" * 60)
test_cases = [
# Shift then count bits
("bitcount(5 << 2)", {}, 2), # 5 << 2 = 20 (10100) = 2 bits
# Combined with other operators
("(5 << 2) | (3 << 4)", {}, 0xCC), # 20 | 48 = 0xCC = 204
]
for expr, vars, expected in test_cases:
try:
result = parse_and_evaluate(expr, vars)
status = "✓" if result == expected else "✗"
print(f"{status} {expr:35} = {result:10} (expected {expected})")
except Exception as e:
print(f"✗ {expr:35} ERROR: {e}")
if __name__ == "__main__":
test_bit_shifts()
test_bit_count()
test_bit_shifts_with_tensors()
test_bit_count_with_tensors()
test_combinations()
print("\n" + "=" * 60)
print("All tests completed!")
print("=" * 60)
+2 -2
View File
@@ -9,7 +9,7 @@ from more_math.helper_functions import parse_expr, checkLazyNew
def run(script, variables=None, optimize=True, stack=None):
tree = parse_expr(script)
tree = parse_expr.__wrapped__(script)
if optimize:
remove_dead_code(tree)
out = io.StringIO()
@@ -109,5 +109,5 @@ def test_last_statement_of_a_block_stays():
def test_unused_inputs_are_no_longer_requested():
tree = remove_dead_code(parse_expr("u = V1 * 2; V0"))
tree = remove_dead_code(parse_expr.__wrapped__("u = V1 * 2; V0"))
assert checkLazyNew(tree, {"V0": None, "V1": None}, {}) == {"V0"}
+10
View File
@@ -102,6 +102,16 @@ class TestMathGuider(unittest.TestCase):
# At step 2 with 2 steps: 2 / 2 = 1.0
self.assertTrue(torch.allclose(res2, torch.tensor(1.0)))
def test_math_guider_batched_sigma_and_substeps(self):
# Samplers pass sigma with one entry per batch item; heun-like samplers also call the model between sigmas
math_guider = MathGuider({"V0": MockGuider(1.0)}, {}, "current_step + sigma * 0", "V0", {})
math_guider.sigmas = torch.tensor([10.0, 5.0, 0.0])
math_guider.steps = 2
x = torch.zeros((2, 1, 1, 1))
self.assertTrue(torch.allclose(math_guider(x, torch.tensor([5.0, 5.0])), torch.tensor(1.0)))
self.assertTrue(torch.allclose(math_guider(x, torch.tensor([7.0, 7.0])), torch.tensor(0.0)))
self.assertTrue(torch.allclose(math_guider(x, torch.tensor([10.0, 10.0])), torch.tensor(0.0)))
def test_math_guider_crop_function(self):
"""Test crop function within guider context"""
g0 = MockGuider(1.0)
+2
View File
@@ -360,6 +360,7 @@ def test_noise_math_5d():
class MockNoise:
def __init__(self, tensor):
self.tensor = tensor
self.seed = 0
def generate_noise(self, input_latent):
return self.tensor
@@ -382,6 +383,7 @@ def test_noise_math_autogrow():
class MockNoise:
def __init__(self, tensor):
self.tensor = tensor
self.seed = 0
def generate_noise(self, input_latent):
return self.tensor
+35 -6
View File
@@ -9,6 +9,9 @@ from PIL import ImageFont
from more_math.Parser.UnifiedMathVisitor import ChangeTrackingDict, MathDict, UnifiedMathVisitor
from more_math.helper_functions import checkLazyNew, parse_expr
from more_math.FloatMathNode import FloatMathNode
from more_math.ImageMathNode import ImageMathNode
from more_math.LatentMathNode import LatentMathNode
from more_math.MaskMathNode import MaskMathNode
def _visit(expression, variables):
@@ -80,17 +83,12 @@ def test_lambda_calls_require_exact_argument_count(call):
def test_text_image_size_expression_is_evaluated_once(monkeypatch):
visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor")
default_font = ImageFont.load_default()
original_isfile = os.path.isfile
monkeypatch.setattr(
visitor_module.ImageFont,
"truetype",
lambda *args, **kwargs: default_font,
)
monkeypatch.setattr(
visitor_module.os.path,
"isfile",
lambda path: path == "mock-font" or original_isfile(path),
)
monkeypatch.setattr(visitor_module, "_is_allowed_font_file", lambda path: path == "mock-font")
values = MathDict()
_visit(
@@ -101,6 +99,15 @@ def test_text_image_size_expression_is_evaluated_once(monkeypatch):
assert len(values) == 1
def test_text_image_rejects_font_path_outside_font_folders(monkeypatch):
visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor")
monkeypatch.setattr(visitor_module, "_find_font", lambda *args: None)
path = os.path.abspath(__file__).replace("\\", "/")
with pytest.raises(FileNotFoundError):
_visit(f'text_image("x", "{path}", 12)', {})
def test_stack_list_of_tensors_with_negative_dimension():
first = torch.tensor([1.0, 2.0])
second = torch.tensor([3.0, 4.0])
@@ -280,6 +287,28 @@ def test_unconnected_stack_is_not_shared_between_runs():
assert stack == {0: [1.0]}
@pytest.mark.parametrize("node, value, extra", [
(ImageMathNode, torch.zeros(2, 4, 4, 3), {"use_compute_device": False}),
(MaskMathNode, torch.zeros(2, 4, 4), {}),
(LatentMathNode, {"samples": torch.zeros(2, 4, 4, 4)}, {"use_compute_device": False}),
])
@pytest.mark.parametrize("batching", [0, 1])
def test_stack_across_list_runs(node, value, extra, batching):
# ComfyUI runs the node once per list item and passes the same stack object every time
def run(stack, remember):
return node.execute(V={"V0": value}, F={}, Expression="stack_push(0, 1); V0", batching=batching, remember_stack=remember, stack=stack, **extra)[-1]
shared = {}
run(shared, True)
assert run(shared, True) == {0: [1.0, 1.0]}
fresh = {}
first, second = run(fresh, False), run(fresh, False)
assert fresh == {}
assert first == second == {0: [1.0]}
assert first is not second
def test_top_level_return_stops_script():
assert _visit("x = 1; return 5; x = 2; x", {}) == 5
+1 -1
View File
@@ -267,7 +267,7 @@ export const FUNCTION_META = {
"interpolate_area": { minArgs: 2, maxArgs: 2, signature: "interpolate_area(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: [] },
"interpolate_nearest": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest_exact"] },
"interpolate_nearest_exact": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest_exact(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest"] },
"text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] },
"text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] },
"as_nested_tensor": { minArgs: 1, maxArgs: 1, signature: "as_nested_tensor(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested"] },
"as_nested": { minArgs: 1, maxArgs: 1, signature: "as_nested(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested_tensor"] },
"svd": { minArgs: 1, maxArgs: 2, signature: "svd(x, [full_matrices])", forms: [["x", "[full_matrices]"]], description: "computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)", details: "", params: {"x": "matrix, or a batch of matrices in the last two dimensions", "full_matrices": "truthy to return full-size U and Vh (default: false)"}, aliases: ["singular_value_decomposition"] },