From 7b07cff25fa6e187d2f01032c673a629fcd6a83e Mon Sep 17 00:00:00 2001 From: Hannes Deittert Date: Mon, 7 Sep 2026 14:52:27 +0200 Subject: [PATCH 1/2] Carry an image state through the socket.io upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop's upload endpoint reads a `state` from each image's json and files the image into that state instead of its default `inbox`, but `ImageMetadata` declared no such field: dacite dropped the key on the way in and `asdict` could not emit it on the way out, so a client had no way to reach it. Declaring the field is enough — the socket.io handler and the outbox are generic over the dataclass, so both already pass it on. Assisted-by: Claude:claude-opus-5 --- README.md | 3 +++ .../data_classes/image_metadata.py | 2 ++ learning_loop_node/detector/detector_node.py | 4 +++- .../tests/detector/test_client_communication.py | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6372a5b3..d2d8bb20 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ The detector also has a **SocketIO** upload endpoint that can be used to upload The endpoint returns None if the upload was successful and an error message otherwise. +Besides detections and annotations, the metadata may carry `tags`, `source`, `created` and `state`. +`state` names the state the image should enter the loop in (e.g. `trash`); when it is omitted the loop applies its own default (`inbox`). + For both ways to upload an image, the tag `picked_by_system` is automatically added to the image metadata. ### Node and model information diff --git a/learning_loop_node/data_classes/image_metadata.py b/learning_loop_node/data_classes/image_metadata.py index 946b5912..76c6aae2 100644 --- a/learning_loop_node/data_classes/image_metadata.py +++ b/learning_loop_node/data_classes/image_metadata.py @@ -46,6 +46,8 @@ class ImageMetadata(): 'description': 'Creation date of the image'}) source: Optional[str] = field(default=None, metadata={ 'description': 'Source of the image'}) + state: str | None = field(default=None, metadata={ + 'description': 'State the image should enter the loop in (e.g. "trash"); None uses the loop default'}) def __len__(self): return len(self.box_detections) + len(self.point_detections) + len(self.segmentation_detections) + len(self.classification_detections) diff --git a/learning_loop_node/detector/detector_node.py b/learning_loop_node/detector/detector_node.py index eea3647d..90b06fe1 100644 --- a/learning_loop_node/detector/detector_node.py +++ b/learning_loop_node/detector/detector_node.py @@ -339,7 +339,9 @@ async def upload(sid, data: Dict) -> Dict: - bytes: bytes of the ndarray (retrieved via `ndarray.tobytes(order='C')`) - dtype: data type of the ndarray as string (e.g. `uint8`, `float32`, etc.) - shape: shape of the ndarray as tuple of ints (e.g. `(480, 640, 3)`) - - metadata: The metadata for the image (optional) + - metadata: The metadata for the image (optional). Besides detections and annotations it + may carry `tags`, `source`, `created` and `state`. A `state` of e.g. "trash" makes the + loop file the image into that state instead of its default one. - upload_priority: Whether to upload with priority (optional) """ self.log.debug('Processing upload via socketio.') diff --git a/learning_loop_node/tests/detector/test_client_communication.py b/learning_loop_node/tests/detector/test_client_communication.py index 9b80c6de..8b7eb15d 100644 --- a/learning_loop_node/tests/detector/test_client_communication.py +++ b/learning_loop_node/tests/detector/test_client_communication.py @@ -89,6 +89,23 @@ async def test_sio_upload(test_detector_node: DetectorNode, sio_client): assert len(get_outbox_files(test_detector_node.outbox)) == 2, 'There should be one image and one .json file.' +async def test_sio_upload_with_state(test_detector_node: DetectorNode, sio_client): + """The state from the metadata has to reach the json file the outbox uploads to the loop.""" + assert len(get_outbox_files(test_detector_node.outbox)) == 0 + + image = np.array(Image.open(test_image_path)) + result = await sio_client.call('upload', { + 'image': {'bytes': image.tobytes(), 'shape': image.shape, 'dtype': str(image.dtype)}, + 'metadata': {'state': 'trash'}, + }) + assert result.get('status') == 'OK' + + json_files = [file for file in get_outbox_files(test_detector_node.outbox) if file.endswith('.json')] + assert len(json_files) == 1 + with open(json_files[0]) as f: + assert json.load(f)['state'] == 'trash' + + # NOTE: This test seems to be flaky. async def test_about_endpoint(test_detector_node: DetectorNode): await asyncio.sleep(16) From 03042a99543074397ce450a1d1267c41c08b4a4f Mon Sep 17 00:00:00 2001 From: Hannes Deittert <74703485+HannesDeittert@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:24:23 +0200 Subject: [PATCH 2/2] Clarify upload response and metadata details Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2d8bb20..3b3aea5f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ The detector also has a **SocketIO** upload endpoint that can be used to upload - `metadata`: a dictionary representing the image metadata. If metadata contains detections and/or annotations, UUIDs for the classes are automatically determined based on the category names. Metadata should follow the schema of the `ImageMetadata` data class. - `upload_priority`: Optional boolean flag to prioritize the upload (defaults to False) -The endpoint returns None if the upload was successful and an error message otherwise. +The endpoint returns `{'status': 'OK'}` if the upload was successful and `{'error': ''}` otherwise. Besides detections and annotations, the metadata may carry `tags`, `source`, `created` and `state`. `state` names the state the image should enter the loop in (e.g. `trash`); when it is omitted the loop applies its own default (`inbox`).