Skip to content
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e4fd630
chore: PoC + ipynb
rishisurana-labelbox Sep 3, 2025
dbcc7bf
chore: use ms instead of s in sdk interface
rishisurana-labelbox Sep 8, 2025
dbb592f
:art: Cleaned
github-actions[bot] Sep 8, 2025
ff298d4
:memo: README updated
github-actions[bot] Sep 8, 2025
16896fd
chore: it works for temporal text/radio/checklist classifications
rishisurana-labelbox Sep 11, 2025
7a666cc
chore: clean up and organize code
rishisurana-labelbox Sep 11, 2025
ac58ad0
chore: update tests fail and documentation update
rishisurana-labelbox Sep 11, 2025
67dd14a
:art: Cleaned
github-actions[bot] Sep 11, 2025
a1600e5
:memo: README updated
github-actions[bot] Sep 11, 2025
b4d2f42
chore: improve imports
rishisurana-labelbox Sep 11, 2025
fadb14e
chore: restore py version
rishisurana-labelbox Sep 11, 2025
1e12596
chore: restore py version
rishisurana-labelbox Sep 11, 2025
c2a7b4c
chore: cleanup
rishisurana-labelbox Sep 12, 2025
26a35fd
chore: lint
rishisurana-labelbox Sep 12, 2025
b16f2ea
fix: failing build issue due to lint
rishisurana-labelbox Sep 12, 2025
943cb73
chore: simplify
rishisurana-labelbox Sep 19, 2025
a838513
chore: update examples - all tests passing
rishisurana-labelbox Sep 19, 2025
0ca9cd6
chore: use start frame instead of frame
rishisurana-labelbox Sep 22, 2025
7861537
chore: remove audio object annotation
rishisurana-labelbox Sep 22, 2025
6c3c50a
chore: change class shape for text and radio/checklist
rishisurana-labelbox Sep 22, 2025
68773cf
chore: stan comments
rishisurana-labelbox Sep 25, 2025
58b30f7
chore: top level + nested working
rishisurana-labelbox Sep 26, 2025
0a63def
feat: nested class for temporal annotations support
rishisurana-labelbox Sep 29, 2025
538ba66
chore: revert old change
rishisurana-labelbox Sep 29, 2025
9675c73
chore: update tests
rishisurana-labelbox Sep 29, 2025
327800b
chore: clean up and track test files
rishisurana-labelbox Sep 29, 2025
1174ad8
chore: update audio.ipynb to reflect breadth of use cases
rishisurana-labelbox Sep 29, 2025
2361ca3
chore: cursor reported bug
rishisurana-labelbox Sep 29, 2025
59f0cd8
chore: extract generic temporal nested logic
rishisurana-labelbox Sep 29, 2025
b186359
chore: update temporal logic to be 1:1 with v3 script
rishisurana-labelbox Sep 30, 2025
e63b306
chore: simplifiy drastically
rishisurana-labelbox Sep 30, 2025
6b54e26
chore: works perfectly
rishisurana-labelbox Sep 30, 2025
ccad765
:art: Cleaned
github-actions[bot] Sep 30, 2025
735bb09
:memo: README updated
github-actions[bot] Sep 30, 2025
db3fb5e
chore: update audio.ipynb
rishisurana-labelbox Sep 30, 2025
b0d5ee4
:art: Cleaned
github-actions[bot] Sep 30, 2025
1266338
chore: drastically simplify
rishisurana-labelbox Oct 1, 2025
66e4c44
chore: lint
rishisurana-labelbox Oct 1, 2025
471c618
chore: new new interface
rishisurana-labelbox Oct 2, 2025
478fb23
chore: final nail; interface is simple and works with frame arg
rishisurana-labelbox Oct 3, 2025
82e90e1
chore: lint
rishisurana-labelbox Oct 3, 2025
fb8df4a
:art: Cleaned
github-actions[bot] Oct 3, 2025
f202586
chore: revert init py file
rishisurana-labelbox Oct 3, 2025
1e424ef
chore: new new new interface for tempral classes
rishisurana-labelbox Oct 3, 2025
fb209f0
chore: cleanup
rishisurana-labelbox Oct 6, 2025
15bb17b
chore: final nail
rishisurana-labelbox Oct 7, 2025
c28a7ca
chore: docs and tests
rishisurana-labelbox Oct 7, 2025
d2dc658
:art: Cleaned
github-actions[bot] Oct 7, 2025
76bdf35
chore: lint
rishisurana-labelbox Oct 7, 2025
58aaf62
chore: stan + cursor bugbot changes
rishisurana-labelbox Oct 7, 2025
9afd82d
chore: remove extra keyword (unused)
rishisurana-labelbox Oct 7, 2025
ad2223c
chore: lint
rishisurana-labelbox Oct 10, 2025
f49a1d8
:art: Cleaned
github-actions[bot] Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 84 additions & 84 deletions examples/README.md

