Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b29068
Lock processor settings during DLC inference
C-Achard Jun 30, 2026
f6fde2a
Improve processor discovery and logging
C-Achard Jul 1, 2026
1dbe80e
Add processors package exports
C-Achard Jul 1, 2026
a1399a0
Move example socket processors to examples module
C-Achard Jul 1, 2026
989898e
Update plugin docs for processor examples
C-Achard Jul 1, 2026
513ff39
Skip socket base module in processor scan
C-Achard Jul 1, 2026
4f36bc8
Update processor_utils.py
C-Achard Jul 1, 2026
f061f8f
Warn on duplicate processor registration
C-Achard Jul 1, 2026
6f9084b
Update examples.py
C-Achard Jul 1, 2026
7295752
Refine processor package scan typing
C-Achard Jul 1, 2026
66f07b0
Update examples.py
C-Achard Jul 1, 2026
1985850
Fix dlclive Processor import paths
C-Achard Jul 1, 2026
983716b
Extract processor registry into new module
C-Achard Jul 1, 2026
836d57d
Fix dlclive mock structure in processor tests
C-Achard Jul 1, 2026
3a0a33a
Make Engine a str enum and normalize model_type
C-Achard Jul 1, 2026
4b1db4e
Persist custom processor folder in settings
C-Achard Jul 1, 2026
6d4c9a3
Improve recorder error logging and handling
C-Achard Jul 1, 2026
6db62ba
Hide base socket processor from discovery
C-Achard Aug 12, 2026
31597af
Unify processor discovery and scan metadata
C-Achard Aug 12, 2026
1784606
Deprecate legacy processor registry API
C-Achard Aug 12, 2026
9a527d9
Update processor discovery tests
C-Achard Aug 12, 2026
196005e
Require dlclive in socket processor tests
C-Achard Aug 12, 2026
cb540d4
Add discovery tests for built-in processors
C-Achard Aug 12, 2026
06e870f
Update processor plugin system docs
C-Achard Aug 12, 2026
6afa75b
Refine custom processor UI controls
C-Achard Aug 12, 2026
87e7932
Improve processor session override GUI test
C-Achard Aug 12, 2026
73eb420
Add test for processor control re-enable
C-Achard Aug 12, 2026
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
136 changes: 86 additions & 50 deletions dlclivegui/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def _build_dlc_group(self) -> QGroupBox:
# Processor selection
processor_path_layout = QHBoxLayout()
self.processor_folder_edit = QLineEdit()
self.processor_folder_edit.setText(default_processors_dir())
self.processor_folder_edit.setText(self._settings_store.get_processor_folder(default=default_processors_dir()))
processor_path_layout.addWidget(self.processor_folder_edit)

self.browse_processor_folder_button = QPushButton("Browse...")
Expand Down Expand Up @@ -481,13 +481,34 @@ def _build_dlc_group(self) -> QGroupBox:
processing_sttgs = lyts.make_two_field_row(
"Inference camera",
self.dlc_camera_combo,
"Processor",
"Custom processor",
self.processor_combo,
key_width=None,
)
self.dlc_camera_combo.update_shrink_width()
form.addRow(processing_sttgs)

self.processor_status_label = QLabel("Processor: No clients | Recording: No")
self.processor_status_label.setWordWrap(True)
# form.addRow("Processor Status", self.processor_status_label)
self.use_custom_proc_checkbox = QCheckBox("Use custom processor")
self.use_custom_proc_checkbox.setChecked(False)
self.use_custom_proc_checkbox.setToolTip(
"If enabled, the GUI will load and interact with the selected processor plugin.\n"
)
self.processor_toggle_row = lyts.make_two_field_row(
"Processor status",
self.processor_status_label,
None,
self.use_custom_proc_checkbox,
key_width=None,
left_stretch=0,
right_stretch=0,
style_values=False,
)
self.processor_toggle_row.setVisible(False) # Hide until a processor is selected
form.addRow(self.processor_toggle_row)

# Wrap inference buttons in a widget to prevent shifting
inference_button_widget = QWidget()
inference_buttons = QHBoxLayout(inference_button_widget)
Expand All @@ -508,17 +529,6 @@ def _build_dlc_group(self) -> QGroupBox:
# self.show_predictions_checkbox.setChecked(True)
# form.addRow(self.show_predictions_checkbox)

