diff --git a/server/src/wled_controller/api/routes/picture_sources.py b/server/src/wled_controller/api/routes/picture_sources.py index 8a6f850..9dbab7d 100644 --- a/server/src/wled_controller/api/routes/picture_sources.py +++ b/server/src/wled_controller/api/routes/picture_sources.py @@ -417,19 +417,21 @@ async def test_picture_source( if pp_template_ids: try: pp_template = pp_store.get_template(pp_template_ids[0]) - pool = ImagePool() + flat_filters = pp_store.resolve_filter_instances(pp_template.filters) + if flat_filters: + pool = ImagePool() - def apply_filters(img): - arr = np.array(img) - for fi in pp_template.filters: - f = FilterRegistry.create_instance(fi.filter_id, fi.options) - result = f.process_image(arr, pool) - if result is not None: - arr = result - return Image.fromarray(arr) + def apply_filters(img): + arr = np.array(img) + for fi in flat_filters: + f = FilterRegistry.create_instance(fi.filter_id, fi.options) + result = f.process_image(arr, pool) + if result is not None: + arr = result + return Image.fromarray(arr) - thumbnail = apply_filters(thumbnail) - pil_image = apply_filters(pil_image) + thumbnail = apply_filters(thumbnail) + pil_image = apply_filters(pil_image) except ValueError: logger.warning(f"PP template {pp_template_ids[0]} not found, skipping postprocessing preview") diff --git a/server/src/wled_controller/api/routes/picture_targets.py b/server/src/wled_controller/api/routes/picture_targets.py index 6856a3b..c586e1c 100644 --- a/server/src/wled_controller/api/routes/picture_targets.py +++ b/server/src/wled_controller/api/routes/picture_targets.py @@ -578,7 +578,8 @@ async def test_kc_target( except ValueError: logger.warning(f"KC test: PP template {pp_id} not found, skipping") continue - for fi in pp_template.filters: + flat_filters = pp_template_store.resolve_filter_instances(pp_template.filters) + for fi in flat_filters: try: f = FilterRegistry.create_instance(fi.filter_id, fi.options) result = f.process_image(img_array, image_pool) diff --git a/server/src/wled_controller/api/routes/postprocessing.py b/server/src/wled_controller/api/routes/postprocessing.py index b78a72c..3c9cde5 100644 --- a/server/src/wled_controller/api/routes/postprocessing.py +++ b/server/src/wled_controller/api/routes/postprocessing.py @@ -283,13 +283,14 @@ async def test_pp_template( thumbnail = pil_image.copy() thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS) - # Apply postprocessing filters - if pp_template.filters: + # Apply postprocessing filters (expand filter_template references) + flat_filters = pp_store.resolve_filter_instances(pp_template.filters) + if flat_filters: pool = ImagePool() def apply_filters(img): arr = np.array(img) - for fi in pp_template.filters: + for fi in flat_filters: f = FilterRegistry.create_instance(fi.filter_id, fi.options) result = f.process_image(arr, pool) if result is not None: diff --git a/server/src/wled_controller/core/filters/auto_crop.py b/server/src/wled_controller/core/filters/auto_crop.py index 1d0bf2e..cdca02c 100644 --- a/server/src/wled_controller/core/filters/auto_crop.py +++ b/server/src/wled_controller/core/filters/auto_crop.py @@ -7,6 +7,9 @@ import numpy as np from wled_controller.core.filters.base import FilterOptionDef, PostprocessingFilter from wled_controller.core.filters.image_pool import ImagePool from wled_controller.core.filters.registry import FilterRegistry +from wled_controller.utils import get_logger + +logger = get_logger(__name__) @FilterRegistry.register diff --git a/server/src/wled_controller/core/processing/live_stream.py b/server/src/wled_controller/core/processing/live_stream.py index 56cc6db..4b8be27 100644 --- a/server/src/wled_controller/core/processing/live_stream.py +++ b/server/src/wled_controller/core/processing/live_stream.py @@ -256,8 +256,8 @@ class ProcessedLiveStream(LiveStream): if idle_image is not _idle_src_buf: processed = ScreenCapture( image=idle_image, - width=cached_source_frame.width, - height=cached_source_frame.height, + width=idle_image.shape[1], + height=idle_image.shape[0], display_index=cached_source_frame.display_index, ) with self._frame_lock: @@ -293,8 +293,8 @@ class ProcessedLiveStream(LiveStream): processed = ScreenCapture( image=image, - width=source_frame.width, - height=source_frame.height, + width=image.shape[1], + height=image.shape[0], display_index=source_frame.display_index, ) with self._frame_lock: diff --git a/server/src/wled_controller/core/processing/live_stream_manager.py b/server/src/wled_controller/core/processing/live_stream_manager.py index 773ea30..d977d7b 100644 --- a/server/src/wled_controller/core/processing/live_stream_manager.py +++ b/server/src/wled_controller/core/processing/live_stream_manager.py @@ -239,44 +239,24 @@ class LiveStreamManager: return ProcessedLiveStream(source_live, filters), source_stream_id - def _resolve_filters(self, filter_instances, _visited=None): - """Recursively resolve filter instances, expanding filter_template refs. + def _resolve_filters(self, filter_instances): + """Resolve filter instances into instantiated PostprocessingFilter objects. - Args: - filter_instances: List of FilterInstance configs. - _visited: Set of template IDs already in the expansion stack - (prevents circular references). - - Returns: - List of instantiated PostprocessingFilter objects. + Expands filter_template references via the store, then creates instances. """ - if _visited is None: - _visited = set() + if self._pp_template_store: + flat = self._pp_template_store.resolve_filter_instances(filter_instances) + else: + flat = [fi for fi in filter_instances if fi.filter_id != "filter_template"] + resolved = [] - for fi in filter_instances: - if fi.filter_id == "filter_template": - template_id = fi.options.get("template_id", "") - if not template_id or not self._pp_template_store: - continue - if template_id in _visited: - logger.warning( - f"Circular filter template reference detected: {template_id}, skipping" - ) - continue - try: - pp = self._pp_template_store.get_template(template_id) - _visited.add(template_id) - resolved.extend(self._resolve_filters(pp.filters, _visited)) - _visited.discard(template_id) - except ValueError: - logger.warning(f"Referenced filter template '{template_id}' not found, skipping") - else: - try: - resolved.append( - FilterRegistry.create_instance(fi.filter_id, fi.options) - ) - except ValueError as e: - logger.warning(f"Skipping unknown filter '{fi.filter_id}': {e}") + for fi in flat: + try: + resolved.append( + FilterRegistry.create_instance(fi.filter_id, fi.options) + ) + except ValueError as e: + logger.warning(f"Skipping unknown filter '{fi.filter_id}': {e}") return resolved def _create_static_image_live_stream(self, config) -> StaticImageLiveStream: diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index ef24c97..b3799df 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -1367,8 +1367,14 @@ export function renderModalFilterList() { const filteredChoices = (fi.filter_id === 'filter_template' && opt.key === 'template_id' && editingId) ? opt.choices.filter(c => c.value !== editingId) : opt.choices; + // Auto-correct if current value doesn't match any choice + let selectVal = currentVal; + if (filteredChoices.length > 0 && !filteredChoices.some(c => c.value === selectVal)) { + selectVal = filteredChoices[0].value; + fi.options[opt.key] = selectVal; + } const options = filteredChoices.map(c => - `` + `` ).join(''); html += `
@@ -1407,7 +1413,12 @@ export function addFilterFromSelect() { const options = {}; for (const opt of filterDef.options_schema) { - options[opt.key] = opt.default; + // For select options with empty default, use the first choice's value + if (opt.type === 'select' && !opt.default && Array.isArray(opt.choices) && opt.choices.length > 0) { + options[opt.key] = opt.choices[0].value; + } else { + options[opt.key] = opt.default; + } } _modalFilters.push({ filter_id: filterId, options, _expanded: true }); diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 2bd7758..5338349 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -431,7 +431,7 @@ "kc.pattern_template": "Pattern Template:", "kc.pattern_template.hint": "Select the rectangle pattern to use for color extraction", "kc.pattern_template.none": "-- Select a pattern template --", - "kc.brightness_vs": "Brightness Source:", + "kc.brightness_vs": "🔢 Brightness Source:", "kc.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (multiplied with the manual brightness slider)", "kc.brightness_vs.none": "None (manual brightness only)", "kc.created": "Key colors target created successfully", @@ -847,7 +847,7 @@ "value_source.deleted": "Value source deleted", "value_source.delete.confirm": "Are you sure you want to delete this value source?", "value_source.error.name_required": "Please enter a name", - "targets.brightness_vs": "Brightness Source:", + "targets.brightness_vs": "🔢 Brightness Source:", "targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)", "targets.brightness_vs.none": "None (device brightness)" } diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index e70ea20..c00e14c 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -431,7 +431,7 @@ "kc.pattern_template": "Шаблон Паттерна:", "kc.pattern_template.hint": "Выберите шаблон прямоугольников для извлечения цветов", "kc.pattern_template.none": "-- Выберите шаблон паттерна --", - "kc.brightness_vs": "Источник Яркости:", + "kc.brightness_vs": "🔢 Источник Яркости:", "kc.brightness_vs.hint": "Опциональный источник значений, динамически управляющий яркостью каждый кадр (умножается на ручной слайдер яркости)", "kc.brightness_vs.none": "Нет (только ручная яркость)", "kc.created": "Цель ключевых цветов успешно создана", @@ -847,7 +847,7 @@ "value_source.deleted": "Источник значений удалён", "value_source.delete.confirm": "Удалить этот источник значений?", "value_source.error.name_required": "Введите название", - "targets.brightness_vs": "Источник яркости:", + "targets.brightness_vs": "🔢 Источник яркости:", "targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)", "targets.brightness_vs.none": "Нет (яркость устройства)" } diff --git a/server/src/wled_controller/storage/postprocessing_template_store.py b/server/src/wled_controller/storage/postprocessing_template_store.py index 962b7a5..0299eb9 100644 --- a/server/src/wled_controller/storage/postprocessing_template_store.py +++ b/server/src/wled_controller/storage/postprocessing_template_store.py @@ -220,6 +220,30 @@ class PostprocessingTemplateStore: logger.info(f"Deleted postprocessing template: {template_id}") + def resolve_filter_instances(self, filter_instances, _visited=None): + """Recursively resolve filter instances, expanding filter_template references. + + Returns a flat list of FilterInstance objects with no filter_template entries. + """ + if _visited is None: + _visited = set() + resolved = [] + for fi in filter_instances: + if fi.filter_id == "filter_template": + template_id = fi.options.get("template_id", "") + if not template_id or template_id in _visited: + continue + try: + ref_template = self.get_template(template_id) + _visited.add(template_id) + resolved.extend(self.resolve_filter_instances(ref_template.filters, _visited)) + _visited.discard(template_id) + except ValueError: + logger.warning(f"Referenced filter template '{template_id}' not found, skipping") + else: + resolved.append(fi) + return resolved + def get_sources_referencing(self, template_id: str, picture_source_store) -> List[str]: """Return names of picture sources that reference this template.""" return [