Large diffs are not rendered by default.

58 changes: 56 additions & 2 deletions examples/annotation_import/audio.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
},
{
"metadata": {},
"source": "ontology_builder = lb.OntologyBuilder(classifications=[\n lb.Classification(class_type=lb.Classification.Type.TEXT,\n name=\"text_audio\"),\n lb.Classification(\n class_type=lb.Classification.Type.CHECKLIST,\n name=\"checklist_audio\",\n options=[\n lb.Option(value=\"first_checklist_answer\"),\n lb.Option(value=\"second_checklist_answer\"),\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.RADIO,\n name=\"radio_audio\",\n options=[\n lb.Option(value=\"first_radio_answer\"),\n lb.Option(value=\"second_radio_answer\"),\n ],\n ),\n])\n\nontology = client.create_ontology(\n \"Ontology Audio Annotations\",\n ontology_builder.asdict(),\n media_type=lb.MediaType.Audio,\n)",
"source": "ontology_builder = lb.OntologyBuilder(classifications=[\n # Global (non-temporal) classifications\n lb.Classification(class_type=lb.Classification.Type.TEXT, name=\"text_audio\"\n ),\n lb.Classification(\n class_type=lb.Classification.Type.CHECKLIST,\n name=\"checklist_audio\",\n options=[\n lb.Option(value=\"first_checklist_answer\"),\n lb.Option(value=\"second_checklist_answer\"),\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.RADIO,\n name=\"radio_audio\",\n options=[\n lb.Option(value=\"first_radio_answer\"),\n lb.Option(value=\"second_radio_answer\"),\n ],\n ),\n # Temporal classifications (scope=INDEX for frame-based annotations)\n lb.Classification(\n class_type=lb.Classification.Type.TEXT,\n name=\"transcription\",\n scope=lb.Classification.Scope.INDEX,\n options=[\n lb.Classification(\n class_type=lb.Classification.Type.TEXT,\n name=\"speaker_notes\",\n options=[\n lb.Classification(\n class_type=lb.Classification.Type.TEXT,\n name=\"context_tags\",\n )\n ],\n )\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.RADIO,\n name=\"speaker\",\n scope=lb.Classification.Scope.INDEX,\n options=[\n lb.Option(\"user\"),\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.CHECKLIST,\n name=\"audio_quality\",\n scope=lb.Classification.Scope.INDEX,\n options=[\n lb.Option(\"background_noise\"),\n lb.Option(\"echo\"),\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.TEXT,\n name=\"content_notes\",\n scope=lb.Classification.Scope.INDEX,\n options=[\n lb.Classification(\n class_type=lb.Classification.Type.RADIO,\n name=\"clarity_radio\",\n options=[\n lb.Option(\"very_clear\"),\n lb.Option(\"slightly_clear\"),\n ],\n )\n ],\n ),\n lb.Classification(\n class_type=lb.Classification.Type.CHECKLIST,\n name=\"checklist_class\",\n scope=lb.Classification.Scope.INDEX,\n options=[\n lb.Option(\n \"quality_check\",\n options=[\n lb.Classification(\n class_type=lb.Classification.Type.TEXT,\n name=\"notes_text\",\n options=[\n lb.Classification(\n class_type=lb.Classification.Type.RADIO,\n name=\"severity_radio\",\n options=[\n lb.Option(\"minor\"),\n ],\n )\n ],\n )\n ],\n )\n ],\n ),\n])\n\nontology = client.create_ontology(\n \"Ontology Audio Annotations\",\n ontology_builder.asdict(),\n media_type=lb.MediaType.Audio,\n)",
"cell_type": "code",
"outputs": [],
"execution_count": null
Expand Down Expand Up @@ -225,7 +225,7 @@
},
{
"metadata": {},
"source": "label = []\nlabel.append(\n lb_types.Label(\n data={\"global_key\": global_key},\n annotations=[text_annotation, checklist_annotation, radio_annotation],\n ))",
"source": "label = []\n\n# Regular (global) annotations\nlabel.append(\n lb_types.Label(\n data={\"global_key\": global_key},\n annotations=[text_annotation, checklist_annotation, radio_annotation],\n ))\n\n# Temporal annotations (using new API)\ntemporal_label = []\ntemporal_label.append(\n lb_types.Label(\n data={\"global_key\": global_key},\n annotations=[\n temporal_text_annotation,\n temporal_radio_annotation,\n temporal_checklist_annotation,\n nested_text_annotation,\n inductive_annotation,\n complex_annotation,\n ],\n ))\n\nprint(f\"Created {len(label)} label with regular annotations\")\nprint(\n f\"Created {len(temporal_label)} label with {len(temporal_label[0].annotations)} temporal annotations\"\n)",
"cell_type": "code",
"outputs": [],
"execution_count": null
Expand All @@ -252,6 +252,25 @@
],
"cell_type": "markdown"
},
{
"metadata": {},
"source": "## Temporal Audio Annotations\n\nLabelbox supports temporal annotations for audio/video with frame-level precision using the new temporal classification API.\n\n### Key Features:\n- **Frame-based timing**: All annotations use millisecond precision\n- **Deep nesting**: Support for multi-level nested classifications (Text > Text > Text, Radio > Radio > Radio, etc.)\n- **Inductive structures**: Multiple parent values can share nested classifications that are automatically split based on frame overlap\n- **Frame validation**: Frames start at 1 (not 0) and must be non-overlapping for Text and Radio siblings\n\n### Important Constraints:\n1. **Frame indexing**: Frames are 1-based (frame 0 is invalid)\n2. **Non-overlapping siblings**: Text and Radio classifications at the same level cannot have overlapping frame ranges\n3. **Overlapping checklists**: Only Checklist answers can have overlapping frame ranges with their siblings",
"cell_type": "markdown"
},
{
"metadata": {},
"source": "### Example 1: Simple Temporal Text Classification\n\n# Create temporal text annotation with multiple values at different frame ranges\ntemporal_text_annotation = lb_types.TemporalClassificationText(\n name=\"transcription\",\n value=[\n (1000, 1500, \"Hello AI\"),\n (1501, 2000, \"How are you today?\"),\n ],\n)\n\nprint(\"Created temporal text annotation with 2 text values\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "### Example 2: Temporal Radio Question (single answer)\n\n# Create temporal radio annotation with frame range\ntemporal_radio_annotation = lb_types.TemporalClassificationQuestion(\n name=\"speaker\",\n value=[\n lb_types.TemporalClassificationAnswer(\n name=\"user\",\n frames=[(1000, 2000)],\n )\n ],\n)\n\nprint(\"Created temporal radio annotation\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": [
Expand All @@ -260,6 +279,41 @@
],
"cell_type": "markdown"
},
{
"metadata": {},
"source": "# Upload temporal annotations via MAL\ntemporal_upload_job = lb.MALPredictionImport.create_from_objects(\n client=client,\n project_id=project.uid,\n name=f\"temporal_mal_job-{str(uuid.uuid4())}\",\n predictions=temporal_label, # Use the new temporal_label\n)\n\ntemporal_upload_job.wait_until_done()\nprint(\"Temporal upload completed!\")\nprint(\"Errors:\", temporal_upload_job.errors)\nprint(\"Status:\", temporal_upload_job.statuses)\nprint(\"\\nTemporal annotations uploaded:\")\nprint(\" - Simple text classification\")\nprint(\" - Radio classification\")\nprint(\" - Checklist with overlapping answers\")\nprint(\" - Nested text (3 levels)\")\nprint(\" - Inductive structure (shared nested radio)\")\nprint(\" - Complex nesting (Checklist > Text > Radio)\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "### Example 4: Nested Temporal Classifications (Text > Text > Text)\n\n# Create deeply nested text classifications\nnested_text_annotation = lb_types.TemporalClassificationText(\n name=\"transcription\",\n value=[\n (1000, 2000, \"Hello, how can I help you?\"),\n ],\n classifications=[\n lb_types.TemporalClassificationText(\n name=\"speaker_notes\",\n value=[\n (1000, 2000, \"Polite greeting\"),\n ],\n classifications=[\n lb_types.TemporalClassificationText(\n name=\"context_tags\",\n value=[\n (1500, 2000, \"customer service tone\"),\n ],\n )\n ],\n )\n ],\n)\n\nprint(\"Created 3-level nested text classification\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "### Example 5: Inductive Structure (Multiple text values sharing nested classifications)\n\n# This demonstrates an \"inductive structure\" where multiple parent text values\n# share the same nested radio classification. The serializer will automatically\n# split the nested radio so each text value gets only the radio answers that\n# overlap with its frame range.\n\ninductive_annotation = lb_types.TemporalClassificationText(\n name=\"content_notes\",\n value=[\n (1000, 1500, \"Topic is relevant\"),\n (1501, 2000, \"Good pacing\"),\n ],\n classifications=[\n # This nested radio has answers for BOTH parent text values\n lb_types.TemporalClassificationQuestion(\n name=\"clarity_radio\",\n value=[\n lb_types.TemporalClassificationAnswer(\n name=\"very_clear\",\n frames=[(1000, 1500)\n ], # Will be assigned to \"Topic is relevant\"\n ),\n lb_types.TemporalClassificationAnswer(\n name=\"slightly_clear\",\n frames=[(1501, 2000)], # Will be assigned to \"Good pacing\"\n ),\n ],\n )\n ],\n)\n\nprint(\"Created inductive structure annotation\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "### Example 6: Complex Nesting (Checklist > Text > Radio)\n\n# This demonstrates deep nesting with mixed types\ncomplex_annotation = lb_types.TemporalClassificationQuestion(\n name=\"checklist_class\",\n value=[\n lb_types.TemporalClassificationAnswer(\n name=\"quality_check\",\n frames=[(1, 1500), (2000, 3000)],\n classifications=[\n lb_types.TemporalClassificationText(\n name=\"notes_text\",\n value=[\n (1, 1500, \"Audio quality is excellent\"),\n (2000, 2500, \"Some background noise detected\"),\n ],\n classifications=[\n lb_types.TemporalClassificationQuestion(\n name=\"severity_radio\",\n value=[\n lb_types.TemporalClassificationAnswer(\n name=\"minor\",\n frames=[(2000, 2500)],\n )\n ],\n )\n ],\n )\n ],\n )\n ],\n)\n\nprint(\"Created complex nested annotation: Checklist > Text > Radio\")",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "# Upload temporal annotations via MAL\ntemporal_upload_job = lb.MALPredictionImport.create_from_objects(\n client=client,\n project_id=project.uid,\n name=f\"temporal_mal_job-{str(uuid.uuid4())}\",\n predictions=label_with_temporal,\n)\n\ntemporal_upload_job.wait_until_done()\nprint(\"Temporal upload completed!\")\nprint(\"Errors:\", temporal_upload_job.errors)\nprint(\"Status:\", temporal_upload_job.statuses)",
"cell_type": "code",
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"source": "# Upload our label using Model-Assisted Labeling\nupload_job = lb.MALPredictionImport.create_from_objects(\n client=client,\n project_id=project.uid,\n name=f\"mal_job-{str(uuid.uuid4())}\",\n predictions=label,\n)\n\nupload_job.wait_until_done()\nprint(\"Errors:\", upload_job.errors)\nprint(\"Status of uploads: \", upload_job.statuses)",
Expand Down
71 changes: 71 additions & 0 deletions libs/labelbox/src/labelbox/data/annotation_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from .video import MaskInstance
from .video import VideoMaskAnnotation

from .temporal import TemporalClassificationText
from .temporal import TemporalClassificationQuestion
from .temporal import TemporalClassificationAnswer

from .ner import ConversationEntity
from .ner import DocumentEntity
from .ner import DocumentTextSelection
Expand Down Expand Up @@ -59,3 +63,70 @@
MessageRankingTask,
MessageEvaluationTaskAnnotation,
)