self.allow_processor_ctrl_checkbox = QCheckBox("Allow processor-based control")
self.allow_processor_ctrl_checkbox.setChecked(False)
self.allow_processor_ctrl_checkbox.setToolTip(
"If enabled, the GUI will load and interact with the selected processor plugin.\n"
)
form.addRow(self.allow_processor_ctrl_checkbox)

self.processor_status_label = QLabel("Processor: No clients | Recording: No")
self.processor_status_label.setWordWrap(True)
form.addRow("Processor Status", self.processor_status_label)

return group

def _build_recording_group(self) -> QGroupBox:
Expand Down Expand Up @@ -801,8 +811,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())
self.processor_combo.currentIndexChanged.connect(self._on_processor_selection_changed)
self.use_custom_proc_checkbox.stateChanged.connect(lambda _s: self._update_processor_status())

# Recording settings
## Session name persistence + preview updates
Expand Down Expand Up @@ -1085,10 +1095,11 @@ def _action_browse_directory(self) -> None:

def _action_browse_processor_folder(self) -> None:
"""Browse for processor folder."""
current_path = self.processor_folder_edit.text() or default_processors_dir()
current_path = self.processor_folder_edit.text().strip() or default_processors_dir()
directory = QFileDialog.getExistingDirectory(self, "Select processor folder", current_path)
if directory:
self.processor_folder_edit.setText(directory)
self._settings_store.set_processor_folder(directory)
self._refresh_processors()

def _action_open_recording_folder(self) -> None:
Expand Down Expand Up @@ -1132,20 +1143,29 @@ def _action_open_recording_folder(self) -> None:
logger.error(f"Failed to open folder: {exc}")
self.statusBar().showMessage("Could not open recording folder.", 5000)

def _processor_control_enabled(self) -> bool:
def _custom_processor_enabled(self) -> bool:
return bool(
getattr(self, "allow_processor_ctrl_checkbox", None) and self.allow_processor_ctrl_checkbox.isChecked()
getattr(self, "use_custom_proc_checkbox", None)
and self.use_custom_proc_checkbox.isChecked()
and self.processor_combo.currentData() is not None
)

def _refresh_processors(self) -> None:
self.processor_combo.clear()
self.processor_combo.addItem("No Processor", None)

selected_folder = self.processor_folder_edit.text().strip()
if Path(selected_folder).exists():
self._scanned_processors = scan_processor_folder(selected_folder)
selected_path = Path(selected_folder).expanduser() if selected_folder else None

if selected_path is not None and selected_path.is_dir():
resolved_folder = str(selected_path.resolve())
self._settings_store.set_processor_folder(resolved_folder)
self._scanned_processors = scan_processor_folder(resolved_folder)
source_text = resolved_folder
else:
self._scanned_processors = scan_processor_package("dlclivegui.processors")
source_text = "package dlclivegui.processors"

self._processor_keys = list(self._scanned_processors.keys())

for key in self._processor_keys:
Expand All @@ -1154,9 +1174,7 @@ def _refresh_processors(self) -> None:
self.processor_combo.addItem(display_name, key)

self.processor_combo.update_shrink_width()
self.statusBar().showMessage(
f"Found {len(self._processor_keys)} processor(s) in package dlclivegui.processors", 3000
)
self.statusBar().showMessage(f"Found {len(self._processor_keys)} processor(s) in {source_text}", 3000)

# ------------------------------------------------------------------
# Recording path preview and session name persistence
Expand Down Expand Up @@ -1704,23 +1722,20 @@ def _configure_dlc(self) -> bool:

# Instantiate processor if selected
processor = None
if self._processor_control_enabled():
selected_key = self.processor_combo.currentData()
if selected_key is not None and self._scanned_processors:
try:
# For now, instantiate with no parameters
processor = instantiate_from_scan(self._scanned_processors, selected_key)
processor_name = self._scanned_processors[selected_key]["name"]
self.statusBar().showMessage(f"Loaded processor: {processor_name}", 3000)
except Exception as e:
error_msg = f"Failed to instantiate processor: {e}"
self._show_error(error_msg)
logger.error(error_msg)
return False
else:
selected_key = self.processor_combo.currentData()
if selected_key is not None:
self.statusBar().showMessage(f"Processor selection ignored (control disabled): {selected_key}", 3000)
selected_key = self.processor_combo.currentData()
if self._custom_processor_enabled():
try:
# For now, instantiate with no parameters
processor = instantiate_from_scan(self._scanned_processors, selected_key)
processor_name = self._scanned_processors[selected_key]["name"]
self.statusBar().showMessage(f"Loaded processor: {processor_name}", 3000)
except Exception as e:
error_msg = f"Failed to instantiate processor: {e}"
self._show_error(error_msg)
logger.error(error_msg)
return False
elif selected_key is not None:
self.statusBar().showMessage(f"Custom processor disabled: {selected_key}", 3000)

self._dlc.configure(settings, processor=processor)
self._model_path_store.save_if_valid(settings.model_path)
Expand All @@ -1734,24 +1749,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)

Comment thread
C-Achard marked this conversation as resolved.
if hasattr(self, "use_custom_proc_checkbox"):
self.use_custom_proc_checkbox.setEnabled(allow_changes)

def _update_camera_controls_enabled(self) -> None:
multi_cam_recording = self._rec_manager.is_active
Expand Down Expand Up @@ -1841,7 +1860,7 @@ def _update_metrics(self) -> None:
self.dlc_stats_label.setText("DLC processor idle")

# Update processor status (connection and recording state)
if hasattr(self, "processor_status_label") and self._processor_control_enabled():
if hasattr(self, "processor_status_label") and self._custom_processor_enabled():
self._update_processor_status()

# --- Recorder stats ---
Expand All @@ -1853,26 +1872,40 @@ def _update_metrics(self) -> None:
else:
self.recording_stats_label.setText(self._last_recorder_summary)

def _on_processor_selection_changed(
self,
_index: int,
) -> None:
"""Enable custom processing when a processor is selected."""
has_selection = self.processor_combo.currentData() is not None
self.processor_toggle_row.setVisible(has_selection)

self.use_custom_proc_checkbox.blockSignals(True)
self.use_custom_proc_checkbox.setChecked(has_selection)
self.use_custom_proc_checkbox.blockSignals(False)

self._update_processor_status()

def _update_processor_status(self) -> None:
"""Update processor connection and recording status, handle auto-recording."""
if not self._processor_control_enabled():
self.processor_status_label.setText("Processor control disabled")
if not self._custom_processor_enabled():
self.processor_status_label.setText("Disabled")
return

if not self._dlc_active or not self._dlc_initialized:
self.processor_status_label.setText("Processor: Not active")
self.processor_status_label.setText("Not active")
return

# Get processor instance from _dlc
processor = self._dlc._processor

if processor is None:
self.processor_status_label.setText("Processor: None loaded")
self.processor_status_label.setText("None loaded")
return

# Check if processor has the required attributes (socket-based processors)
if not hasattr(processor, "conns") or not hasattr(processor, "_recording"):
self.processor_status_label.setText("Processor: No status info")
self.processor_status_label.setText("No status info")
return

# Get connection count and recording state
Expand All @@ -1885,7 +1918,7 @@ def _update_processor_status(self) -> None:
self.processor_status_label.setText(f"Clients: {client_str} | Recording: {recording_str}")

# Handle auto-recording based on processor's video recording flag
if hasattr(processor, "_vid_recording") and self.allow_processor_ctrl_checkbox.isChecked():
if hasattr(processor, "_vid_recording") and self.use_custom_proc_checkbox.isChecked():
current_vid_recording = processor.video_recording

# Check if video recording state changed
Expand Down Expand Up @@ -2157,6 +2190,9 @@ def closeEvent(self, event: QCloseEvent) -> None: # pragma: no cover - GUI beha

# Remember model path on exit
self._model_path_store.save_if_valid(self.model_path_edit.text().strip())
# Remember processor folder on exit
if hasattr(self, "processor_folder_edit"):
self._settings_store.set_processor_folder(self.processor_folder_edit.text().strip())

# Close the window
super().closeEvent(event)
9 changes: 8 additions & 1 deletion dlclivegui/gui/recording_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,14 @@ def write_frame(
timestamp_metadata=timestamp_metadata,
)
except Exception as exc:
log.warning("Failed to write frame for %s: %s", cam_id, exc)
log.warning(
"Failed to write frame for %s: %s: %s frame_shape=%s dtype=%s",
cam_id,
type(exc).__name__,
str(exc) or repr(exc),
getattr(frame, "shape", None),
getattr(frame, "dtype", None),
)
try:
rec.stop()
except Exception:
Expand Down
Loading