learning-loop-node 0.9.3__tar.gz → 0.10.1__tar.gz

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.

Files changed (102) hide show
  1. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/PKG-INFO +1 -1
  2. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/__init__.py +2 -3
  3. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/annotation/annotator_logic.py +2 -2
  4. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/annotation/annotator_node.py +16 -15
  5. learning_loop_node-0.10.1/learning_loop_node/data_classes/__init__.py +19 -0
  6. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/data_classes/detections.py +7 -2
  7. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/data_classes/general.py +4 -5
  8. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/data_classes/training.py +49 -21
  9. learning_loop_node-0.10.1/learning_loop_node/data_exchanger.py +172 -0
  10. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/detector_node.py +10 -13
  11. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +4 -7
  12. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/outbox.py +0 -1
  13. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/about.py +1 -0
  14. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/conftest.py +0 -1
  15. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/test_client_communication.py +5 -3
  16. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/test_outbox.py +2 -0
  17. learning_loop_node-0.10.1/learning_loop_node/detector/tests/testing_detector.py +22 -0
  18. learning_loop_node-0.10.1/learning_loop_node/globals.py +8 -0
  19. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/helpers/gdrive_downloader.py +1 -1
  20. learning_loop_node-0.10.1/learning_loop_node/helpers/misc.py +216 -0
  21. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/loop_communication.py +57 -25
  22. learning_loop_node-0.10.1/learning_loop_node/node.py +179 -0
  23. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_downloader.py +8 -7
  24. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_executor.py +14 -11
  25. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_helper.py +3 -5
  26. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/downloader.py +1 -1
  27. learning_loop_node-0.10.1/learning_loop_node/trainer/executor.py +109 -0
  28. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/io_helpers.py +68 -9
  29. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/rest/backdoor_controls.py +10 -5
  30. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/rest/controls.py +3 -1
  31. learning_loop_node-0.10.1/learning_loop_node/trainer/tests/conftest.py +66 -0
  32. learning_loop_node-0.10.1/learning_loop_node/trainer/tests/states/__init__.py +0 -0
  33. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_cleanup.py +5 -3
  34. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_detecting.py +23 -20
  35. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_download_train_model.py +18 -12
  36. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_prepare.py +13 -12
  37. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_sync_confusion_matrix.py +21 -18
  38. learning_loop_node-0.10.1/learning_loop_node/trainer/tests/states/test_state_train.py +69 -0
  39. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_upload_detections.py +34 -32
  40. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/states/test_state_upload_model.py +22 -20
  41. learning_loop_node-0.10.1/learning_loop_node/trainer/tests/test_errors.py +45 -0
  42. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/test_trainer_states.py +4 -5
  43. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/testing_trainer_logic.py +25 -30
  44. learning_loop_node-0.10.1/learning_loop_node/trainer/trainer_logic.py +187 -0
  45. learning_loop_node-0.10.1/learning_loop_node/trainer/trainer_logic_generic.py +495 -0
  46. learning_loop_node-0.10.1/learning_loop_node/trainer/trainer_node.py +94 -0
  47. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/pyproject.toml +1 -1
  48. learning_loop_node-0.9.3/learning_loop_node/converter/converter_logic.py +0 -68
  49. learning_loop_node-0.9.3/learning_loop_node/converter/converter_node.py +0 -125
  50. learning_loop_node-0.9.3/learning_loop_node/converter/tests/test_converter.py +0 -55
  51. learning_loop_node-0.9.3/learning_loop_node/data_classes/__init__.py +0 -12
  52. learning_loop_node-0.9.3/learning_loop_node/data_exchanger.py +0 -226
  53. learning_loop_node-0.9.3/learning_loop_node/detector/__init__.py +0 -1
  54. learning_loop_node-0.9.3/learning_loop_node/detector/tests/testing_detector.py +0 -29
  55. learning_loop_node-0.9.3/learning_loop_node/globals.py +0 -8
  56. learning_loop_node-0.9.3/learning_loop_node/helpers/misc.py +0 -109
  57. learning_loop_node-0.9.3/learning_loop_node/node.py +0 -252
  58. learning_loop_node-0.9.3/learning_loop_node/trainer/executor.py +0 -105
  59. learning_loop_node-0.9.3/learning_loop_node/trainer/tests/conftest.py +0 -75
  60. learning_loop_node-0.9.3/learning_loop_node/trainer/tests/states/test_state_train.py +0 -70
  61. learning_loop_node-0.9.3/learning_loop_node/trainer/tests/test_errors.py +0 -37
  62. learning_loop_node-0.9.3/learning_loop_node/trainer/trainer_logic.py +0 -697
  63. learning_loop_node-0.9.3/learning_loop_node/trainer/trainer_node.py +0 -173
  64. learning_loop_node-0.9.3/learning_loop_node/trainer/training_syncronizer.py +0 -52
  65. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/README.md +0 -0
  66. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/annotation/__init__.py +0 -0
  67. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/annotation/tests/test_annotator_node.py +0 -0
  68. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/conftest.py +0 -0
  69. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/data_classes/annotations.py +0 -0
  70. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/data_classes/socket_response.py +0 -0
  71. {learning_loop_node-0.9.3/learning_loop_node/converter → learning_loop_node-0.10.1/learning_loop_node/detector}/__init__.py +0 -0
  72. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/detector_logic.py +0 -0
  73. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/__init__.py +0 -0
  74. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
  75. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/tests/test_observation.py +0 -0
  76. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/tests/test_relevance_group.py +0 -0
  77. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/inbox_filter/tests/test_unexpected_observations_count.py +0 -0
  78. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/__init__.py +0 -0
  79. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
  80. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/detect.py +0 -0
  81. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/operation_mode.py +0 -0
  82. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/rest/upload.py +0 -0
  83. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/__init__.py +0 -0
  84. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/test.jpg +0 -0
  85. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/detector/tests/test_relevance_filter.py +0 -0
  86. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/examples/novelty_score_updater.py +0 -0
  87. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/helpers/__init__.py +0 -0
  88. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/helpers/environment_reader.py +0 -0
  89. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/helpers/log_conf.py +0 -0
  90. /learning_loop_node-0.9.3/learning_loop_node/tests/__init__.py → /learning_loop_node-0.10.1/learning_loop_node/py.typed +0 -0
  91. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/pytest.ini +0 -0
  92. {learning_loop_node-0.9.3/learning_loop_node/trainer → learning_loop_node-0.10.1/learning_loop_node/tests}/__init__.py +0 -0
  93. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/conftest.py +0 -0
  94. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_data/file_1.txt +0 -0
  95. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_data/file_2.txt +0 -0
  96. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_data/model.json +0 -0
  97. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_data_classes.py +0 -0
  98. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/tests/test_learning_loop_node.py +0 -0
  99. {learning_loop_node-0.9.3/learning_loop_node/trainer/rest → learning_loop_node-0.10.1/learning_loop_node/trainer}/__init__.py +0 -0
  100. {learning_loop_node-0.9.3/learning_loop_node/trainer/tests → learning_loop_node-0.10.1/learning_loop_node/trainer/rest}/__init__.py +0 -0
  101. {learning_loop_node-0.9.3/learning_loop_node/trainer/tests/states → learning_loop_node-0.10.1/learning_loop_node/trainer/tests}/__init__.py +0 -0
  102. {learning_loop_node-0.9.3 → learning_loop_node-0.10.1}/learning_loop_node/trainer/tests/state_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: learning-loop-node
