- 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.
Replace two calibration nodes with a single smart LCSLoadData that
fingerprints VAE weights (sha256 of first decoder weight) and
auto-manages cached calibration files as data/lcs_{fingerprint}.safetensors.
Frontend JS syncs preset values into sliders on user interaction,
but headless runners (e.g. Lumi-Batcher) skip the frontend entirely.
When a preset is selected but all sliders are still at defaults,
execute() now applies the preset values server-side as a fallback.
The warm_dir was a unit vector (norm=1) but LCS chroma vectors have
norms ~40-46, so color_temperature=0.5 only shifted chroma by 1.2%.
Scale warm_dir by mean anchor chroma magnitude so the shift is
proportional. Slider range narrowed from ±2.0 to ±1.0 and preset
values adjusted accordingly (Warm=0.15, Cool=-0.15, Cinematic=0.05).
Selecting a preset now updates slider values in real-time via a
frontend JS extension, and users can tweak values after selecting.
Sliders are always visible (removed optional=True). Preset values
live only in JS (single source of truth); Python keeps just the
name list for the combo widget. Sync only fires on explicit user
dropdown interaction, preserving saved slider tweaks on workflow load.
Float equality (!=) can fail after JS→JSON→Python round-trips on
slider values. Switch to epsilon-based _is_default_tone() helper.
Also precompute the warm/cool direction vector once at closure-build
time instead of recomputing it on every denoising step.
The node now controls contrast, brightness, saturation, and color
temperature — "Contrast Adjust" was misleading. Adds 9 one-click
tonal presets (Cinematic, HDR, Vivid, Dramatic, Low Key, High Key,
Warm, Cool, Desaturated) plus Custom mode for manual slider control.
Shifts chroma along the warm (Red+Yellow) ↔ cool (Blue+Cyan) axis
via additive offset before saturation scaling, so saturation=0 still
yields grayscale. Warm direction is derived from chromatic projections
of the 6 hue anchors (stripping their lightness component).
l_scalar (lightness projection onto achromatic axis) can naturally
extend beyond [0,1] during denoising. Clamping destroys highlight/shadow
detail — directly contradicting the purpose of contrast adjustment.
Type I color intervention already operates without clamping LCS coords.
The no-op skip in execute() ensures default params have zero effect.
Operates directly in 3D LCS space by decomposing patches into lightness
(projection onto black→white achromatic axis) and chroma (perpendicular
residual). No HSL round-trip needed, making it faster than the color
intervention path.