learning-loop-node 0.10.12__py3-none-any.whl → 0.10.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of learning-loop-node might be problematic. Click here for more details.
- learning_loop_node/data_classes/detections.py +2 -1
- learning_loop_node/detector/detector_logic.py +7 -1
- learning_loop_node/detector/detector_node.py +65 -45
- learning_loop_node/detector/inbox_filter/relevance_filter.py +9 -3
- learning_loop_node/detector/outbox.py +8 -1
- learning_loop_node/detector/rest/about.py +1 -0
- learning_loop_node/detector/rest/backdoor_controls.py +1 -3
- learning_loop_node/detector/rest/detect.py +12 -5
- learning_loop_node/helpers/log_conf.py +5 -0
- learning_loop_node/node.py +4 -1
- learning_loop_node/rest.py +32 -0
- learning_loop_node/tests/detector/conftest.py +34 -0
- learning_loop_node/tests/detector/test_detector_node.py +86 -0
- learning_loop_node/trainer/io_helpers.py +3 -6
- learning_loop_node/trainer/trainer_logic.py +4 -4
- learning_loop_node/trainer/trainer_logic_generic.py +8 -8
- learning_loop_node/trainer/trainer_node.py +1 -1
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.13.dist-info}/METADATA +16 -15
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.13.dist-info}/RECORD +20 -18
- {learning_loop_node-0.10.12.dist-info → learning_loop_node-0.10.13.dist-info}/WHEEL +0 -0
|
@@ -118,7 +118,8 @@ class Detections():
|
|
|
118
118
|
classification_detections: List[ClassificationDetection] = field(default_factory=list)
|
|
119
119
|
tags: List[str] = field(default_factory=list)
|
|
120
120
|
date: Optional[str] = field(default_factory=current_datetime)
|
|
121
|
-
image_id: Optional[str] = None # used for detection of trainers
|
|
121
|
+
image_id: Optional[str] = None # (actually UUID) used for detection of trainers
|
|
122
|
+
source: Optional[str] = None
|
|
122
123
|
|
|
123
124
|
def __len__(self):
|
|
124
125
|
return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from abc import abstractmethod
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import List, Optional
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
@@ -46,6 +46,12 @@ class DetectorLogic():
|
|
|
46
46
|
def init(self):
|
|
47
47
|
"""Called when a (new) model was loaded. Initialize the model. Model information available via `self.model_info`"""
|
|
48
48
|
|
|
49
|
+
def evaluate_with_all_info(self, image: np.ndarray, tags: List[str], source: Optional[str] = None) -> Detections: # pylint: disable=unused-argument
|
|
50
|
+
"""Called by the detector node when an image should be evaluated (REST or SocketIO).
|
|
51
|
+
Tags, source come from the caller and may be used in this function.
|
|
52
|
+
By default, this function simply calls `evaluate`"""
|
|
53
|
+
return self.evaluate(image)
|
|
54
|
+
|
|
49
55
|
@abstractmethod
|
|
50
56
|
def evaluate(self, image: np.ndarray) -> Detections:
|
|
51
57
|
"""Evaluate the image and return the detections.
|
|
@@ -9,9 +9,9 @@ from threading import Thread
|
|
|
9
9
|
from typing import Dict, List, Optional, Union
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
|
+
import socketio
|
|
12
13
|
from dacite import from_dict
|
|
13
14
|
from fastapi.encoders import jsonable_encoder
|
|
14
|
-
from fastapi_socketio import SocketManager
|
|
15
15
|
from socketio import AsyncClient
|
|
16
16
|
|
|
17
17
|
from ..data_classes import Category, Context, Detections, DetectionStatus, ModelInformation, Shape
|
|
@@ -41,7 +41,7 @@ class DetectorNode(Node):
|
|
|
41
41
|
self.organization = environment_reader.organization()
|
|
42
42
|
self.project = environment_reader.project()
|
|
43
43
|
assert self.organization and self.project, 'Detector node needs an organization and an project'
|
|
44
|
-
self.log.info(
|
|
44
|
+
self.log.info('Using %s/%s', self.organization, self.project)
|
|
45
45
|
self.operation_mode: OperationMode = OperationMode.Startup
|
|
46
46
|
self.connected_clients: List[str] = []
|
|
47
47
|
|
|
@@ -70,7 +70,7 @@ class DetectorNode(Node):
|
|
|
70
70
|
self.include_router(rest_outbox_mode.router, tags=["outbox_mode"])
|
|
71
71
|
self.include_router(rest_version_control.router, tags=["model_version"])
|
|
72
72
|
|
|
73
|
-
if use_backdoor_controls:
|
|
73
|
+
if use_backdoor_controls or os.environ.get('USE_BACKDOOR_CONTROLS', '0').lower() in ('1', 'true'):
|
|
74
74
|
self.include_router(backdoor_controls.router)
|
|
75
75
|
|
|
76
76
|
self.setup_sio_server()
|
|
@@ -126,22 +126,32 @@ class DetectorNode(Node):
|
|
|
126
126
|
|
|
127
127
|
def setup_sio_server(self) -> None:
|
|
128
128
|
"""The DetectorNode acts as a SocketIO server. This method sets up the server and defines the event handlers."""
|
|
129
|
-
|
|
130
129
|
# pylint: disable=unused-argument
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
# Initialize the Socket.IO server
|
|
132
|
+
self.sio = socketio.AsyncServer(async_mode='asgi')
|
|
133
|
+
# Initialize and mount the ASGI app
|
|
134
|
+
self.sio_app = socketio.ASGIApp(self.sio, socketio_path='/socket.io')
|
|
135
|
+
self.mount('/ws', self.sio_app)
|
|
136
|
+
# Register event handlers
|
|
137
|
+
|
|
138
|
+
self.log.info('>>>>>>>>>>>>>>>>>>>>>>> Setting up the SIO server')
|
|
139
|
+
|
|
140
|
+
@self.sio.event
|
|
141
|
+
async def detect(sid, data: Dict) -> Dict:
|
|
142
|
+
self.log.debug('running detect via socketio')
|
|
134
143
|
try:
|
|
135
144
|
np_image = np.frombuffer(data['image'], np.uint8)
|
|
136
145
|
det = await self.get_detections(
|
|
137
146
|
raw_image=np_image,
|
|
138
147
|
camera_id=data.get('camera-id', None) or data.get('mac', None),
|
|
139
148
|
tags=data.get('tags', []),
|
|
140
|
-
|
|
149
|
+
source=data.get('source', None),
|
|
150
|
+
autoupload=data.get('autoupload', None)
|
|
141
151
|
)
|
|
142
152
|
if det is None:
|
|
143
153
|
return {'error': 'no model loaded'}
|
|
144
|
-
self.log.
|
|
154
|
+
self.log.debug('detect via socketio finished')
|
|
145
155
|
return det
|
|
146
156
|
except Exception as e:
|
|
147
157
|
self.log.exception('could not detect via socketio')
|
|
@@ -149,12 +159,14 @@ class DetectorNode(Node):
|
|
|
149
159
|
f.write(data['image'])
|
|
150
160
|
return {'error': str(e)}
|
|
151
161
|
|
|
152
|
-
|
|
162
|
+
@self.sio.event
|
|
163
|
+
async def info(sid) -> Union[str, Dict]:
|
|
153
164
|
if self.detector_logic.is_initialized:
|
|
154
165
|
return asdict(self.detector_logic.model_info)
|
|
155
166
|
return 'No model loaded'
|
|
156
167
|
|
|
157
|
-
|
|
168
|
+
@self.sio.event
|
|
169
|
+
async def upload(sid, data: Dict) -> Optional[Dict]:
|
|
158
170
|
'''upload an image with detections'''
|
|
159
171
|
|
|
160
172
|
detection_data = data.get('detections', {})
|
|
@@ -171,38 +183,33 @@ class DetectorNode(Node):
|
|
|
171
183
|
tags = data.get('tags', [])
|
|
172
184
|
tags.append('picked_by_system')
|
|
173
185
|
|
|
186
|
+
source = data.get('source', None)
|
|
187
|
+
|
|
174
188
|
loop = asyncio.get_event_loop()
|
|
175
189
|
try:
|
|
176
|
-
await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags)
|
|
190
|
+
await loop.run_in_executor(None, self.outbox.save, data['image'], detections, tags, source)
|
|
177
191
|
except Exception as e:
|
|
178
192
|
self.log.exception('could not upload via socketio')
|
|
179
193
|
return {'error': str(e)}
|
|
180
194
|
return None
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
@self.sio.event
|
|
197
|
+
def connect(sid, environ, auth) -> None:
|
|
183
198
|
self.connected_clients.append(sid)
|
|
184
199
|
|
|
185
|
-
print('>>>>>>>>>>>>>>>>>>>>>>> setting up sio server', flush=True)
|
|
186
|
-
|
|
187
|
-
self.sio_server = SocketManager(app=self)
|
|
188
|
-
self.sio_server.on('detect', _detect)
|
|
189
|
-
self.sio_server.on('info', _info)
|
|
190
|
-
self.sio_server.on('upload', _upload)
|
|
191
|
-
self.sio_server.on('connect', _connect)
|
|
192
|
-
|
|
193
200
|
async def _check_for_update(self) -> None:
|
|
194
201
|
if self.operation_mode == OperationMode.Startup:
|
|
195
202
|
return
|
|
196
203
|
try:
|
|
197
|
-
self.log.info(
|
|
204
|
+
self.log.info('Current operation mode is %s', self.operation_mode)
|
|
198
205
|
try:
|
|
199
206
|
await self.sync_status_with_learning_loop()
|
|
200
207
|
except Exception as e:
|
|
201
|
-
self.log.error(
|
|
208
|
+
self.log.error('Could not check for updates: %s', e)
|
|
202
209
|
return
|
|
203
210
|
|
|
204
211
|
if self.operation_mode != OperationMode.Idle:
|
|
205
|
-
self.log.info(
|
|
212
|
+
self.log.info('not checking for updates; operation mode is %s', self.operation_mode)
|
|
206
213
|
return
|
|
207
214
|
|
|
208
215
|
self.status.reset_error('update_model')
|
|
@@ -210,11 +217,11 @@ class DetectorNode(Node):
|
|
|
210
217
|
self.log.info('not checking for updates; no target model selected')
|
|
211
218
|
return
|
|
212
219
|
|
|
213
|
-
current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None
|
|
220
|
+
current_version = self.detector_logic._model_info.version if self.detector_logic._model_info is not None else None # pylint: disable=protected-access
|
|
214
221
|
|
|
215
222
|
if not self.detector_logic.is_initialized or self.target_model.version != current_version:
|
|
216
|
-
self.log.info(
|
|
217
|
-
|
|
223
|
+
self.log.info('Current model "%s" needs to be updated to %s',
|
|
224
|
+
current_version or "-", self.target_model.version)
|
|
218
225
|
|
|
219
226
|
with step_into(GLOBALS.data_folder):
|
|
220
227
|
model_symlink = 'model'
|
|
@@ -232,7 +239,7 @@ class DetectorNode(Node):
|
|
|
232
239
|
except Exception:
|
|
233
240
|
pass
|
|
234
241
|
os.symlink(target_model_folder, model_symlink)
|
|
235
|
-
self.log.info(
|
|
242
|
+
self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
|
|
236
243
|
|
|
237
244
|
self.detector_logic.load_model()
|
|
238
245
|
try:
|
|
@@ -283,13 +290,13 @@ class DetectorNode(Node):
|
|
|
283
290
|
model_format=self.detector_logic.model_format,
|
|
284
291
|
)
|
|
285
292
|
|
|
286
|
-
self.log.info(
|
|
293
|
+
self.log.info('sending status %s', status)
|
|
287
294
|
response = await self.sio_client.call('update_detector', (self.organization, self.project, jsonable_encoder(asdict(status))))
|
|
288
295
|
|
|
289
296
|
assert response is not None
|
|
290
297
|
socket_response = from_dict(data_class=SocketResponse, data=response)
|
|
291
298
|
if not socket_response.success:
|
|
292
|
-
self.log.error(
|
|
299
|
+
self.log.error('Statusupdate failed: %s', response)
|
|
293
300
|
raise Exception(f'Statusupdate failed: {response}')
|
|
294
301
|
|
|
295
302
|
assert socket_response.payload is not None
|
|
@@ -303,19 +310,19 @@ class DetectorNode(Node):
|
|
|
303
310
|
|
|
304
311
|
if self.version_control == rest_version_control.VersionMode.FollowLoop:
|
|
305
312
|
self.target_model = self.loop_deployment_target
|
|
306
|
-
self.log.info(
|
|
313
|
+
self.log.info('After sending status. Target_model is %s', self.target_model.version)
|
|
307
314
|
|
|
308
315
|
async def set_operation_mode(self, mode: OperationMode):
|
|
309
316
|
self.operation_mode = mode
|
|
310
317
|
try:
|
|
311
318
|
await self.sync_status_with_learning_loop()
|
|
312
319
|
except Exception as e:
|
|
313
|
-
self.log.warning(
|
|
320
|
+
self.log.warning('Operation mode set to %s, but sync failed: %s', mode, e)
|
|
314
321
|
|
|
315
322
|
def reload(self, reason: str):
|
|
316
323
|
'''provide a cause for the reload'''
|
|
317
324
|
|
|
318
|
-
self.log.info(
|
|
325
|
+
self.log.info('########## reloading app because %s', reason)
|
|
319
326
|
if os.path.isfile('/app/app_code/restart/restart.py'):
|
|
320
327
|
subprocess.call(['touch', '/app/app_code/restart/restart.py'])
|
|
321
328
|
elif os.path.isfile('/app/main.py'):
|
|
@@ -325,32 +332,36 @@ class DetectorNode(Node):
|
|
|
325
332
|
else:
|
|
326
333
|
self.log.error('could not reload app')
|
|
327
334
|
|
|
328
|
-
async def get_detections(self,
|
|
329
|
-
|
|
335
|
+
async def get_detections(self,
|
|
336
|
+
raw_image: np.ndarray,
|
|
337
|
+
camera_id: Optional[str],
|
|
338
|
+
tags: List[str],
|
|
339
|
+
source: Optional[str] = None,
|
|
340
|
+
autoupload: Optional[str] = None) -> Optional[Dict]:
|
|
341
|
+
""" Main processing function for the detector node when an image is received via REST or SocketIO.
|
|
342
|
+
This function infers the detections from the image, cares about uploading to the loop and returns the detections as a dictionary.
|
|
343
|
+
Note: raw_image is a numpy array of type uint8, but not in the correct shape!
|
|
330
344
|
It can be converted e.g. using cv2.imdecode(raw_image, cv2.IMREAD_COLOR)"""
|
|
331
|
-
|
|
345
|
+
|
|
332
346
|
await self.detection_lock.acquire()
|
|
333
|
-
|
|
347
|
+
loop = asyncio.get_event_loop()
|
|
348
|
+
detections = await loop.run_in_executor(None, self.detector_logic.evaluate_with_all_info, raw_image, tags, source)
|
|
334
349
|
self.detection_lock.release()
|
|
335
|
-
for seg_detection in detections.segmentation_detections:
|
|
336
|
-
if isinstance(seg_detection.shape, Shape):
|
|
337
|
-
shapes = ','.join([str(value) for p in seg_detection.shape.points for _,
|
|
338
|
-
value in asdict(p).items()])
|
|
339
|
-
seg_detection.shape = shapes # TODO This seems to be a quick fix.. check how loop upload detections deals with this
|
|
340
350
|
|
|
351
|
+
fix_shape_detections(detections)
|
|
341
352
|
n_bo, n_cl = len(detections.box_detections), len(detections.classification_detections)
|
|
342
353
|
n_po, n_se = len(detections.point_detections), len(detections.segmentation_detections)
|
|
343
|
-
self.log.
|
|
354
|
+
self.log.debug('Detected: %d boxes, %d points, %d segs, %d classes', n_bo, n_po, n_se, n_cl)
|
|
344
355
|
|
|
345
356
|
if autoupload is None or autoupload == 'filtered': # NOTE default is filtered
|
|
346
357
|
Thread(target=self.relevance_filter.may_upload_detections,
|
|
347
|
-
args=(detections, camera_id, raw_image, tags)).start()
|
|
358
|
+
args=(detections, camera_id, raw_image, tags, source)).start()
|
|
348
359
|
elif autoupload == 'all':
|
|
349
|
-
Thread(target=self.outbox.save, args=(raw_image, detections, tags)).start()
|
|
360
|
+
Thread(target=self.outbox.save, args=(raw_image, detections, tags, source)).start()
|
|
350
361
|
elif autoupload == 'disabled':
|
|
351
362
|
pass
|
|
352
363
|
else:
|
|
353
|
-
self.log.error(
|
|
364
|
+
self.log.error('unknown autoupload value %s', autoupload)
|
|
354
365
|
return jsonable_encoder(asdict(detections))
|
|
355
366
|
|
|
356
367
|
async def upload_images(self, images: List[bytes]):
|
|
@@ -393,3 +404,12 @@ def step_into(new_dir):
|
|
|
393
404
|
yield
|
|
394
405
|
finally:
|
|
395
406
|
os.chdir(previous_dir)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def fix_shape_detections(detections: Detections):
|
|
410
|
+
# TODO This is a quick fix.. check how loop upload detections deals with this
|
|
411
|
+
for seg_detection in detections.segmentation_detections:
|
|
412
|
+
if isinstance(seg_detection.shape, Shape):
|
|
413
|
+
points = ','.join([str(value) for p in seg_detection.shape.points for _,
|
|
414
|
+
value in asdict(p).items()])
|
|
415
|
+
seg_detection.shape = points
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
2
|
|
|
3
3
|
from ...data_classes.detections import Detections
|
|
4
4
|
from ..outbox import Outbox
|
|
@@ -11,7 +11,13 @@ class RelevanceFilter():
|
|
|
11
11
|
self.cam_histories: Dict[str, CamObservationHistory] = {}
|
|
12
12
|
self.outbox: Outbox = outbox
|
|
13
13
|
|
|
14
|
-
def may_upload_detections(self,
|
|
14
|
+
def may_upload_detections(self,
|
|
15
|
+
dets: Detections,
|
|
16
|
+
cam_id: str,
|
|
17
|
+
raw_image: bytes,
|
|
18
|
+
tags: List[str],
|
|
19
|
+
source: Optional[str] = None
|
|
20
|
+
) -> List[str]:
|
|
15
21
|
for group in self.cam_histories.values():
|
|
16
22
|
group.forget_old_detections()
|
|
17
23
|
|
|
@@ -23,5 +29,5 @@ class RelevanceFilter():
|
|
|
23
29
|
if len(causes) > 0:
|
|
24
30
|
tags = tags if tags is not None else []
|
|
25
31
|
tags.extend(causes)
|
|
26
|
-
self.outbox.save(raw_image, dets, tags)
|
|
32
|
+
self.outbox.save(raw_image, dets, tags, source)
|
|
27
33
|
return causes
|
|
@@ -55,7 +55,13 @@ class Outbox():
|
|
|
55
55
|
|
|
56
56
|
self.upload_counter = 0
|
|
57
57
|
|
|
58
|
-
def save(self,
|
|
58
|
+
def save(self,
|
|
59
|
+
image: bytes,
|
|
60
|
+
detections: Optional[Detections] = None,
|
|
61
|
+
tags: Optional[List[str]] = None,
|
|
62
|
+
source: Optional[str] = None
|
|
63
|
+
) -> None:
|
|
64
|
+
|
|
59
65
|
if not self._is_valid_jpg(image):
|
|
60
66
|
self.log.error('Invalid jpg image')
|
|
61
67
|
return
|
|
@@ -71,6 +77,7 @@ class Outbox():
|
|
|
71
77
|
tmp = f'{GLOBALS.data_folder}/tmp/{identifier}'
|
|
72
78
|
detections.tags = tags
|
|
73
79
|
detections.date = identifier
|
|
80
|
+
detections.source = source or 'unknown'
|
|
74
81
|
os.makedirs(tmp, exist_ok=True)
|
|
75
82
|
|
|
76
83
|
with open(tmp + '/image.json', 'w') as f:
|
|
@@ -22,4 +22,5 @@ async def get_about(request: Request):
|
|
|
22
22
|
'state': app.status.state,
|
|
23
23
|
'model_info': app.detector_logic._model_info, # pylint: disable=protected-access
|
|
24
24
|
'target_model': app.target_model.version if app.target_model is not None else 'None',
|
|
25
|
+
'version_control': app.version_control.value,
|
|
25
26
|
}
|
|
@@ -22,10 +22,8 @@ async def _socketio(request: Request):
|
|
|
22
22
|
curl -X PUT -d "on" http://localhost:8007/socketio
|
|
23
23
|
'''
|
|
24
24
|
state = str(await request.body(), 'utf-8')
|
|
25
|
-
|
|
25
|
+
detector_node: 'DetectorNode' = request.app
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
async def _switch_socketio(state: str, detector_node: 'DetectorNode'):
|
|
29
27
|
if state == 'off':
|
|
30
28
|
logging.info('BC: turning socketio off')
|
|
31
29
|
await detector_node.sio_client.disconnect()
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from fastapi import APIRouter, File, Header, Request, UploadFile
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..detector_node import DetectorNode
|
|
10
|
+
|
|
11
|
+
|
|
8
12
|
router = APIRouter()
|
|
9
13
|
|
|
10
14
|
|
|
@@ -15,6 +19,7 @@ async def http_detect(
|
|
|
15
19
|
camera_id: Optional[str] = Header(None),
|
|
16
20
|
mac: Optional[str] = Header(None),
|
|
17
21
|
tags: Optional[str] = Header(None),
|
|
22
|
+
source: Optional[str] = Header(None),
|
|
18
23
|
autoupload: Optional[str] = Header(None),
|
|
19
24
|
):
|
|
20
25
|
"""
|
|
@@ -35,10 +40,12 @@ async def http_detect(
|
|
|
35
40
|
raise Exception(f'Uploaded file {file.filename} is no image file.') from exc
|
|
36
41
|
|
|
37
42
|
try:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
app: 'DetectorNode' = request.app
|
|
44
|
+
detections = await app.get_detections(raw_image=np_image,
|
|
45
|
+
camera_id=camera_id or mac or None,
|
|
46
|
+
tags=tags.split(',') if tags else [],
|
|
47
|
+
source=source,
|
|
48
|
+
autoupload=autoupload)
|
|
42
49
|
except Exception as exc:
|
|
43
50
|
logging.exception(f'Error during detection of image {file.filename}.')
|
|
44
51
|
raise Exception(f'Error during detection of image {file.filename}.') from exc
|
learning_loop_node/node.py
CHANGED
|
@@ -18,6 +18,7 @@ from .data_exchanger import DataExchanger
|
|
|
18
18
|
from .helpers import log_conf
|
|
19
19
|
from .helpers.misc import ensure_socket_response, read_or_create_uuid
|
|
20
20
|
from .loop_communication import LoopCommunicator
|
|
21
|
+
from .rest import router
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class Node(FastAPI):
|
|
@@ -41,7 +42,7 @@ class Node(FastAPI):
|
|
|
41
42
|
self.uuid = uuid or read_or_create_uuid(self.name)
|
|
42
43
|
self.needs_login = needs_login
|
|
43
44
|
|
|
44
|
-
self.log = logging.getLogger()
|
|
45
|
+
self.log = logging.getLogger('Node')
|
|
45
46
|
self.loop_communicator = LoopCommunicator()
|
|
46
47
|
self.websocket_url = self.loop_communicator.websocket_url()
|
|
47
48
|
self.data_exchanger = DataExchanger(None, self.loop_communicator)
|
|
@@ -56,6 +57,8 @@ class Node(FastAPI):
|
|
|
56
57
|
|
|
57
58
|
self.repeat_task: Any = None
|
|
58
59
|
|
|
60
|
+
self.include_router(router)
|
|
61
|
+
|
|
59
62
|
@property
|
|
60
63
|
def sio_client(self) -> AsyncClient:
|
|
61
64
|
if self._sio_client is None:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Request, HTTPException
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .node import Node
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
logger = logging.getLogger('Node.rest')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.put("/debug_logging")
|
|
15
|
+
async def _debug_logging(request: Request):
|
|
16
|
+
'''
|
|
17
|
+
Example Usage
|
|
18
|
+
|
|
19
|
+
curl -X PUT -d "on" http://localhost:8007/debug_logging
|
|
20
|
+
'''
|
|
21
|
+
state = str(await request.body(), 'utf-8')
|
|
22
|
+
node: 'Node' = request.app
|
|
23
|
+
|
|
24
|
+
if state == 'off':
|
|
25
|
+
logger.info('turning debug logging off')
|
|
26
|
+
node.log.setLevel('INFO')
|
|
27
|
+
return 'off'
|
|
28
|
+
if state == 'on':
|
|
29
|
+
logger.info('turning debug logging on')
|
|
30
|
+
node.log.setLevel('DEBUG')
|
|
31
|
+
return 'on'
|
|
32
|
+
raise HTTPException(status_code=400, detail='Invalid state')
|
|
@@ -12,6 +12,9 @@ import pytest
|
|
|
12
12
|
import socketio
|
|
13
13
|
import uvicorn
|
|
14
14
|
|
|
15
|
+
from learning_loop_node.data_classes import BoxDetection, Detections
|
|
16
|
+
from learning_loop_node.detector.detector_logic import DetectorLogic
|
|
17
|
+
|
|
15
18
|
from ...detector.detector_node import DetectorNode
|
|
16
19
|
from ...detector.outbox import Outbox
|
|
17
20
|
from ...globals import GLOBALS
|
|
@@ -113,6 +116,37 @@ def get_outbox_files(outbox: Outbox):
|
|
|
113
116
|
files = glob(f'{outbox.path}/**/*', recursive=True)
|
|
114
117
|
return [file for file in files if os.path.isfile(file)]
|
|
115
118
|
|
|
119
|
+
|
|
120
|
+
@pytest.fixture
|
|
121
|
+
def mock_detector_logic():
|
|
122
|
+
class MockDetectorLogic(DetectorLogic): # pylint: disable=abstract-method
|
|
123
|
+
def __init__(self):
|
|
124
|
+
super().__init__('mock')
|
|
125
|
+
self.detections = Detections(
|
|
126
|
+
box_detections=[BoxDetection(category_name="test",
|
|
127
|
+
category_id="1",
|
|
128
|
+
confidence=0.9,
|
|
129
|
+
x=0, y=0, width=10, height=10,
|
|
130
|
+
model_name="mock",
|
|
131
|
+
)]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_initialized(self):
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def evaluate_with_all_info(self, image, tags, source): # pylint: disable=signature-differs
|
|
139
|
+
return self.detections
|
|
140
|
+
|
|
141
|
+
return MockDetectorLogic()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.fixture
|
|
145
|
+
def detector_node(mock_detector_logic):
|
|
146
|
+
os.environ['ORGANIZATION'] = 'test_organization'
|
|
147
|
+
os.environ['PROJECT'] = 'test_project'
|
|
148
|
+
return DetectorNode(name="test_node", detector=mock_detector_logic)
|
|
149
|
+
|
|
116
150
|
# ====================================== REDUNDANT FIXTURES IN ALL CONFTESTS ! ======================================
|
|
117
151
|
|
|
118
152
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from learning_loop_node.detector.detector_node import DetectorNode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_get_detections(detector_node: DetectorNode, monkeypatch):
|
|
9
|
+
# Mock raw image data
|
|
10
|
+
raw_image = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
11
|
+
|
|
12
|
+
# Mock relevance_filter and outbox
|
|
13
|
+
filtered_upload_called = False
|
|
14
|
+
save_called = False
|
|
15
|
+
|
|
16
|
+
save_args = []
|
|
17
|
+
|
|
18
|
+
def mock_filtered_upload(*args, **kwargs): # pylint: disable=unused-argument
|
|
19
|
+
nonlocal filtered_upload_called
|
|
20
|
+
filtered_upload_called = True
|
|
21
|
+
|
|
22
|
+
def mock_save(*args, **kwargs):
|
|
23
|
+
nonlocal save_called
|
|
24
|
+
nonlocal save_args
|
|
25
|
+
save_called = True
|
|
26
|
+
save_args = (args, kwargs)
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(detector_node.relevance_filter, "may_upload_detections", mock_filtered_upload)
|
|
29
|
+
monkeypatch.setattr(detector_node.outbox, "save", mock_save)
|
|
30
|
+
|
|
31
|
+
# Test cases
|
|
32
|
+
test_cases = [
|
|
33
|
+
(None, True, False),
|
|
34
|
+
("filtered", True, False),
|
|
35
|
+
("all", False, True),
|
|
36
|
+
("disabled", False, False),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
expected_save_args = {
|
|
40
|
+
'image': raw_image,
|
|
41
|
+
'detections': detector_node.detector_logic.detections, # type: ignore
|
|
42
|
+
'tags': ['test_tag'],
|
|
43
|
+
'source': 'test_source',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for autoupload, expect_filtered, expect_all in test_cases:
|
|
47
|
+
filtered_upload_called = False
|
|
48
|
+
save_called = False
|
|
49
|
+
|
|
50
|
+
result = await detector_node.get_detections(
|
|
51
|
+
raw_image=raw_image,
|
|
52
|
+
camera_id="test_camera",
|
|
53
|
+
tags=["test_tag"],
|
|
54
|
+
source="test_source",
|
|
55
|
+
autoupload=autoupload
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Check if detections were processed
|
|
59
|
+
assert result is not None
|
|
60
|
+
assert "box_detections" in result
|
|
61
|
+
assert len(result["box_detections"]) == 1
|
|
62
|
+
assert result["box_detections"][0]["category_name"] == "test"
|
|
63
|
+
|
|
64
|
+
# Check if the correct upload method was called
|
|
65
|
+
assert filtered_upload_called == expect_filtered
|
|
66
|
+
assert save_called == expect_all
|
|
67
|
+
|
|
68
|
+
if save_called:
|
|
69
|
+
save_pos_args, save_kwargs = save_args # pylint: disable=unbalanced-tuple-unpacking
|
|
70
|
+
expected_values = list(expected_save_args.values())
|
|
71
|
+
assert len(save_pos_args) + len(save_kwargs) == len(expected_values)
|
|
72
|
+
|
|
73
|
+
# Check positional arguments
|
|
74
|
+
for arg, expected in zip(save_pos_args, expected_values[:len(save_pos_args)]):
|
|
75
|
+
if isinstance(arg, (list, np.ndarray)):
|
|
76
|
+
assert np.array_equal(arg, expected)
|
|
77
|
+
else:
|
|
78
|
+
assert arg == expected
|
|
79
|
+
|
|
80
|
+
# Check keyword arguments
|
|
81
|
+
for key, value in save_kwargs.items():
|
|
82
|
+
expected = expected_save_args[key]
|
|
83
|
+
if isinstance(value, (list, np.ndarray)):
|
|
84
|
+
assert np.array_equal(value, expected)
|
|
85
|
+
else:
|
|
86
|
+
assert value == expected
|
|
@@ -16,12 +16,9 @@ from ..loop_communication import LoopCommunicator
|
|
|
16
16
|
|
|
17
17
|
class EnvironmentVars:
|
|
18
18
|
def __init__(self) -> None:
|
|
19
|
-
self.restart_after_training = os.environ.get(
|
|
20
|
-
|
|
21
|
-
self.
|
|
22
|
-
'KEEP_OLD_TRAININGS', 'FALSE').lower() in ['true', '1']
|
|
23
|
-
self.inference_batch_size = int(
|
|
24
|
-
os.environ.get('INFERENCE_BATCH_SIZE', '10'))
|
|
19
|
+
self.restart_after_training = os.environ.get('RESTART_AFTER_TRAINING', 'FALSE').lower() in ['true', '1']
|
|
20
|
+
self.keep_old_trainings = os.environ.get('KEEP_OLD_TRAININGS', 'FALSE').lower() in ['true', '1']
|
|
21
|
+
self.inference_batch_size = int(os.environ.get('INFERENCE_BATCH_SIZE', '10'))
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
class LastTrainingIO:
|
|
@@ -25,7 +25,7 @@ class TrainerLogic(TrainerLogicGeneric):
|
|
|
25
25
|
self._detection_progress: Optional[float] = None
|
|
26
26
|
self._executor: Optional[Executor] = None
|
|
27
27
|
self.start_training_task: Optional[Coroutine] = None
|
|
28
|
-
self.inference_batch_size =
|
|
28
|
+
self.inference_batch_size = self._environment_vars.inference_batch_size
|
|
29
29
|
|
|
30
30
|
# ---------------------------------------- IMPLEMENTED ABSTRACT PROPERTIES ----------------------------------------
|
|
31
31
|
|
|
@@ -92,7 +92,7 @@ class TrainerLogic(TrainerLogicGeneric):
|
|
|
92
92
|
|
|
93
93
|
shutil.rmtree(tmp_folder, ignore_errors=True)
|
|
94
94
|
os.makedirs(tmp_folder)
|
|
95
|
-
logging.info(
|
|
95
|
+
logging.info('downloading detection model to %s', tmp_folder)
|
|
96
96
|
|
|
97
97
|
await self.node.data_exchanger.download_model(tmp_folder, context, model_id, self.model_format)
|
|
98
98
|
with open(f'{tmp_folder}/model.json', 'r') as f:
|
|
@@ -104,10 +104,10 @@ class TrainerLogic(TrainerLogicGeneric):
|
|
|
104
104
|
image_ids = []
|
|
105
105
|
for state, p in zip(['inbox', 'annotate', 'review', 'complete'], [0.1, 0.2, 0.3, 0.4]):
|
|
106
106
|
self._detection_progress = p
|
|
107
|
-
logging.info(
|
|
107
|
+
logging.info('fetching image ids of state %s', state)
|
|
108
108
|
new_ids = await self.node.data_exchanger.fetch_image_uuids(query_params=f'state={state}')
|
|
109
109
|
image_ids += new_ids
|
|
110
|
-
logging.info(
|
|
110
|
+
logging.info('downloading %d images', len(new_ids))
|
|
111
111
|
await self.node.data_exchanger.download_images(new_ids, image_folder)
|
|
112
112
|
self._detection_progress = 0.42
|
|
113
113
|
# await delete_corrupt_images(image_folder)
|
|
@@ -210,7 +210,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
210
210
|
|
|
211
211
|
self._active_training_io = ActiveTrainingIO(
|
|
212
212
|
self._training.training_folder, self.node.loop_communicator, context)
|
|
213
|
-
logger.info(
|
|
213
|
+
logger.info('new training initialized: %s', self._training)
|
|
214
214
|
|
|
215
215
|
async def _run(self) -> None:
|
|
216
216
|
"""Called on `begin_training` event from the Learning Loop.
|
|
@@ -229,8 +229,8 @@ class TrainerLogicGeneric(ABC):
|
|
|
229
229
|
self._may_restart()
|
|
230
230
|
else:
|
|
231
231
|
logger.info('CancelledError in _run - shutting down')
|
|
232
|
-
except Exception
|
|
233
|
-
logger.exception(
|
|
232
|
+
except Exception:
|
|
233
|
+
logger.exception('(Ignored) exception in trainer_logic._run:')
|
|
234
234
|
|
|
235
235
|
# ---------------------------------------- TRAINING STATES ----------------------------------------
|
|
236
236
|
|
|
@@ -271,7 +271,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
271
271
|
'''
|
|
272
272
|
|
|
273
273
|
await asyncio.sleep(0.1)
|
|
274
|
-
logger.info(
|
|
274
|
+
logger.info('Performing state: %s', state_during)
|
|
275
275
|
previous_state = self.training.training_state
|
|
276
276
|
self.training.training_state = state_during
|
|
277
277
|
await asyncio.sleep(0.1)
|
|
@@ -283,12 +283,12 @@ class TrainerLogicGeneric(ABC):
|
|
|
283
283
|
|
|
284
284
|
except asyncio.CancelledError:
|
|
285
285
|
if self.shutdown_event.is_set():
|
|
286
|
-
logger.info(
|
|
286
|
+
logger.info('CancelledError in %s - shutdown event set', state_during)
|
|
287
287
|
raise
|
|
288
|
-
logger.info(
|
|
288
|
+
logger.info('CancelledError in %s - cleaning up', state_during)
|
|
289
289
|
self.training.training_state = TrainerState.ReadyForCleanup
|
|
290
290
|
except CriticalError as e:
|
|
291
|
-
logger.error(
|
|
291
|
+
logger.error('CriticalError in %s - Exception: %s', state_during, e)
|
|
292
292
|
self.errors.set(error_key, str(e))
|
|
293
293
|
self.training.training_state = TrainerState.ReadyForCleanup
|
|
294
294
|
except Exception as e:
|
|
@@ -297,7 +297,7 @@ class TrainerLogicGeneric(ABC):
|
|
|
297
297
|
self.training.training_state = previous_state
|
|
298
298
|
return
|
|
299
299
|
else:
|
|
300
|
-
logger.info(
|
|
300
|
+
logger.info('Successfully finished state: %s', state_during)
|
|
301
301
|
if not reset_early:
|
|
302
302
|
self.errors.reset(error_key)
|
|
303
303
|
self.training.training_state = state_after
|
|
@@ -32,7 +32,7 @@ class TrainerNode(Node):
|
|
|
32
32
|
self.log.info(
|
|
33
33
|
f'Trainer started with an idle_timeout of {self.idle_timeout} seconds. Note that shutdown does not work if docker container has the restart policy set to always')
|
|
34
34
|
|
|
35
|
-
if use_backdoor_controls:
|
|
35
|
+
if use_backdoor_controls or os.environ.get('USE_BACKDOOR_CONTROLS', '0').lower() in ('1', 'true'):
|
|
36
36
|
self.include_router(backdoor_controls.router, tags=["controls"])
|
|
37
37
|
|
|
38
38
|
# ----------------------------------- NODE LIVECYCLE METHODS --------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: learning-loop-node
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.13
|
|
4
4
|
Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
|
|
5
5
|
Home-page: https://github.com/zauberzeug/learning_loop_node
|
|
6
6
|
License: MIT
|
|
@@ -57,20 +57,21 @@ To start a node you have to implement the logic by inheriting from the correspon
|
|
|
57
57
|
|
|
58
58
|
You can configure connection to our Learning Loop by specifying the following environment variables before starting:
|
|
59
59
|
|
|
60
|
-
| Name | Alias | Purpose | Required by
|
|
61
|
-
| ------------------------ | ------------ | ------------------------------------------------------------ |
|
|
62
|
-
| LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all
|
|
63
|
-
| LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector
|
|
64
|
-
| LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector
|
|
65
|
-
| LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.)
|
|
66
|
-
| LOOP_ORGANIZATION | ORGANIZATION | Organization name | Detector
|
|
67
|
-
| LOOP_PROJECT | PROJECT | Project name | Detector
|
|
68
|
-
| MIN_UNCERTAIN_THRESHOLD |
|
|
69
|
-
| MAX_UNCERTAIN_THRESHOLD |
|
|
70
|
-
| INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.)
|
|
71
|
-
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.)
|
|
72
|
-
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.)
|
|
73
|
-
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.)
|
|
60
|
+
| Name | Alias | Purpose | Required by |
|
|
61
|
+
| ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- |
|
|
62
|
+
| LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
|
|
63
|
+
| LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
|
|
64
|
+
| LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
|
|
65
|
+
| LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) |
|
|
66
|
+
| LOOP_ORGANIZATION | ORGANIZATION | Organization name | Detector |
|
|
67
|
+
| LOOP_PROJECT | PROJECT | Project name | Detector (opt.) |
|
|
68
|
+
| MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
|
|
69
|
+
| MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
|
|
70
|
+
| INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
|
|
71
|
+
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
|
|
72
|
+
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
|
|
73
|
+
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
|
|
74
|
+
| USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
|
|
74
75
|
|
|
75
76
|
#### Testing
|
|
76
77
|
|
|
@@ -4,22 +4,22 @@ learning_loop_node/annotation/annotator_logic.py,sha256=BTaopkJZkIf1CI5lfsVKsxbx
|
|
|
4
4
|
learning_loop_node/annotation/annotator_node.py,sha256=wk11CQtM3A0Dr7efCn_Mw2X7ql5xn2sgEJzrIeSBC6Q,4043
|
|
5
5
|
learning_loop_node/data_classes/__init__.py,sha256=wCX88lDgbb8V-gtVCVe9i-NvvZuMe5FX7eD_UJgYYXw,1305
|
|
6
6
|
learning_loop_node/data_classes/annotations.py,sha256=iInU0Nuy_oYT_sj4k_n-W0UShCBI2cHQYrt8imymbtM,1211
|
|
7
|
-
learning_loop_node/data_classes/detections.py,sha256=
|
|
7
|
+
learning_loop_node/data_classes/detections.py,sha256=iFSrnbvYvwhL8k9niz1BACn2QJV4RyW0DKezpmlUn9M,4398
|
|
8
8
|
learning_loop_node/data_classes/general.py,sha256=Bd0ngYhYvS_9OYOO6lAKEnDzLuSdPmR4I2YV-0DRsxs,4694
|
|
9
9
|
learning_loop_node/data_classes/socket_response.py,sha256=tIdt-oYf6ULoJIDYQCecNM9OtWR6_wJ9tL0Ksu83Vko,655
|
|
10
10
|
learning_loop_node/data_classes/training.py,sha256=hnMHZMk-WNRERyo7U97qL09v1tIdhnzPfTH-JgifLwU,6164
|
|
11
11
|
learning_loop_node/data_exchanger.py,sha256=U_MrBKSq1MbBwBmjrjxoIo_7xV4Lcwtk6uZDIgmhT_4,8914
|
|
12
12
|
learning_loop_node/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
learning_loop_node/detector/detector_logic.py,sha256=
|
|
14
|
-
learning_loop_node/detector/detector_node.py,sha256=
|
|
13
|
+
learning_loop_node/detector/detector_logic.py,sha256=Xlwc5yRUSe62gqcxdeLH7KXMujaW3JrCjfZo4iCWdHI,2087
|
|
14
|
+
learning_loop_node/detector/detector_node.py,sha256=xuNwXwJYdWZhe3IzP3oz8WUhREXF8k3XI97LvvZqFLQ,19181
|
|
15
15
|
learning_loop_node/detector/inbox_filter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
learning_loop_node/detector/inbox_filter/cam_observation_history.py,sha256=TD346I9ymtIP0_CJXCIKMRuiXbfVVanXNu_iHAwDd7Q,3318
|
|
17
|
-
learning_loop_node/detector/inbox_filter/relevance_filter.py,sha256=
|
|
18
|
-
learning_loop_node/detector/outbox.py,sha256=
|
|
17
|
+
learning_loop_node/detector/inbox_filter/relevance_filter.py,sha256=7_-x8D8Zf6KJeJXmiC2VrRHU8Ig_R98uhdXVwwX0N4M,1240
|
|
18
|
+
learning_loop_node/detector/outbox.py,sha256=HrYeS6XJLC-1kqq2hDufxXLRmOYGiBlz-m9B6HG5Ie8,8227
|
|
19
19
|
learning_loop_node/detector/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
learning_loop_node/detector/rest/about.py,sha256=
|
|
21
|
-
learning_loop_node/detector/rest/backdoor_controls.py,sha256=
|
|
22
|
-
learning_loop_node/detector/rest/detect.py,sha256=
|
|
20
|
+
learning_loop_node/detector/rest/about.py,sha256=VVICeSHONk0VneUvbkOYV-Wbey1sXSej3pJFl57rQig,677
|
|
21
|
+
learning_loop_node/detector/rest/backdoor_controls.py,sha256=ZtQAjxYpeskCHHQGjZd994eyq-u8LSrjObFtxLbU-Ds,1654
|
|
22
|
+
learning_loop_node/detector/rest/detect.py,sha256=a_0A_65GQhuI-9eIgwj6gVyNu_CC9lqSMfAAN9HWFhQ,2065
|
|
23
23
|
learning_loop_node/detector/rest/model_version_control.py,sha256=PKG7foFyNSvjoMhWCDb7w3mq-2e0bx5gq3ov7Rao8HU,3703
|
|
24
24
|
learning_loop_node/detector/rest/operation_mode.py,sha256=eIo6_56qyZECftf4AEN8wJMABIojC0TRazvWeg0Uj_s,1664
|
|
25
25
|
learning_loop_node/detector/rest/outbox_mode.py,sha256=anSZHB6jliz1t3fxrmEzgwNB62UHNdWNc9ZYOc5Nn9s,1018
|
|
@@ -29,18 +29,19 @@ learning_loop_node/globals.py,sha256=tgw_8RYOipPV9aYlyUhYtXfUxvJKRvfUk6u-qVAtZmY
|
|
|
29
29
|
learning_loop_node/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
learning_loop_node/helpers/environment_reader.py,sha256=OtCTDc0KT9r-SMygkZB_Mw-ZIJPfUZVyUzHJoDCgJP8,1658
|
|
31
31
|
learning_loop_node/helpers/gdrive_downloader.py,sha256=zeYJciTAJVRpu_eFjwgYLCpIa6hU1d71anqEBb564Rk,1145
|
|
32
|
-
learning_loop_node/helpers/log_conf.py,sha256=
|
|
32
|
+
learning_loop_node/helpers/log_conf.py,sha256=z_0PHh7U7DkJbSbKoSPyUfS7NhBHtRxXHdNcj67Hpbc,951
|
|
33
33
|
learning_loop_node/helpers/misc.py,sha256=j4is8Rv0ttnCqF-R-wP3xwEi67OI6IBJav5Woo5lyDk,7701
|
|
34
34
|
learning_loop_node/loop_communication.py,sha256=rG5MdavSTaREZ6OWfAUIT_qkkYPw3is2_FujLmHQeIc,6576
|
|
35
|
-
learning_loop_node/node.py,sha256=
|
|
35
|
+
learning_loop_node/node.py,sha256=M846fxhutHU2djpd2Kz512iMscLJFk4DVUVO8fe2xF4,8051
|
|
36
36
|
learning_loop_node/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
learning_loop_node/rest.py,sha256=5jxdfNJKbGklaVKufmprHclZF-_EKG67BeEV1XPW7mc,783
|
|
37
38
|
learning_loop_node/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
39
|
learning_loop_node/tests/annotator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
40
|
learning_loop_node/tests/annotator/conftest.py,sha256=G4ZvdZUdvPp9bYCzg3eEVkGCeXn9INZ3AcN7d5CyLkU,1931
|
|
40
41
|
learning_loop_node/tests/annotator/pytest.ini,sha256=8QdjmawLy1zAzXrJ88or1kpFDhJw0W5UOnDfGGs_igU,262
|
|
41
42
|
learning_loop_node/tests/annotator/test_annotator_node.py,sha256=TPNPPrQAxQ_zEecQcH7hlczgD3ABtTCNtUvWD1_oApk,1985
|
|
42
43
|
learning_loop_node/tests/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
-
learning_loop_node/tests/detector/conftest.py,sha256=
|
|
44
|
+
learning_loop_node/tests/detector/conftest.py,sha256=kBg4ujWtKI03IRrftOcMly17CVFPgxWvYFpMA40OkU4,5394
|
|
44
45
|
learning_loop_node/tests/detector/inbox_filter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
46
|
learning_loop_node/tests/detector/inbox_filter/test_observation.py,sha256=k4WYdvnuV7d_r7zI4M2aA8WuBjm0aycQ0vj1rGE2q4w,1370
|
|
46
47
|
learning_loop_node/tests/detector/inbox_filter/test_relevance_group.py,sha256=XjiMsS0LgvM0OkPf5-s2rjFbG7C42LTmz_rDVMGHKoY,7603
|
|
@@ -48,6 +49,7 @@ learning_loop_node/tests/detector/inbox_filter/test_unexpected_observations_coun
|
|
|
48
49
|
learning_loop_node/tests/detector/pytest.ini,sha256=8QdjmawLy1zAzXrJ88or1kpFDhJw0W5UOnDfGGs_igU,262
|
|
49
50
|
learning_loop_node/tests/detector/test.jpg,sha256=msA-vHPmvPiro_D102Qmn1fn4vNfooqYYEXPxZUmYpk,161390
|
|
50
51
|
learning_loop_node/tests/detector/test_client_communication.py,sha256=2gJARodJSDuJHgeN1_xLMbvDcPQkXpBXEefu7MOyePk,8998
|
|
52
|
+
learning_loop_node/tests/detector/test_detector_node.py,sha256=oLmmMu0EEiatAlpEA5rqPXA4VGYctAAspgVHdxV05_k,2924
|
|
51
53
|
learning_loop_node/tests/detector/test_outbox.py,sha256=5RMKQfuu1-rvpVCpEtt_D70bYgma-sIrTHWxHdTdU9Y,3001
|
|
52
54
|
learning_loop_node/tests/detector/test_relevance_filter.py,sha256=3VLhHKaxPzLYmiNZagvgg9ZHkPhWk4_-qpmkJw36wBU,2046
|
|
53
55
|
learning_loop_node/tests/detector/testing_detector.py,sha256=FeQroV85IvsT8dmalQBqf1FLNt_buCtZK3-lgtmbrBI,542
|
|
@@ -81,13 +83,13 @@ learning_loop_node/trainer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
81
83
|
learning_loop_node/trainer/downloader.py,sha256=Qk-oBcrGCVuWTVs3hvAJzQSqCIHPGZ7NXLJ_fAqvCoY,1469
|
|
82
84
|
learning_loop_node/trainer/exceptions.py,sha256=hLLDGncC6PLZjKg4lZBpu-QA8itQIxiuxExz1uptgnw,40
|
|
83
85
|
learning_loop_node/trainer/executor.py,sha256=-0BxDqmAI1NCiISi7Rw8McJQfgxxVy1gSa1epYuL3U0,3942
|
|
84
|
-
learning_loop_node/trainer/io_helpers.py,sha256=
|
|
86
|
+
learning_loop_node/trainer/io_helpers.py,sha256=hGEtNAQBSBbVB56U1ndwfP8qK5K4YIwMQrjCDcaMy9I,7218
|
|
85
87
|
learning_loop_node/trainer/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
86
88
|
learning_loop_node/trainer/rest/backdoor_controls.py,sha256=YQcG0KwxzKDNYeMtHrSwr26q__N7ty0o6Kar6CLWAd0,5869
|
|
87
89
|
learning_loop_node/trainer/test_executor.py,sha256=6BVGDN_6f5GEMMEvDLSG1yzMybSvgXaP5uYpSfsVPP0,2224
|
|
88
|
-
learning_loop_node/trainer/trainer_logic.py,sha256=
|
|
89
|
-
learning_loop_node/trainer/trainer_logic_generic.py,sha256=
|
|
90
|
-
learning_loop_node/trainer/trainer_node.py,sha256=
|
|
91
|
-
learning_loop_node-0.10.
|
|
92
|
-
learning_loop_node-0.10.
|
|
93
|
-
learning_loop_node-0.10.
|
|
90
|
+
learning_loop_node/trainer/trainer_logic.py,sha256=PlYExIskU9pWJO0e9m_0KJnUdOI10GtW0oDOevYmg1o,8461
|
|
91
|
+
learning_loop_node/trainer/trainer_logic_generic.py,sha256=7ueRSkiViIRfX2T4RM7yIqrzKkqFICCyDjuj40-Y-LE,25826
|
|
92
|
+
learning_loop_node/trainer/trainer_node.py,sha256=0baQKXVUJjf8KRu0pH8i2o_01bRQWmUGooTptC-ZSsE,5334
|
|
93
|
+
learning_loop_node-0.10.13.dist-info/METADATA,sha256=kV-BUra2MEWVvJlQ_tW0-JbHzdrdXTZFEV4AWC5bx4A,11907
|
|
94
|
+
learning_loop_node-0.10.13.dist-info/WHEEL,sha256=WGfLGfLX43Ei_YORXSnT54hxFygu34kMpcQdmgmEwCQ,88
|
|
95
|
+
learning_loop_node-0.10.13.dist-info/RECORD,,
|
|
File without changes
|