3
- Version: 0.9.3
3
+ Version: 0.10.1
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
@@ -1,12 +1,11 @@
1
1
  import logging
2
- import os
3
- import sys
4
2
 
5
- from .converter.converter_node import ConverterNode
6
3
  # from . import log_conf
7
4
  from .detector.detector_logic import DetectorLogic
8
5
  from .detector.detector_node import DetectorNode
9
6
  from .globals import GLOBALS
10
7
  from .trainer.trainer_node import TrainerNode
11
8
 
9
+ __all__ = ['TrainerNode', 'DetectorNode', 'DetectorLogic', 'GLOBALS']
10
+
12
11
  logging.info('>>>>>>>>>>>>>>>>>> LOOP INITIALIZED <<<<<<<<<<<<<<<<<<<<<<<')
@@ -7,10 +7,10 @@ from ..node import Node
7
7
 
8
8
  class AnnotatorLogic():
9
9
 
10
- def __init__(self):
10
+ def __init__(self) -> None:
11
11
  self._node: Optional[Node] = None
12
12
 
13
- def init(self, node: Node):
13
+ def init(self, node: Node) -> None:
14
14
  self._node = node
15
15
 
16
16
  @abstractmethod
@@ -8,7 +8,7 @@ from socketio import AsyncClient
8
8
  from ..data_classes import AnnotationNodeStatus, Context, NodeState, UserInput
9
9
  from ..data_classes.socket_response import SocketResponse
10
10
  from ..data_exchanger import DataExchanger
11
- from ..helpers.misc import create_image_folder
11
+ from ..helpers.misc import create_image_folder, create_project_folder
12
12
  from ..node import Node
13
13
  from .annotator_logic import AnnotatorLogic
14
14
 
@@ -18,10 +18,11 @@ from .annotator_logic import AnnotatorLogic
18
18
  class AnnotatorNode(Node):
19
19
 
20
20
  def __init__(self, name: str, annotator_logic: AnnotatorLogic, uuid: Optional[str] = None):
21
- super().__init__(name, uuid)
21
+ super().__init__(name, uuid, 'annotation_node')
22
22
  self.tool = annotator_logic
23
23
  self.histories: Dict = {}
24
24
  annotator_logic.init(self)
25
+ self.status_sent = False
25
26
 
26
27
  def register_sio_events(self, sio_client: AsyncClient):
27
28
 
@@ -50,8 +51,6 @@ class AnnotatorNode(Node):
50
51
  raise
51
52
 
52
53
  if tool_result.annotation:
53
- if not self.sio_is_initialized():
54
- raise Exception('Socket client waas not initialized')
55
54
  await self.sio_client.call('update_segmentation_annotation', (user_input.data.context.organization,
56
55
  user_input.data.context.project,
57
56
  jsonable_encoder(asdict(tool_result.annotation))), timeout=30)
@@ -67,6 +66,9 @@ class AnnotatorNode(Node):
67
66
  return self.histories.setdefault(frontend_id, self.tool.create_empty_history())
68
67
 
69
68
  async def send_status(self):
69
+ if self.status_sent:
70
+ return
71
+
70
72
  status = AnnotationNodeStatus(
71
73
  id=self.uuid,
72
74
  name=self.name,
@@ -75,28 +77,27 @@ class AnnotatorNode(Node):
75
77
  )
76
78
 
77
79
  self.log.info(f'Sending status {status}')
78
- if self._sio_client is None:
79
- raise Exception('No socket client')
80
- result = await self._sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
80
+ try:
81
+ result = await self.sio_client.call('update_annotation_node', jsonable_encoder(asdict(status)), timeout=10)
82
+ except Exception as e:
83
+ self.log.error(f'Error for updating: {str(e)}')
84
+ return
85
+
81
86
  assert isinstance(result, Dict)
82
87
  response = from_dict(data_class=SocketResponse, data=result)
83
88
 
84
89
  if not response.success:
85
90
  self.log.error(f'Error for updating: Response from loop was : {asdict(response)}')
91
+ else:
92
+ self.status_sent = True
86
93
 
87
94
  async def download_image(self, context: Context, uuid: str):
88
- project_folder = Node.create_project_folder(context)
95
+ project_folder = create_project_folder(context)
89
96
  images_folder = create_image_folder(project_folder)
90
97
 
91
98
  downloader = DataExchanger(context=context, loop_communicator=self.loop_communicator)
92
99
  await downloader.download_images([uuid], images_folder)
93
100
 
94
- async def get_state(self):
95
- return NodeState.Online
96
-
97
- def get_node_type(self):
98
- return 'annotation_node'
99
-
100
101
  async def on_startup(self):
101
102
  pass
102
103
 
@@ -104,4 +105,4 @@ class AnnotatorNode(Node):
104
105
  pass
105
106
 
106
107
  async def on_repeat(self):
107
- pass
108
+ await self.send_status()
@@ -0,0 +1,19 @@
1
+ from .annotations import AnnotationData, AnnotationEventType, SegmentationAnnotation, ToolOutput, UserInput
2
+ from .detections import (BoxDetection, ClassificationDetection, Detections, Observation, Point, PointDetection,
3
+ SegmentationDetection, Shape)
4
+ from .general import (AnnotationNodeStatus, Category, CategoryType, Context, DetectionStatus, ErrorConfiguration,
5
+ ModelInformation, NodeState, NodeStatus)
6
+ from .socket_response import SocketResponse
7
+ from .training import (Errors, Hyperparameter, Model, PretrainedModel, TrainerState, Training, TrainingData,
8
+ TrainingError, TrainingOut, TrainingStateData, TrainingStatus)
9
+
10
+ __all__ = [
11
+ 'AnnotationData', 'AnnotationEventType', 'SegmentationAnnotation', 'ToolOutput', 'UserInput',
12
+ 'BoxDetection', 'ClassificationDetection', 'Detections', 'Observation', 'Point', 'PointDetection',
13
+ 'SegmentationDetection', 'Shape',
14
+ 'AnnotationNodeStatus', 'Category', 'CategoryType', 'Context', 'DetectionStatus', 'ErrorConfiguration',
15
+ 'ModelInformation', 'NodeState', 'NodeStatus',
16
+ 'SocketResponse',
17
+ 'Errors', 'Hyperparameter', 'Model', 'PretrainedModel', 'TrainerState', 'Training', 'TrainingData',
18
+ 'TrainingError', 'TrainingOut', 'TrainingStateData', 'TrainingStatus',
19
+ ]
@@ -13,8 +13,11 @@ KWONLY_SLOTS = {'kw_only': True, 'slots': True} if sys.version_info >= (3, 10) e
13
13
 
14
14
  @dataclass(**KWONLY_SLOTS)
15
15
  class BoxDetection():
16
+ """Coordinates according to COCO format. x,y is the top left corner of the box.
17
+ x increases to the right, y increases downwards.
18
+ """
16
19
  category_name: str
17
- x: int # TODO add definition of x,y,w,h
20
+ x: int
18
21
  y: int
19
22
  width: int
20
23
  height: int
@@ -47,6 +50,8 @@ class BoxDetection():
47
50
 
48
51
  @dataclass(**KWONLY_SLOTS)
49
52
  class PointDetection():
53
+ """Coordinates according to COCO format. x,y is the center of the point.
54
+ x increases to the right, y increases downwards."""
50
55
  category_name: str
51
56
  x: float
52
57
  y: float
@@ -111,7 +116,7 @@ class Detections():
111
116
  point_detections: List[PointDetection] = field(default_factory=list)
112
117
  segmentation_detections: List[SegmentationDetection] = field(default_factory=list)
113
118
  classification_detections: List[ClassificationDetection] = field(default_factory=list)
114
- tags: Optional[List[str]] = field(default_factory=list)
119
+ tags: List[str] = field(default_factory=list)
115
120
  date: Optional[str] = field(default_factory=current_datetime)
116
121
  image_id: Optional[str] = None # used for detection of trainers
117
122
 
@@ -34,10 +34,6 @@ class Category():
34
34
  return [from_dict(data_class=Category, data=value) for value in values]
35
35
 
36
36
 
37
- def create_category(identifier: str, name: str, ctype: Union[CategoryType, str]): # TODO: This is probably unused
38
- return Category(id=identifier, name=name, description='', hotkey='', color='', type=ctype, point_size=None)
39
-
40
-
41
37
  @dataclass(**KWONLY_SLOTS)
42
38
  class Context():
43
39
  organization: str
@@ -57,6 +53,7 @@ class ModelInformation():
57
53
  categories: List[Category]
58
54
  resolution: Optional[int] = None
59
55
  model_root_path: Optional[str] = None
56
+ model_size: Optional[str] = None
60
57
 
61
58
  @property
62
59
  def context(self):
@@ -64,6 +61,8 @@ class ModelInformation():
64
61
 
65
62
  @staticmethod
66
63
  def load_from_disk(model_root_path: str) -> Optional['ModelInformation']:
64
+ """Load model.json from model_root_path and return ModelInformation object.
65
+ """
67
66
  model_info_file_path = f'{model_root_path}/model.json'
68
67
  if not os.path.exists(model_info_file_path):
69
68
  logging.warning(f"could not find model information file '{model_info_file_path}'")
@@ -121,7 +120,7 @@ class NodeState(str, Enum):
121
120
  class NodeStatus():
122
121
  id: str
123
122
  name: str
124
- state: Optional[NodeState] = NodeState.Offline
123
+ state: Optional[NodeState] = NodeState.Online
125
124
  uptime: Optional[int] = 0
126
125
  errors: Dict = field(default_factory=dict)
127
126
  capabilities: List[str] = field(default_factory=list)
@@ -1,8 +1,10 @@
1
1
 
2
2
  import sys
3
+ import time
3
4
  from dataclasses import dataclass, field
4
5
  from enum import Enum
5
- from typing import Dict, List, Optional, Union
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
6
8
 
7
9
  # pylint: disable=no-name-in-module
8
10
  from .general import Category, Context
@@ -16,6 +18,14 @@ class Hyperparameter():
16
18
  flip_rl: bool
17
19
  flip_ud: bool
18
20
 
21
+ @staticmethod
22
+ def from_data(data: Dict):
23
+ return Hyperparameter(
24
+ resolution=data['resolution'],
25
+ flip_rl=data.get('flip_rl', False),
26
+ flip_ud=data.get('flip_ud', False)
27
+ )
28
+
19
29
 
20
30
  @dataclass(**KWONLY_SLOTS)
21
31
  class TrainingData():
@@ -41,14 +51,15 @@ class PretrainedModel():
41
51
  description: str
42
52
 
43
53
 
44
- class TrainingState(str, Enum):
54
+ class TrainerState(str, Enum):
55
+ Idle = 'idle'
45
56
  Initialized = 'initialized'
46
57
  Preparing = 'preparing'
47
58
  DataDownloading = 'data_downloading'
48
59
  DataDownloaded = 'data_downloaded'
49
60
  TrainModelDownloading = 'train_model_downloading'
50
61
  TrainModelDownloaded = 'train_model_downloaded'
51
- TrainingRunning = 'training_running'
62
+ TrainingRunning = 'running'
52
63
  TrainingFinished = 'training_finished'
53
64
  ConfusionMatrixSyncing = 'confusion_matrix_syncing'
54
65
  ConfusionMatrixSynced = 'confusion_matrix_synced'
@@ -62,9 +73,9 @@ class TrainingState(str, Enum):
62
73
 
63
74
  @dataclass(**KWONLY_SLOTS)
64
75
  class TrainingStatus():
65
- id: str # TODO this must not be changed, but tests wont detect it -> update tests!
76
+ id: str # NOTE this must not be changed, but tests wont detect a change -> update tests!
66
77
  name: str
67
- state: Union[Optional[TrainingState], str]
78
+ state: Optional[str]
68
79
  errors: Optional[Dict]
69
80
  uptime: Optional[float]
