From 8ceb0b27776b8592bf8b789083e67bfe15a8353c Mon Sep 17 00:00:00 2001 From: Talmo Pereira Date: Thu, 28 May 2026 10:54:08 -0700 Subject: [PATCH] feat(gui): centroid-only models foundation - skeleton template + feature flag (1/2) Lay the groundwork for centroid-only model support behind a single, easily-removable feature flag so default behavior is unchanged. - Add sleap/skeletons/centroid.json: a single-node skeleton template with one node named "centroid" and no edges. The SkeletonDock template chooser auto-populates from this folder, so no code wiring is needed. Copied from a pre-verified candidate (the SkeletonEncoder drops edge-less nodes, so the file is shipped verbatim rather than regenerated). Includes an RGBA preview image. - Add sleap/gui/learning/features.py with is_centroid_models_enabled(), gated on the SLEAP_ENABLE_CENTROID_MODELS environment variable (default: off; truthy: "1"/"true"/"yes"/"on", case-insensitive). The helper also honors the "Experimental Features" toggle in the Help menu via an experimental_features argument threaded in by the caller; the env var remains a developer override. This module is temporary: it gates UI for a feature whose inference half is still blocked on sleap-nn (epic talmolab/sleap-nn#508 / PR #562). Remove it and its call sites once centroid-only inference lands. Tests cover the flag's env-var parsing, the Experimental Features toggle, and that the shipped template loads to a single-node, edge-less skeleton with an RGBA preview. Co-Authored-By: Claude Opus 4.8 (1M context) --- sleap/gui/learning/features.py | 33 +++++++++++++++++++++ sleap/skeletons/centroid.json | 28 ++++++++++++++++++ tests/gui/learning/test_features.py | 43 ++++++++++++++++++++++++++++ tests/gui/test_skeleton_templates.py | 29 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 sleap/gui/learning/features.py create mode 100644 sleap/skeletons/centroid.json create mode 100644 tests/gui/learning/test_features.py create mode 100644 tests/gui/test_skeleton_templates.py diff --git a/sleap/gui/learning/features.py b/sleap/gui/learning/features.py new file mode 100644 index 0000000000..053dc6a4d1 --- /dev/null +++ b/sleap/gui/learning/features.py @@ -0,0 +1,33 @@ +"""Temporary feature flags for in-progress functionality. + +NOTE: This module is intentionally short-lived. The centroid-only models flag +gates UI for a feature whose *inference* half is still blocked on sleap-nn +(epic talmolab/sleap-nn#508 / PR #562). Once centroid-only inference lands and +the feature is fully supported, delete this module and its call sites +(grep for is_centroid_models_enabled). + +In the GUI, this feature is enabled via the "Experimental Features" toggle in +the Help menu. The SLEAP_ENABLE_CENTROID_MODELS environment variable remains +available as a developer override. +""" + +import os + + +def is_centroid_models_enabled(experimental_features: bool = False) -> bool: + """Return True if the experimental centroid-only models UI is enabled. + + Enabled when EITHER: + - experimental_features is True (the "Experimental Features" toggle in + the Help menu, threaded in by the caller), or + - the SLEAP_ENABLE_CENTROID_MODELS environment variable is truthy + ("1"/"true"/"yes"/"on", case-insensitive). + """ + if experimental_features: + return True + return os.environ.get("SLEAP_ENABLE_CENTROID_MODELS", "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) diff --git a/sleap/skeletons/centroid.json b/sleap/skeletons/centroid.json new file mode 100644 index 0000000000..907bd9d82c --- /dev/null +++ b/sleap/skeletons/centroid.json @@ -0,0 +1,28 @@ +{ + "description": "Single-node centroid skeleton for centroid-only models.", + "nx_graph": { + "directed": true, + "graph": { + "name": "centroid", + "num_edges_inserted": 0 + }, + "links": [], + "multigraph": true, + "nodes": [ + { + "id": { + "py/object": "sleap.skeleton.Node", + "py/state": { + "py/tuple": [ + "centroid", + 1.0 + ] + } + } + } + ] + }, + "preview_image": { + "py/b64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAABQElEQVR4nO3cwQ3CMBBFwUAZ1EMplEUp1EMbUAGLlOSvg5m5IhQJ/Lw+WFkWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADiTvlH8M3lcX99+ux5vfmPBvLjHzCKT8TS7zzgmX9vTRxbvsd6JkijPRe4adLDBGmy9+5vmvQQSIPUYhZJnkCgIJCw9C5vimQJZILFK5IcgUBBIFAQyCTHHsesDIFAQSBQEAgUBAIFgUBBIFAQSEj3dXTX3zMEAgWBQEEgExx7HK9yBPLji1ccWQKBgkAapHZ50yNPIE32Xszi6OG1PwNsuZoujF4myABrF7k4+pkgB+DdvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwTO8NFK1ENqv3Iq8AAAAASUVORK5CYII=" + } +} \ No newline at end of file diff --git a/tests/gui/learning/test_features.py b/tests/gui/learning/test_features.py new file mode 100644 index 0000000000..cbf8d26a31 --- /dev/null +++ b/tests/gui/learning/test_features.py @@ -0,0 +1,43 @@ +"""Tests for the temporary centroid-only models feature flag.""" + +import pytest + +from sleap.gui.learning.features import is_centroid_models_enabled + + +def test_disabled_by_default(monkeypatch): + """The flag is off when the env var is unset.""" + monkeypatch.delenv("SLEAP_ENABLE_CENTROID_MODELS", raising=False) + assert is_centroid_models_enabled() is False + assert is_centroid_models_enabled(False) is False + + +@pytest.mark.parametrize("value", ["1", "true", "yes", "on"]) +@pytest.mark.parametrize("transform", [str.lower, str.upper, str.title]) +def test_enabled_for_truthy_values(monkeypatch, value, transform): + """Truthy values enable the flag regardless of case.""" + monkeypatch.setenv("SLEAP_ENABLE_CENTROID_MODELS", transform(value)) + assert is_centroid_models_enabled() is True + assert is_centroid_models_enabled(False) is True + + +@pytest.mark.parametrize("value", ["0", "false", ""]) +def test_disabled_for_falsy_values(monkeypatch, value): + """Falsy values keep the flag off.""" + monkeypatch.setenv("SLEAP_ENABLE_CENTROID_MODELS", value) + assert is_centroid_models_enabled() is False + assert is_centroid_models_enabled(False) is False + + +def test_enabled_by_experimental_features_without_env(monkeypatch): + """The Experimental Features toggle enables the flag without the env var.""" + monkeypatch.delenv("SLEAP_ENABLE_CENTROID_MODELS", raising=False) + assert is_centroid_models_enabled(experimental_features=True) is True + assert is_centroid_models_enabled(True) is True + + +@pytest.mark.parametrize("value", ["0", "false", ""]) +def test_experimental_features_overrides_falsy_env(monkeypatch, value): + """The Experimental Features toggle enables the flag even with a falsy env.""" + monkeypatch.setenv("SLEAP_ENABLE_CENTROID_MODELS", value) + assert is_centroid_models_enabled(experimental_features=True) is True diff --git a/tests/gui/test_skeleton_templates.py b/tests/gui/test_skeleton_templates.py new file mode 100644 index 0000000000..08514a0956 --- /dev/null +++ b/tests/gui/test_skeleton_templates.py @@ -0,0 +1,29 @@ +"""Tests for shipped skeleton template files.""" + +import base64 +import io +import json + +from PIL import Image + +import sleap.util +from sleap.gui.commands import OpenSkeleton + + +def test_centroid_template_loads_single_node(): + """The shipped centroid.json loads to a single-node, edge-less skeleton.""" + path = sleap.util.get_package_file("skeletons/centroid.json") + skeleton = OpenSkeleton.load_skeleton(path) + assert skeleton.node_names == ["centroid"] + assert len(skeleton.edges) == 0 + + +def test_centroid_template_preview_is_rgba(): + """The centroid.json preview image decodes to an RGBA PIL image.""" + path = sleap.util.get_package_file("skeletons/centroid.json") + with open(path, "r") as f: + skeleton_data = json.load(f) + + b64 = skeleton_data["preview_image"]["py/b64"] + image = Image.open(io.BytesIO(base64.b64decode(b64))) + assert image.mode == "RGBA"