learning-loop-node 0.18.4__py3-none-any.whl → 0.19.0__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.
@@ -1,11 +1,11 @@
1
1
  import logging
2
2
 
3
3
  # from . import log_conf
4
- from .detector.detector_logic import DetectorLogic
4
+ from .detector.detector_logic import DetectorLogic, DetectorLogicFactory
5
5
  from .detector.detector_node import DetectorNode
6
6
  from .globals import GLOBALS
7
7
  from .trainer.trainer_node import TrainerNode
8
8
 
9
- __all__ = ['TrainerNode', 'DetectorNode', 'DetectorLogic', 'GLOBALS']
9
+ __all__ = ['TrainerNode', 'DetectorNode', 'DetectorLogic', 'DetectorLogicFactory', 'GLOBALS']
10
10
 
11
11
  logging.info('>>>>>>>>>>>>>>>>>> LOOP INITIALIZED <<<<<<<<<<<<<<<<<<<<<<<')
@@ -0,0 +1,6 @@
1
+ from .detector_logic import DetectorLogic, DetectorLogicFactory
2
+
3
+ __all__ = [
4
+ 'DetectorLogic',
5
+ 'DetectorLogicFactory',
6
+ ]
@@ -1,79 +1,42 @@
1
- import logging
2
- from abc import abstractmethod
3
- from typing import List, Optional
1
+ from typing import List, Protocol
4
2
 
5
3
  import numpy as np
6
4
 
7
5
  from ..data_classes import ImageMetadata, ImagesMetadata, ModelInformation
8
- from ..globals import GLOBALS
9
- from .exceptions import NodeNeedsRestartError
10
6
 
11
7
 
12
- class DetectorLogic():
8
+ class DetectorLogicFactory(Protocol):
9
+ """Protocol for building DetectorLogic instances.
13
10
 
14
- def __init__(self, model_format: str) -> None:
15
- self.model_format: str = model_format
16
- self.model_info: Optional[ModelInformation] = None
11
+ The factory controls how the detector is constructed — implementations
12
+ can build synchronously or offload heavy work to a thread pool.
13
+ """
14
+ @property
15
+ def model_format(self) -> str: ...
17
16
 
18
- self._remaining_init_attempts: int = 2
17
+ async def build(self, model_info: ModelInformation) -> 'DetectorLogic': ...
19
18
 
20
- async def soft_reload(self):
21
- self.model_info = None
22
19
 
23
- def load_model_info_and_init_model(self):
24
- """
25
- Load model information from disk and initialize the model.
26
-
27
- The detector node uses a lock to make sure that this is not called
28
- concurrently with evaluate() or batch_evaluate().
29
- """
30
- logging.info('Loading model from %s', GLOBALS.data_folder)
31
- self.model_info = ModelInformation.load_from_disk(f'{GLOBALS.data_folder}/model')
32
- if self.model_info is None:
33
- logging.error('No model found')
34
- self.model_info = None
35
- return
20
+ class DetectorLogic(Protocol):
21
+ """Protocol for detector implementations.
36
22
 
37
- try:
38
- self.init()
39
- logging.info('Successfully loaded model %s', self.model_info)
40
- self._remaining_init_attempts = 2
41
- except Exception:
42
- self._remaining_init_attempts -= 1
43
- self.model_info = None
44
- logging.error('Could not init model %s. Retries left: %s', self.model_info, self._remaining_init_attempts)
45
- if self._remaining_init_attempts == 0:
46
- raise NodeNeedsRestartError('Could not init model') from None
47
- raise
48
-
49
- @abstractmethod
50
- def init(self):
51
- """
52
- Initialize the model.
23
+ Implementations receive the ModelInformation via their constructor (called by the
24
+ DetectorLogic factory in DetectorNode) and are free to store or ignore it.
25
+ """
53
26
 
54
- Called when a (new) model was loaded.
55
- Model information available via `self.model_info`
56
- The detector node uses a lock to make sure that this is not called
57
- concurrently with evaluate() or batch_evaluate().
58
- """
59
-
60
- @abstractmethod
61
27
  def evaluate(self, image: np.ndarray) -> ImageMetadata:
62
- """
63
- Evaluate the image and return the detections.
28
+ """Evaluate the image and return the detections.
64
29
 
65
30
  Called by the detector node when an image should be evaluated (REST or SocketIO).
66
31
  The resulting detections should be stored in the ImageMetadata.
67
32
  Tags stored in the ImageMetadata will be uploaded to the learning loop.
68
- The function should return empty metadata if the detector is not initialized.
69
33
  """
34
+ ...
70
35
 
71
- @abstractmethod
72
36
  def batch_evaluate(self, images: List[np.ndarray]) -> ImagesMetadata:
73
- """
74
- Evaluate a batch of images and return the detections.
37
+ """Evaluate a batch of images and return the detections.
75
38
 
76
39
  The resulting detections per image should be stored in the ImagesMetadata.
77
40
  Tags stored in the ImagesMetadata will be uploaded to the learning loop.
78
- The function should return empty metadata if the detector is not initialized.
79
41
  """
42
+ ...
@@ -1,15 +1,16 @@
1
1
  import asyncio
2
2
  import contextlib
3
+ import gc
4
+ import logging
3
5
  import os
4
6
  import shutil
5
7
  import subprocess
6
8
  import sys
7
- from dataclasses import asdict
9
+ from dataclasses import asdict, dataclass
8
10
  from datetime import datetime
9
- from typing import Dict, List, Literal, Optional
11
+ from typing import Dict, List, Literal, Optional, Union
10
12
 
11
13
  import numpy as np
12
-
13
14
  import socketio
14
15
  from dacite import from_dict
15
16
  from fastapi.encoders import jsonable_encoder
@@ -32,7 +33,7 @@ from ..globals import GLOBALS
32
33
  from ..helpers import background_tasks, environment_reader, run
33
34
  from ..helpers.misc import numpy_image_from_dict
34
35
  from ..node import Node
35
- from .detector_logic import DetectorLogic
36
+ from .detector_logic import DetectorLogic, DetectorLogicFactory
36
37
  from .exceptions import NodeNeedsRestartError
37
38
  from .inbox_filter.relevance_filter import RelevanceFilter
38
39
  from .outbox import Outbox
@@ -47,9 +48,13 @@ from .rest import upload as rest_upload
47
48
 
48
49
  class DetectorNode(Node):
49
50
 
50
- def __init__(self, name: str, detector: DetectorLogic, uuid: Optional[str] = None, use_backdoor_controls: bool = False) -> None:
51
+ def __init__(self, name: str, detector_factory: DetectorLogicFactory,
52
+ uuid: Optional[str] = None, use_backdoor_controls: bool = False) -> None:
51
53
  super().__init__(name, uuid=uuid, node_type='detector', needs_login=False, needs_sio=False)
52
- self.detector_logic = detector
54
+ self._detector_factory = detector_factory
55
+ self._detector: _DetectorState = _Initializing()
56
+ self._exclusive_model_build: bool = os.environ.get('EXCLUSIVE_MODEL_BUILD', '0').lower() in ('1', 'true')
57
+ self._remaining_init_attempts: int = 2
53
58
  self.organization = environment_reader.organization()
54
59
  self.project = environment_reader.project()
55
60
  assert self.organization and self.project, 'Detector node needs an organization and an project'
@@ -95,13 +100,13 @@ class DetectorNode(Node):
95
100
  return AboutResponse(
96
101
  operation_mode=self.operation_mode.value,
97
102
  state=self.status.state,
98
- model_info=self.detector_logic.model_info, # pylint: disable=protected-access
103
+ model_info=self._detector.model_info if isinstance(self._detector, _ActiveDetector) else None,
99
104
  target_model=self.target_model.version if self.target_model else None,
100
105
  version_control=self.version_control.value
101
106
  )
102
107
 
103
108
  def get_model_version_response(self) -> ModelVersionResponse:
104
- current_version = self.detector_logic.model_info.version if self.detector_logic.model_info is not None else 'None' # pylint: disable=protected-access
109
+ current_version = self._detector.model_info.version if isinstance(self._detector, _ActiveDetector) else 'None'
105
110
  target_version = self.target_model.version if self.target_model is not None else 'None'
106
111
  loop_version = self.loop_deployment_target.version if self.loop_deployment_target is not None else 'None'
107
112
 
@@ -183,15 +188,12 @@ class DetectorNode(Node):
183
188
  self.socket_connection_broken = True
184
189
  await self.on_startup()
185
190
 
186
- # simulate startup
187
- await self.detector_logic.soft_reload()
188
- await self._load_model_info_and_init_model()
189
- self.operation_mode = OperationMode.Idle
190
-
191
191
  async def on_startup(self) -> None:
192
192
  try:
193
193
  self.outbox.ensure_continuous_upload()
194
- await self._load_model_info_and_init_model()
194
+ current_model_dir = self._current_model_path()
195
+ if current_model_dir and os.path.isdir(current_model_dir):
196
+ await self._build_and_swap_detector(current_model_dir)
195
197
  except Exception:
196
198
  self.log.exception("error during 'startup'")
197
199
  self.operation_mode = OperationMode.Idle
@@ -251,14 +253,11 @@ class DetectorNode(Node):
251
253
  autoupload=data.get('autoupload', 'filtered'),
252
254
  creation_date=data.get('creation_date', None)
253
255
  )
254
- if det is None:
255
- return {'error': 'no model loaded'}
256
- detection_dict = jsonable_encoder(asdict(det))
257
- return detection_dict
256
+ return jsonable_encoder(asdict(det))
257
+ except DetectorUnavailableError as e:
258
+ return {'error': str(e)}
258
259
  except Exception as e:
259
260
  self.log.exception('could not detect via socketio')
260
- # with open('/tmp/bad_img_from_socket_io.jpg', 'wb') as f:
261
- # f.write(data['image'])
262
261
  return {'error': str(e)}
263
262
 
264
263
  @self.sio.event
@@ -266,7 +265,7 @@ class DetectorNode(Node):
266
265
  """
267
266
  Detect objects in a batch of images sent via SocketIO.
268
267
 
269
- Data dict follows the schema of the detect endpoint,
268
+ Data dict follows the schema of the detect endpoint,
270
269
  but 'images' is a list of image dicts.
271
270
  """
272
271
  try:
@@ -285,21 +284,22 @@ class DetectorNode(Node):
285
284
  autoupload=data.get('autoupload', 'filtered'),
286
285
  creation_date=data.get('creation_date', None)
287
286
  )
288
- if det is None:
289
- return {'error': 'no model loaded'}
290
- detection_dict = jsonable_encoder(asdict(det))
291
- return detection_dict
287
+ return jsonable_encoder(asdict(det))
288
+ except DetectorUnavailableError as e:
289
+ return {'error': str(e)}
292
290
  except Exception as e:
293
291
  self.log.exception('could not detect via socketio')
294
- # with open('/tmp/bad_img_from_socket_io.jpg', 'wb') as f:
295
- # f.write(data['image'])
296
292
  return {'error': str(e)}
297
293
 
298
294
  @self.sio.event
299
295
  async def info(sid) -> Dict:
300
- if self.detector_logic.model_info is not None:
301
- return asdict(self.detector_logic.model_info)
302
- return {"status": "No model loaded"}
296
+ match self._detector:
297
+ case _ActiveDetector(model_info=info):
298
+ return asdict(info)
299
+ case _Updating() as u:
300
+ return {"status": f"Updating model to version {u.version}"}
301
+ case _Initializing():
302
+ return {"status": "No model loaded"}
303
303
 
304
304
  @self.sio.event
305
305
  async def about(sid) -> Dict:
@@ -350,8 +350,11 @@ class DetectorNode(Node):
350
350
  except Exception as e:
351
351
  self.log.exception('could not parse detections')
352
352
  return {'error': str(e)}
353
- if self.detector_logic.model_info is not None:
354
- image_metadata = self.add_category_id_to_detections(self.detector_logic.model_info, image_metadata)
353
+ try:
354
+ detector = _unwrap_detector(self._detector)
355
+ image_metadata = self.add_category_id_to_detections(detector.model_info, image_metadata)
356
+ except DetectorUnavailableError as e:
357
+ self.log.warning('Cannot add category IDs: %s', e)
355
358
  else:
356
359
  image_metadata = ImageMetadata()
357
360
 
@@ -393,11 +396,7 @@ class DetectorNode(Node):
393
396
  async def _sync_status_with_loop(self) -> None:
394
397
  """Sync status of the detector with the Learning Loop."""
395
398
 
396
- if self.detector_logic.model_info is not None:
397
- current_model = self.detector_logic.model_info.version
398
- else:
399
- current_model = None
400
-
399
+ current_model = self._detector.model_info.version if isinstance(self._detector, _ActiveDetector) else None
401
400
  target_model_version = self.target_model.version if self.target_model else None
402
401
 
403
402
  status = DetectorStatus(
@@ -409,7 +408,7 @@ class DetectorNode(Node):
409
408
  operation_mode=self.operation_mode,
410
409
  current_model=current_model,
411
410
  target_model=target_model_version,
412
- model_format=self.detector_logic.model_format,
411
+ model_format=self._detector_factory.model_format,
413
412
  )
414
413
 
415
414
  self.log_status_on_change(status.state, status)
@@ -427,7 +426,7 @@ class DetectorNode(Node):
427
426
  async def _update_model_if_required(self) -> None:
428
427
  """Check if a new model is available and update if necessary.
429
428
  The Learning Loop will respond with the model info of the deployment target.
430
- If version_control is set to FollowLoop or the chosen target model is not used,
429
+ If version_control is set to FollowLoop or the chosen target model is not used,
431
430
  the detector will update the target_model."""
432
431
  try:
433
432
  if self.operation_mode != OperationMode.Idle:
@@ -441,8 +440,14 @@ class DetectorNode(Node):
441
440
  self.log.debug('not running any updates; target model is None')
442
441
  return
443
442
 
444
- current_version = self.detector_logic.model_info.version \
445
- if self.detector_logic.model_info is not None else None
443
+ match self._detector:
444
+ case _ActiveDetector(model_info=info):
445
+ current_version = info.version
446
+ case _Updating():
447
+ self.log.debug('not checking for updates; model update already in progress')
448
+ return
449
+ case _Initializing():
450
+ current_version = None
446
451
 
447
452
  if current_version != self.target_model.version:
448
453
  self.log.info('Updating model from %s to %s',
@@ -499,7 +504,7 @@ class DetectorNode(Node):
499
504
  await self.data_exchanger.download_model(target_model_folder,
500
505
  Context(organization=self.organization,
501
506
  project=self.project),
502
- target_model.id, self.detector_logic.model_format)
507
+ target_model.id, self._detector_factory.model_format)
503
508
  self.log.info('Downloaded model %s', target_model.version)
504
509
  except Exception:
505
510
  self.log.exception('Could not download model %s', target_model.version)
@@ -507,32 +512,76 @@ class DetectorNode(Node):
507
512
  self.target_model = None
508
513
  return
509
514
 
510
- model_symlink = 'model'
511
515
  try:
512
- os.unlink(model_symlink)
513
- os.remove(model_symlink)
514
- except Exception:
515
- pass
516
- os.symlink(target_model_folder, model_symlink)
517
- self.log.info('Updated symlink for model to %s', os.readlink(model_symlink))
518
-
519
- try:
520
- await self._load_model_info_and_init_model()
516
+ await self._build_and_swap_detector(target_model_folder)
521
517
  except NodeNeedsRestartError:
522
518
  self.log.error('Node needs restart')
523
519
  sys.exit(0)
524
520
  except Exception:
525
- self.log.exception('Could not load model, will retry download on next check')
521
+ self.log.exception('Could not build detector, will retry download on next check')
526
522
  shutil.rmtree(target_model_folder, ignore_errors=True)
527
523
  self.target_model = None
528
524
  return
529
525
 
526
+ self._update_current_model_symlink(target_model_folder)
530
527
  await self._sync_status_with_loop()
531
- # self.reload(reason='new model installed')
532
528
 
533
- async def _load_model_info_and_init_model(self) -> None:
534
- async with self.detection_lock:
535
- self.detector_logic.load_model_info_and_init_model()
529
+ async def _build_and_swap_detector(self, model_dir: str) -> None:
530
+ """Load ModelInformation from model_dir, build a new DetectorLogic via the factory,
531
+ then atomically swap self._detector when ready.
532
+ The old detector continues to serve requests until the swap.
533
+
534
+ If EXCLUSIVE_MODEL_BUILD is set and a detector is active, the old detector is torn down
535
+ first (freeing e.g. GPU VRAM) and detections are rejected until the new one is ready.
536
+ """
537
+ logging.info('Loading model from %s', model_dir)
538
+ model_info = ModelInformation.load_from_disk(os.path.abspath(model_dir))
539
+ if model_info is None:
540
+ logging.warning('No model.json found in %s', model_dir)
541
+ raise Exception('model.json not found')
542
+
543
+ if self._exclusive_model_build and isinstance(self._detector, _ActiveDetector):
544
+ async with self.detection_lock:
545
+ old_logic = self._detector.logic
546
+ self._detector = _Updating(version=model_info.version)
547
+
548
+ # Ensure that we actually delete the old DetectorLogic here to
549
+ # free resources before building a new detector.
550
+ refcount = sys.getrefcount(old_logic) # 2 = old_logic local + getrefcount arg
551
+ assert refcount == 2, f'expected 2 refs to old detector logic, got {refcount}'
552
+ del old_logic
553
+ gc.collect()
554
+
555
+ try:
556
+ new_detector = await self._detector_factory.build(model_info)
557
+ logging.info('Successfully built detector for model %s', model_info)
558
+ self._remaining_init_attempts = 2
559
+ except Exception as e:
560
+ self._remaining_init_attempts -= 1
561
+ logging.error('Could not build detector for model %s. Retries left: %s. Error: %s',
562
+ model_info, self._remaining_init_attempts, e)
563
+ if not isinstance(self._detector, _ActiveDetector):
564
+ self._detector = _Initializing() # no model to fall back to
565
+ if self._remaining_init_attempts == 0:
566
+ raise NodeNeedsRestartError('Could not build detector') from None
567
+ raise
568
+ self._detector = _ActiveDetector(new_detector, model_info)
569
+
570
+ @staticmethod
571
+ def _current_model_path() -> Optional[str]:
572
+ """Return the absolute path the current_model symlink points to, or None."""
573
+ link = os.path.join(GLOBALS.data_folder, 'current_model')
574
+ if os.path.islink(link):
575
+ return os.path.realpath(link)
576
+ return None
577
+
578
+ @staticmethod
579
+ def _update_current_model_symlink(model_dir: str) -> None:
580
+ """Atomically update the current_model symlink to point to model_dir."""
581
+ link = os.path.join(GLOBALS.data_folder, 'current_model')
582
+ tmp_link = link + '.tmp'
583
+ os.symlink(model_dir, tmp_link)
584
+ os.rename(tmp_link, link)
536
585
 
537
586
  # ================================== API Implementations ==================================
538
587
 
@@ -565,12 +614,13 @@ class DetectorNode(Node):
565
614
  Main processing function for the detector node.
566
615
 
567
616
  Used when an image is received via REST or SocketIO.
568
- This function infers the detections from the image,
617
+ This function infers the detections from the image,
569
618
  cares about uploading to the loop and returns the detections as ImageMetadata object.
619
+ Raises exception if no model is loaded.
570
620
  """
571
-
572
621
  async with self.detection_lock:
573
- metadata = await run.io_bound(self.detector_logic.evaluate, image)
622
+ detector = _unwrap_detector(self._detector)
623
+ metadata = await run.io_bound(detector.logic.evaluate, image)
574
624
 
575
625
  metadata.tags.extend(tags)
576
626
  metadata.source = source
@@ -600,14 +650,15 @@ class DetectorNode(Node):
600
650
  autoupload: str = 'filtered',
601
651
  creation_date: Optional[str] = None) -> ImagesMetadata:
602
652
  """
603
- Processing function for the detector node when a a batch inference is requested via SocketIO.
653
+ Processing function for the detector node when a batch inference is requested via SocketIO.
604
654
 
605
- This function infers the detections from all images,
655
+ This function infers the detections from all images,
606
656
  cares about uploading to the loop and returns the detections as a list of ImageMetadata.
