Codebase review: stability, performance, usability, and i18n fixes
Stability: - Fix race condition: set _is_running before create_task in target processors - Await probe task after cancel in wled_target_processor - Replace raw fetch() with fetchWithAuth() across devices, kc-targets, pattern-templates - Add try/catch to showTestTemplateModal in streams.js - Wrap blocking I/O in asyncio.to_thread (picture_targets, system restore) - Fix dashboardStopAll to filter only running targets with ok guard Performance: - Vectorize fire effect spark loop with numpy in effect_stream - Vectorize FFT band binning with cumulative sum in analysis.py - Rewrite pixel_processor with vectorized numpy (accept ndarray or list) - Add httpx.AsyncClient connection pooling with lock in wled_provider - Optimize _send_pixels_http to avoid np.hstack allocation in wled_client - Mutate chart arrays in-place in dashboard, perf-charts, targets - Merge dashboard 2-batch fetch into single Promise.all - Hoist frame_time outside loop in mapped_stream Usability: - Fix health check interval load/save in device settings - Swap confirm modal button classes (No=secondary, Yes=danger) - Add aria-modal to audio/value source editors, fix close button aria-labels - Add modal footer close button to settings modal - Add dedicated calibration LED count validation error keys i18n: - Replace ~50 hardcoded English strings with t() calls across 12 JS files - Add 50 new keys to en.json, ru.json, zh.json - Localize inline toasts in index.html with window.t fallback - Add data-i18n to command palette footer - Add localization policy to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
"""Pixel processing utilities for color correction and manipulation."""
|
||||
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Union
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ColorList = Union[List[Tuple[int, int, int]], np.ndarray]
|
||||
|
||||
|
||||
def _as_array(colors: ColorList) -> np.ndarray:
|
||||
"""Convert list-of-tuples to (N,3) uint8 array, or pass through if already ndarray."""
|
||||
if isinstance(colors, np.ndarray):
|
||||
return colors
|
||||
return np.array(colors, dtype=np.uint8)
|
||||
|
||||
|
||||
def smooth_colors(
|
||||
current_colors: List[Tuple[int, int, int]],
|
||||
previous_colors: List[Tuple[int, int, int]],
|
||||
current_colors: ColorList,
|
||||
previous_colors: ColorList,
|
||||
smoothing_factor: float = 0.5,
|
||||
) -> List[Tuple[int, int, int]]:
|
||||
) -> np.ndarray:
|
||||
"""Smooth color transitions between frames.
|
||||
|
||||
Args:
|
||||
@@ -21,96 +30,71 @@ def smooth_colors(
|
||||
smoothing_factor: Smoothing amount (0.0-1.0, where 0=no smoothing, 1=full smoothing)
|
||||
|
||||
Returns:
|
||||
Smoothed colors
|
||||
Smoothed colors as (N,3) uint8 ndarray
|
||||
"""
|
||||
if not current_colors or not previous_colors:
|
||||
return current_colors
|
||||
if not len(current_colors) or not len(previous_colors):
|
||||
return _as_array(current_colors)
|
||||
|
||||
if len(current_colors) != len(previous_colors):
|
||||
logger.warning(
|
||||
f"Color count mismatch: current={len(current_colors)}, "
|
||||
f"previous={len(previous_colors)}. Skipping smoothing."
|
||||
)
|
||||
return current_colors
|
||||
return _as_array(current_colors)
|
||||
|
||||
if smoothing_factor <= 0:
|
||||
return current_colors
|
||||
return _as_array(current_colors)
|
||||
if smoothing_factor >= 1:
|
||||
return previous_colors
|
||||
return _as_array(previous_colors)
|
||||
|
||||
# Convert to numpy arrays
|
||||
current = np.array(current_colors, dtype=np.float32)
|
||||
previous = np.array(previous_colors, dtype=np.float32)
|
||||
|
||||
# Blend between current and previous
|
||||
current = np.asarray(current_colors, dtype=np.float32)
|
||||
previous = np.asarray(previous_colors, dtype=np.float32)
|
||||
smoothed = current * (1 - smoothing_factor) + previous * smoothing_factor
|
||||
|
||||
# Convert back to integers
|
||||
smoothed = np.clip(smoothed, 0, 255).astype(np.uint8)
|
||||
|
||||
return [tuple(color) for color in smoothed]
|
||||
return np.clip(smoothed, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def adjust_brightness_global(
|
||||
colors: List[Tuple[int, int, int]],
|
||||
colors: ColorList,
|
||||
target_brightness: int,
|
||||
) -> List[Tuple[int, int, int]]:
|
||||
) -> np.ndarray:
|
||||
"""Adjust colors to achieve target global brightness.
|
||||
|
||||
Args:
|
||||
colors: List of (R, G, B) tuples
|
||||
colors: List of (R, G, B) tuples or (N,3) ndarray
|
||||
target_brightness: Target brightness (0-255)
|
||||
|
||||
Returns:
|
||||
Adjusted colors
|
||||
Adjusted colors as (N,3) uint8 ndarray
|
||||
"""
|
||||
if not colors or target_brightness == 255:
|
||||
return colors
|
||||
arr = _as_array(colors)
|
||||
if not len(arr) or target_brightness == 255:
|
||||
return arr
|
||||
|
||||
# Calculate scaling factor
|
||||
scale = target_brightness / 255.0
|
||||
|
||||
# Scale all colors
|
||||
scaled = [
|
||||
(
|
||||
int(r * scale),
|
||||
int(g * scale),
|
||||
int(b * scale),
|
||||
)
|
||||
for r, g, b in colors
|
||||
]
|
||||
|
||||
return scaled
|
||||
return (arr.astype(np.float32) * scale).astype(np.uint8)
|
||||
|
||||
|
||||
def limit_brightness(
|
||||
colors: List[Tuple[int, int, int]],
|
||||
colors: ColorList,
|
||||
max_brightness: int = 255,
|
||||
) -> List[Tuple[int, int, int]]:
|
||||
) -> np.ndarray:
|
||||
"""Limit maximum brightness of any color channel.
|
||||
|
||||
Args:
|
||||
colors: List of (R, G, B) tuples
|
||||
colors: List of (R, G, B) tuples or (N,3) ndarray
|
||||
max_brightness: Maximum allowed brightness (0-255)
|
||||
|
||||
Returns:
|
||||
Limited colors
|
||||
Limited colors as (N,3) uint8 ndarray
|
||||
"""
|
||||
if not colors or max_brightness == 255:
|
||||
return colors
|
||||
arr = _as_array(colors)
|
||||
if not len(arr) or max_brightness == 255:
|
||||
return arr
|
||||
|
||||
limited = []
|
||||
for r, g, b in colors:
|
||||
# Find max channel value
|
||||
max_val = max(r, g, b)
|
||||
|
||||
if max_val > max_brightness:
|
||||
# Scale down proportionally
|
||||
scale = max_brightness / max_val
|
||||
r = int(r * scale)
|
||||
g = int(g * scale)
|
||||
b = int(b * scale)
|
||||
|
||||
limited.append((r, g, b))
|
||||
|
||||
return limited
|
||||
arr_f = arr.astype(np.float32)
|
||||
max_vals = np.max(arr_f, axis=1)
|
||||
need_scale = max_vals > max_brightness
|
||||
if need_scale.any():
|
||||
scales = np.where(need_scale, max_brightness / np.maximum(max_vals, 1.0), 1.0)
|
||||
arr_f *= scales[:, np.newaxis]
|
||||
return arr_f.astype(np.uint8)
|
||||
|
||||
Reference in New Issue
Block a user