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
2 changes: 2 additions & 0 deletions dlclivegui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
DLC_DO_LOG_TIMING: bool = True
### Trigger debug logging
DEBUG_TRIGGER_LOGS = False
### Extra logs for DLC lifecycle (model loading, etc)
DLC_LIFECYCLE_EXTRA_LOGS: bool = True
# MAIN_WINDOW_DO_LOG_TIMING: bool = False
#### Backends
BASLER_DO_LOG_TIMING: bool = False
Expand Down
219 changes: 184 additions & 35 deletions dlclivegui/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@
)

from ..processors.processor_utils import (
create_spec_from_scan,
default_processors_dir,
instantiate_from_scan,
log_processor_context,
processor_builds_in_worker,
scan_processor_folder,
scan_processor_package,
)
Expand Down Expand Up @@ -933,7 +936,7 @@ def _apply_config(self, config: ApplicationSettings) -> None:
color_ui.set_bbox_combo_from_bgr(self.bbox_color_combo, self._bbox_color)

# Processor
## Allow processor control checkbox state
## Use custom processor checkbox state
if hasattr(self, "use_custom_proc_checkbox"):
self.use_custom_proc_checkbox.setChecked(
self._settings_store.get_processor_control_enabled(default=False)
Expand Down Expand Up @@ -1905,6 +1908,7 @@ def _start_multi_camera_recording(self) -> None:
if run_dir is None:
self._show_error("Failed to start recording.")
return
self._notify_processor_recording_started(run_dir)
self.multi_camera_controller.set_recording_sink(self._rec_manager.write_frame)
self.multi_camera_controller.set_recording_frame_do_emit(True)

Expand Down Expand Up @@ -1946,7 +1950,125 @@ def worker():
daemon=True,
).start()

def _get_dlc_processor_instance(self):
"""Return the active custom DLC processor instance, if available."""
processor = getattr(self._dlc, "_processor", None)

if processor is not None:
return processor

# Fallback: if DLCLive owns it internally.
dlc_obj = getattr(self._dlc, "_dlc", None)
if dlc_obj is not None:
return getattr(dlc_obj, "processor", None)

return None

def _save_processor_data_if_available(self) -> None:
"""Best-effort generic processor save.

The GUI intentionally does not pass a path here. This lets custom processors
use their own save_path / filename / internal policy.

Expected processor contract:
processor.save() -> int | bool | None

Return values are only logged; failure should not crash the GUI.
"""
processor = self._get_dlc_processor_instance()

if processor is None:
logger.debug("Processor save skipped: no processor instance available.")
return

save = getattr(processor, "save", None)
if not callable(save):
logger.debug("Processor save skipped: processor has no callable save().")
return

try:
result = save()
logger.info("Processor save() completed with result: %r", result)
except Exception:
logger.exception("Processor save() failed.")

def _notify_processor_recording_started(self, run_dir) -> None:
processor = self._get_dlc_processor_instance()
if processor is None:
return

hook = getattr(processor, "on_recording_started", None)
if not callable(hook):
return

try:
context = self._build_processor_recording_context(run_dir)
hook(context)
logger.info("Notified processor recording started: %s", context)
except Exception:
logger.exception("Processor on_recording_started hook failed")

from pathlib import Path

def _build_processor_recording_context(self, run_dir) -> dict:
run_dir = Path(run_dir) if run_dir is not None else None

file_context = {}
try:
file_context = self._rec_manager.get_recording_file_context()
except Exception:
logger.exception("Failed to get recording file context from RecordingManager")
file_context = {}

if run_dir is None:
run_dir = file_context.get("run_dir", None)
if run_dir is not None:
run_dir = Path(run_dir)

session_name = ""
if hasattr(self, "session_name_edit"):
session_name = self.session_name_edit.text().strip()

filename = ""
if hasattr(self, "filename_edit"):
filename = self.filename_edit.text().strip()

filename_stem = Path(filename or session_name or "recording").stem

ctx = {
"run_dir": run_dir,
"session_name": session_name,
"filename": filename,
"filename_stem": filename_stem,
"processor_base_path": run_dir / filename_stem if run_dir is not None else None,
}
ctx.update(file_context)
return ctx

def _notify_processor_recording_stopped(self) -> None:
processor = self._get_dlc_processor_instance()
if processor is None:
return False

hook = getattr(processor, "on_recording_stopped", None)
if not callable(hook):
return False

try:
run_dir = getattr(self._rec_manager, "run_dir", None)
context = self._build_processor_recording_context(run_dir)
hook(context)
logger.info("Notified processor recording stopped")
return True
except Exception:
logger.exception("Processor on_recording_stopped hook failed")
return False

def _on_recording_stopped_async(self) -> None:
handled_by_stop_hook = self._notify_processor_recording_stopped()
if not handled_by_stop_hook:
self._save_processor_data_if_available()

self._recording_stopping = False
self.start_record_button.setEnabled(True)
self.stop_record_button.setEnabled(False)
Expand Down Expand Up @@ -2069,15 +2191,25 @@ def _stop_preview(self) -> None:
def _configure_dlc(self) -> bool:
try:
settings = self._dlc_settings_from_ui()
except (ValueError, RuntimeError, json.JSONDecodeError) as exc:
self._show_error(f"Invalid DLCLive settings: {exc}")
except (
ValueError,
RuntimeError,
json.JSONDecodeError,
) as exc:
self._show_error(
f"Invalid DLCLive settings: {exc}"
)
return False

if not settings.model_path:
self._show_error("Please select a DLCLive model before starting inference.")
self._show_error(
"Please select a DLCLive model before "
"starting inference."
)
return False

# Instantiate processor if selected
processor = None
processor_spec = None
selected_key = self.processor_combo.currentData()

self._settings_store.set_processor_key(
Expand All @@ -2086,37 +2218,68 @@ def _configure_dlc(self) -> bool:

if self._custom_processor_enabled():
try:
processor = instantiate_from_scan(
self._scanned_processors,
selected_key,
)
processor_name = (
self._scanned_processors[
selected_key
]["name"]
processor_info = self._scanned_processors[
selected_key
]
processor_class = processor_info["class"]
processor_name = processor_info.get(
"name",
processor_class.__name__,
)

if processor_builds_in_worker(
processor_class
):
processor_spec = create_spec_from_scan(
self._scanned_processors,
selected_key,
)

log_processor_context(
"MainWindow._configure_dlc - "
f"SPEC: {processor_class.__name__}",
logger,
)
else:
processor = instantiate_from_scan(
self._scanned_processors,
selected_key,
)

log_processor_context(
"MainWindow._configure_dlc - "
f"INSTANCE: {type(processor).__name__}",
logger,
)

self.statusBar().showMessage(
f"Loaded processor: {processor_name}",
3000,
)

except Exception as exc:
error_msg = (
"Failed to instantiate processor: "
"Failed to configure processor: "
f"{exc}"
)
self._show_error(error_msg)
logger.error(error_msg)
logger.exception(error_msg)
return False

elif selected_key is not None:
self.statusBar().showMessage(
f"Custom processor disabled: "
f"{selected_key}",
f"Custom processor disabled: {selected_key}",
3000,
)

self._dlc.configure(settings, processor=processor)
self._model_path_store.save_if_valid(settings.model_path)

self._dlc.configure(
settings,
processor=processor,
processor_spec=processor_spec,
)
self._model_path_store.save_if_valid(
settings.model_path
)
return True

def _update_inference_buttons(self) -> None:
Expand Down Expand Up @@ -2250,20 +2413,6 @@ 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._custom_processor_enabled():
Expand Down Expand Up @@ -2596,7 +2745,7 @@ def closeEvent(self, event: QCloseEvent) -> None: # pragma: no cover - GUI beha

# Remember processor-control checkbox state on exit
if hasattr(self, "use_custom_proc_checkbox"):
self._settings_store.set_processor_control_enabled(self.use_custom_proc_checkbox.isChecked())
self._settings_store.set_processor_control_enabled(self._custom_processor_enabled())

# Flush QSettings best-effort
try:
Expand Down
Loading
Loading