657
+ Raises exception if no model is loaded.
607
658
  """
608
-
609
659
  async with self.detection_lock:
610
- all_detections = await run.io_bound(self.detector_logic.batch_evaluate, images)
660
+ detector = _unwrap_detector(self._detector)
661
+ all_detections = await run.io_bound(detector.logic.batch_evaluate, images)
611
662
 
612
663
  for metadata in all_detections.items:
613
664
  metadata.tags.extend(tags)
@@ -680,6 +731,40 @@ class DetectorNode(Node):
680
731
  pass
681
732
 
682
733
 
734
+ class _Initializing:
735
+ """No model ready yet — first build pending or in progress."""
736
+
737
+
738
+ @dataclass
739
+ class _Updating:
740
+ """Exclusive model replacement in progress — detections rejected."""
741
+ version: str
742
+
743
+
744
+ @dataclass
745
+ class _ActiveDetector:
746
+ logic: DetectorLogic
747
+ model_info: ModelInformation
748
+
749
+
750
+ _DetectorState = Union[_Initializing, _Updating, _ActiveDetector]
751
+
752
+
753
+ class DetectorUnavailableError(Exception):
754
+ pass
755
+
756
+
757
+ def _unwrap_detector(state: '_DetectorState') -> _ActiveDetector:
758
+ """Return the active detector or raise DetectorUnavailableError."""
759
+ match state:
760
+ case _ActiveDetector() as d:
761
+ return d
762
+ case _Updating() as u:
763
+ raise DetectorUnavailableError(f'updating model to version {u.version}')
764
+ case _Initializing():
765
+ raise DetectorUnavailableError('detector not yet initialized')
766
+
767
+
683
768
  @contextlib.contextmanager
684
769
  def step_into(new_dir):
685
770
  previous_dir = os.getcwd()
@@ -1,7 +1,9 @@
1
+ import os
2
+
1
3
 
2
4
  class Globals():
3
5
  def __init__(self) -> None:
4
- self.data_folder: str = '/data'
6
+ self.data_folder: str = os.getenv('DATA_FOLDER', '/data')
5
7
  self.detector_port: int = 5004 # NOTE used for tests
6
8
 
7
9
 
@@ -60,11 +60,22 @@ def _handle_task_result(task: asyncio.Task,
60
60
 
61
61
 
62
62
  def get_free_memory_mb() -> float: # NOTE used by yolov5
63
- pynvml.nvmlInit()
64
- h = pynvml.nvmlDeviceGetHandleByIndex(0)
65
- info = pynvml.nvmlDeviceGetMemoryInfo(h)
66
- free = float(info.free) / 1024 / 1024
67
- return free
63
+ """
64
+ Get the free memory available to the graphics card (index 0) in MB.
65
+ If VRAM information is unavailable, for example if the system shares ram
66
+ between CPU and GPU, the available RAM is returned instead.
67
+ """
68
+ try:
69
+ pynvml.nvmlInit()
70
+
71
+ h = pynvml.nvmlDeviceGetHandleByIndex(0)
72
+ info = pynvml.nvmlDeviceGetMemoryInfo(h)
73
+ free_mb = float(info.free) / 1024 / 1024
74
+ except pynvml.nvml.NVMLError:
75
+ logger = logging.getLogger(__name__)
76
+ logger.warning('Could not get GPU memory info, returning free CPU memory instead')
77
+ free_mb = float(os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_AVPHYS_PAGES')) / 1024 / 1024
78
+ return free_mb
68
79
 
69
80
 
70
81
  async def is_valid_image(filename: str, check_jpeg: bool) -> bool:
@@ -203,7 +203,7 @@ class Node(FastAPI):
203
203
 
204
204
  if self.sio_client is not None:
205
205
  try:
206
- await self.sio_client.disconnect()
206
+ await self.sio_client.shutdown()
207
207
  self.log.info('disconnected from loop via sio')
208
208
  # NOTE: without waiting for the disconnect event, we might disconnect the next connection too early
209
209
  await asyncio.wait_for(self.DISCONNECTED_FROM_LOOP.wait(), timeout=5)
@@ -173,7 +173,7 @@ class ActiveTrainingIO:
173
173
  response = await self.loop_communicator.post(
174
174
  f'/{context.organization}/projects/{context.project}/detections', json=detections_json)
175
175
  if response.status_code != 200:
176
- msg = f'could not upload detections. {str(response)}'
176
+ msg = f'could not upload detections. HTTP {response.status_code}: {response.text}'
177
177
  logging.error(msg)
178
178
  raise Exception(msg)
179
179
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning_loop_node
3
- Version: 0.18.4
3
+ Version: 0.19.0
4
4
  Summary: Python Library for Nodes which connect to the Zauberzeug Learning Loop
5
5
  Author-email: Zauberzeug GmbH <info@zauberzeug.com>
6
6
  License: MIT
7
7
  Project-URL: Repository, https://github.com/zauberzeug/learning_loop_node
8
- Requires-Python: >=3.9
8
+ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: fastapi<1.0,>=0.128
11
11
  Requires-Dist: uvicorn[standard]<0.23.0,>=0.22.0
@@ -17,7 +17,7 @@ Requires-Dist: icecream<3.0.0,>=2.1.0
17
17
  Requires-Dist: aiofiles<0.8.0,>=0.7.0
18
18
  Requires-Dist: async_generator<2.0.0,>=1.10
19
19
  Requires-Dist: pytest-watch<5.0.0,>=4.2.0
20
- Requires-Dist: python-multipart<0.0.19,>=0.0.18
20
+ Requires-Dist: python-multipart>=0.0.22
21
21
  Requires-Dist: psutil<6.0.0,>=5.8.0
22
22
  Requires-Dist: numpy<3.0,>=2.0
23
23
  Requires-Dist: Pillow<12.0.0,>=11.0.0
@@ -53,21 +53,22 @@ To start a node you have to implement the logic by inheriting from the correspon
53
53
 
54
54
  You can configure connection to our Learning Loop by specifying the following environment variables before starting:
55
55
 
56
- | Name | Alias | Purpose | Required by |
57
- | ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- |
58
- | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
59
- | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
60
- | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
61
- | LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) |
62
- | LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector |
63
- | LOOP_PROJECT | PROJECT | Project ID | Detector (opt.) |
64
- | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
65
- | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
66
- | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) |
67
- | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) |
68
- | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) |
69
- | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) |
70
- | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) |
56
+ | Name | Alias | Purpose | Required by | Default |
57
+ | ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- | ------------ |
58
+ | LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all | - |
59
+ | LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector | - |
60
+ | LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector | - |
61
+ | LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) | - |
62
+ | LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector | - |
63
+ | LOOP_PROJECT | PROJECT | Project ID | Detector | - |
64
+ | MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.3 |
65
+ | MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.6 |
66
+ | EXCLUSIVE_MODEL_BUILD | - | Reject detections during update to save VRAM (set to 1) | Detector (opt.) | 0 |
67
+ | INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) | 10 |
68
+ | RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
69
+ | KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
70
+ | TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
71
+ | USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) | 0 |
71
72
 
72
73
  Note that organization and project IDs are always lower case and may differ from the names in the Learning Loop which can have uppercase letters.
73
74
 
@@ -274,5 +275,5 @@ After a short time the converted model should be available as well.
274
275
  - `id`: the model UUID (currently not needed by anyone, since host, org, project, version clearly identify the model)
275
276
  - `format`: the format e.g. yolo, tkdnn, yolor etc.
276
277
  - Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
277
- - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, ` resolution_x`` resolution_y ` would also be conceivable or `resolutions` to give a list of possible resolutions)
278
+ - `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, `resolution_x`` resolution_y` would also be conceivable or `resolutions` to give a list of possible resolutions)
278
279
  - `categories`: list of categories with name, id, (later also type), in the order in which they are used by the model -- this is neccessary to be robust about renamings
@@ -1,8 +1,8 @@
1
- learning_loop_node/__init__.py,sha256=onN5s8-x_xBsCM6NLmJO0Ym1sJHeCFaGw8qb0oQZmz8,364
1
+ learning_loop_node/__init__.py,sha256=LzNoXBrcL6yJTyisuwJokZ9LTiYQwY3yRY3tmy83FlI,410
2
2
  learning_loop_node/data_exchanger.py,sha256=nd9JNPLn9amIeTcSIyUPpbE97ORAcb5yNphvmpgWSUQ,9095
3
- learning_loop_node/globals.py,sha256=tgw_8RYOipPV9aYlyUhYtXfUxvJKRvfUk6u-qVAtZmY,174
3
+ learning_loop_node/globals.py,sha256=a50Qg4z2205Zcw4Cs8z417kn1x8s78o2FJfZB9-ZZI4,211
4
4
  learning_loop_node/loop_communication.py,sha256=d1tJZ9T_y22FrEq1XzvidgH6d-byGxFq3ovLvPBS4FA,7611
5
- learning_loop_node/node.py,sha256=xK-xODRo7ov-dNNMcpLW2GAauvjKAK3K9RQh4P9S994,12160
5
+ learning_loop_node/node.py,sha256=CABqe-sqUlcwi9CYVXBkrfxh0Xgtzj3AFLQcupSX-Ms,12158
6
6
  learning_loop_node/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  learning_loop_node/rest.py,sha256=5X9IVW9kf1gNf8jifGW9g_gI_-9TEeoMMOW16jvwpRE,1599
8
8
  learning_loop_node/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -16,9 +16,9 @@ learning_loop_node/data_classes/general.py,sha256=GQ6vPEIm4qqBV4RZT_YS_dPeKMdbCK
16
16
  learning_loop_node/data_classes/image_metadata.py,sha256=4FFs89iZAfr2Jh-40QqZ_GGmJ8hl_W61eHcCqHflJLE,2293
17
17
  learning_loop_node/data_classes/socket_response.py,sha256=tIdt-oYf6ULoJIDYQCecNM9OtWR6_wJ9tL0Ksu83Vko,655
18
18
  learning_loop_node/data_classes/training.py,sha256=TybwcCDf_NUaDUaOj30lPm-7Z3Qk9XFRibEX5qIv96Y,5737
19
- learning_loop_node/detector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- learning_loop_node/detector/detector_logic.py,sha256=8J70Fulrhex9IHFBAGYLR3a0-W-zsNj4h7mYLnrmUqM,2899
21
- learning_loop_node/detector/detector_node.py,sha256=g1moqT-d6I6pNv7XKweUT47P4QY53XdwRme_qXYFrBc,31805
19
+ learning_loop_node/detector/__init__.py,sha256=wroy0W1X5MX16C-peA-UHYrcdXet7GhleIlC_Pqa6hI,128
20
+ learning_loop_node/detector/detector_logic.py,sha256=XJaHNiw5PaHoCWnJ-qsxdUELLevvztYx54TQ8KcHl18,1479
21
+ learning_loop_node/detector/detector_node.py,sha256=CIHbBmKf49wxxxhrITCENeL72Wm-sJdZQV5x9_6ZlUc,35716
22
22
  learning_loop_node/detector/exceptions.py,sha256=C6KbNPlSbtfgDrZx2Hbhm7Suk9jVoR3fMRCO0CkrMsQ,196
23
23
  learning_loop_node/detector/outbox.py,sha256=6F0MjFM-zNX-xWP_YGbmU0UYuyb0odsX_OcAxMPSzS0,12288
24
24
  learning_loop_node/detector/inbox_filter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -42,20 +42,20 @@ learning_loop_node/helpers/background_tasks.py,sha256=sbbS8JO1ls0ZCbNW68KOT9QLOF
42
42
  learning_loop_node/helpers/environment_reader.py,sha256=6DxDJecLHxiGczByhyVa_JssAwwft7vuNCGaEzoSY2I,1662
43
43
  learning_loop_node/helpers/gdrive_downloader.py,sha256=zeYJciTAJVRpu_eFjwgYLCpIa6hU1d71anqEBb564Rk,1145
44
44
  learning_loop_node/helpers/log_conf.py,sha256=hqVAa_9NnYEU6N0dcOKmph82p7MpgKqeF_eomTLYzWY,961
45
- learning_loop_node/helpers/misc.py,sha256=bHIYeOoX_5FA1qhcNlG7_kVgVXDF5jipByDwTvs8Huw,8407
45
+ learning_loop_node/helpers/misc.py,sha256=Lw1gr7CdyTJbrv44bsFNXWtGVDCRQrpcDIJsIrKjtqU,8936
46
46
  learning_loop_node/helpers/run.py,sha256=_uox-j3_K_bL3yCAwy3JYSOiIxrnhzVxyxWpCe8_J9U,876
47
47
  learning_loop_node/trainer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  learning_loop_node/trainer/downloader.py,sha256=AIyEKM4XYtd6VgmXrP3VayV9DpJzdURK1Brx81ePNSM,1470
49
49
  learning_loop_node/trainer/exceptions.py,sha256=vbuoE6kssLQuA8zd3LiDHmZglP6E2IJJwEi5AZtWXxY,420
50
50
  learning_loop_node/trainer/executor.py,sha256=XQ1TwgOF2pQv7bgzSaljDWG4I1ySjYXL8r9c_p9MX1Q,3947
51
- learning_loop_node/trainer/io_helpers.py,sha256=ZnAPVqhq8XCHe1NoiOQJ_w0B-estcc8CBQHnb423UDw,7226
51
+ learning_loop_node/trainer/io_helpers.py,sha256=zWl0-HLP8zxOG3ybm76vXgEozULj_1QRl95jkrF126k,7255
52
52
  learning_loop_node/trainer/test_executor.py,sha256=6BVGDN_6f5GEMMEvDLSG1yzMybSvgXaP5uYpSfsVPP0,2224
53
53
  learning_loop_node/trainer/trainer_logic.py,sha256=eK-01qZzi10UjLMCQX8vy5eW2FoghPj3rzzDC-s3Si4,8792
54
54
  learning_loop_node/trainer/trainer_logic_generic.py,sha256=KcHmXr-Hp8_Wuejzj8odY6sRPqi6aw1SEXv3YlbjM98,27057
55
55
  learning_loop_node/trainer/trainer_node.py,sha256=tsAMzJewdS7Bi_1b9FwG0d2lGlv2lY37pgOLWr0bP_I,4582
56
56
  learning_loop_node/trainer/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  learning_loop_node/trainer/rest/backdoor_controls.py,sha256=ZnK8ypY5r_q0-YZbtaOxhQThzuZvMsQHM5gJGESd_dE,5131
58
- learning_loop_node-0.18.4.dist-info/METADATA,sha256=V9kzX_sICIu_uGtGUYzFJT8UYxkcyVPJ63LCkW0jzJA,15017
59
- learning_loop_node-0.18.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
60
- learning_loop_node-0.18.4.dist-info/top_level.txt,sha256=nS5qiFc4stVF_VXVCVJxZnrqVOv3IGX5-_E3sLby6OE,19
61
- learning_loop_node-0.18.4.dist-info/RECORD,,
58
+ learning_loop_node-0.19.0.dist-info/METADATA,sha256=XrZLdSNokLlWJvcMmORSSxkx3HhYy9t9bCyjoK-a0Bk,15383
59
+ learning_loop_node-0.19.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
60
+ learning_loop_node-0.19.0.dist-info/top_level.txt,sha256=nS5qiFc4stVF_VXVCVJxZnrqVOv3IGX5-_E3sLby6OE,19
61
+ learning_loop_node-0.19.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5