70
81
  progress: Optional[float]
@@ -77,13 +88,13 @@ class TrainingStatus():
77
88
  architecture: Optional[str] = None
78
89
  context: Optional[Context] = None
79
90
 
80
- def short_str(self):
91
+ def short_str(self) -> str:
81
92
  prgr = f'{self.progress * 100:.0f}%' if self.progress else ''
82
93
  trtesk = f'{self.train_image_count}/{self.test_image_count}/{self.skipped_image_count}' if self.train_image_count else 'n.a.'
83
94
  cntxt = f'{self.context.organization}/{self.context.project}' if self.context else ''
84
95
  hyps = f'({self.hyperparameters})' if self.hyperparameters else ''
85
96
  arch = f'.{self.architecture} - ' if self.architecture else ''
86
- return f'[{str(self.state)} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
97
+ return f'[{str(self.state).rsplit(".", maxsplit=1)[-1]} {prgr}. {self.name}({self.id}). Tr/Ts/Tsk: {trtesk} {cntxt}{arch}{hyps}]'
87
98
 
88
99
 
89
100
  @dataclass(**KWONLY_SLOTS)
@@ -91,21 +102,35 @@ class Training():
91
102
  id: str
92
103
  context: Context
93
104
 
94
- project_folder: str
95
- images_folder: str
96
- training_folder: str
105
+ project_folder: str # f'{GLOBALS.data_folder}/{context.organization}/{context.project}'
106
+ images_folder: str # f'{project_folder}/images'
107
+ training_folder: str # f'{project_folder}/trainings/{trainings_id}'
108
+ start_time: float = field(default_factory=time.time)
109
+
110
+ # model uuid to download (to continue training) | is not a uuid when training from scratch (blank or pt-name from provided_pretrained_models->name)
111
+ base_model_uuid_or_name: Optional[str] = None
97
112
 
98
- base_model_id: Optional[str] = None
99
113
  data: Optional[TrainingData] = None
100
114
  training_number: Optional[int] = None
101
- training_state: Optional[Union[TrainingState, str]] = None
102
- model_id_for_detecting: Optional[str] = None
115
+ training_state: Optional[str] = None
116
+ model_uuid_for_detecting: Optional[str] = None
103
117
  hyperparameters: Optional[Dict] = None
104
118
 
119
+ @property
120
+ def training_folder_path(self) -> Path:
121
+ return Path(self.training_folder)
122
+
123
+ def set_values_from_data(self, data: Dict) -> None:
124
+ self.data = TrainingData(categories=Category.from_list(data['categories']))
125
+ self.data.hyperparameter = Hyperparameter.from_data(data=data)
126
+ self.training_number = data['training_number']
127
+ self.base_model_uuid_or_name = data['id']
128
+ self.training_state = TrainerState.Initialized
129
+
105
130
 
106
131
  @dataclass(**KWONLY_SLOTS)
107
132
  class TrainingOut():
108
- confusion_matrix: Optional[Dict] = None
133
+ confusion_matrix: Optional[Dict] = None # This is actually just class-wise metrics
109
134
  train_image_count: Optional[int] = None
110
135
  test_image_count: Optional[int] = None
111
136
  trainer_id: Optional[str] = None
@@ -113,9 +138,9 @@ class TrainingOut():
113
138
 
114
139
 
115
140
  @dataclass(**KWONLY_SLOTS)
116
- class BasicModel():
117
- confusion_matrix: Optional[Dict] = None
118
- meta_information: Optional[Dict] = None
141
+ class TrainingStateData():
142
+ confusion_matrix: Dict = field(default_factory=dict)
143
+ meta_information: Dict = field(default_factory=dict)
119
144
 
120
145
 
121
146
  @dataclass(**KWONLY_SLOTS)
@@ -130,8 +155,8 @@ class Model():
130
155
 
131
156
 
132
157
  class Errors():
133
- def __init__(self):
134
- self._errors: Dict = {}
158
+ def __init__(self) -> None:
159
+ self._errors: Dict[str, str] = {}
135
160
 
136
161
  def set(self, key: str, value: str):
137
162
  self._errors[key] = value
@@ -140,7 +165,7 @@ class Errors():
140
165
  def errors(self) -> Dict:
141
166
  return self._errors
142
167
 
143
- def reset(self, key: str):
168
+ def reset(self, key: str) -> None:
144
169
  try:
145
170
  del self._errors[key]
146
171
  except AttributeError:
@@ -148,7 +173,7 @@ class Errors():
148
173
  except KeyError:
149
174
  pass
150
175
 
151
- def reset_all(self):
176
+ def reset_all(self) -> None:
152
177
  self._errors = {}
153
178
 
154
179
  def has_error_for(self, key: str) -> bool:
@@ -162,3 +187,6 @@ class TrainingError(Exception):
162
187
  def __init__(self, cause: str, *args: object) -> None:
163
188
  super().__init__(*args)
164
189
  self.cause = cause
190
+
191
+ def __str__(self) -> str:
192
+ return f'TrainingError: {self.cause}'
@@ -0,0 +1,172 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import zipfile
6
+ from glob import glob
7
+ from http import HTTPStatus
8
+ from io import BytesIO
9
+ from time import time
10
+ from typing import Dict, List, Optional
11
+
12
+ import aiofiles # type: ignore
13
+
14
+ from .data_classes import Context
15
+ from .helpers.misc import create_resource_paths, create_task, is_valid_image
16
+ from .loop_communication import LoopCommunicator
17
+
18
+
19
+ class DownloadError(Exception):
20
+
21
+ def __init__(self, cause: str, *args: object) -> None:
22
+ super().__init__(*args)
23
+ self.cause = cause
24
+
25
+ def __str__(self) -> str:
26
+ return f'DownloadError: {self.cause}'
27
+
28
+
29
+ class DataExchanger():
30
+
31
+ def __init__(self, context: Optional[Context], loop_communicator: LoopCommunicator):
32
+ """Exchanges data with the learning loop via the loop_communicator (rest api).
33
+
34
+ Args:
35
+ context (Optional[Context]): The context of the node. This is the organization and project name.
36
+ loop_communicator (LoopCommunicator): The loop_communicator to use for communication with the learning loop.
37
+
38
+ Note:
39
+ The context can be set later with the set_context method.
40
+ """
41
+ self.set_context(context)
42
+ self.progress = 0.0
43
+ self.loop_communicator = loop_communicator
44
+
45
+ self.check_jpeg = shutil.which('jpeginfo') is not None
46
+ if self.check_jpeg:
47
+ logging.info('Detected command line tool "jpeginfo". Images will be checked for validity')
48
+ else:
49
+ logging.error('Missing command line tool "jpeginfo". We cannot check for validity of images.')
50
+
51
+ def set_context(self, context: Optional[Context]) -> None:
52
+ self._context = context
53
+ self.progress = 0.0
54
+
55
+ @property
56
+ def context(self) -> Context:
57
+ assert self._context, 'DataExchanger: Context was not set yet.. call set_context() first.'
58
+ return self._context
59
+
60
+ # ---------------------------- END OF INIT ----------------------------
61
+
62
+ async def fetch_image_uuids(self, query_params: Optional[str] = '') -> List[str]:
63
+ """Fetch image uuids from the learning loop data endpoint."""
64
+ logging.info(f'Fetching image uuids for {self.context.organization}/{self.context.project}..')
65
+
66
+ response = await self.loop_communicator.get(f'/{self.context.organization}/projects/{self.context.project}/data?{query_params}')
67
+ assert response.status_code == 200, response
68
+ return (response.json())['image_ids']
69
+
70
+ async def download_images_data(self, image_uuids: List[str], chunk_size: int = 100) -> List[Dict]:
71
+ """Download image annotations, tags, set and other information for the given image uuids."""
72
+ logging.info(f'Fetching annotations, tags, sets, etc. for {len(image_uuids)} images..')
73
+
74
+ num_image_ids = len(image_uuids)
75
+ if num_image_ids == 0:
76
+ logging.info('got empty list. No images were downloaded')
77
+ return []
78
+
79
+ progress_factor = 0.5 / num_image_ids # 50% of progress is for downloading data
80
+ images_data: List[Dict] = []
81
+ for i in range(0, num_image_ids, chunk_size):
82
+ self.progress = i * progress_factor
83
+ chunk_ids = image_uuids[i:i+chunk_size]
84
+ response = await self.loop_communicator.get(f'/{self.context.organization}/projects/{self.context.project}/images?ids={",".join(chunk_ids)}')
85
+ if response.status_code != 200:
86
+ logging.error(f'Error {response.status_code} during downloading image data. Continue with next batch..')
87
+ continue
88
+ images_data += response.json()['images']
89
+
90
+ return images_data
91
+
92
+ async def download_images(self, image_uuids: List[str], image_folder: str, chunk_size: int = 10) -> None:
93
+ """Downloads images (actual image data). Will skip existing images"""
94
+ logging.info(f'Downloading {len(image_uuids)} images (actual image data).. skipping existing images.')
95
+ if not image_uuids:
96
+ return
97
+
98
+ existing_uuids = {os.path.splitext(os.path.basename(image))[0] for image in glob(f'{image_folder}/*.jpg')}
99
+ new_image_uuids = [id for id in image_uuids if id not in existing_uuids]
100
+
101
+ paths, _ = create_resource_paths(self.context.organization, self.context.project, new_image_uuids)
102
+ num_image_ids = len(image_uuids)
103
+ os.makedirs(image_folder, exist_ok=True)
104
+
105
+ progress_factor = 0.5 / num_image_ids # second 50% of progress is for downloading images
106
+ for i in range(0, num_image_ids, chunk_size):
107
+ self.progress = 0.5 + i * progress_factor
108
+ chunk_paths = paths[i:i+chunk_size]
109
+ chunk_ids = image_uuids[i:i+chunk_size]
110
+ tasks = []
111
+ for j, chunk_j in enumerate(chunk_paths):
112
+ start = time()
113
+ tasks.append(create_task(self._download_one_image(chunk_j, chunk_ids[j], image_folder)))
114
+ await asyncio.sleep(max(0, 0.02 - (time() - start))) # prevent too many requests at once
115
+ await asyncio.gather(*tasks)
116
+
117
+ async def _download_one_image(self, path: str, image_id: str, image_folder: str) -> None:
118
+ response = await self.loop_communicator.get(path)
119
+ if response.status_code != HTTPStatus.OK:
120
+ logging.error(f'bad status code {response.status_code} for {path}. Details: {response.text}')
121
+ return
122
+ filename = f'{image_folder}/{image_id}.jpg'
123
+ async with aiofiles.open(filename, 'wb') as f:
124
+ await f.write(response.content)
125
+ if not await is_valid_image(filename, self.check_jpeg):
126
+ os.remove(filename)
127
+
128
+ async def download_model(self, target_folder: str, context: Context, model_uuid: str, model_format: str) -> List[str]:
129
+ """Downloads a model (and additional meta data like model.json) and returns the paths of the downloaded files.
130
+ Used before training a model (when continuing a finished training) or before detecting images.
131
+ """
132
+ logging.info(f'Downloading model data for uuid {model_uuid} from the loop to {target_folder}..')
133
+
134
+ path = f'/{context.organization}/projects/{context.project}/models/{model_uuid}/{model_format}/file'
135
+ response = await self.loop_communicator.get(path, requires_login=False)
136
+ if response.status_code != 200:
137
+ content = response.json()
138
+ logging.error(f'could not download loop/{path}: {response.status_code}, content: {content}')
139
+ raise DownloadError(content['detail'])
140
+ try:
141
+ provided_filename = response.headers.get(
142
+ "Content-Disposition").split("filename=")[1].strip('"')
143
+ content = response.content
144
+ except:
145
+ logging.exception(f'Error during downloading model {path}:')
146
+ raise
147
+
148
+ tmp_path = f'/tmp/{os.path.splitext(provided_filename)[0]}'
149
+ shutil.rmtree(tmp_path, ignore_errors=True)
150
+ with zipfile.ZipFile(BytesIO(content), 'r') as zip_:
151
+ zip_.extractall(tmp_path)
152
+
153
+ created_files = []
154
+ for file in glob(f'{tmp_path}/**/*', recursive=True):
155
+ new_file = shutil.move(file, target_folder)
156
+ created_files.append(new_file)
157
+
158
+ shutil.rmtree(tmp_path, ignore_errors=True)
159
+ logging.info(f'Downloaded model {model_uuid}({model_format}) to {target_folder}.')
160
+ return created_files
161
+
162
+ async def upload_model_get_uuid(self, context: Context, files: List[str], training_number: Optional[int], mformat: str) -> Optional[str]:
163
+ """Used by the trainers. Function returns the new model uuid to use for detection."""
164
+ response = await self.loop_communicator.put(f'/{context.organization}/projects/{context.project}/trainings/{training_number}/models/latest/{mformat}/file', files=files)
165
+ if response.status_code != 200:
166
+ logging.error(f'Could not upload model for training {training_number}, format {mformat}: {response.text}')
167
+ response.raise_for_status()
168
+ return None
169
+
170
+ uploaded_model = response.json()
171
+ logging.info(f'Uploaded model for training {training_number}, format {mformat}. Response is: {uploaded_model}')
172
+ return uploaded_model['id']
@@ -14,7 +14,7 @@ from fastapi.encoders import jsonable_encoder
14
14
  from fastapi_socketio import SocketManager
