Skip to content

[ar-api] Add explicit video selection to SDK and CLI search (VID-52) - #542

Merged
digaobarbosa merged 5 commits into
mainfrom
bc/VID-52
Oct 6, 2026
Merged

digaobarbosa merged 5 commits into
mainfrom
bc/VID-52

Conversation

@digaobarbosa

@digaobarbosa digaobarbosa commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Why

Project.search/search_all, Workspace.search/search_all and the CLI search commands could only ever reach images. None of them sent the mediaTypes field the search API accepts, so the API applied its default of ["image"] and native video Sources were undiscoverable through the SDK — even though they index with mediaType: "video" and a signed video URL.

This adds explicit image/video/mixed selection to those existing surfaces. It does not add a parallel video search API.

What changed

SDK — a keyword-only media_types parameter on Project.search/search_all, Workspace.search/search_all, and rfapi.workspace_search:

# Native videos only, with a signed video URL on each hit
videos = project.search(media_types=["video"], fields=["id", "name", "url"])
videos[0]["mediaType"]  # "video"
videos[0]["videoUrl"]   # signed URL to the original bytes

# Images and videos together
project.search(media_types=["image", "video"])

# Page through every video in the workspace
for page in workspace.search_all("*", media_types=["video"]):
    ...

CLI — --media-types on roboflow search and roboflow image search:

roboflow search "*" --media-types video --fields id,filename,url
roboflow search "tag:reviewed" --media-types image,video
roboflow image search "*" -p my-project --media-types video --fields id,url

roboflow image search also gains --fields. Without it the signed videoUrl is unreachable from that command, since the API only attaches it when url is requested.

Behaviour preserved

  • Omitting the selection leaves mediaTypes off the request body entirely, so the API default (images) applies and nothing about existing image search changes. Tests assert the field's absence, not just an image value.
  • media_types is keyword-only everywhere, so every existing positional call signature is untouched.
  • Existing CLI credential precedence (--api-key → saved/env) is unchanged.

Pagination

The selection is forwarded through all three pagination paths: Project.search_all offset paging, Workspace.search_all continuation-token paging, and the CLI --cursor option.

Errors

A shared roboflow.util.search_utils mirrors the API contract — non-empty list of image/video, lowercased and de-duplicated — so an invalid selection raises ValueError before any HTTP request rather than surfacing an opaque 400:

media_types=["audio"]  -> invalid media type 'audio' - media_types must only contain: 'image', 'video'
media_types=[]         -> media_types must be a non-empty list containing any of: 'image', 'video'
media_types="video"    -> media_types must be a list, not a string - use media_types=['video']

The CLI reports these as structured errors with exit code 1, and rejects --media-types together with --export because the search export route does not accept the field.

Verification on staging

Exercised against real api.roboflow.one with a disposable private Action Recognition project in model-evaluation-workspace, holding three fresh canonical Sources (all duplicate: null): an MP4 video, a MOV video, and one image.

Check Result
project.search() with no selection returns only the image Source — image default preserved
media_types=["video"] both canonical video IDs, mediaType=video, signed videoUrl on each
media_types=["image","video"] all three Sources, both media types present
videoUrl without url in fields withheld, as the API specifies
url on a video hit stays the JPEG poster frame (Content-Type: image/jpeg)
signed videoUrl fetched HTTP 200, video/mp4 and video/quicktime, sha256 matches the uploaded bytes
project.search_all(limit=1) selection carried across every offset page
workspace.search_all(page_size=1) selection carried across every continuation page
CLI --limit 1 + --cursor selection carried to page 2, distinct video per page
Project.image(<searched id>) reads back as mediaType=video
fresh-process re-read both video IDs still returned — persisted

Authenticated app inspection after restoring only the owned Trash project showed both native video batches and the image on Annotate Board. Each video opened in the actual editor with its original first frame rendered (MP4: 3.000s, Frame 1/75; MOV: 3.000s, Frame 1/90), SAVED and with no segments. Dataset showed zero. No annotations were created. The project was then re-deleted through public recoverable Trash; active uncached GET returned 404, the exact Trash ID matched, and fresh workspace listing/search no longer found these fixtures.

All staging fixtures were created and deleted by this verification run. No shared or deduplicated Source was touched.

Tests

43 focused tests (tests/test_search_media_types.py, tests/cli/test_search_media_types.py) covering the validator, both SDK surfaces, the adapter, both pagination paths, both CLI paths, and the image-default regression.

Full current-head suite: 1162 tests run (1 skipped); 14 exact-head CheckRuns passed, plus the green pre-commit.ci status (15 green statuses total). ruff check and mypy clean.

Scope notes

  • The search export route does not accept mediaTypes, so --export rejects it rather than silently ignoring it. Export-side video support is out of scope here.
  • Image-similarity search (like_image) and annotation format= have no video equivalent and are not presented as video support.
  • roboflow image search -p continues to pass the API response through verbatim, so it does not echo mediaTypes the way the workspace path does. That is pre-existing behaviour for that path and is left unchanged.

digaobarbosa and others added 5 commits October 5, 2026 11:36
Project.search/search_all, Workspace.search/search_all and the CLI search
commands could only reach images: they never sent `mediaTypes`, so the API
applied its default of `["image"]` and native video Sources were
undiscoverable through the SDK.

Add a keyword-only `media_types` parameter to each search surface and a
`--media-types` option to `roboflow search` and `roboflow image search`.
Omitting it still leaves `mediaTypes` off the request body, so the existing
image-only default and every existing positional signature are unchanged.

The selection is forwarded through every pagination path: `Project.search_all`
offset paging, `Workspace.search_all` continuation-token paging, and the CLI
`--cursor` option.

A shared `roboflow.util.search_utils` mirrors the API contract (non-empty list
of image/video, lowercased and de-duplicated) so an invalid selection raises
before any request instead of returning an opaque 400. The CLI reports it as a
structured error, and rejects `--media-types` with `--export` because the
export route does not accept the field.

`roboflow image search` also gains `--fields`, which is what makes the signed
`videoUrl` reachable from that command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rich styles each `-` of an option name as its own span, so the literal
`--media-types` never appears in `--help` output when color is enabled. The
assertions passed locally (no TTY, no color) and failed on CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The group is still named `image`; renaming its docstring was unnecessary
scope for this change.

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

Two corrections to the previous commits.

`rfapi.workspace_search` took `media_types` as a positional-or-keyword
parameter, so the "keyword-only everywhere" claim was only true of the
higher-level SDK methods. Put it behind a bare `*`. The six pre-existing
parameters keep their positions, and a test now pins both the kind and the
positional order.

The CLI export guard tested `--media-types` for truthiness, so an explicit but
empty `--media-types ""` was falsy and fell through to the export route instead
of being rejected — verified by dispatch: the old guard got as far as resolving
the export dataset. Compare against `None` so any explicit value is caught, and
reject the blank value on the search path too.

`parse_media_types_option` also documented an empty string as meaning "use the
API default" while the code raised on it. The code is right — a blank value is
an explicit empty selection — so the docstring now says that.
@digaobarbosa
digaobarbosa marked this pull request as ready for review October 5, 2026 17:44
@digaobarbosa
digaobarbosa requested a review from a team October 5, 2026 17:44
@digaobarbosa digaobarbosa self-assigned this Oct 5, 2026

@lucas-fochesatto lucas-fochesatto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@digaobarbosa
digaobarbosa merged commit 7faa0dd into main Oct 6, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants