learning-loop-node 0.18.4__tar.gz → 0.19.0__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.
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/PKG-INFO +20 -19
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/README.md +17 -16
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/__init__.py +2 -2
- learning_loop_node-0.19.0/learning_loop_node/detector/__init__.py +6 -0
- learning_loop_node-0.19.0/learning_loop_node/detector/detector_logic.py +42 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/detector_node.py +149 -64
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/globals.py +3 -1
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/misc.py +16 -5
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/node.py +1 -1
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/io_helpers.py +1 -1
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/PKG-INFO +20 -19
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/requires.txt +1 -1
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/pyproject.toml +45 -3
- learning_loop_node-0.18.4/learning_loop_node/detector/detector_logic.py +0 -79
- learning_loop_node-0.18.4/learning_loop_node/trainer/rest/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/annotation/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/annotation/annotator_logic.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/annotation/annotator_node.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/annotation_data.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/annotations.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/detections.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/general.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/image_metadata.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/socket_response.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/training.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_exchanger.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/exceptions.py +0 -0
- {learning_loop_node-0.18.4/learning_loop_node/detector → learning_loop_node-0.19.0/learning_loop_node/detector/inbox_filter}/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/inbox_filter/cam_observation_history.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/inbox_filter/relevance_filter.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/outbox.py +0 -0
- {learning_loop_node-0.18.4/learning_loop_node/detector/inbox_filter → learning_loop_node-0.19.0/learning_loop_node/detector/rest}/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/about.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/detect.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/model_version_control.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/operation_mode.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/outbox_mode.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/upload.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/annotator.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/detector.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/general.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/trainer.py +0 -0
- {learning_loop_node-0.18.4/learning_loop_node/detector/rest → learning_loop_node-0.19.0/learning_loop_node/helpers}/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/background_tasks.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/environment_reader.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/gdrive_downloader.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/log_conf.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/run.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/loop_communication.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/py.typed +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/rest.py +0 -0
- {learning_loop_node-0.18.4/learning_loop_node/helpers → learning_loop_node-0.19.0/learning_loop_node/trainer}/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/downloader.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/exceptions.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/executor.py +0 -0
- {learning_loop_node-0.18.4/learning_loop_node/trainer → learning_loop_node-0.19.0/learning_loop_node/trainer/rest}/__init__.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/rest/backdoor_controls.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/test_executor.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/trainer_logic.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/trainer_logic_generic.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/trainer_node.py +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/SOURCES.txt +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/dependency_links.txt +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/top_level.txt +0 -0
- {learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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, `
|
|
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
|
|
@@ -17,21 +17,22 @@ To start a node you have to implement the logic by inheriting from the correspon
|
|
|
17
17
|
|
|
18
18
|
You can configure connection to our Learning Loop by specifying the following environment variables before starting:
|
|
19
19
|
|
|
20
|
-
| Name | Alias | Purpose | Required by |
|
|
21
|
-
| ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- |
|
|
22
|
-
| LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all |
|
|
23
|
-
| LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector |
|
|
24
|
-
| LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector |
|
|
25
|
-
| LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) |
|
|
26
|
-
| LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector |
|
|
27
|
-
| LOOP_PROJECT | PROJECT | Project ID | Detector
|
|
28
|
-
| MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) |
|
|
29
|
-
| MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) |
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
20
|
+
| Name | Alias | Purpose | Required by | Default |
|
|
21
|
+
| ------------------------ | ------------ | ------------------------------------------------------------ | ------------------------- | ------------ |
|
|
22
|
+
| LOOP_HOST | HOST | Learning Loop address (e.g. learning-loop.ai) | all | - |
|
|
23
|
+
| LOOP_USERNAME | USERNAME | Learning Loop user name | all besides Detector | - |
|
|
24
|
+
| LOOP_PASSWORD | PASSWORD | Learning Loop password | all besides Detector | - |
|
|
25
|
+
| LOOP_SSL_CERT_PATH | - | Path to the SSL certificate | all (opt.) | - |
|
|
26
|
+
| LOOP_ORGANIZATION | ORGANIZATION | Organization ID | Detector | - |
|
|
27
|
+
| LOOP_PROJECT | PROJECT | Project ID | Detector | - |
|
|
28
|
+
| MIN_UNCERTAIN_THRESHOLD | - | smallest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.3 |
|
|
29
|
+
| MAX_UNCERTAIN_THRESHOLD | - | largest confidence (float) at which auto-upload will happen | Detector (opt.) | 0.6 |
|
|
30
|
+
| EXCLUSIVE_MODEL_BUILD | - | Reject detections during update to save VRAM (set to 1) | Detector (opt.) | 0 |
|
|
31
|
+
| INFERENCE_BATCH_SIZE | - | Batch size of trainer when calculating detections | Trainer (opt.) | 10 |
|
|
32
|
+
| RESTART_AFTER_TRAINING | - | Restart the trainer after training (set to 1) | Trainer (opt.) | 0 |
|
|
33
|
+
| KEEP_OLD_TRAININGS | - | Do not delete old trainings (set to 1) | Trainer (opt.) | 0 |
|
|
34
|
+
| TRAINER_IDLE_TIMEOUT_SEC | - | Automatically shutdown trainer after timeout (in seconds) | Trainer (opt.) | 0 (disabled) |
|
|
35
|
+
| USE_BACKDOOR_CONTROLS | - | Always enable backdoor controls (set to 1) | Trainer / Detector (opt.) | 0 |
|
|
35
36
|
|
|
36
37
|
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.
|
|
37
38
|
|
|
@@ -238,5 +239,5 @@ After a short time the converted model should be available as well.
|
|
|
238
239
|
- `id`: the model UUID (currently not needed by anyone, since host, org, project, version clearly identify the model)
|
|
239
240
|
- `format`: the format e.g. yolo, tkdnn, yolor etc.
|
|
240
241
|
- Nodes add properties to `model.json`, which contains all the information which are needed by subsequent nodes. These are typically the properties:
|
|
241
|
-
- `resolution`: resolution in which the model expects images (as `int`, since the resolution is mostly square - later, `
|
|
242
|
+
- `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)
|
|
242
243
|
- `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,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,42 @@
|
|
|
1
|
+
from typing import List, Protocol
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..data_classes import ImageMetadata, ImagesMetadata, ModelInformation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DetectorLogicFactory(Protocol):
|
|
9
|
+
"""Protocol for building DetectorLogic instances.
|
|
10
|
+
|
|
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: ...
|
|
16
|
+
|
|
17
|
+
async def build(self, model_info: ModelInformation) -> 'DetectorLogic': ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DetectorLogic(Protocol):
|
|
21
|
+
"""Protocol for detector implementations.
|
|
22
|
+
|
|
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
|
+
"""
|
|
26
|
+
|
|
27
|
+
def evaluate(self, image: np.ndarray) -> ImageMetadata:
|
|
28
|
+
"""Evaluate the image and return the detections.
|
|
29
|
+
|
|
30
|
+
Called by the detector node when an image should be evaluated (REST or SocketIO).
|
|
31
|
+
The resulting detections should be stored in the ImageMetadata.
|
|
32
|
+
Tags stored in the ImageMetadata will be uploaded to the learning loop.
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def batch_evaluate(self, images: List[np.ndarray]) -> ImagesMetadata:
|
|
37
|
+
"""Evaluate a batch of images and return the detections.
|
|
38
|
+
|
|
39
|
+
The resulting detections per image should be stored in the ImagesMetadata.
|
|
40
|
+
Tags stored in the ImagesMetadata will be uploaded to the learning loop.
|
|
41
|
+
"""
|
|
42
|
+
...
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/detector_node.py
RENAMED
|
@@ -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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
445
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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()
|
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
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)
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/io_helpers.py
RENAMED
|
@@ -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. {
|
|
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
|
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/PKG-INFO
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning_loop_node
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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, `
|
|
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,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "learning_loop_node"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "Python Library for Nodes which connect to the Zauberzeug Learning Loop"
|
|
5
5
|
authors = [{ name = "Zauberzeug GmbH", email = "info@zauberzeug.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
10
10
|
"fastapi>=0.128,<1.0",
|
|
11
11
|
"uvicorn[standard]>=0.22.0,<0.23.0",
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"aiofiles>=0.7.0,<0.8.0",
|
|
18
18
|
"async_generator>=1.10,<2.0.0",
|
|
19
19
|
"pytest-watch>=4.2.0,<5.0.0",
|
|
20
|
-
"python-multipart>=0.0.
|
|
20
|
+
"python-multipart>=0.0.22",
|
|
21
21
|
"psutil>=5.8.0,<6.0.0",
|
|
22
22
|
"numpy>=2.0,<3.0",
|
|
23
23
|
"Pillow>=11.0.0,<12.0.0",
|
|
@@ -48,3 +48,45 @@ exclude = ["learning_loop_node.tests*", "learning_loop_node.examples*"]
|
|
|
48
48
|
|
|
49
49
|
[tool.mypy]
|
|
50
50
|
ignore_missing_imports = true
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
indent-width = 4
|
|
55
|
+
line-length = 120
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = [
|
|
60
|
+
"I", # isort
|
|
61
|
+
"E", # pycodestyle
|
|
62
|
+
"W", # pycodestyle
|
|
63
|
+
"B", # bugbear
|
|
64
|
+
"F", # pyflakes
|
|
65
|
+
"UP", # pyupgrade
|
|
66
|
+
"RUF", # ruff
|
|
67
|
+
"PL", # pylint
|
|
68
|
+
"NPY201", # NumPy 2.0
|
|
69
|
+
"Q", # flake8-quotes
|
|
70
|
+
]
|
|
71
|
+
fixable = [
|
|
72
|
+
"I", # isort
|
|
73
|
+
"RUF022", # `__all__` is not sorted
|
|
74
|
+
"Q", # flake8-quotes
|
|
75
|
+
]
|
|
76
|
+
ignore = [
|
|
77
|
+
"E501", # line too long
|
|
78
|
+
"E741",
|
|
79
|
+
"PLR2004", # magic value comparison
|
|
80
|
+
"Q001", # multi line quotes
|
|
81
|
+
"Q002", # docstring quotes
|
|
82
|
+
"PLR0911", # too many
|
|
83
|
+
"PLR0912", # too many
|
|
84
|
+
"PLR0913", # too many arguments
|
|
85
|
+
"PLR0915", # too many arguments
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.flake8-quotes]
|
|
89
|
+
inline-quotes = "single"
|
|
90
|
+
multiline-quotes = "single"
|
|
91
|
+
docstring-quotes = "double"
|
|
92
|
+
avoid-escape = true
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from abc import abstractmethod
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
from ..data_classes import ImageMetadata, ImagesMetadata, ModelInformation
|
|
8
|
-
from ..globals import GLOBALS
|
|
9
|
-
from .exceptions import NodeNeedsRestartError
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DetectorLogic():
|
|
13
|
-
|
|
14
|
-
def __init__(self, model_format: str) -> None:
|
|
15
|
-
self.model_format: str = model_format
|
|
16
|
-
self.model_info: Optional[ModelInformation] = None
|
|
17
|
-
|
|
18
|
-
self._remaining_init_attempts: int = 2
|
|
19
|
-
|
|
20
|
-
async def soft_reload(self):
|
|
21
|
-
self.model_info = None
|
|
22
|
-
|
|
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
|
|
36
|
-
|
|
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.
|
|
53
|
-
|
|
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
|
-
def evaluate(self, image: np.ndarray) -> ImageMetadata:
|
|
62
|
-
"""
|
|
63
|
-
Evaluate the image and return the detections.
|
|
64
|
-
|
|
65
|
-
Called by the detector node when an image should be evaluated (REST or SocketIO).
|
|
66
|
-
The resulting detections should be stored in the ImageMetadata.
|
|
67
|
-
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
|
-
"""
|
|
70
|
-
|
|
71
|
-
@abstractmethod
|
|
72
|
-
def batch_evaluate(self, images: List[np.ndarray]) -> ImagesMetadata:
|
|
73
|
-
"""
|
|
74
|
-
Evaluate a batch of images and return the detections.
|
|
75
|
-
|
|
76
|
-
The resulting detections per image should be stored in the ImagesMetadata.
|
|
77
|
-
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
|
-
"""
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/general.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_classes/training.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/data_exchanger.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/outbox.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/about.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/detect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/detector/rest/upload.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/__init__.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/annotator.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/enums/detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/helpers/log_conf.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/loop_communication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/downloader.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/exceptions.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/executor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/test_executor.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/trainer_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node/trainer/trainer_node.py
RENAMED
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{learning_loop_node-0.18.4 → learning_loop_node-0.19.0}/learning_loop_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|