Skip to content

Commit 1e424ef

Browse files
chore: new new new interface for tempral classes
1 parent f202586 commit 1e424ef

File tree

8 files changed

+719
-898
lines changed

8 files changed

+719
-898
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
from .video import MaskInstance
2020
from .video import VideoMaskAnnotation
2121

22-
from .audio import AudioClassificationAnnotation
22+
from .temporal import TemporalClassificationText
23+
from .temporal import TemporalClassificationQuestion
24+
from .temporal import TemporalClassificationAnswer
2325

2426
from .ner import ConversationEntity
2527
from .ner import DocumentEntity
@@ -30,6 +32,7 @@
3032
from .classification import ClassificationAnswer
3133
from .classification import Radio
3234
from .classification import Text
35+
from .classification import FrameLocation
3336

3437
from .data import GenericDataRowData
3538
from .data import MaskData

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

Lines changed: 0 additions & 36 deletions
This file was deleted.

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from .metrics import ScalarMetric, ConfusionMatrixMetric
1414
from .video import VideoClassificationAnnotation
1515
from .video import VideoObjectAnnotation, VideoMaskAnnotation
16-
from .audio import AudioClassificationAnnotation
16+
from .temporal import (
17+
TemporalClassificationText,
18+
TemporalClassificationQuestion,
19+
)
1720
from .mmc import MessageEvaluationTaskAnnotation
1821
from pydantic import BaseModel, field_validator
1922

@@ -45,7 +48,8 @@ class Label(BaseModel):
4548
ClassificationAnnotation,
4649
ObjectAnnotation,
4750
VideoMaskAnnotation,
48-
AudioClassificationAnnotation,
51+
TemporalClassificationText,
52+
TemporalClassificationQuestion,
4953
ScalarMetric,
5054
ConfusionMatrixMetric,
5155
RelationshipAnnotation,
@@ -82,7 +86,8 @@ def frame_annotations(
8286
Union[
8387
VideoObjectAnnotation,
8488
VideoClassificationAnnotation,
85-
AudioClassificationAnnotation,
89+
TemporalClassificationText,
90+
TemporalClassificationQuestion,
8691
],
8792
]:
8893
"""Get temporal annotations organized by frame
@@ -92,7 +97,11 @@ def frame_annotations(
9297
9398
Example:
9499
>>> label.frame_annotations()
95-
{2500: [VideoClassificationAnnotation(...), AudioClassificationAnnotation(...)]}
100+
{2500: [VideoClassificationAnnotation(...), TemporalClassificationText(...)]}
101+
102+
Note:
103+
For TemporalClassificationText/Question, returns dictionary mapping to start of first frame range.
104+
These annotations may have multiple discontinuous frame ranges.
96105
"""
97106
frame_dict = defaultdict(list)
98107
for annotation in self.annotations:
@@ -101,8 +110,13 @@ def frame_annotations(
101110
(VideoObjectAnnotation, VideoClassificationAnnotation),
102111
):
103112
frame_dict[annotation.frame].append(annotation)
104-
elif isinstance(annotation, AudioClassificationAnnotation):
105-
frame_dict[annotation.start_frame].append(annotation)
113+
elif isinstance(annotation, (TemporalClassificationText, TemporalClassificationQuestion)):
114+
# For temporal annotations with multiple values/answers, use first frame
115+
if isinstance(annotation, TemporalClassificationText) and annotation.value:
116+
frame_dict[annotation.value[0][0]].append(annotation) # value[0][0] is start_frame
117+
elif isinstance(annotation, TemporalClassificationQuestion) and annotation.value:
118+
if annotation.value[0].frames:
119+
frame_dict[annotation.value[0].frames[0][0]].append(annotation) # frames[0][0] is start_frame
106120
return dict(frame_dict)
107121

