LTXV can produce latents with odd H or W (e.g. 11x20) which can't be
divided into 2x2 patches. Patchify now pads odd dims to even using
replicate padding before rearranging, and unpatchify crops back to the
original size. This keeps patch_size=2 consistent between calibration
and inference for all models.
Replaces hardcoded SCALE_FACTOR/SHIFT_FACTOR with model.latent_format.process_in/out
so LCS works with any model (FLUX, LTXV, SD, etc). Adds LTXAV packed tensor
unpack/repack support for audio+video models. All hooks (color, tone, sharpness,
observer) now use the model-aware conversion from args["model"].
LTXAV uses a flattened 1D latent layout (e.g. [1, 1, 466048]) where
spatial H/W < 2, making 2x2 patchification impossible. patchify() now
returns None for such formats, and all hooks skip intervention cleanly.
Video VAEs output [B, C, T, H, W]. Patchify now merges T into batch,
processes all frames, and unpatchify restores the 5D shape.
Updated all callers to pass extra_shape through.
Video VAEs like Wan don't support batch encoding — input B images
produces only 1 latent. Detect this by comparing output batch size
to input, and fall back to encoding images one by one.
Fixes both color calibration and sharpness calibration.
- Precompute edit_vec = (strength * sign) * dc_removed_pc1_dir once in
closure instead of recomputing pc1_dir mean removal every denoising step
- Remove SharpnessData.pc1_std (stored but never used since dropping
pc1_std multiplier)
- Blur single-channel gray noise, expand to 3-channel per-batch instead
of allocating full 201 MB 3-channel copy upfront
- Remove unused LCSData import
256 vs 512 difference (97.5% vs 97.3%) is within random variation —
different image_size consumes different random numbers from the same
seed, producing different images rather than a controlled comparison.
Keep 512 consistent with LCS color calibration. Remove per-blur-level
mean logging that was used during development.
256px gives the best PC1 (97.5%) and fastest calibration.
Larger images average more patches, reducing per-sample variance
but not improving PCA purity. Added image_size input to
LCSSharpnessCalibrate for experimentation.
RGB noise has independent per-channel variation that blur affects
differently, introducing inter-channel variance captured as PC2.
Grayscale noise (same value across RGB) ensures blur affects all
channels identically, raising PC1 from 95.8% to 96.7%.
Also added per-vector zero-mean to remove VAE brightness drift
and diagnostic logging for per-blur-level latent means.
pc1_std=12.78 represents the spread across blur levels 0-16 during
calibration, producing edits ~34x larger than the patch mean. Since PCA
basis vectors are already unit norm, strength directly controls the
L2 magnitude of the edit. Widen UI range to -5..5 to compensate.
Root cause of color shift and mosaic artifacts:
1. delta = strength * pc1_std was too large (pc1_std=12.78)
2. Removing residual when using LCS destroyed 62/64 dimensions → mosaic
3. Complex projection/residual/color-removal logic was unnecessary
The correct approach is simple: patches + delta * pc1_dir.
Adding along one direction preserves all other dimensions by construction.
LCS color removal only matters at calibration time (makes pc1_dir orthogonal
to color). At intervention time, just add along the orthogonal direction.
Removed lcs_data input from LCSSharpnessIntervene (only needed at calibration).
Removed all debug logging.
When calibration used LCS color removal, the intervention must also
remove the color component before projecting onto the sharpness basis,
then add it back after reconstruction. This ensures sharpness edits
don't affect color.
Workflow:
1. LCSSharpnessCalibrate(vae, lcs_data) → sharpness_data
2. LCSSharpnessIntervene(model, sharpness_data, strength, lcs_data=lcs_data)
Both nodes now have optional lcs_data input. When connected, color is
preserved during sharpness intervention.
When LCS color data is connected, the sharpness PC1 will be orthogonal
to the color subspace, preventing color shifts during intervention.
Changes:
- calibrate_sharpness() now accepts optional lcs_data parameter
- LCSSharpnessCalibrate node has optional lcs_data input
- Cache files use "_lcs" suffix when LCS is used (different subspace)
- Generate base images once upfront, then apply all blur levels to the
SAME images (was generating different random images per blur level)
- Add separable convolution for kernel_size > 15 (O(2k) vs O(k²))
- Add kernel caching to avoid recomputation
- Add per-blur-level progress logging
This matches SubspaceLab's methodology and ensures PCA receives
properly paired stimuli (same image × multiple blur levels).
New calibration (core/sharpness.py) generates blur stimuli, VAE-encodes,
and extracts PC1 as the sharpness direction. New nodes (nodes/sharpen.py)
provide LCSSharpnessCalibrate (auto-cached per-VAE) and
LCSSharpnessIntervene (post-CFG hook with strength, step window, mask).
Positive strength = sharper, negative = blurrier.
- Print calibration parameters at start
- Show encoding progress with batch count
- Print PCA results (variance explained per component)
- Show anchor color encoding status
- Display final basis shape and anchor coordinates
- Add _wrap_hue_diff() and _hue_lerp() to core/color_space.py for
reuse across the codebase
- Remove duplicate _hue_lerp from nodes/intervene.py, import from core
- Update diagnostics.py to use shared _hue_lerp instead of inline logic
- Move input_var computation outside strength loop in test_type_ii_uniformity
- Move get_alpha_beta_t50() outside sigma loop in test_early_timestep_amplification
- Extract repeated bicone factor formula (1 - |2L - 1|) into _bicone_factor()
helper function, used in 4 places across color_space.py
- Optimize test_type_ii_uniformity: move decode outside strength loop
- Add named constants for test parameters in diagnostics.py
- Remove unused imports and variables
The chromatic anchors (R,G,B,C,M,Y) have different lightness levels
(L=0.38-0.63), so their observed chroma radii are already scaled by
the bicone factor (1 - |2L - 1|). The old code double-counted this
factor, causing round-trip errors of 10-12 units and color distortion.
Fix: In _hue_to_chroma_vector, normalize anchor radii to equatorial
(radius at L=0.5) before interpolation. During encode/decode, apply
bicone factor at target lightness. This ensures proper round-trip:
- encode(lcs) → hsl → encode(hsl) ≈ lcs with error < 1e-5
- Saturation now correctly returns 1.0 for saturated anchor colors
Also add diagnostics module for blurriness analysis:
- test_round_trip_consistency: verify bicone math
- test_normalization_stability: check timestep amplification
- test_type_ii_uniformity: measure variance loss at different strengths
- analyze_blurriness_causes: comprehensive diagnostic report
Exact float comparison (sigmas == sigma_val) fails when the sigma
tensor is bfloat16 (FLUX default) but sample_sigmas is float32,
causing intervention hooks to match wrong steps or not fire at all.
Replace with torch.isclose after casting to float32, with argmin
fallback. Both color and tone hooks now share _find_step_index.
Deduplicate the Gram-Schmidt basis construction that was copy-pasted
in decode_lcs_to_hsl, encode_hsl_to_lcs, and calibration.py. Extract
anchor hue values into a module-level constant to prevent silent
divergence. Also remove unnecessary tensor clone in _angle_to_hue and
use torch.empty instead of torch.zeros where all elements are overwritten.
Replace _hue_to_polygon_point with _hue_to_chroma_vector to fix
Type II and interpolated color intervention modes. The old function
interpolated raw 3D anchor positions (mixing lightness into chroma
directions) and ignored calibrated anchor angles. The new function
projects anchors onto the plane perpendicular to the achromatic axis
to get pure chroma radii, then interpolates radius and angle in the
same segment structure as _angle_to_hue for round-trip consistency.