15
15
  from socketio import AsyncClient
16
16
 
17
- from ..data_classes import Category, Context, Detections, DetectionStatus, ModelInformation, NodeState, Shape
17
+ from ..data_classes import Category, Context, Detections, DetectionStatus, ModelInformation, Shape
18
18
  from ..data_classes.socket_response import SocketResponse
19
19
  from ..data_exchanger import DataExchanger, DownloadError
20
20
  from ..globals import GLOBALS
@@ -34,9 +34,8 @@ from .rest.operation_mode import OperationMode
34
34
  class DetectorNode(Node):
35
35
 
36
36
  def __init__(self, name: str, detector: DetectorLogic, uuid: Optional[str] = None, use_backdoor_controls: bool = False) -> None:
37
- super().__init__(name, uuid)
37
+ super().__init__(name, uuid, 'detector', False)
38
38
  self.detector_logic = detector
39
- self.needs_login = False
40
39
  self.organization = environment_reader.organization()
41
40
  self.project = environment_reader.project()
42
41
  assert self.organization and self.project, 'Detector node needs an organization and an project'
@@ -170,6 +169,8 @@ class DetectorNode(Node):
170
169
  def _connect(sid, environ, auth) -> None:
171
170
  self.connected_clients.append(sid)
172
171
 
172
+ print('>>>>>>>>>>>>>>>>>>>>>>> setting up sio server', flush=True)
173
+
173
174
  self.sio_server = SocketManager(app=self)
174
175
  self.sio_server.on('detect', _detect)
175
176
  self.sio_server.on('info', _info)
@@ -185,7 +186,9 @@ class DetectorNode(Node):
185
186
  if not update_to_model_id:
186
187
  self.log.info('could not check for updates')
187
188
  return
188
- if self.detector_logic.is_initialized: # TODO: solve race condition !!!
189
+
190
+ # TODO: solve race condition (it should not be required to recheck if model_info is not None, but it is!)
191
+ if self.detector_logic.is_initialized:
189
192
  model_info = self.detector_logic._model_info # pylint: disable=protected-access
190
193
  if model_info is not None:
191
194
  self.log.info(f'Current model: {model_info.version} with id {model_info.id}')
@@ -220,8 +223,7 @@ class DetectorNode(Node):
220
223
  await self.data_exchanger.download_model(target_model_folder,
221
224
  Context(organization=self.organization,
222
225
  project=self.project),
223
- update_to_model_id,
224
- self.detector_logic.model_format)
226
+ update_to_model_id, self.detector_logic.model_format)
225
227
  try:
226
228
  os.unlink(model_symlink)
227
229
  os.remove(model_symlink)
@@ -256,7 +258,7 @@ class DetectorNode(Node):
256
258
  name=self.name,
257
259
  state=self.status.state,
258
260
  errors=self.status.errors,
259
- uptime=int((datetime.now() - self.startup_time).total_seconds()),
261
+ uptime=int((datetime.now() - self.startup_datetime).total_seconds()),
260
262
  operation_mode=self.operation_mode,
261
263
  current_model=current_model,
262
264
  target_model=self.target_model,
@@ -272,13 +274,11 @@ class DetectorNode(Node):
272
274
  return False
273
275
 
274
276
  assert socket_response.payload is not None
277
+ # TODO This is weird because target_model_version is stored in self and target_model_id is returned
275
278
  self.target_model = socket_response.payload['target_model_version']
276
279
  self.log.info(f'After sending status. Target_model is {self.target_model}')
277
280
  return socket_response.payload['target_model_id']
278
281
 
279
- async def get_state(self):
280
- return NodeState.Online # NOTE At the moment only trainer-nodes use a meaningful state
281
-
282
282
  async def set_operation_mode(self, mode: OperationMode):
283
283
  self.operation_mode = mode
284
284
  await self.send_status()
@@ -353,9 +353,6 @@ class DetectorNode(Node):
353
353
  classification_detection.category_id = category_id
354
354
  return detections
355
355
 
356
- def get_node_type(self):
357
- return 'detector'
358
-
359
356
  def register_sio_events(self, sio_client: AsyncClient):
360
357
  pass
361
358
 
@@ -1,20 +1,17 @@
1
1
  import os
2
2
  from typing import List, Union
3
3
 
4
- from learning_loop_node.data_classes import (BoxDetection,
5
- ClassificationDetection,
6
- Detections, Observation,
7
- PointDetection,
8
- SegmentationDetection)
4
+ from learning_loop_node.data_classes import (BoxDetection, ClassificationDetection, Detections, Observation,
5
+ PointDetection, SegmentationDetection)
9
6
 
10
7
 
11
8
  class CamObservationHistory:
12
- def __init__(self):
9
+ def __init__(self) -> None:
13
10
  self.reset_time = 3600
14
11
  self.recent_observations: List[Observation] = []
15
12
  self.iou_threshold = 0.5
16
13
 
17
- def forget_old_detections(self):
14
+ def forget_old_detections(self) -> None:
18
15
  self.recent_observations = [detection
19
16
  for detection in self.recent_observations
20
17
  if not detection.is_older_than(self.reset_time)]
@@ -53,7 +53,6 @@ class Outbox():
53
53
  with open(tmp + '/image.json', 'w') as f:
54
54
  json.dump(jsonable_encoder(asdict(detections)), f)
55
55
 
56
- # TODO sometimes No such file or directory: '/tmp/learning_loop_lib_data/tmp/2023-09-07_13:27:38.399/image.jpg'
57
56
  with open(tmp + '/image.jpg', 'wb') as f:
58
57
  f.write(image)
59
58
 
@@ -16,6 +16,7 @@ async def get_about(request: Request):
16
16
  curl http://localhost/about
17
17
  '''
18
18
  app: 'DetectorNode' = request.app
19
+
19
20
  return {
20
21
  'operation_mode': app.operation_mode.value,
21
22
  'state': app.status.state,