Skip to content

Commit 58b30f7

Browse files
chore: top level + nested working
1 parent 68773cf commit 58b30f7

File tree

3 files changed

+70
-62
lines changed

3 files changed

+70
-62
lines changed

libs/labelbox/src/labelbox/data/annotation_types/audio.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class AudioClassificationAnnotation(ClassificationAnnotation):
2626

2727
start_frame: int = Field(
2828
validation_alias=AliasChoices("start_frame", "frame"),
29-
serialization_alias="frame",
29+
serialization_alias="startframe",
3030
)
3131
end_frame: Optional[int] = Field(
3232
default=None,
@@ -35,33 +35,3 @@ class AudioClassificationAnnotation(ClassificationAnnotation):
3535
)
3636
segment_index: Optional[int] = None
3737

38-
39-
class AudioTextClassificationAnnotation(ClassificationAnnotation):
40-
"""Audio classification for specific time range
41-
42-
Examples:
43-
- Speaker identification from 2500ms to 4100ms
44-
- Audio quality assessment for a segment
45-
- Language detection for audio segments
46-
47-
Args:
48-
name (Optional[str]): Name of the classification
49-
feature_schema_id (Optional[Cuid]): Feature schema identifier
50-
value (Union[Text, Checklist, Radio]): Classification value
51-
start_frame (int): The frame index in milliseconds (e.g., 2500 = 2.5 seconds)
52-
end_frame (Optional[int]): End frame in milliseconds (for time ranges)
53-
segment_index (Optional[int]): Index of audio segment this annotation belongs to
54-
extra (Dict[str, Any]): Additional metadata
55-
"""
56-
57-
start_frame: int = Field(
58-
validation_alias=AliasChoices("start_frame", "frame"),
59-
serialization_alias="frame",
60-
)
61-
end_frame: Optional[int] = Field(
62-
default=None,
63-
validation_alias=AliasChoices("end_frame", "endFrame"),
64-
serialization_alias="end_frame",
65-
)
66-
67-

libs/labelbox/src/labelbox/data/serialization/ndjson/classification.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,6 @@ def serialize_model(self, handler):
6060
return res
6161

6262

63-
class FrameLocation(BaseModel):
64-
end: int
65-
start: int
66-
67-
68-
class VideoSupported(BaseModel):
69-
# Note that frames are only allowed as top level inferences for video
70-
frames: Optional[List[FrameLocation]] = None
71-
72-
@model_serializer(mode="wrap")
73-
def serialize_model(self, handler):
74-
res = handler(self)
75-
# This means these are no video frames ..
76-
if self.frames is None:
77-
res.pop("frames")
78-
return res
7963

8064

8165
class NDTextSubclass(NDAnswer):
@@ -223,7 +207,7 @@ def from_common(
223207
# ====== End of subclasses
224208

225209

226-
class NDText(NDAnnotation, NDTextSubclass, VideoSupported):
210+
class NDText(NDAnnotation, NDTextSubclass):
227211
@classmethod
228212
def from_common(
229213
cls,
@@ -249,7 +233,7 @@ def from_common(
249233
)
250234

251235

252-
class NDChecklist(NDAnnotation, NDChecklistSubclass, VideoSupported):
236+
class NDChecklist(NDAnnotation, NDChecklistSubclass):
253237
@model_serializer(mode="wrap")
254238
def serialize_model(self, handler):
255239
res = handler(self)
@@ -296,7 +280,7 @@ def from_common(
296280
)
297281

298282

299-
class NDRadio(NDAnnotation, NDRadioSubclass, VideoSupported):
283+
class NDRadio(NDAnnotation, NDRadioSubclass):
300284
@classmethod
301285
def from_common(
302286
cls,

libs/labelbox/src/labelbox/data/serialization/ndjson/label.py

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import copy
33
from itertools import groupby
44
from operator import itemgetter
5-
from typing import Generator, List, Tuple, Union
5+
from typing import Any, Dict, Generator, List, Tuple, Union
66
from uuid import uuid4
77

88
from pydantic import BaseModel
@@ -168,25 +168,79 @@ def _create_video_annotations(
168168
@classmethod
169169
def _create_audio_annotations(
170170
cls, label: Label
171-
) -> Generator[Union[NDChecklistSubclass, NDRadioSubclass], None, None]:
172-
"""Create audio annotations serialized in Video NDJSON classification format."""
171+
) -> Generator[BaseModel, None, None]:
172+
"""Create audio annotations grouped by classification name in v2.py format."""
173173
audio_annotations = defaultdict(list)
174174

175175
# Collect audio annotations by name/schema_id
176176
for annot in label.annotations:
177177
if isinstance(annot, AudioClassificationAnnotation):
178178
audio_annotations[annot.feature_schema_id or annot.name].append(annot)
179179

180-
for annotation_group in audio_annotations.values():
181-
# Simple grouping: one NDJSON entry per annotation group (same as video)
182-
annotation = annotation_group[0]
183-
frames_data = []
180+
# Create v2.py format for each classification group
181+
for classification_name, annotation_group in audio_annotations.items():
182+
# Group annotations by value (like v2.py does)
183+
value_groups = defaultdict(list)
184+
184185
for ann in annotation_group:
185-
start = ann.start_frame
186-
end = getattr(ann, "end_frame", None) or ann.start_frame
187-
frames_data.append({"start": start, "end": end})
188-
annotation.extra.update({"frames": frames_data})
189-
yield NDClassification.from_common(annotation, label.data)
186+
# Extract value based on classification type for grouping
187+
if hasattr(ann.value, 'answer'):
188+
if isinstance(ann.value.answer, list):
189+
# Checklist classification - convert list to string for grouping
190+
value = str(sorted([item.name for item in ann.value.answer]))
191+
elif hasattr(ann.value.answer, 'name'):
192+
# Radio classification - ann.value.answer is ClassificationAnswer with name
193+
value = ann.value.answer.name
194+
else:
195+
# Text classification
196+
value = ann.value.answer
197+
else:
198+
value = str(ann.value)
199+
200+
# Group by value
201+
value_groups[value].append(ann)
202+
203+
# Create answer items with grouped frames (like v2.py)
204+
answer_items = []
205+
for value, annotations_with_same_value in value_groups.items():
206+
frames = []
207+
for ann in annotations_with_same_value:
208+
frames.append({"start": ann.start_frame, "end": ann.end_frame})
209+
210+
# Extract the actual value for the output (not the grouping key)
211+
first_ann = annotations_with_same_value[0]
212+
213+
# Use different field names based on classification type
214+
if hasattr(first_ann.value, 'answer') and isinstance(first_ann.value.answer, list):
215+
# Checklist - use "name" field (like v2.py)
216+
answer_items.append({
217+
"name": first_ann.value.answer[0].name, # Single item for now
218+
"frames": frames
219+
})
220+
elif hasattr(first_ann.value, 'answer') and hasattr(first_ann.value.answer, 'name'):
221+
# Radio - use "name" field (like v2.py)
222+
answer_items.append({
223+
"name": first_ann.value.answer.name,
224+
"frames": frames
225+
})
226+
else:
227+
# Text - use "value" field (like v2.py)
228+
answer_items.append({
229+
"value": first_ann.value.answer,
230+
"frames": frames
231+
})
232+
233+
# Create a simple Pydantic model for the v2.py format
234+
class AudioNDJSON(BaseModel):
235+
name: str
236+
answer: List[Dict[str, Any]]
237+
dataRow: Dict[str, str]
238+
239+
yield AudioNDJSON(
240+
name=classification_name,
241+
answer=answer_items,
242+
dataRow={"globalKey": label.data.global_key}
243+
)
190244

191245

192246

0 commit comments

Comments
 (0)