CSS: add led_count field; calibration dialog improvements; color corrections collapsible section

- Add explicit led_count to PictureColorStripSource (0 = auto from calibration)
- Stream pads with black or truncates to match led_count exactly
- Calibration dialog: show led_count input above visual editor in CSS mode
- Calibration dialog: pre-populate led_count with effective count (cal sum) when stored value is 0
- Calibration dialog: sync preview label live as led_count input changes
- CSS editor: group brightness/saturation/gamma into collapsible "Color Corrections" section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 16:42:32 +03:00
parent 7de3546b14
commit a3aeafef13
14 changed files with 173 additions and 47 deletions

View File

@@ -51,6 +51,7 @@ def _css_to_response(source) -> ColorStripSourceResponse:
gamma=getattr(source, "gamma", None), gamma=getattr(source, "gamma", None),
smoothing=getattr(source, "smoothing", None), smoothing=getattr(source, "smoothing", None),
interpolation_mode=getattr(source, "interpolation_mode", None), interpolation_mode=getattr(source, "interpolation_mode", None),
led_count=getattr(source, "led_count", 0),
calibration=calibration, calibration=calibration,
description=source.description, description=source.description,
created_at=source.created_at, created_at=source.created_at,
@@ -93,6 +94,7 @@ async def create_color_strip_source(
gamma=data.gamma, gamma=data.gamma,
smoothing=data.smoothing, smoothing=data.smoothing,
interpolation_mode=data.interpolation_mode, interpolation_mode=data.interpolation_mode,
led_count=data.led_count,
calibration=calibration, calibration=calibration,
description=data.description, description=data.description,
) )
@@ -143,6 +145,7 @@ async def update_color_strip_source(
gamma=data.gamma, gamma=data.gamma,
smoothing=data.smoothing, smoothing=data.smoothing,
interpolation_mode=data.interpolation_mode, interpolation_mode=data.interpolation_mode,
led_count=data.led_count,
calibration=calibration, calibration=calibration,
description=data.description, description=data.description,
) )

View File

@@ -20,6 +20,7 @@ class ColorStripSourceCreate(BaseModel):
gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0) gamma: float = Field(default=1.0, description="Gamma correction (1.0=none, <1=brighter, >1=darker mids)", ge=0.1, le=3.0)
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0) smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0=none, 1.0=full)", ge=0.0, le=1.0)
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)") interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
led_count: int = Field(default=0, description="Total LED count (0 = auto from calibration)", ge=0)
calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)") calibration: Optional[Calibration] = Field(None, description="LED calibration (position and count per edge)")
description: Optional[str] = Field(None, description="Optional description", max_length=500) description: Optional[str] = Field(None, description="Optional description", max_length=500)
@@ -35,6 +36,7 @@ class ColorStripSourceUpdate(BaseModel):
gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0) gamma: Optional[float] = Field(None, description="Gamma correction (0.1-3.0)", ge=0.1, le=3.0)
smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0) smoothing: Optional[float] = Field(None, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)") interpolation_mode: Optional[str] = Field(None, description="Interpolation mode (average, median, dominant)")
led_count: Optional[int] = Field(None, description="Total LED count (0 = auto from calibration)", ge=0)
calibration: Optional[Calibration] = Field(None, description="LED calibration") calibration: Optional[Calibration] = Field(None, description="LED calibration")
description: Optional[str] = Field(None, description="Optional description", max_length=500) description: Optional[str] = Field(None, description="Optional description", max_length=500)
@@ -52,6 +54,7 @@ class ColorStripSourceResponse(BaseModel):
gamma: Optional[float] = Field(None, description="Gamma correction") gamma: Optional[float] = Field(None, description="Gamma correction")
smoothing: Optional[float] = Field(None, description="Temporal smoothing") smoothing: Optional[float] = Field(None, description="Temporal smoothing")
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode") interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
led_count: int = Field(0, description="Total LED count (0 = auto from calibration)")
calibration: Optional[Calibration] = Field(None, description="LED calibration") calibration: Optional[Calibration] = Field(None, description="LED calibration")
description: Optional[str] = Field(None, description="Description") description: Optional[str] = Field(None, description="Description")
created_at: datetime = Field(description="Creation timestamp") created_at: datetime = Field(description="Creation timestamp")

View File

@@ -142,7 +142,8 @@ class PictureColorStripStream(ColorStripStream):
self._pixel_mapper = PixelMapper( self._pixel_mapper = PixelMapper(
self._calibration, interpolation_mode=self._interpolation_mode self._calibration, interpolation_mode=self._interpolation_mode
) )
self._led_count: int = self._calibration.get_total_leds() cal_leds = self._calibration.get_total_leds()
self._led_count: int = source.led_count if source.led_count > 0 else cal_leds
self._gamma_lut: np.ndarray = _build_gamma_lut(self._gamma) self._gamma_lut: np.ndarray = _build_gamma_lut(self._gamma)
# Thread-safe color cache # Thread-safe color cache
@@ -224,10 +225,12 @@ class PictureColorStripStream(ColorStripStream):
if ( if (
source.interpolation_mode != self._interpolation_mode source.interpolation_mode != self._interpolation_mode
or source.calibration != self._calibration or source.calibration != self._calibration
or source.led_count != self._led_count
): ):
self._interpolation_mode = source.interpolation_mode self._interpolation_mode = source.interpolation_mode
self._calibration = source.calibration self._calibration = source.calibration
self._led_count = source.calibration.get_total_leds() cal_leds = source.calibration.get_total_leds()
self._led_count = source.led_count if source.led_count > 0 else cal_leds
self._pixel_mapper = PixelMapper( self._pixel_mapper = PixelMapper(
source.calibration, interpolation_mode=source.interpolation_mode source.calibration, interpolation_mode=source.interpolation_mode
) )
@@ -263,6 +266,15 @@ class PictureColorStripStream(ColorStripStream):
led_colors = self._pixel_mapper.map_border_to_leds(border_pixels) led_colors = self._pixel_mapper.map_border_to_leds(border_pixels)
t2 = time.perf_counter() t2 = time.perf_counter()
# Pad or truncate to match the declared led_count
target_count = self._led_count
if target_count > 0 and len(led_colors) != target_count:
if len(led_colors) < target_count:
pad = np.zeros((target_count - len(led_colors), 3), dtype=np.uint8)
led_colors = np.concatenate([led_colors, pad])
else:
led_colors = led_colors[:target_count]
# Temporal smoothing # Temporal smoothing
smoothing = self._smoothing smoothing = self._smoothing
if ( if (

View File

@@ -83,6 +83,7 @@
color: #FFC107; color: #FFC107;
} }
.inputs-dimmed .edge-led-input { .inputs-dimmed .edge-led-input {
opacity: 0.2; opacity: 0.2;
pointer-events: none; pointer-events: none;

View File

@@ -221,6 +221,47 @@
color: var(--text-primary); color: var(--text-primary);
} }
.form-collapse {
margin-bottom: 12px;
border-top: 1px solid var(--border-color);
padding-top: 8px;
}
.form-collapse > summary {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary, #888);
padding: 4px 0;
user-select: none;
}
.form-collapse > summary::-webkit-details-marker { display: none; }
.form-collapse > summary::before {
content: '▶';
font-size: 0.6rem;
opacity: 0.6;
transition: transform 0.15s;
flex-shrink: 0;
}
.form-collapse[open] > summary::before {
transform: rotate(90deg);
}
.form-collapse > summary:hover {
color: var(--text-color);
}
.form-collapse-body {
padding-top: 8px;
}
.error-message { .error-message {
background: rgba(244, 67, 54, 0.1); background: rgba(244, 67, 54, 0.1);
border: 1px solid var(--danger-color); border: 1px solid var(--danger-color);

View File

@@ -30,6 +30,7 @@ class CalibrationModal extends Modal {
skip_start: this.$('cal-skip-start').value, skip_start: this.$('cal-skip-start').value,
skip_end: this.$('cal-skip-end').value, skip_end: this.$('cal-skip-end').value,
border_width: this.$('cal-border-width').value, border_width: this.$('cal-border-width').value,
led_count: this.$('cal-css-led-count').value,
}; };
} }
@@ -112,6 +113,7 @@ export async function showCalibration(deviceId) {
document.getElementById('calibration-device-id').value = device.id; document.getElementById('calibration-device-id').value = device.id;
document.getElementById('cal-device-led-count-inline').textContent = device.led_count; document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
document.getElementById('cal-css-led-count-group').style.display = 'none';
document.getElementById('cal-start-position').value = calibration.start_position; document.getElementById('cal-start-position').value = calibration.start_position;
document.getElementById('cal-layout').value = calibration.layout; document.getElementById('cal-layout').value = calibration.layout;
@@ -216,6 +218,11 @@ export async function showCSSCalibration(cssId) {
const preview = document.querySelector('.calibration-preview'); const preview = document.querySelector('.calibration-preview');
preview.style.aspectRatio = ''; preview.style.aspectRatio = '';
document.getElementById('cal-device-led-count-inline').textContent = '—'; document.getElementById('cal-device-led-count-inline').textContent = '—';
const ledCountGroup = document.getElementById('cal-css-led-count-group');
ledCountGroup.style.display = '';
const calLeds = (calibration.leds_top || 0) + (calibration.leds_right || 0) +
(calibration.leds_bottom || 0) + (calibration.leds_left || 0);
document.getElementById('cal-css-led-count').value = source.led_count || calLeds || 0;
document.getElementById('cal-start-position').value = calibration.start_position || 'bottom_left'; document.getElementById('cal-start-position').value = calibration.start_position || 'bottom_left';
document.getElementById('cal-layout').value = calibration.layout || 'clockwise'; document.getElementById('cal-layout').value = calibration.layout || 'clockwise';
@@ -286,8 +293,17 @@ export function updateCalibrationPreview() {
parseInt(document.getElementById('cal-left-leds').value || 0); parseInt(document.getElementById('cal-left-leds').value || 0);
const totalEl = document.querySelector('.preview-screen-total'); const totalEl = document.querySelector('.preview-screen-total');
const inCSS = _isCSS(); const inCSS = _isCSS();
const deviceCount = inCSS ? null : parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0); const declaredCount = inCSS
const mismatch = !inCSS && total !== deviceCount; ? parseInt(document.getElementById('cal-css-led-count').value || 0)
: parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
if (inCSS) {
document.getElementById('cal-device-led-count-inline').textContent = declaredCount || '—';
}
// In device mode: calibration total must exactly equal device LED count
// In CSS mode: warn only if calibrated LEDs exceed the declared total (padding handles the rest)
const mismatch = inCSS
? (declaredCount > 0 && total > declaredCount)
: (total !== declaredCount);
document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total; document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total;
if (totalEl) totalEl.classList.toggle('mismatch', mismatch); if (totalEl) totalEl.classList.toggle('mismatch', mismatch);
@@ -831,10 +847,18 @@ export async function saveCalibration() {
const leftLeds = parseInt(document.getElementById('cal-left-leds').value || 0); const leftLeds = parseInt(document.getElementById('cal-left-leds').value || 0);
const total = topLeds + rightLeds + bottomLeds + leftLeds; const total = topLeds + rightLeds + bottomLeds + leftLeds;
const declaredLedCount = cssMode
? parseInt(document.getElementById('cal-css-led-count').value) || 0
: parseInt(document.getElementById('cal-device-led-count-inline').textContent) || 0;
if (!cssMode) { if (!cssMode) {
const deviceLedCount = parseInt(document.getElementById('cal-device-led-count-inline').textContent); if (total !== declaredLedCount) {
if (total !== deviceLedCount) { error.textContent = `Total LEDs (${total}) must equal device LED count (${declaredLedCount})`;
error.textContent = `Total LEDs (${total}) must equal device LED count (${deviceLedCount})`; error.style.display = 'block';
return;
}
} else {
if (declaredLedCount > 0 && total > declaredLedCount) {
error.textContent = `Calibrated LEDs (${total}) exceed total LED count (${declaredLedCount})`;
error.style.display = 'block'; error.style.display = 'block';
return; return;
} }
@@ -862,7 +886,7 @@ export async function saveCalibration() {
if (cssMode) { if (cssMode) {
response = await fetchWithAuth(`/color-strip-sources/${cssId}`, { response = await fetchWithAuth(`/color-strip-sources/${cssId}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ calibration }), body: JSON.stringify({ calibration, led_count: declaredLedCount }),
}); });
} else { } else {
response = await fetchWithAuth(`/devices/${deviceId}/calibration`, { response = await fetchWithAuth(`/devices/${deviceId}/calibration`, {

View File

@@ -22,6 +22,7 @@ class CSSEditorModal extends Modal {
brightness: document.getElementById('css-editor-brightness').value, brightness: document.getElementById('css-editor-brightness').value,
saturation: document.getElementById('css-editor-saturation').value, saturation: document.getElementById('css-editor-saturation').value,
gamma: document.getElementById('css-editor-gamma').value, gamma: document.getElementById('css-editor-gamma').value,
led_count: document.getElementById('css-editor-led-count').value,
}; };
} }
} }
@@ -35,7 +36,8 @@ export function createColorStripCard(source, pictureSourceMap) {
? pictureSourceMap[source.picture_source_id].name ? pictureSourceMap[source.picture_source_id].name
: source.picture_source_id || '—'; : source.picture_source_id || '—';
const cal = source.calibration || {}; const cal = source.calibration || {};
const ledCount = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0); const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
return ` return `
<div class="card" data-css-id="${source.id}"> <div class="card" data-css-id="${source.id}">
@@ -107,6 +109,8 @@ export async function showCSSEditor(cssId = null) {
document.getElementById('css-editor-gamma').value = gamma; document.getElementById('css-editor-gamma').value = gamma;
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2); document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
document.getElementById('css-editor-led-count').value = css.led_count ?? 0;
document.getElementById('css-editor-title').textContent = t('color_strip.edit'); document.getElementById('css-editor-title').textContent = t('color_strip.edit');
} else { } else {
document.getElementById('css-editor-id').value = ''; document.getElementById('css-editor-id').value = '';
@@ -122,6 +126,7 @@ export async function showCSSEditor(cssId = null) {
document.getElementById('css-editor-saturation-value').textContent = '1.00'; document.getElementById('css-editor-saturation-value').textContent = '1.00';
document.getElementById('css-editor-gamma').value = 1.0; document.getElementById('css-editor-gamma').value = 1.0;
document.getElementById('css-editor-gamma-value').textContent = '1.00'; document.getElementById('css-editor-gamma-value').textContent = '1.00';
document.getElementById('css-editor-led-count').value = 0;
document.getElementById('css-editor-title').textContent = t('color_strip.add'); document.getElementById('css-editor-title').textContent = t('color_strip.add');
} }
@@ -160,6 +165,7 @@ export async function saveCSSEditor() {
brightness: parseFloat(document.getElementById('css-editor-brightness').value), brightness: parseFloat(document.getElementById('css-editor-brightness').value),
saturation: parseFloat(document.getElementById('css-editor-saturation').value), saturation: parseFloat(document.getElementById('css-editor-saturation').value),
gamma: parseFloat(document.getElementById('css-editor-gamma').value), gamma: parseFloat(document.getElementById('css-editor-gamma').value),
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
}; };
try { try {

View File

@@ -100,9 +100,7 @@ export function createDeviceCard(device) {
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}"> <button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
⚙️ ⚙️
</button> </button>
<button class="btn btn-icon btn-secondary" onclick="showCalibration('${device.id}')" title="${t('device.button.calibrate')}">
📐
</button>
</div> </div>
</div> </div>
`; `;

View File

@@ -552,6 +552,7 @@
"color_strip.interpolation.dominant": "Dominant", "color_strip.interpolation.dominant": "Dominant",
"color_strip.smoothing": "Smoothing:", "color_strip.smoothing": "Smoothing:",
"color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", "color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
"color_strip.color_corrections": "Color Corrections",
"color_strip.brightness": "Brightness:", "color_strip.brightness": "Brightness:",
"color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.", "color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.",
"color_strip.saturation": "Saturation:", "color_strip.saturation": "Saturation:",
@@ -561,6 +562,8 @@
"color_strip.test_device": "Test on Device:", "color_strip.test_device": "Test on Device:",
"color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles", "color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles",
"color_strip.leds": "LED count", "color_strip.leds": "LED count",
"color_strip.led_count": "LED Count:",
"color_strip.led_count.hint": "Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. Useful when the strip has LEDs behind the TV that are not mapped to screen edges — those LEDs will be sent black.",
"color_strip.created": "Color strip source created", "color_strip.created": "Color strip source created",
"color_strip.updated": "Color strip source updated", "color_strip.updated": "Color strip source updated",
"color_strip.deleted": "Color strip source deleted", "color_strip.deleted": "Color strip source deleted",

View File

@@ -552,6 +552,7 @@
"color_strip.interpolation.dominant": "Доминирующий", "color_strip.interpolation.dominant": "Доминирующий",
"color_strip.smoothing": "Сглаживание:", "color_strip.smoothing": "Сглаживание:",
"color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.", "color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.",
"color_strip.color_corrections": "Цветокоррекция",
"color_strip.brightness": "Яркость:", "color_strip.brightness": "Яркость:",
"color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.", "color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.",
"color_strip.saturation": "Насыщенность:", "color_strip.saturation": "Насыщенность:",
@@ -561,6 +562,8 @@
"color_strip.test_device": "Тестировать на устройстве:", "color_strip.test_device": "Тестировать на устройстве:",
"color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку", "color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку",
"color_strip.leds": "Количество светодиодов", "color_strip.leds": "Количество светодиодов",
"color_strip.led_count": "Количество LED:",
"color_strip.led_count.hint": "Общее число светодиодов на физической полосе. 0 = взять из калибровки. Укажите явно, если на полосе есть светодиоды за телевизором, не привязанные к краям экрана — им будет отправлен чёрный цвет.",
"color_strip.created": "Источник цветовой полосы создан", "color_strip.created": "Источник цветовой полосы создан",
"color_strip.updated": "Источник цветовой полосы обновлён", "color_strip.updated": "Источник цветовой полосы обновлён",
"color_strip.deleted": "Источник цветовой полосы удалён", "color_strip.deleted": "Источник цветовой полосы удалён",

View File

@@ -20,7 +20,6 @@ from wled_controller.core.capture.calibration import (
CalibrationConfig, CalibrationConfig,
calibration_from_dict, calibration_from_dict,
calibration_to_dict, calibration_to_dict,
create_default_calibration,
) )
@@ -53,6 +52,7 @@ class ColorStripSource:
"smoothing": None, "smoothing": None,
"interpolation_mode": None, "interpolation_mode": None,
"calibration": None, "calibration": None,
"led_count": None,
} }
@staticmethod @staticmethod
@@ -82,7 +82,7 @@ class ColorStripSource:
calibration = ( calibration = (
calibration_from_dict(calibration_data) calibration_from_dict(calibration_data)
if calibration_data if calibration_data
else create_default_calibration(0) else CalibrationConfig(layout="clockwise", start_position="bottom_left")
) )
# Only "picture" type for now; extend with elif branches for future types # Only "picture" type for now; extend with elif branches for future types
@@ -97,6 +97,7 @@ class ColorStripSource:
smoothing=data["smoothing"] if data.get("smoothing") is not None else 0.3, smoothing=data["smoothing"] if data.get("smoothing") is not None else 0.3,
interpolation_mode=data.get("interpolation_mode") or "average", interpolation_mode=data.get("interpolation_mode") or "average",
calibration=calibration, calibration=calibration,
led_count=data.get("led_count") or 0,
) )
@@ -115,7 +116,10 @@ class PictureColorStripSource(ColorStripSource):
gamma: float = 1.0 # 1.0 = no correction; <1 = brighter, >1 = darker mids gamma: float = 1.0 # 1.0 = no correction; <1 = brighter, >1 = darker mids
smoothing: float = 0.3 # temporal smoothing (0.0 = none, 1.0 = full) smoothing: float = 0.3 # temporal smoothing (0.0 = none, 1.0 = full)
interpolation_mode: str = "average" # "average" | "median" | "dominant" interpolation_mode: str = "average" # "average" | "median" | "dominant"
calibration: CalibrationConfig = field(default_factory=lambda: create_default_calibration(0)) calibration: CalibrationConfig = field(
default_factory=lambda: CalibrationConfig(layout="clockwise", start_position="bottom_left")
)
led_count: int = 0 # explicit LED count; 0 = auto (derived from calibration)
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
@@ -127,4 +131,5 @@ class PictureColorStripSource(ColorStripSource):
d["smoothing"] = self.smoothing d["smoothing"] = self.smoothing
d["interpolation_mode"] = self.interpolation_mode d["interpolation_mode"] = self.interpolation_mode
d["calibration"] = calibration_to_dict(self.calibration) d["calibration"] = calibration_to_dict(self.calibration)
d["led_count"] = self.led_count
return d return d

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from wled_controller.core.capture.calibration import calibration_to_dict from wled_controller.core.capture.calibration import CalibrationConfig, calibration_to_dict
from wled_controller.storage.color_strip_source import ( from wled_controller.storage.color_strip_source import (
ColorStripSource, ColorStripSource,
PictureColorStripSource, PictureColorStripSource,
@@ -98,6 +98,7 @@ class ColorStripStore:
smoothing: float = 0.3, smoothing: float = 0.3,
interpolation_mode: str = "average", interpolation_mode: str = "average",
calibration=None, calibration=None,
led_count: int = 0,
description: Optional[str] = None, description: Optional[str] = None,
) -> ColorStripSource: ) -> ColorStripSource:
"""Create a new color strip source. """Create a new color strip source.
@@ -105,8 +106,6 @@ class ColorStripStore:
Raises: Raises:
ValueError: If validation fails ValueError: If validation fails
""" """
from wled_controller.core.capture.calibration import create_default_calibration
if not name or not name.strip(): if not name or not name.strip():
raise ValueError("Name is required") raise ValueError("Name is required")
@@ -115,7 +114,7 @@ class ColorStripStore:
raise ValueError(f"Color strip source with name '{name}' already exists") raise ValueError(f"Color strip source with name '{name}' already exists")
if calibration is None: if calibration is None:
calibration = create_default_calibration(0) calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
source_id = f"css_{uuid.uuid4().hex[:8]}" source_id = f"css_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow() now = datetime.utcnow()
@@ -135,6 +134,7 @@ class ColorStripStore:
smoothing=smoothing, smoothing=smoothing,
interpolation_mode=interpolation_mode, interpolation_mode=interpolation_mode,
calibration=calibration, calibration=calibration,
led_count=led_count,
) )
self._sources[source_id] = source self._sources[source_id] = source
@@ -155,6 +155,7 @@ class ColorStripStore:
smoothing: Optional[float] = None, smoothing: Optional[float] = None,
interpolation_mode: Optional[str] = None, interpolation_mode: Optional[str] = None,
calibration=None, calibration=None,
led_count: Optional[int] = None,
description: Optional[str] = None, description: Optional[str] = None,
) -> ColorStripSource: ) -> ColorStripSource:
"""Update an existing color strip source. """Update an existing color strip source.
@@ -193,6 +194,8 @@ class ColorStripStore:
source.interpolation_mode = interpolation_mode source.interpolation_mode = interpolation_mode
if calibration is not None: if calibration is not None:
source.calibration = calibration source.calibration = calibration
if led_count is not None:
source.led_count = led_count
source.updated_at = datetime.utcnow() source.updated_at = datetime.utcnow()
self._save() self._save()

View File

@@ -18,6 +18,16 @@
<small class="input-hint" style="display:none" data-i18n="color_strip.test_device.hint">Select a device to send test pixels to when clicking edge toggles</small> <small class="input-hint" style="display:none" data-i18n="color_strip.test_device.hint">Select a device to send test pixels to when clicking edge toggles</small>
<select id="calibration-test-device"></select> <select id="calibration-test-device"></select>
</div> </div>
<!-- LED count input (CSS calibration mode only) -->
<div id="cal-css-led-count-group" class="form-group" style="display:none; margin-bottom: 12px; padding: 0 4px;">
<div class="label-row">
<label for="cal-css-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the physical strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
<input type="number" id="cal-css-led-count" min="0" max="1500" step="1" value="0" oninput="updateCalibrationPreview()">
</div>
<!-- Interactive Preview with integrated LED inputs and test toggles --> <!-- Interactive Preview with integrated LED inputs and test toggles -->
<div style="margin-bottom: 12px; padding: 0 24px;"> <div style="margin-bottom: 12px; padding: 0 24px;">
<div class="calibration-preview"> <div class="calibration-preview">

View File

@@ -63,6 +63,9 @@
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)"> <input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
</div> </div>
<details class="form-collapse">
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
<div class="form-collapse-body">
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="css-editor-brightness"> <label for="css-editor-brightness">
@@ -98,6 +101,17 @@
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, &lt;1=brighter midtones, &gt;1=darker midtones)</small> <small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, &lt;1=brighter midtones, &gt;1=darker midtones)</small>
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)"> <input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
</div> </div>
</div>
</details>
<div class="form-group">
<div class="label-row">
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to use the sum from calibration. If your strip has LEDs behind the TV that are not mapped to screen edges, set the exact count here and they will be filled with black.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div> <div id="css-editor-error" class="error-message" style="display: none;"></div>
</form> </form>