Skip to content

[ar-api] Add CLI video segment annotation (VID-59) - #544

Merged
digaobarbosa merged 24 commits into
mainfrom
bc/VID-59
Oct 9, 2026
Merged

digaobarbosa merged 24 commits into
mainfrom
bc/VID-59

Conversation

@digaobarbosa

@digaobarbosa digaobarbosa commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Action Recognition users can upload a native video from the CLI, but they could not submit its segment labels there. They had to switch to Python for the last step.

roboflow video annotate reads one complete video-coco document and sends it unchanged to the video ID that video upload reported. Native frame indices, uneven timestamps and the rational time base stay exactly as written. Users can pick a Dataset split, request --overwrite, and turn Dataset membership on or off. Without a membership flag, the API default applies: the video is added.

A conflicting save tells the user to re-run with --overwrite. An unknown video exits 3, and a rejected API key exits 2. The file is read as UTF-8 on every platform, so non-ASCII class names stay intact on Windows.

Stack and dependency

Targets main. This branch contains PR #541 (VID-53). Merge that first and this diff reduces to video annotate. The SDK dependency PR #534 is already merged.

Changes since reopening

This PR was closed by mistake and revived on 2026-10-09 with main merged in and a simplification pass. The shared upload changes are described in #541.

  • Every annotation-file problem produces one message, Cannot read annotation file <path>: <cause>, with exit 1. A missing file, non-UTF-8 bytes, malformed JSON and a non-object document all land there, because decode and JSON errors are ValueErrors.
  • Removed the --help-only tests. The real dispatch tests cover each command.

Verification

  1. Full unit suite at head 95bedc2: 1,229 tests pass, one skipped, also with Rich color forced as in CI. ruff check, ruff format --check and mypy roboflow pass.
  2. Staging E2E at d95f560: 27/27 passed against api.roboflow.one / model-evaluation-workspace with the installed CLI, in 46 s. That is 11 upload/status cases from [ar-api] Add native video upload and ingestion status to CLI (VID-53) #541 and 16 annotate cases. Segments were read back through the SDK with exact native PTS. The only later commit changes one test assertion. The run's project was moved to Trash and confirmed gone.
  3. CI: the Python/Windows build matrix runs now that the base is main.
Staging E2E results: annotate (head d95f560, 2026-10-09)

Commands ran as roboflow --json -k $KEY -w model-evaluation-workspace … (shown without the key and workspace). $P is a private Action Recognition project the run created and then moved to Trash. Uploads are byte-unique stream copies of real 24 fps clips (time base 1/12288), so no shared Source is touched. The cases are scripted in run_native_video_e2e.sh from the e2e-test skill. A and B are the two fresh Sources. a_conflict.json moves one segment's end to frame 14 (end PTS 7680).

# Case Command Exit Result
1 help roboflow video annotate --help 0 PASS
2 default annotate A roboflow --json video annotate -a $SCRATCH/a.json -i <id> -p $P 0 PASS
3 identical resubmit A roboflow --json video annotate -a $SCRATCH/a.json -i <id> -p $P 0 PASS
4 conflict without --overwrite roboflow --json video annotate -a $SCRATCH/a_conflict.json -i <id> -p $P 1 PASS
5 conflict with --overwrite roboflow --json video annotate -a $SCRATCH/a_conflict.json -i <id> -p $P --overwrite 0 PASS
6 text mode roboflow video annotate -a $SCRATCH/a_conflict.json -i <id> -p $P 0 PASS
7 --no-add-to-dataset B roboflow --json video annotate -a $SCRATCH/b.json -i <id> -p $P --no-add-to-dataset 0 PASS
8 --add-to-dataset -s valid B roboflow --json video annotate -a $SCRATCH/b.json -i <id> -p $P --add-to-dataset -s valid 0 PASS
9 readback (SDK project.image) SDK project.image(<A>) and project.image(<B>) 0 PASS
10 unknown video id roboflow --json video annotate -a $SCRATCH/a.json -i doesnotexist123 -p $P 3 PASS
11 zero segments roboflow --json video annotate -a $SCRATCH/a_empty.json -i <id> -p $P 1 PASS
12 malformed JSON roboflow --json video annotate -a $SCRATCH/bad.json -i <id> -p $P 1 PASS
12b non-object JSON roboflow --json video annotate -a $SCRATCH/list.json -i <id> -p $P 1 PASS
13 missing file roboflow --json video annotate -a $SCRATCH/nope.json -i <id> -p $P 1 PASS
14 unknown project roboflow --json video annotate -a $SCRATCH/a.json -i <id> -p no-such-project-xyz 3 PASS
15 bad api key roboflow --json -k bogus video annotate -a $SCRATCH/a.json -i <id> -p $P 2 PASS
Staging E2E results: upload and upload-status at the same head
# Case Command Exit Result
U1 upload --no-wait roboflow --json video upload -f $SCRATCH/unique-copy.mp4 -p $P --no-wait 0 PASS
U2 upload-status single read roboflow --json video upload-status <id> -p $P 0 PASS
U3 upload-status --wait roboflow --json video upload-status <id> -p $P --wait --poll-timeout 120 0 PASS
U4 re-upload is deduplicated onto Source A roboflow --json video upload -f $SCRATCH/unique-copy.mp4 -p $P 0 PASS
U5 upload-status unknown id roboflow --json video upload-status doesnotexist123 -p $P 3 PASS
U6 unsupported container roboflow --json video upload -f $SCRATCH/clip.avi -p $P 1 PASS
U7 missing file roboflow --json video upload -f $SCRATCH/absent.mp4 -p $P 1 PASS
U8 unknown project roboflow --json video upload -f $SCRATCH/unique-copy.mp4 -p no-such-project-xyz 3 PASS
U9 bad api key roboflow --json -k bogus video upload-status <id> -p $P 2 PASS
U10 upload --poll-interval 0 is a usage error roboflow --json video upload -f $SCRATCH/unique-copy.mp4 -p $P --poll-interval 0 2 PASS
U11 upload-status --poll-timeout -1 is a usage error roboflow --json video upload-status <id> -p $P --wait --poll-timeout -1 2 PASS