__all__ = [
# Geometry
"Line",
"Point",
"Mask",
"Polygon",
"Rectangle",
"Geometry",
"DocumentRectangle",
"RectangleUnit",
# Annotation
"ClassificationAnnotation",
"ObjectAnnotation",
# Relationship
"RelationshipAnnotation",
"Relationship",
# Video
"VideoClassificationAnnotation",
"VideoObjectAnnotation",
"MaskFrame",
"MaskInstance",
"VideoMaskAnnotation",
# Temporal
"TemporalClassificationText",
"TemporalClassificationQuestion",
"TemporalClassificationAnswer",
# NER
"ConversationEntity",
"DocumentEntity",
"DocumentTextSelection",
"TextEntity",
# Classification
"Checklist",
"ClassificationAnswer",
"Radio",
"Text",
# Data
"GenericDataRowData",
"MaskData",
# Label
"Label",
"LabelGenerator",
# Metrics
"ScalarMetric",
"ScalarMetricAggregation",
"ConfusionMatrixMetric",
"ConfusionMatrixAggregation",
"ScalarMetricValue",
"ConfusionMatrixMetricValue",
# Tiled Image
"EPSG",
"EPSGTransformer",
"TiledBounds",
"TiledImageData",
"TileLayer",
# LLM Prompt Response
"PromptText",
"PromptClassificationAnnotation",
# MMC
"MessageInfo",
"OrderedMessageInfo",
"MessageSingleSelectionTask",
"MessageMultiSelectionTask",
"MessageRankingTask",
"MessageEvaluationTaskAnnotation",
]
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ class ClassificationAnnotation(

value: Union[Text, Checklist, Radio]
message_id: Optional[str] = None

42 changes: 38 additions & 4 deletions libs/labelbox/src/labelbox/data/annotation_types/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
from .metrics import ScalarMetric, ConfusionMatrixMetric
from .video import VideoClassificationAnnotation
from .video import VideoObjectAnnotation, VideoMaskAnnotation
from .temporal import (
TemporalClassificationText,
TemporalClassificationQuestion,
)
from .mmc import MessageEvaluationTaskAnnotation
from pydantic import BaseModel, field_validator

Expand Down Expand Up @@ -44,6 +48,8 @@ class Label(BaseModel):
ClassificationAnnotation,
ObjectAnnotation,
VideoMaskAnnotation,
TemporalClassificationText,
TemporalClassificationQuestion,
ScalarMetric,
ConfusionMatrixMetric,
RelationshipAnnotation,
Expand All @@ -63,8 +69,8 @@ def validate_data(cls, data):
def object_annotations(self) -> List[ObjectAnnotation]:
return self._get_annotations_by_type(ObjectAnnotation)

def classification_annotations(self) -> List[ClassificationAnnotation]:
return self._get_annotations_by_type(ClassificationAnnotation)
def classification_annotations(self) -> List[Union[ClassificationAnnotation, TemporalClassificationText, TemporalClassificationQuestion]]:
return self._get_annotations_by_type((ClassificationAnnotation, TemporalClassificationText, TemporalClassificationQuestion))

def _get_annotations_by_type(self, annotation_type):
return [
Expand All @@ -75,15 +81,43 @@ def _get_annotations_by_type(self, annotation_type):

def frame_annotations(
self,
) -> Dict[str, Union[VideoObjectAnnotation, VideoClassificationAnnotation]]:
) -> Dict[
int,
Union[
VideoObjectAnnotation,
VideoClassificationAnnotation,
TemporalClassificationText,
TemporalClassificationQuestion,
],
]:
"""Get temporal annotations organized by frame

Returns:
Dict[int, List]: Dictionary mapping frame (milliseconds) to list of temporal annotations

Example:
>>> label.frame_annotations()
{2500: [VideoClassificationAnnotation(...), TemporalClassificationText(...)]}

Note:
For TemporalClassificationText/Question, returns dictionary mapping to start of first frame range.
These annotations may have multiple discontinuous frame ranges.
"""
frame_dict = defaultdict(list)
for annotation in self.annotations:
if isinstance(
annotation,
(VideoObjectAnnotation, VideoClassificationAnnotation),
):
frame_dict[annotation.frame].append(annotation)
return frame_dict
elif isinstance(annotation, (TemporalClassificationText, TemporalClassificationQuestion)):
# For temporal annotations with multiple values/answers, use first frame
if isinstance(annotation, TemporalClassificationText) and annotation.value:
frame_dict[annotation.value[0][0]].append(annotation) # value[0][0] is start_frame
elif isinstance(annotation, TemporalClassificationQuestion) and annotation.value:
if annotation.value[0].frames:
frame_dict[annotation.value[0].frames[0][0]].append(annotation) # frames[0][0] is start_frame
return dict(frame_dict)

def add_url_to_masks(self, signer) -> "Label":
"""
Expand Down
Loading
Loading