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
fb4bded
Lock processor settings during DLC inference
C-Achard Jun 30, 2026
cc0f5da
Improve processor discovery and logging
C-Achard Jul 1, 2026
b5a6c84
Add processors package exports
C-Achard Jul 1, 2026
1150f26
Move example socket processors to examples module
C-Achard Jul 1, 2026
31d1c0f
Update plugin docs for processor examples
C-Achard Jul 1, 2026
33a6592
Skip socket base module in processor scan
C-Achard Jul 1, 2026
c58bbef
Update processor_utils.py
C-Achard Jul 1, 2026
e5a8bba
Warn on duplicate processor registration
C-Achard Jul 1, 2026
2f54a91
Update examples.py
C-Achard Jul 1, 2026
0523759
Refine processor package scan typing
C-Achard Jul 1, 2026
0e8caf0
Update examples.py
C-Achard Jul 1, 2026
60e757a
Fix dlclive Processor import paths
C-Achard Jul 1, 2026
9c805a2
Extract processor registry into new module
C-Achard Jul 1, 2026
fb27336
Fix dlclive mock structure in processor tests
C-Achard Jul 1, 2026
ed3da71
Make Engine a str enum and normalize model_type
C-Achard Jul 1, 2026
27f4fd9
Persist custom processor folder in settings
C-Achard Jul 1, 2026
7c18a0f
Improve recorder error logging and handling
C-Achard Jul 1, 2026
5b7222d
Hide base socket processor from discovery
C-Achard Aug 12, 2026
f2aae7c
Unify processor discovery and scan metadata
C-Achard Aug 12, 2026
2d75f4a
Deprecate legacy processor registry API
C-Achard Aug 12, 2026
d0bbed4
Update processor discovery tests
C-Achard Aug 12, 2026
1a0cf15
Require dlclive in socket processor tests
C-Achard Aug 12, 2026
c314533
Add discovery tests for built-in processors
C-Achard Aug 12, 2026
c3a1c87
Update processor plugin system docs
C-Achard Aug 12, 2026
d451166
Refine custom processor UI controls
C-Achard Aug 12, 2026
a67ae4c
Improve processor session override GUI test
C-Achard Aug 12, 2026
25f8dee
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 @@ -2162,6 +2195,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 @@ -205,7 +205,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