Notes

  1. Segments are stored on the Source. A re-upload of known bytes returns duplicate: true and links the existing Source into the new project. Annotating that Source changes every project that holds it. An early E2E pass overwrote one segment of a shared staging project this way. It was restored from the original export and verified, and the E2E now refuses deduplicated uploads.
  2. --no-add-to-dataset only skips adding. It does not remove a video that is already in the Dataset.
  3. An unknown project shows the server's generic --help hint. The shared output_error lets a server hint win for every command, so it is out of scope here.

🤖 Generated with Claude Code

digaobarbosa and others added 10 commits October 1, 2026 13:07
…e CLI (VID-53)

Action Recognition projects take whole videos as Sources, but the CLI had no
way to ingest or label one. `roboflow video` only reached the legacy async
video *inference* job API.

Extend the existing video group with three commands over the already-published
SDK methods:

- `video upload` streams original MP4/MOV bytes and reports the canonical
  video ID, waiting for a terminal state by default.
- `video upload-status` reads ingestion state. The name keeps it unambiguous
  against `video status`, which still checks an inference job.
- `video annotate` forwards a complete roboflow-video-coco file unchanged, so
  native frame indices, PTS and rational time bases survive as authored.

Upload composes `upload_video(wait=False)` with `wait_for_video_upload` rather
than the SDK's internal wait, so a bounded-wait timeout can still name the
video ID to re-check. Credential precedence, `--json` schemas and the
0/1/2/3 exit-code contract follow the existing handlers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-53)

Staging verification showed the import schema puts `segments` at the top level
and takes `time_base` as a rational object, with `images`/`annotations` kept
empty. The test fixture used a nested `annotations.segments` shape that the API
would reject, so it documented something untrue even though the forwarding
assertion passed.

Rebuild the fixture from the MOV actually submitted to staging. Its presentation
timestamps are not frame_index * ticks_per_frame, which is what makes "forwarded
unchanged" worth asserting.

Also split the upload ValueError hint: a missing path and an unsupported
container arrive as the same exception type but need different fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
digaobarbosa and others added 6 commits October 5, 2026 15:09
…ID-53)

Upload, upload-status and the project lookup now go through the shared
output_api_error helper, so a rejected API key exits 2 and only a real 404
exits 3. get_project carries the HTTP status so the lookup can tell them apart.
Fold duplicated video CLI tests into table-driven cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(VID-59)

Route annotate rejections through output_api_error so a rejected key exits 2,
and give the video-coco hint only to 400s instead of transport failures.
Fold the annotate CLI tests into table-driven cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without an explicit encoding, Windows decodes the file with its locale
codepage and silently corrupts non-ASCII class names. A file in another
encoding (e.g. UTF-16 from PowerShell redirection) now gets an actionable hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bad --poll-interval/--poll-timeout values and a missing file are now rejected
before any network call, so an upload never stores bytes and then reports
failure without an ID. Upload API errors follow the exit-code contract, and
their hint no longer blames the file when the failing call came after the PUT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@digaobarbosa digaobarbosa added the LGTJarb Approved by Jarbas Local label Oct 5, 2026
@iurisilvio
iurisilvio deleted the branch main October 7, 2026 13:52
@iurisilvio iurisilvio closed this Oct 7, 2026
@digaobarbosa digaobarbosa reopened this Oct 9, 2026
@digaobarbosa
digaobarbosa changed the base branch from bc/VID-53 to main October 9, 2026 11:42
digaobarbosa and others added 6 commits October 9, 2026 08:44
Let typer enforce the poll bounds with min= constraints instead of a
handler-side check, collapse the metadata parsing into one error path,
and drop CLI tests that only re-checked --help or shared resolver
behaviour. Add a direct test for the get_project status code that the
exit-code mapping relies on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rors

Collapse the three annotation-file error branches into one, since
decode and JSON errors are both ValueErrors, and drop the --help-only
tests that the real dispatch tests already cover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
click's str(BadParameter) drops the parameter name, so --json callers
saw "0.0 is not in the range x>=0.1." with nothing to act on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@digaobarbosa
digaobarbosa requested a review from a team October 9, 2026 12:03
@digaobarbosa digaobarbosa self-assigned this Oct 9, 2026
@digaobarbosa
digaobarbosa marked this pull request as ready for review October 9, 2026 12:03
@digaobarbosa
digaobarbosa merged commit bf165e3 into main Oct 9, 2026
15 checks passed
@digaobarbosa digaobarbosa mentioned this pull request Oct 9, 2026
1 of 2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

LGTJarb Approved by Jarbas Local

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants