Skip to content

Commit cc7c7ee

Browse files
committed
Simplify video annotation examples and request fixtures
1 parent 256f90b commit cc7c7ee

3 files changed

Lines changed: 40 additions & 83 deletions

File tree

‎docs/core/project.md‎

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,16 @@ official `roboflow-video-coco` format; keep its PTS and rational time base from
77
the video rather than converting them to seconds or rounding frames.
88

99
```python
10+
import json
1011
import roboflow
1112

1213
project = roboflow.Roboflow(api_key="MY_API_KEY").workspace("my-workspace").project("actions")
13-
video_id = final_upload_status["videoId"] # status is "uploaded"
14-
document = {
15-
"info": {"format": "roboflow-video-coco"},
16-
"videos": [{
17-
"id": 1, "file_name": "clip.mp4", "width": 640, "height": 360,
18-
"duration": 10, "fps": 24,
19-
"time_base": {"numerator": 1, "denominator": 12288},
20-
}],
21-
"categories": [{"id": 1, "name": "jumping"}],
22-
"segments": [{
23-
"id": 1, "video_id": 1, "category_id": 1,
24-
"start_frame": 48, "end_frame": 95,
25-
"start_pts": 24576, "end_pts": 49152,
26-
}],
27-
}
28-
result = project.annotate_video_segments(video_id, document)
14+
with open("clip.video-coco.json") as annotations:
15+
document = json.load(annotations) # An official video-coco document for clip.mp4.
16+
status = project.upload_video("clip.mp4", wait=True)
17+
if status["status"] != "uploaded":
18+
raise RuntimeError(status["message"])
19+
result = project.annotate_video_segments(status["videoId"], document)
2920
```
3021

3122
The API adds the video to the Dataset by default. Use `add_to_dataset=False`

‎roboflow/core/project.py‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -602,22 +602,13 @@ def annotate_video_segments(
602602
split: Optional[str] = None,
603603
add_to_dataset: Optional[bool] = None,
604604
) -> Dict:
605-
"""Annotate a canonical video Source in an Action Recognition project.
605+
"""Send a complete roboflow-video-coco document without converting timestamps.
606606
607-
``document`` is a complete ``roboflow-video-coco`` document. Its native PTS,
608-
frame indices, and rational time base are forwarded without conversion.
609-
Use the final ``videoId`` from an uploaded video's status, which may differ
610-
from the initial upload ID after content reuse.
611-
612-
The API adds the Source to the Dataset by default. Set ``add_to_dataset=False``
613-
to keep its existing membership, ``split`` to assign train/valid/test, or
614-
``overwrite=True`` to replace different existing segments. An identical
615-
retry succeeds without overwrite. Server rejections, including HTTP 409
616-
preservation, raise ``AnnotationSaveError`` with ``status_code``.
617-
618-
Returns:
619-
The API response, including ``success``, ``inDataset``, and
620-
``createdClasses``.
607+
Use the final uploaded-status ``videoId``. The API adds the video to the
608+
Dataset unless ``add_to_dataset=False``; ``split`` assigns train/valid/test.
609+
Identical retries succeed; different segments require ``overwrite=True``.
610+
Returns the API response (``success``, ``inDataset``, ``createdClasses``).
611+
Rejections raise ``AnnotationSaveError`` with the server's HTTP status.
621612
"""
622613
return rfapi.annotate_video_segments(
623614
self.__api_key,

‎tests/test_video_segment_annotation.py‎

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,14 @@
99

1010
from roboflow.adapters.rfapi import AnnotationSaveError
1111
from roboflow.config import API_URL
12-
from roboflow.core.project import Project
12+
from tests import PROJECT_NAME, ROBOFLOW_API_KEY, WORKSPACE_NAME, RoboflowTest
1313

1414

15-
class TestVideoSegmentAnnotation(unittest.TestCase):
15+
class TestVideoSegmentAnnotation(RoboflowTest):
1616
def setUp(self):
17-
self.project = Project(
18-
"test-key",
19-
{
20-
"annotation": "actions",
21-
"classes": {},
22-
"colors": {},
23-
"created": 0,
24-
"id": "my-workspace/actions",
25-
"images": 0,
26-
"name": "Actions",
27-
"public": False,
28-
"splits": {},
29-
"type": "action-recognition",
30-
"unannotated": 0,
31-
"updated": 0,
32-
},
33-
)
34-
self.url = f"{API_URL}/my-workspace/actions/annotate/final-source-id"
17+
super().setUp()
18+
self.project.type = "action-recognition"
19+
self.url = f"{API_URL}/{WORKSPACE_NAME}/{PROJECT_NAME}/annotate/final-source-id"
3520
self.document = {
3621
"info": {"format": "roboflow-video-coco"},
3722
"videos": [
@@ -59,56 +44,46 @@ def setUp(self):
5944
],
6045
}
6146

62-
def test_acceptance_and_identical_retry_forward_the_canonical_document(self):
47+
def test_finalized_source_and_identical_retry_forward_the_canonical_document(self):
6348
first = {"success": True, "inDataset": True, "createdClasses": ["jumping"]}
6449
retry = {"success": True, "inDataset": True, "createdClasses": []}
6550
with responses.RequestsMock() as http:
51+
http.add(
52+
responses.GET,
53+
f"{API_URL}/{WORKSPACE_NAME}/upload/video/reservation-id",
54+
json={"status": "uploaded", "videoId": "final-source-id"},
55+
)
6656
http.add(responses.POST, self.url, json=first)
6757
http.add(responses.POST, self.url, json=retry)
68-
self.assertEqual(self.project.annotate_video_segments("final-source-id", self.document), first)
69-
self.assertEqual(self.project.annotate_video_segments("final-source-id", self.document), retry)
58+
status = self.project.wait_for_video_upload("reservation-id")
59+
self.assertEqual(self.project.annotate_video_segments(status["videoId"], self.document), first)
60+
self.assertEqual(self.project.annotate_video_segments(status["videoId"], self.document), retry)
7061

71-
self.assertEqual(len(http.calls), 2)
72-
for call in http.calls:
62+
self.assertEqual([call.request.method for call in http.calls], ["GET", "POST", "POST"])
63+
for call in http.calls[1:]:
7364
query = parse_qs(urlparse(call.request.url).query)
74-
self.assertEqual(query, {"api_key": ["test-key"], "name": ["annotations.json"]})
65+
self.assertEqual(query, {"api_key": [ROBOFLOW_API_KEY], "name": ["annotations.json"]})
7566
self.assertEqual(json.loads(call.request.body), {"annotationFile": json.dumps(self.document)})
7667

7768
def test_conflict_is_raised_and_explicit_overwrite_and_split_are_forwarded(self):
78-
def annotate(request):
79-
query = parse_qs(urlparse(request.url).query)
80-
if "overwrite" not in query:
81-
return (
82-
409,
83-
{"Content-Type": "application/json"},
84-
json.dumps(
85-
{
86-
"error": {
87-
"message": "This video already has annotations. Send overwrite=true to replace them."
88-
}
89-
}
90-
),
91-
)
92-
self.assertEqual(query["overwrite"], ["true"])
93-
self.assertEqual(query["split"], ["valid"])
94-
self.assertEqual(query["addToDataset"], ["false"])
95-
return (
96-
200,
97-
{"Content-Type": "application/json"},
98-
json.dumps({"success": True, "inDataset": False, "createdClasses": []}),
99-
)
100-
69+
message = "This video already has annotations. Send overwrite=true to replace them."
70+
accepted = {"success": True, "inDataset": False, "createdClasses": []}
10171
with responses.RequestsMock() as http:
102-
http.add_callback(responses.POST, self.url, callback=annotate)
72+
http.add(responses.POST, self.url, json={"error": {"message": message}}, status=409)
73+
http.add(responses.POST, self.url, json=accepted)
10374
with self.assertRaises(AnnotationSaveError) as error:
10475
self.project.annotate_video_segments("final-source-id", self.document)
10576
self.assertEqual(error.exception.status_code, 409)
106-
self.assertIn("already has annotations", str(error.exception))
77+
self.assertEqual(str(error.exception), message)
10778

10879
result = self.project.annotate_video_segments(
10980
"final-source-id", self.document, overwrite=True, split="valid", add_to_dataset=False
11081
)
111-
self.assertEqual(result, {"success": True, "inDataset": False, "createdClasses": []})
82+
self.assertEqual(result, accepted)
83+
query = parse_qs(urlparse(http.calls[-1].request.url).query)
84+
self.assertEqual(query["overwrite"], ["true"])
85+
self.assertEqual(query["split"], ["valid"])
86+
self.assertEqual(query["addToDataset"], ["false"])
11287

11388
def test_server_validation_and_transport_errors_are_not_recast_as_success(self):
11489
with responses.RequestsMock() as http:

0 commit comments

Comments
 (0)