108122
def add_url_to_masks(self, signer) -> "Label":
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""
2+
Temporal classification annotations for audio, video, and other time-based media.
3+
4+
These classes provide a unified, recursive structure for temporal annotations with
5+
frame-level precision. All temporal classifications support nested hierarchies.
6+
"""
7+
8+
from typing import List, Optional, Tuple, Union
9+
from pydantic import Field
10+
11+
from labelbox.data.annotation_types.annotation import ClassificationAnnotation
12+
from labelbox.data.annotation_types.classification.classification import (
13+
ClassificationAnswer,
14+
FrameLocation,
15+
)
16+
17+
18+
class TemporalClassificationAnswer(ClassificationAnswer):
19+
"""
20+
Temporal answer for Radio/Checklist questions with frame ranges.
21+
22+
Represents a single answer option that can exist at multiple discontinuous
23+
time ranges and contain nested classifications.
24+
25+
Args:
26+
name (str): Name of the answer option
27+
frames (List[Tuple[int, int]]): List of (start_frame, end_frame) ranges in milliseconds
28+
classifications (Optional[List[Union[TemporalClassificationText, TemporalClassificationQuestion]]]):
29+
Nested classifications within this answer
30+
feature_schema_id (Optional[str]): Feature schema identifier
31+
extra (dict): Additional metadata
32+
33+
Example:
34+
>>> # Radio answer with nested classifications
35+
>>> answer = TemporalClassificationAnswer(
36+
>>> name="user",
37+
>>> frames=[(200, 1600)],
38+
>>> classifications=[
39+
>>> TemporalClassificationQuestion(
40+
>>> name="tone",
41+
>>> answers=[
42+
>>> TemporalClassificationAnswer(
43+
>>> name="professional",
44+
>>> frames=[(1000, 1600)]
45+
>>> )
46+
>>> ]
47+
>>> )
48+
>>> ]
49+
>>> )
50+
"""
51+
52+
frames: List[Tuple[int, int]] = Field(
53+
default_factory=list,
54+
description="List of (start_frame, end_frame) tuples in milliseconds",
55+
)
56+
classifications: Optional[
57+
List[Union["TemporalClassificationText", "TemporalClassificationQuestion"]]
58+
] = None
59+
60+
61+
class TemporalClassificationText(ClassificationAnnotation):
62+
"""
63+
Temporal text classification with multiple text values at different frame ranges.
64+
65+
Allows multiple text annotations at different time segments, each with precise
66+
frame ranges. Supports recursive nesting of text and question classifications.
67+
68+
Args:
69+
name (str): Name of the text classification
70+
values (List[Tuple[int, int, str]]): List of (start_frame, end_frame, text_value) tuples
71+
classifications (Optional[List[Union[TemporalClassificationText, TemporalClassificationQuestion]]]):
72+
Nested classifications
73+
feature_schema_id (Optional[str]): Feature schema identifier
74+
extra (dict): Additional metadata
75+
76+
Example:
77+
>>> # Simple text with multiple temporal values
78+
>>> transcription = TemporalClassificationText(
79+
>>> name="transcription",
80+
>>> values=[
81+
>>> (1600, 2000, "Hello, how can I help you?"),
82+
>>> (2500, 3000, "Thank you for calling!"),
83+
>>> ]
84+
>>> )
85+
>>>
86+
>>> # Text with nested classifications
87+
>>> transcription_with_notes = TemporalClassificationText(
88+
>>> name="transcription",
89+
>>> values=[
90+
>>> (1600, 2000, "Hello, how can I help you?"),
91+
>>> ],
92+
>>> classifications=[
93+
>>> TemporalClassificationText(
94+
>>> name="speaker_notes",
95+
>>> values=[
96+
>>> (1600, 2000, "Polite greeting"),
97+
>>> ]
98+
>>> )
99+
>>> ]
100+
>>> )
101+
"""
102+
103+
# Override parent's value field
104+
value: List[Tuple[int, int, str]] = Field(
105+
default_factory=list,
106+
description="List of (start_frame, end_frame, text_value) tuples",
107+
)
108+
classifications: Optional[
109+
List[Union["TemporalClassificationText", "TemporalClassificationQuestion"]]
110+
] = None
111+
112+
113+
class TemporalClassificationQuestion(ClassificationAnnotation):
114+
"""
115+
Temporal Radio/Checklist question with multiple answer options.
116+
117+
Represents a question with one or more answer options, each having their own
118+
frame ranges. Radio questions have a single answer, Checklist can have multiple.
119+
120+
Args:
121+
name (str): Name of the question/classification
122+
answers (List[TemporalClassificationAnswer]): List of answer options with frame ranges
123+
feature_schema_id (Optional[str]): Feature schema identifier
124+
extra (dict): Additional metadata
125+
126+
Note:
127+
- Radio: Single answer in the answers list
128+
- Checklist: Multiple answers in the answers list
129+
The serializer automatically handles the distinction based on the number of answers.
130+
131+
Example:
132+
>>> # Radio question (single answer)
133+
>>> speaker = TemporalClassificationQuestion(
134+
>>> name="speaker",
135+
>>> answers=[
136+
>>> TemporalClassificationAnswer(
137+
>>> name="user",
138+
>>> frames=[(200, 1600)]
139+
>>> )
140+
>>> ]
141+
>>> )
142+
>>>
143+
>>> # Checklist question (multiple answers)
144+
>>> audio_quality = TemporalClassificationQuestion(
145+
>>> name="audio_quality",
146+
>>> answers=[
147+
>>> TemporalClassificationAnswer(
148+
>>> name="background_noise",
149+
>>> frames=[(0, 1500), (2000, 3000)]
150+
>>> ),
151+
>>> TemporalClassificationAnswer(
152+
>>> name="echo",
153+
>>> frames=[(2200, 2900)]
154+
>>> )
155+
>>> ]
156+
>>> )
157+
>>>
158+
>>> # Nested structure: Radio > Radio > Radio
159+
>>> speaker_with_tone = TemporalClassificationQuestion(
160+
>>> name="speaker",
161+
>>> answers=[
162+
>>> TemporalClassificationAnswer(
163+
>>> name="user",
164+
>>> frames=[(200, 1600)],
165+
>>> classifications=[
166+
>>> TemporalClassificationQuestion(
167+
>>> name="tone",
168+
>>> answers=[
169+
>>> TemporalClassificationAnswer(
170+
>>> name="professional",
171+
>>> frames=[(1000, 1600)]
172+
>>> )
173+
>>> ]
174+
>>> )
175+
>>> ]
176+
>>> )
177+
>>> ]
178+
>>> )
179+
"""
180+
181+
# Override parent's value field
182+
value: List[TemporalClassificationAnswer] = Field(
183+
default_factory=list,
184+
description="List of temporal answer options",
185+
)
186+
classifications: Optional[
187+
List[Union["TemporalClassificationText", "TemporalClassificationQuestion"]]
188+
] = None
189+
190+
191+
# Update forward references for recursive types
192+
TemporalClassificationAnswer.model_rebuild()
193+
TemporalClassificationText.model_rebuild()
194+
TemporalClassificationQuestion.model_rebuild()

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
VideoObjectAnnotation,
2626
)
2727
from typing import List
28-
from ...annotation_types.audio import (
29-
AudioClassificationAnnotation,
28+
from ...annotation_types.temporal import (
29+
TemporalClassificationText,
30+
TemporalClassificationQuestion,
31+
TemporalClassificationAnswer,
3032
)
31-
from .temporal import create_audio_ndjson_annotations
33+
from .temporal import create_temporal_ndjson_annotations
3234
from labelbox.types import DocumentRectangle, DocumentEntity
3335
from .classification import (
3436
NDChecklistSubclass,
@@ -169,20 +171,20 @@ def _create_video_annotations(
169171
def _create_audio_annotations(
170172
cls, label: Label
171173
) -> Generator[BaseModel, None, None]:
172-
"""Create audio annotations with nested classifications using modular hierarchy builder."""
173-
# Extract audio annotations from the label
174-
audio_annotations = [
174+
"""Create temporal annotations with nested classifications using new temporal classes."""
175+
# Extract temporal annotations from the label
176+
temporal_annotations = [
175177
annot
176178
for annot in label.annotations
177-
if isinstance(annot, AudioClassificationAnnotation)
179+
if isinstance(annot, (TemporalClassificationText, TemporalClassificationQuestion))
178180
]
179181

180-
if not audio_annotations:
182+
if not temporal_annotations:
181183
return
182184

183-
# Use the modular hierarchy builder to create NDJSON annotations
184-
ndjson_annotations = create_audio_ndjson_annotations(
185-
audio_annotations, label.data.global_key
185+
# Use the new temporal serializer to create NDJSON annotations
186+
ndjson_annotations = create_temporal_ndjson_annotations(
187+
temporal_annotations, label.data.global_key
186188
)
187189

188190
# Yield each NDJSON annotation
@@ -200,7 +202,8 @@ def _create_non_video_annotations(cls, label: Label):
200202
VideoClassificationAnnotation,
201203
VideoObjectAnnotation,
202204
VideoMaskAnnotation,
203-
AudioClassificationAnnotation,
205+
TemporalClassificationText,
206+
TemporalClassificationQuestion,
204207
RelationshipAnnotation,
205208
),
206209
)

0 commit comments

Comments
 (0)