Loading trapper/trapper-project/trapper/apps/citizen_science/serializers.py +5 −0 Original line number Diff line number Diff line Loading @@ -693,6 +693,11 @@ class CSBBoxesSerializer(serializers.Serializer): file_fps = self.context.get("file_fps") or 0 bboxes = instance.bboxes first_frame = instance.first_frame_index # Handle None case: treat as 0 (default for images and first occurrence) if first_frame is None: first_frame = 0 if bboxes and len(bboxes) == 4 and first_frame <= file_fps: return { "left": bboxes[0], Loading trapper/trapper-project/trapper/apps/citizen_science/tests.py +20 −10 Original line number Diff line number Diff line Loading @@ -465,7 +465,8 @@ class CSMediaTests(BaseAPITestCase): self.dynamic_ai_classification1 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification1, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification2 = Classification.objects.get( project=self.classification_project, resource=self.resource2 Loading @@ -479,7 +480,8 @@ class CSMediaTests(BaseAPITestCase): self.dynamic_ai_classification2 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification2, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification3 = Classification.objects.get( project=self.classification_project, resource=self.resource3 Loading @@ -494,7 +496,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification3, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification4 = Classification.objects.get( project=self.classification_project, resource=self.resource4 Loading @@ -511,7 +514,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification4, observation_type=ObservationType.ANIMAL, species=self.species2, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification5 = ClassificationFactory( Loading @@ -537,7 +541,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification5, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) ) # Set source_classification to AI classification (so it's not expert-approved) Loading Loading @@ -817,7 +822,8 @@ class CSMediaScrollTests(BaseAPITestCase): self.dynamic_ai_classification1 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification1, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification2 = Classification.objects.get( project=self.classification_project, resource=self.resource2 Loading @@ -831,7 +837,8 @@ class CSMediaScrollTests(BaseAPITestCase): self.dynamic_ai_classification2 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification2, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification3 = Classification.objects.get( project=self.classification_project, resource=self.resource3 Loading @@ -846,7 +853,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification3, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification4 = Classification.objects.get( project=self.classification_project, resource=self.resource4 Loading @@ -861,7 +869,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification4, observation_type=ObservationType.ANIMAL, species=self.species2, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification5 = ClassificationFactory( Loading @@ -886,7 +895,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification5, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) ) self.classification1.source_classification = self.ai_classification1 Loading trapper/trapper-project/trapper/apps/media_classification/frames.py +9 −0 Original line number Diff line number Diff line Loading @@ -268,6 +268,15 @@ class SmartFrameService: if dyn_id is None: continue # Skip objects with non-numeric IDs (should not happen if serializer filtered properly) # This is a defensive check for msgpack data from other sources if not isinstance(dyn_id, int): try: dyn_id = int(dyn_id) except (ValueError, TypeError): # Skip objects that couldn't be matched to dynamic attrs continue # Delete existing rows for this dynamic attrs id self.filter_ts( classification=cls, Loading trapper/trapper-project/trapper/apps/media_classification/serializers_rest.py +11 −0 Original line number Diff line number Diff line Loading @@ -478,6 +478,17 @@ class SingleClassifySerializer(BaseClassifyMixin, serializers.ModelSerializer): # Now save frames from msgpack if exist and rebuild msgpack file for user classification if bboxes_msgpack: # Filter out unmatched objects (those with string IDs that were never matched to object_id) # Only keep objects whose "id" was updated to a numeric database ID matched_objects = [ obj for obj in bboxes_msgpack["objects"] if isinstance(obj.get("id"), int) ] bboxes_msgpack["objects"] = matched_objects # Only process if there are matched objects if matched_objects: frames_service = SmartFrameService() frames_service.upsert_msgpack( payload=bboxes_msgpack, Loading trapper/trapper-project/trapper/apps/media_classification/tests/factories/classification.py +2 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,8 @@ class ClassificationFactory(factory.django.DjangoModelFactory): created_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=10)) updated_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=10)) updated_by = owner # Helper field that caches resource's date_recorded for efficient ordering date_recorded = factory.LazyAttribute(lambda o: o.resource.date_recorded) classification_type = ClassificationType.FINAL is_approved = False Loading Loading
trapper/trapper-project/trapper/apps/citizen_science/serializers.py +5 −0 Original line number Diff line number Diff line Loading @@ -693,6 +693,11 @@ class CSBBoxesSerializer(serializers.Serializer): file_fps = self.context.get("file_fps") or 0 bboxes = instance.bboxes first_frame = instance.first_frame_index # Handle None case: treat as 0 (default for images and first occurrence) if first_frame is None: first_frame = 0 if bboxes and len(bboxes) == 4 and first_frame <= file_fps: return { "left": bboxes[0], Loading
trapper/trapper-project/trapper/apps/citizen_science/tests.py +20 −10 Original line number Diff line number Diff line Loading @@ -465,7 +465,8 @@ class CSMediaTests(BaseAPITestCase): self.dynamic_ai_classification1 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification1, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification2 = Classification.objects.get( project=self.classification_project, resource=self.resource2 Loading @@ -479,7 +480,8 @@ class CSMediaTests(BaseAPITestCase): self.dynamic_ai_classification2 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification2, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification3 = Classification.objects.get( project=self.classification_project, resource=self.resource3 Loading @@ -494,7 +496,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification3, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification4 = Classification.objects.get( project=self.classification_project, resource=self.resource4 Loading @@ -511,7 +514,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification4, observation_type=ObservationType.ANIMAL, species=self.species2, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification5 = ClassificationFactory( Loading @@ -537,7 +541,8 @@ class CSMediaTests(BaseAPITestCase): classification=self.user_classification5, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) ) # Set source_classification to AI classification (so it's not expert-approved) Loading Loading @@ -817,7 +822,8 @@ class CSMediaScrollTests(BaseAPITestCase): self.dynamic_ai_classification1 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification1, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification2 = Classification.objects.get( project=self.classification_project, resource=self.resource2 Loading @@ -831,7 +837,8 @@ class CSMediaScrollTests(BaseAPITestCase): self.dynamic_ai_classification2 = AIClassificationDynamicAttrsFactory( classification=self.ai_classification2, observation_type=ObservationType.ANIMAL, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification3 = Classification.objects.get( project=self.classification_project, resource=self.resource3 Loading @@ -846,7 +853,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification3, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification4 = Classification.objects.get( project=self.classification_project, resource=self.resource4 Loading @@ -861,7 +869,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification4, observation_type=ObservationType.ANIMAL, species=self.species2, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) self.classification5 = ClassificationFactory( Loading @@ -886,7 +895,8 @@ class CSMediaScrollTests(BaseAPITestCase): classification=self.user_classification5, observation_type=ObservationType.ANIMAL, species=self.species1, bboxes=[[0.444, 0.444, 0.333, 0.333]], bboxes=[0.444, 0.444, 0.333, 0.333], first_frame_index=0, ) ) self.classification1.source_classification = self.ai_classification1 Loading
trapper/trapper-project/trapper/apps/media_classification/frames.py +9 −0 Original line number Diff line number Diff line Loading @@ -268,6 +268,15 @@ class SmartFrameService: if dyn_id is None: continue # Skip objects with non-numeric IDs (should not happen if serializer filtered properly) # This is a defensive check for msgpack data from other sources if not isinstance(dyn_id, int): try: dyn_id = int(dyn_id) except (ValueError, TypeError): # Skip objects that couldn't be matched to dynamic attrs continue # Delete existing rows for this dynamic attrs id self.filter_ts( classification=cls, Loading
trapper/trapper-project/trapper/apps/media_classification/serializers_rest.py +11 −0 Original line number Diff line number Diff line Loading @@ -478,6 +478,17 @@ class SingleClassifySerializer(BaseClassifyMixin, serializers.ModelSerializer): # Now save frames from msgpack if exist and rebuild msgpack file for user classification if bboxes_msgpack: # Filter out unmatched objects (those with string IDs that were never matched to object_id) # Only keep objects whose "id" was updated to a numeric database ID matched_objects = [ obj for obj in bboxes_msgpack["objects"] if isinstance(obj.get("id"), int) ] bboxes_msgpack["objects"] = matched_objects # Only process if there are matched objects if matched_objects: frames_service = SmartFrameService() frames_service.upsert_msgpack( payload=bboxes_msgpack, Loading
trapper/trapper-project/trapper/apps/media_classification/tests/factories/classification.py +2 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,8 @@ class ClassificationFactory(factory.django.DjangoModelFactory): created_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=10)) updated_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=10)) updated_by = owner # Helper field that caches resource's date_recorded for efficient ordering date_recorded = factory.LazyAttribute(lambda o: o.resource.date_recorded) classification_type = ClassificationType.FINAL is_approved = False Loading