Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions dlclivegui/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,8 @@ def _connect_signals(self) -> None:
self._dlc.initialized.connect(self._on_dlc_initialised)
self.dlc_camera_combo.currentIndexChanged.connect(self._on_dlc_camera_changed)
self.dlc_camera_combo.currentTextChanged.connect(self.dlc_camera_combo.update_shrink_width)
self.allow_processor_ctrl_checkbox.stateChanged.connect(lambda _s: self._update_dlc_controls_enabled())
self.allow_processor_ctrl_checkbox.stateChanged.connect(lambda _s: self._update_processor_status())

# Recording settings
## Session name persistence + preview updates
Expand Down Expand Up @@ -1425,6 +1427,7 @@ def _on_multi_camera_started(self) -> None:
self.statusBar().showMessage(f"Multi-camera preview started: {active_count} camera(s)", 5000)
self._update_inference_buttons()
self._update_camera_controls_enabled()
self._update_dlc_controls_enabled()

def _on_multi_camera_stopped(self) -> None:
"""Handle all cameras stopped event."""
Expand All @@ -1440,6 +1443,7 @@ def _on_multi_camera_stopped(self) -> None:
self.statusBar().showMessage("Multi-camera preview stopped", 3000)
self._update_inference_buttons()
self._update_camera_controls_enabled()
self._update_dlc_controls_enabled()

def _on_multi_camera_error(self, camera_id: str, message: str) -> None:
"""Handle error from a camera in multi-camera mode."""
Expand Down Expand Up @@ -1653,24 +1657,28 @@ def _update_inference_buttons(self) -> None:
def _update_dlc_controls_enabled(self) -> None:
"""Enable/disable DLC settings based on inference state."""
allow_changes = not self._dlc_active
processor_controls = allow_changes and self._processor_control_enabled()

widgets = [
self.model_path_edit,
self.browse_model_button,
self.dlc_camera_combo,
# self.additional_options_edit,
]

processor_widgets = [
self.processor_folder_edit,
self.browse_processor_folder_button,
self.refresh_processors_button,
self.processor_combo,
]

for widget in widgets:
widget.setEnabled(allow_changes)

for widget in processor_widgets:
widget.setEnabled(processor_controls)
widget.setEnabled(allow_changes)

if hasattr(self, "allow_processor_ctrl_checkbox"):
self.allow_processor_ctrl_checkbox.setEnabled(allow_changes)

def _update_camera_controls_enabled(self) -> None:
multi_cam_recording = self._rec_manager.is_active
Expand Down
96 changes: 60 additions & 36 deletions dlclivegui/processors/processor_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,64 @@ def default_processors_dir() -> str:
return str(path)


def _processor_base_class():
from dlclive import Processor

return Processor


def _is_processor_subclass(obj, *, include_base: bool = False) -> bool:
"""Return True for dlclive.Processor subclasses, including indirect subclasses."""
if not inspect.isclass(obj):
return False

try:
processor_base = _processor_base_class()
except Exception:
logger.exception("Could not import dlclive.Processor")
return False

try:
if obj is processor_base:
return bool(include_base)
return issubclass(obj, processor_base)
except TypeError:
return False
Comment on lines +37 to +42


def _processor_info_from_class(cls, fallback_name: str) -> dict:
return {
"class": cls,
"name": getattr(cls, "PROCESSOR_NAME", fallback_name),
"description": getattr(cls, "PROCESSOR_DESCRIPTION", ""),
"params": getattr(cls, "PROCESSOR_PARAMS", {}),
}


def discover_processor_classes(module, *, only_defined_in_module: bool = True) -> dict[str, dict]:
"""Discover dlclive.Processor subclasses in a module.

Includes indirect subclasses of Processor.

Args:
module: Imported Python module.
only_defined_in_module: If True, ignore Processor subclasses imported
from other modules to avoid duplicate registry entries.
"""
processors: dict[str, dict] = {}

for name, obj in inspect.getmembers(module, inspect.isclass):
if only_defined_in_module and getattr(obj, "__module__", None) != module.__name__:
continue

if not _is_processor_subclass(obj):
continue

processors[name] = _processor_info_from_class(obj, name)

return processors


def scan_processor_folder(folder_path):
all_processors = {}
folder = Path(folder_path)
Expand Down Expand Up @@ -65,22 +123,7 @@ def scan_processor_package(package_name: str = "dlclivegui.processors") -> dict[
processors = mod.get_available_processors()
else:
# Fallback: scan for dlclive.Processor subclasses
from dlclive import Processor

processors = {}
for attr_name in dir(mod):
obj = getattr(mod, attr_name)
try:
if isinstance(obj, type) and obj is not Processor and issubclass(obj, Processor):
processors[attr_name] = {
"class": obj,
"name": getattr(obj, "PROCESSOR_NAME", attr_name),
"description": getattr(obj, "PROCESSOR_DESCRIPTION", ""),
"params": getattr(obj, "PROCESSOR_PARAMS", {}),
}
except Exception:
# Non-class or weird metaclass; ignore
pass
processors = discover_processor_classes(mod)
Comment on lines 124 to +126

# Normalize into your “file::class” shape
module_file = mod.__name__.split(".")[-1] + ".py"
Expand Down Expand Up @@ -131,26 +174,7 @@ def load_processors_from_file(file_path: str | Path):
return processors

# Fallback path: discover subclasses of dlclive.Processor
from dlclive import Processor

processors: dict[str, dict] = {}
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj is Processor:
continue
# Guard: module might define other classes; only include Processor subclasses
try:
if issubclass(obj, Processor):
processors[name] = {
"class": obj,
"name": getattr(obj, "PROCESSOR_NAME", name),
"description": getattr(obj, "PROCESSOR_DESCRIPTION", ""),
"params": getattr(obj, "PROCESSOR_PARAMS", {}),
}
except Exception:
# Some "classes" can fail issubclass checks; ignore safely
continue

return processors
return discover_processor_classes(module)
Comment on lines 175 to +177

except Exception:
# Full traceback helps a ton when a plugin fails to import
Expand Down
Loading