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 += `