Skip to content

Commit 767bc8f

Browse files
committed
Update processor plugin system docs
Revise PLUGIN_SYSTEM.md to reflect class-based processor discovery (including PROCESSOR_DISCOVERABLE behavior) instead of registry-driven discovery, and document legacy registration compatibility. Clarify GUI control-gating semantics, socket processor expectations, and modern instructions for creating and configuring custom processors.
1 parent 01f54e0 commit 767bc8f

1 file changed

Lines changed: 132 additions & 139 deletions

File tree

Lines changed: 132 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,64 @@
1-
# DeepLabCut Live GUI — Processor Plugin System
1+
# DeepLabCut Live GUI: Processor Plugin System
22

33
This repository includes a **plugin-style processor system** that lets the GUI discover and instantiate **DLCLive processors** dynamically.
44

5-
Processors are Python classes (typically subclasses of `dlclive.Processor`) that can optionally:
5+
Processors are Python classes that subclass `dlclive.processor.Processor`, directly or indirectly, and can optionally:
66

7-
- receive pose estimates during inference (via `process(pose, **kwargs)`),
8-
- broadcast pose-derived data to external clients (e.g., for experiment control),
9-
- expose metadata so the GUI can list them and (optionally) build simple parameter UIs.
7+
- Receive pose estimates during inference through `process(pose, **kwargs)`
8+
- Broadcast pose-derived data, for example for experiment control
9+
- Expose metadata so the GUI can list them and support processor configuration
1010

11-
> **Security / control note:** The GUI should treat processors as **optional, user-controlled extensions**. In our current design, the GUI exposes an opt-in toggle (recommended label: **“Allow processor control”**) that gates whether processor plugins are instantiated and whether the GUI reads/acts on processor state.
12-
13-
---
11+
> The GUI should treat processors as **optional, user-controlled extensions**.
12+
> In our current design, the GUI exposes an opt-in toggle, **Allow processor-based control**, that controls whether processor plugins are instantiated and whether the GUI reads or acts on processor state.
1413
1514
## Overview
1615

1716
### Useful files
1817

19-
- `dlclivegui/processors/dlc_processor_socket.py` — Example socket-based processor base class
20-
- `dlclivegui/processors/examples.py` — Example processor implementations (e.g., One-Euro filter)
21-
- `dlclivegui/processors/processor_utils.py` — Scanning + instantiation helpers used by the GUI
22-
23-
---
18+
- `dlclivegui/processors/dlc_processor_socket.py`: Example socket-based processor base class
19+
- `dlclivegui/processors/examples.py`: Example processor implementations, such as One-Euro filtering
20+
- `dlclivegui/processors/processor_utils.py`: Scanning and instantiation helpers used by the GUI
2421

2522
## Architecture
2623

27-
### 1) Processor registry (module-level)
24+
### 1) Processor class discovery
2825

29-
A typical processor module defines a registry and a decorator. The decorator registers classes into `PROCESSOR_REGISTRY` using either `PROCESSOR_ID` (if present) or the class name.
26+
A processor module defines one or more classes that subclass `dlclive.processor.Processor`, directly or indirectly.
3027

31-
```python
32-
# Registry for GUI discovery
33-
PROCESSOR_REGISTRY = {}
28+
The GUI discovers eligible processor classes by inspecting the imported module.
3429

35-
def register_processor(cls):
36-
registry_key = getattr(cls, "PROCESSOR_ID", cls.__name__)
37-
PROCESSOR_REGISTRY[registry_key] = cls
38-
return cls
39-
```
30+
```python
31+
from dlclive.processor import Processor
4032

41-
Register processors by decorating the class:
4233

43-
```python
44-
@register_processor
45-
class ExampleProcessor(BaseProcessorSocket):
34+
class ExampleProcessor(Processor):
4635
PROCESSOR_NAME = "Example Processor"
4736
PROCESSOR_DESCRIPTION = "Example description"
4837
PROCESSOR_PARAMS = {}
38+
39+
def process(self, pose, **kwargs):
40+
return pose
4941
```
5042

43+
Only processor classes defined in the scanned module are included. Processor classes imported from another module are ignored to avoid duplicate entries.
44+
45+
Reusable base classes that should not appear in the GUI can explicitly opt out:
46+
47+
```python
48+
class BaseProcessorSocket(Processor):
49+
PROCESSOR_DISCOVERABLE = False
50+
```
51+
52+
Concrete subclasses of a non-discoverable base class remain discoverable by default.
53+
5154
### 2) Processor metadata
5255

5356
Each processor class should define metadata attributes to help GUI discovery:
5457

5558
```python
5659
class MyProcessorSocket(BaseProcessorSocket):
57-
PROCESSOR_NAME = "Mouse Pose Processor" # Human-readable
58-
PROCESSOR_DESCRIPTION = "Broadcasts processed pose values"
60+
PROCESSOR_NAME = "Use Pose Processor" # Human-readable
61+
PROCESSOR_DESCRIPTION = "BBroadcasts processed pose values"
5962
PROCESSOR_PARAMS = {
6063
"bind": {
6164
"type": "tuple",
@@ -77,64 +80,46 @@ class MyProcessorSocket(BaseProcessorSocket):
7780

7881
> **Recommendation:** For security, prefer binding to `127.0.0.1` unless you explicitly want LAN exposure.
7982
80-
### 3) Module-level discovery helpers (optional)
8183

82-
Processor modules can expose:
83-
84-
- `get_available_processors()` — returns a dictionary of available processors and metadata
85-
86-
Example:
87-
88-
```python
89-
def get_available_processors():
90-
return {
91-
name: {
92-
"class": cls,
93-
"name": getattr(cls, "PROCESSOR_NAME", name),
94-
"description": getattr(cls, "PROCESSOR_DESCRIPTION", ""),
95-
"params": getattr(cls, "PROCESSOR_PARAMS", {}),
96-
}
97-
for name, cls in PROCESSOR_REGISTRY.items()
98-
}
99-
```
100-
101-
---
102-
103-
## Discovery & instantiation (current utilities)
84+
## Discovery & instantiation
10485

10586
The GUI uses utilities from `dlclivegui/processors/processor_utils.py`:
10687

107-
- `scan_processor_folder(folder_path)` — discover processors from `*.py` files in a folder
108-
- `scan_processor_package(package_name="dlclivegui.processors")` — discover processors from a package namespace
109-
- `instantiate_from_scan(processors_dict, processor_key, **kwargs)` — instantiate a processor from scan output
88+
- `discover_processor_classes(module)`: discover eligible processor classes in an imported module
89+
- `scan_processor_folder(folder_path)`: discover processors from `*.py` files in a folder
90+
- `scan_processor_package(package_name="dlclivegui.processors")`: discover processors from a package namespace
91+
- `instantiate_from_scan(processors_dict, processor_key, **kwargs)`: instantiate a processor from scan output
92+
93+
Package and folder scanning use different module-loading mechanisms, but both use the same class-based processor discovery.
11094

11195
### Key format
11296

11397
Scan results are dictionaries keyed like:
11498

115-
```
116-
"some_file.py::SomeProcessorClassOrId"
99+
```text
100+
some_file.py::SomeProcessorClass
117101
```
118102

119-
Each entry contains (at least):
103+
Each entry contains at least:
120104

121105
- `class`: the processor class object
122106
- `name`: display name
123107
- `description`: description text
124108
- `params`: parameter schema
125109
- `file`: module filename
126-
- `class_name`: class/registry key
110+
- `class_name`: processor class name
127111
- `file_path`: full path to the module file
128112

129113
### Example: scanning and instantiating
130114

131115
```python
132116
from dlclivegui.processors.processor_utils import (
133-
scan_processor_package,
134-
scan_processor_folder,
135117
instantiate_from_scan,
118+
scan_processor_folder,
119+
scan_processor_package,
136120
)
137121

122+
138123
# Built-in processors
139124
processors = scan_processor_package("dlclivegui.processors")
140125

@@ -143,121 +128,129 @@ processors = scan_processor_package("dlclivegui.processors")
143128

144129
# List
145130
for key, info in processors.items():
146-
print(f"{info['name']} ({key}) — {info['description']}")
131+
print(f"{info['name']} ({key}): {info['description']}")
147132

148133
# Instantiate
149-
selected_key = next(iter(processors.keys()))
150-
proc = instantiate_from_scan(processors, selected_key, bind=("127.0.0.1", 6000))
134+
selected_key = next(iter(processors))
135+
proc = instantiate_from_scan(
136+
processors,
137+
selected_key,
138+
bind=("127.0.0.1", 6000),
139+
)
151140
```
152141

153-
---
142+
### Legacy registration compatibility
154143

155-
## GUI integration & the “Allow processor control” gate
144+
Earlier processor modules may still import and use:
156145

157-
### Recommended behavior
146+
```python
147+
from dlclivegui.processors import PROCESSOR_REGISTRY, register_processor
148+
```
158149

159-
To keep processor behavior explicit and opt-in, the GUI provides a toggle (**Allow processor-based control**) with these semantics:
150+
The registry and decorator remain temporarily available for compatibility with existing processor modules. However:
160151

161-
- **Disabled (default):**
162-
- the GUI does **not instantiate** any processor plugin;
163-
- the GUI does **not read or act** on processor state (connections, recording flags, remote commands);
164-
- inference runs with `processor=None`.
165-
- *processor code may be imported by the discovery process*
152+
- GUI discovery does not use `PROCESSOR_REGISTRY`
153+
- GUI discovery does not call `get_available_processors()`
154+
- Decorating a class is not required for discovery
155+
- An existing decorated class remains discoverable because the decorator returns the original class
166156

167-
- **Enabled:**
168-
- the GUI may instantiate the selected processor and (optionally) reflect processor state in the UI.
169-
- the processor will be used by the `DLCLive` instance during inference.
157+
New processor modules should rely on subclass discovery instead of defining a registry or discovery function.
170158

171-
This lets users decide whether they want to run processor plugins and whether those plugins may influence UI/recording behavior.
159+
## GUI integration & enabling custom processors
172160

173-
> We recommend users to follow this design patter when designing their own processors
174-
> to help ensure predictable behavior and clear user control over processor-based features.<br>
175-
> **We are not responsible for any unexpected behavior caused by custom processors,**
176-
> **and the examples are provided as-is with no guarantees.**
161+
### Recommended behavior
177162

178-
---
163+
To keep processor behavior explicit and opt-in, the GUI provides an **Allow processor-based control** toggle with these effects:
179164

180-
## Socket-based processors (example base class)
165+
- **Disabled by default:**
166+
- The GUI does **not instantiate** any processor plugin
167+
- The GUI does **not read or act** on processor state, such as connections, recording flags, or remote commands
168+
- Inference runs with `processor=None`
169+
- Processor code may still be imported by the discovery process
181170

182-
The built-in `BaseProcessorSocket` (in `dlc_processor_socket.py`) demonstrates a simple approach for:
171+
- **Enabled:**
172+
- The GUI may instantiate the selected processor and reflect processor state in the UI
173+
- The processor is used by the `DLCLive` instance during inference
183174

184-
- accepting multiple clients,
185-
- receiving control messages (e.g., start/stop recording),
186-
- broadcasting payloads to connected clients,
187-
- cleaning up reliably on shutdown.
175+
This lets users decide whether they want to run processor plugins and whether those plugins may influence UI or recording behavior.
188176

189-
### Key points
177+
> We recommend that users follow this design pattern when creating processors to help ensure predictable behavior and clear user control over processor-based features.
178+
> **We are not responsible for unexpected behavior caused by custom processors, and the examples are provided as-is with no guarantees.**
190179
191-
- Socket server is optional: `BaseProcessorSocket` supports `start_server(...)`.
192-
- Connections are tracked in `self.conns`.
193-
- `broadcast(payload)` sends to all clients; failing clients are dropped.
194-
- `stop()` closes clients and listener, joins threads, and attempts to wake `accept()` during shutdown.
180+
## Socket-based processors
195181

196-
> **Tip:** If you publish processors for others to use, keep module import side-effect free (define classes/functions only).
182+
The built-in `BaseProcessorSocket` in `dlc_processor_socket.py` demonstrates a simple approach for:
197183

198-
---
184+
- Accepting multiple clients
185+
- Receiving control messages, such as start and stop recording,
186+
- Broadcasting payloads to connected clients,
187+
- Cleaning up reliably on shutdown.
199188

200-
## Adding a new processor
189+
`BaseProcessorSocket` is a reusable base class and is not shown as a selectable processor in the GUI:
201190

202-
1) Create a new module file in a processor folder (or inside `dlclivegui/processors/`).
191+
```python
192+
PROCESSOR_DISCOVERABLE = False
193+
```
203194

204-
2) Define a processor class and metadata:
195+
Concrete subclasses defined in processor modules are discovered normally.
205196

206-
```python
207-
from dlclive import Processor
208-
from dlclivegui.processors import register_processor, PROCESSOR_REGISTRY
197+
### Key points
209198

210-
@register_processor
211-
class MyNewProcessor(Processor):
212-
PROCESSOR_NAME = "My New Processor"
213-
PROCESSOR_DESCRIPTION = "Does something cool"
214-
PROCESSOR_PARAMS = {
215-
"my_param": {"type": "bool", "default": True, "description": "Enable cool feature"}
216-
}
199+
- The socket server is optional: `BaseProcessorSocket` supports `start_server(...)`.
200+
- Connections are tracked in `self.conns`.
201+
- `broadcast(payload)` sends to all clients, and failing clients are dropped.
202+
- `stop()` closes clients and the listener, joins threads, and attempts to wake `accept()` during shutdown.
217203

218-
def process(self, pose, **kwargs):
219-
# Do something with pose
220-
return pose
204+
> **Tip:** If you publish processors for others to use, keep module imports side-effect free where possible. Define classes and functions during import, and initialize sockets, hardware, or other resources when the processor is instantiated.
221205
206+
## Adding a new processor
222207

223-
def get_available_processors():
224-
return {
225-
name: {
226-
"class": cls,
227-
"name": getattr(cls, "PROCESSOR_NAME", name),
228-
"description": getattr(cls, "PROCESSOR_DESCRIPTION", ""),
229-
"params": getattr(cls, "PROCESSOR_PARAMS", {}),
208+
1. Create a new module file in a processor folder or inside `dlclivegui/processors/`.
209+
210+
2. Define a processor class and metadata:
211+
```python
212+
from dlclive.processor import Processor
213+
214+
class MyNewProcessor(Processor):
215+
PROCESSOR_NAME = "My New Processor"
216+
PROCESSOR_DESCRIPTION = "Does something useful"
217+
PROCESSOR_PARAMS = {
218+
"my_param": {
219+
"type": "bool",
220+
"default": True,
221+
"description": "Enable optional behavior",
222+
}
230223
}
231-
for name, cls in PROCESSOR_REGISTRY.items()
232-
}
233-
```
234224

235-
3) Refresh processors in the GUI, select your processor, and start inference (with processor control enabled if required).
225+
def __init__(self, my_param: bool = True):
226+
super().__init__()
227+
self.my_param = my_param
228+
229+
def process(self, pose, **kwargs):
230+
# Do something with pose
231+
return pose
232+
```
233+
No registration decorator, module-level registry, or `get_available_processors()` function is required.
236234

237-
---
235+
3. Refresh processors in the GUI, select your processor, and start inference with processor control enabled if required.
238236

239237
## Parameter schema types
240238

241239
Supported `PROCESSOR_PARAMS` types:
242240

243-
- `"bool"` — checkbox
244-
- `"int"` — integer input
245-
- `"float"` — float input
246-
- `"str"` — string input
247-
- `"bytes"` — string that gets encoded to bytes
248-
- `"tuple"` — tuple (e.g., `(host, port)`)
249-
- `"dict"` — dictionary
250-
- `"list"` — list
241+
- `"bool"`: checkbox
242+
- `"int"`: integer input
243+
- `"float"`: float input
244+
- `"str"`: string input
245+
- `"bytes"`: string that gets encoded to bytes
246+
- `"tuple"`: tuple, for example `(host, port)`
247+
- `"dict"`: dictionary
248+
- `"list"`: list
251249

252-
---
250+
The processor constructor remains the base definition of accepted arguments and values.
253251

254252
## Notes on external processors
255253

256-
External processors are arbitrary Python code. Only load processors you trust.
257-
258-
259-
260-
## License
254+
External processors are arbitrary Python code and are imported during discovery. Only load processors you trust.
261255

262-
This project is distributed under its project license.
263-
See `LICENSE` in the repository.
256+
Where possible, processor modules should avoid import-time side effects and initialize files, sockets, hardware, or other resources only when the processor is instantiated.

0 commit comments

Comments
 (0)