From d20028080b63b89da998260a252c0ec7605775d7 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Thu, 24 Sep 2026 20:43:18 -0300 Subject: [PATCH 1/8] Support Action Recognition projects and Cosmos training in the SDK get_model_format fell back to yolov5pytorch for Cosmos, so version.train exported the wrong format. Map cosmos3-edge to video-coco and its image sibling cosmos3-edge-vlm to jsonl, matching the server's model registry. Add the action-recognition project type to config and the CLI. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RXBUpHfmPSeNYibcqpPnpY --- CHANGELOG.md | 11 +++++++++++ roboflow/__init__.py | 2 +- roboflow/cli/handlers/project.py | 1 + roboflow/config.py | 1 + roboflow/util/versions.py | 3 +++ tests/cli/test_project_handler.py | 20 ++++++++++++++++++++ tests/util/test_versions.py | 2 ++ 7 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f2bee6..fe2a11a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## 1.5.2 + +### Added + +- Action recognition support: + - `roboflow project create --type action-recognition` creates an action + recognition project. `roboflow.config.TYPE_ACTION_RECOGNITION` names the type. + - `Version.train(model_type="cosmos3-edge")` now exports `video-coco`, the + format the server trains Cosmos on. `cosmos3-edge-vlm` exports `jsonl`. + Both previously fell back to `yolov5pytorch`. + ## 1.5.1 ### Added diff --git a/roboflow/__init__.py b/roboflow/__init__.py index 6c556500..f829fd0c 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -21,7 +21,7 @@ CLIPModel = None # type: ignore[assignment,misc] GazeModel = None # type: ignore[assignment,misc] -__version__ = "1.5.1" +__version__ = "1.5.2" def check_key(api_key, model, notebook, num_retries=0): diff --git a/roboflow/cli/handlers/project.py b/roboflow/cli/handlers/project.py index fd072dc0..05e3070d 100644 --- a/roboflow/cli/handlers/project.py +++ b/roboflow/cli/handlers/project.py @@ -19,6 +19,7 @@ class ProjectType(str, Enum): instance_segmentation = "instance-segmentation" semantic_segmentation = "semantic-segmentation" keypoint_detection = "keypoint-detection" + action_recognition = "action-recognition" project_app = typer.Typer(cls=SortedGroup, help="Manage projects", no_args_is_help=True) diff --git a/roboflow/config.py b/roboflow/config.py index 800ef6ac..60147af6 100644 --- a/roboflow/config.py +++ b/roboflow/config.py @@ -74,6 +74,7 @@ def get_conditional_configuration_variable(key, default): TYPE_SEMANTIC_SEGMENTATION = "semantic-segmentation" TYPE_KEYPOINT_DETECTION = "keypoint-detection" TYPE_TEXT_IMAGE_PAIRS = "text-image-pairs" +TYPE_ACTION_RECOGNITION = "action-recognition" TASK_DET = "det" TASK_SEG = "seg" diff --git a/roboflow/util/versions.py b/roboflow/util/versions.py index f07bf006..b09289f4 100644 --- a/roboflow/util/versions.py +++ b/roboflow/util/versions.py @@ -126,6 +126,9 @@ def get_model_format(model_type: str) -> str: "rfdetr": "coco", "rf-detr": "coco", "deep": "png-mask-semantic", + # Substring match: the image VLM sibling must win before the video model's prefix. + "cosmos3-edge-vlm": "jsonl", + "cosmos3-edge": "video-coco", } for prefix, format in model_formats.items(): diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 129a90a0..b3e58f06 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -50,6 +50,26 @@ def test_subcommands_visible(self) -> None: self.assertIn("restore", result.output) +class TestProjectCreateHandler(unittest.TestCase): + """project create passes the chosen type through to Workspace.create_project.""" + + def test_create_action_recognition_project(self) -> None: + from unittest.mock import MagicMock, patch + + workspace = MagicMock() + with patch("roboflow.Roboflow") as mock_rf: + mock_rf.return_value.workspace.return_value = workspace + result = runner.invoke(app, ["project", "create", "Clips", "--type", "action-recognition"]) + + self.assertEqual(result.exit_code, 0, result.output) + workspace.create_project.assert_called_once_with( + project_name="Clips", + project_type="action-recognition", + project_license="Private", + annotation="Clips", + ) + + class TestProjectDeleteHandler(unittest.TestCase): """project delete calls rfapi.delete_project and honors --yes.""" diff --git a/tests/util/test_versions.py b/tests/util/test_versions.py index 5a7803f2..6a9d6bef 100644 --- a/tests/util/test_versions.py +++ b/tests/util/test_versions.py @@ -36,6 +36,8 @@ def test_get_model_format_with_various_ids(self): ("resnet14", "folder"), ("resenet38", "yolov5pytorch"), ("invlid-type", "yolov5pytorch"), + ("cosmos3-edge", "video-coco"), + ("cosmos3-edge-vlm", "jsonl"), ] for model_type, expected_format in cases: From 5e3fc482dcd5c732292058b8e306b6675f67661e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 21:09:41 +0000 Subject: [PATCH 2/8] Drop the mocked project create CLI test The test mocked Roboflow and the workspace to check one enum value. The enum itself shows the new type, and the other CLI tests keep covering the command's registration. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YFK32iFwJmoResQA754RPr --- tests/cli/test_project_handler.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index b3e58f06..129a90a0 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -50,26 +50,6 @@ def test_subcommands_visible(self) -> None: self.assertIn("restore", result.output) -class TestProjectCreateHandler(unittest.TestCase): - """project create passes the chosen type through to Workspace.create_project.""" - - def test_create_action_recognition_project(self) -> None: - from unittest.mock import MagicMock, patch - - workspace = MagicMock() - with patch("roboflow.Roboflow") as mock_rf: - mock_rf.return_value.workspace.return_value = workspace - result = runner.invoke(app, ["project", "create", "Clips", "--type", "action-recognition"]) - - self.assertEqual(result.exit_code, 0, result.output) - workspace.create_project.assert_called_once_with( - project_name="Clips", - project_type="action-recognition", - project_license="Private", - annotation="Clips", - ) - - class TestProjectDeleteHandler(unittest.TestCase): """project delete calls rfapi.delete_project and honors --yes.""" From 364f3de182fe44825f64fa4823013698a36411d4 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 11:19:20 -0300 Subject: [PATCH 3/8] Refuse Cosmos models in the legacy Version.train() Version.train() cannot send the training recipe that Cosmos models need. It also has no model class to return for them, so a successful run ended in "Unsupported model type". It now raises before it exports or starts a paid run, and points to describe_train_recipe + create_training. Every other model type behaves as before. --- roboflow/core/version.py | 13 +++++++++ tests/test_version.py | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/roboflow/core/version.py b/roboflow/core/version.py index dee2cce2..ab09a3ea 100644 --- a/roboflow/core/version.py +++ b/roboflow/core/version.py @@ -46,6 +46,9 @@ load_dotenv() +# Cosmos models train only from a recipe, which the legacy Version.train() cannot send. +RECIPE_ONLY_MODEL_TYPES = ("cosmos3-edge", "cosmos3-edge-vlm") + class Version: """ @@ -480,10 +483,20 @@ def train( An instance of the trained model class Raises: + ValueError: If ``model_type`` is a Cosmos model (``cosmos3-edge``, ``cosmos3-edge-vlm``). + Train those with :meth:`create_training` and a recipe from :meth:`describe_train_recipe`. RuntimeError: If the Roboflow API returns an error with a helpful JSON body HTTPError: If the Network/Roboflow API fails and does not return JSON """ # noqa: E501 // docs + # Checked before any export or paid run: this path cannot send their recipe or return their model. + if model_type in RECIPE_ONLY_MODEL_TYPES: + raise ValueError( + f"Version.train() does not support model_type={model_type!r}. It needs a training recipe. Use " + f'recipe = version.describe_train_recipe("{model_type}")["template"], then ' + f'version.create_training(model_type="{model_type}", train_recipe=recipe).' + ) + self.__wait_if_generating() train_model_format = get_model_format(model_type) diff --git a/tests/test_version.py b/tests/test_version.py index 4cb66e91..8f8ad23a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,3 +1,4 @@ +import json import os import unittest from types import SimpleNamespace @@ -8,6 +9,8 @@ from roboflow.adapters import rfapi from roboflow.config import ( + API_URL, + TYPE_ACTION_RECOGNITION, TYPE_CLASSICATION, TYPE_INSTANCE_SEGMENTATION, TYPE_KEYPOINT_DETECTION, @@ -470,3 +473,60 @@ def test_export_skipped_when_format_already_present(self): self.version.exports = ["coco"] _, _, _, mock_export = self._create(model_type="rfdetr-medium") mock_export.assert_not_called() + + +class TestCosmosTraining(unittest.TestCase): + """Cosmos trains through the recipe-aware v2 path; the legacy blocking train() refuses it.""" + + TRAININGS_URL = f"{API_URL}/test-workspace/test-project/4/v2/trainings" + RECIPE = {"schema_version": 1, "input": {"video_sampling": {"fps": 2}}, "hyperparameters": {}} + + def setUp(self): + super().setUp() + self.version = get_version( + project_name="Test Dataset", + id="test-workspace/test-project/2", + version_number="4", + type=TYPE_ACTION_RECOGNITION, + ) + + @responses.activate + def test_train_rejects_cosmos_before_any_http_call(self): + for model_type in ("cosmos3-edge", "cosmos3-edge-vlm"): + with self.subTest(model_type=model_type), patch.object(Version, "export") as mock_export: + with self.assertRaises(ValueError) as ctx: + self.version.train(model_type=model_type) + + self.assertIn("create_training", str(ctx.exception)) + self.assertIn("describe_train_recipe", str(ctx.exception)) + mock_export.assert_not_called() + self.assertEqual(len(responses.calls), 0) + + @responses.activate + def test_recipe_from_describe_is_forwarded_to_create_training(self): + self.version.exports = ["video-coco"] + responses.add( + responses.GET, + f"{self.TRAININGS_URL}/recipe", + json={"modelType": "cosmos3-edge", "schema": {}, "template": self.RECIPE}, + ) + responses.add( + responses.GET, + f"{API_URL}/Test Workspace Name/Test Dataset/4", + json={"version": {"generating": False, "progress": 1.0, "images": 10}}, + ) + responses.add( + responses.POST, + self.TRAININGS_URL, + json={"trainingId": "t-cosmos", "status": "queued", "jobId": "job-1"}, + ) + + recipe = self.version.describe_train_recipe("cosmos3-edge")["template"] + training = self.version.create_training(model_type="cosmos3-edge", train_recipe=recipe) + + self.assertEqual(training.training_id, "t-cosmos") + self.assertEqual(training.status, "queued") + create_call = next(c for c in responses.calls if c.request.method == "POST") + body = json.loads(create_call.request.body) + self.assertEqual(body["modelType"], "cosmos3-edge") + self.assertEqual(body["trainRecipe"], self.RECIPE) From dd5cecd20f3a632e20c9c4132f82ef1803bc6390 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 11:19:20 -0300 Subject: [PATCH 4/8] Document the Action Recognition platform dependency and v2 Cosmos path project create --type action-recognition needs roboflow/roboflow#16234, which adds the type to the public create-project route. The changelog now says so, and shows the recipe-aware create_training example for Cosmos. A CLI test captures the real create-project HTTP request and checks that it carries type "action-recognition". --- CHANGELOG.md | 22 ++++++++++++-- tests/cli/test_project_handler.py | 49 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2a11a6..3ab5aec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,25 @@ All notable changes to this project will be documented in this file. - Action recognition support: - `roboflow project create --type action-recognition` creates an action recognition project. `roboflow.config.TYPE_ACTION_RECOGNITION` names the type. - - `Version.train(model_type="cosmos3-edge")` now exports `video-coco`, the - format the server trains Cosmos on. `cosmos3-edge-vlm` exports `jsonl`. - Both previously fell back to `yolov5pytorch`. + It needs a Roboflow platform that includes roboflow/roboflow#16234, which + adds the type to the public create-project route. Older platforms reject + it with HTTP 422. + - Cosmos models train through the recipe-aware v2 path: + + ```python + recipe = version.describe_train_recipe("cosmos3-edge")["template"] + training = version.create_training(model_type="cosmos3-edge", train_recipe=recipe) + ``` + + `cosmos3-edge` exports `video-coco`, and `cosmos3-edge-vlm` exports + `jsonl`. Both previously fell back to `yolov5pytorch`. + +### Changed + +- `Version.train()` raises `ValueError` for `cosmos3-edge` and + `cosmos3-edge-vlm` before it exports or starts a run. It cannot send the + training recipe these models need or return their model. The error points to + `create_training` with a recipe. ## 1.5.1 diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 129a90a0..e3ab9d7b 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -1,7 +1,9 @@ """Tests for the project CLI handler.""" +import json import unittest +import responses from typer.testing import CliRunner from roboflow.cli import app @@ -50,6 +52,53 @@ def test_subcommands_visible(self) -> None: self.assertIn("restore", result.output) +class TestProjectCreateHandler(unittest.TestCase): + """project create sends the chosen type in the public create-project request.""" + + @responses.activate + def test_create_sends_action_recognition_type(self) -> None: + from unittest.mock import patch + + from roboflow.config import API_URL + from roboflow.core.workspace import Workspace + + workspace = Workspace( + {"workspace": {"name": "My WS", "url": "my-ws", "projects": []}}, + api_key="fake-key", + default_workspace="my-ws", + model_format="yolov8", + ) + responses.add( + responses.POST, + f"{API_URL}/my-ws/projects", + json={ + "id": "my-ws/clips", + "name": "Clips", + "type": "action-recognition", + "annotation": "Clips", + "classes": {}, + "colors": {}, + "created": 0, + "updated": 0, + "images": 0, + "public": False, + "splits": {}, + "unannotated": 0, + }, + ) + + with patch("roboflow.Roboflow") as mock_rf: + mock_rf.return_value.workspace.return_value = workspace + result = runner.invoke(app, ["--json", "project", "create", "Clips", "--type", "action-recognition"]) + + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual(len(responses.calls), 1) + self.assertEqual( + json.loads(responses.calls[0].request.body), + {"name": "Clips", "type": "action-recognition", "license": "Private", "annotation": "Clips"}, + ) + + class TestProjectDeleteHandler(unittest.TestCase): """project delete calls rfapi.delete_project and honors --yes.""" From db4e63479d4a176494209d668b0c65f4eefb467a Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 11:34:55 -0300 Subject: [PATCH 5/8] Move the Cosmos training changes out of this PR This PR now only lets the SDK and CLI create Action Recognition projects. The Cosmos export-format mapping, the Version.train() Cosmos guard, their tests and the CHANGELOG training example move to a follow-up PR. --- CHANGELOG.md | 27 ++++------------- roboflow/core/version.py | 13 -------- roboflow/util/versions.py | 3 -- tests/test_version.py | 60 ------------------------------------- tests/util/test_versions.py | 2 -- 5 files changed, 5 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab5aec7..bc473227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,28 +6,11 @@ All notable changes to this project will be documented in this file. ### Added -- Action recognition support: - - `roboflow project create --type action-recognition` creates an action - recognition project. `roboflow.config.TYPE_ACTION_RECOGNITION` names the type. - It needs a Roboflow platform that includes roboflow/roboflow#16234, which - adds the type to the public create-project route. Older platforms reject - it with HTTP 422. - - Cosmos models train through the recipe-aware v2 path: - - ```python - recipe = version.describe_train_recipe("cosmos3-edge")["template"] - training = version.create_training(model_type="cosmos3-edge", train_recipe=recipe) - ``` - - `cosmos3-edge` exports `video-coco`, and `cosmos3-edge-vlm` exports - `jsonl`. Both previously fell back to `yolov5pytorch`. - -### Changed - -- `Version.train()` raises `ValueError` for `cosmos3-edge` and - `cosmos3-edge-vlm` before it exports or starts a run. It cannot send the - training recipe these models need or return their model. The error points to - `create_training` with a recipe. +- `roboflow project create --type action-recognition` creates an action + recognition project. `roboflow.config.TYPE_ACTION_RECOGNITION` names the type. + It needs a Roboflow platform that includes roboflow/roboflow#16234, which + adds the type to the public create-project route. Older platforms reject + it with HTTP 422. ## 1.5.1 diff --git a/roboflow/core/version.py b/roboflow/core/version.py index ab09a3ea..dee2cce2 100644 --- a/roboflow/core/version.py +++ b/roboflow/core/version.py @@ -46,9 +46,6 @@ load_dotenv() -# Cosmos models train only from a recipe, which the legacy Version.train() cannot send. -RECIPE_ONLY_MODEL_TYPES = ("cosmos3-edge", "cosmos3-edge-vlm") - class Version: """ @@ -483,20 +480,10 @@ def train( An instance of the trained model class Raises: - ValueError: If ``model_type`` is a Cosmos model (``cosmos3-edge``, ``cosmos3-edge-vlm``). - Train those with :meth:`create_training` and a recipe from :meth:`describe_train_recipe`. RuntimeError: If the Roboflow API returns an error with a helpful JSON body HTTPError: If the Network/Roboflow API fails and does not return JSON """ # noqa: E501 // docs - # Checked before any export or paid run: this path cannot send their recipe or return their model. - if model_type in RECIPE_ONLY_MODEL_TYPES: - raise ValueError( - f"Version.train() does not support model_type={model_type!r}. It needs a training recipe. Use " - f'recipe = version.describe_train_recipe("{model_type}")["template"], then ' - f'version.create_training(model_type="{model_type}", train_recipe=recipe).' - ) - self.__wait_if_generating() train_model_format = get_model_format(model_type) diff --git a/roboflow/util/versions.py b/roboflow/util/versions.py index b09289f4..f07bf006 100644 --- a/roboflow/util/versions.py +++ b/roboflow/util/versions.py @@ -126,9 +126,6 @@ def get_model_format(model_type: str) -> str: "rfdetr": "coco", "rf-detr": "coco", "deep": "png-mask-semantic", - # Substring match: the image VLM sibling must win before the video model's prefix. - "cosmos3-edge-vlm": "jsonl", - "cosmos3-edge": "video-coco", } for prefix, format in model_formats.items(): diff --git a/tests/test_version.py b/tests/test_version.py index 8f8ad23a..4cb66e91 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,4 +1,3 @@ -import json import os import unittest from types import SimpleNamespace @@ -9,8 +8,6 @@ from roboflow.adapters import rfapi from roboflow.config import ( - API_URL, - TYPE_ACTION_RECOGNITION, TYPE_CLASSICATION, TYPE_INSTANCE_SEGMENTATION, TYPE_KEYPOINT_DETECTION, @@ -473,60 +470,3 @@ def test_export_skipped_when_format_already_present(self): self.version.exports = ["coco"] _, _, _, mock_export = self._create(model_type="rfdetr-medium") mock_export.assert_not_called() - - -class TestCosmosTraining(unittest.TestCase): - """Cosmos trains through the recipe-aware v2 path; the legacy blocking train() refuses it.""" - - TRAININGS_URL = f"{API_URL}/test-workspace/test-project/4/v2/trainings" - RECIPE = {"schema_version": 1, "input": {"video_sampling": {"fps": 2}}, "hyperparameters": {}} - - def setUp(self): - super().setUp() - self.version = get_version( - project_name="Test Dataset", - id="test-workspace/test-project/2", - version_number="4", - type=TYPE_ACTION_RECOGNITION, - ) - - @responses.activate - def test_train_rejects_cosmos_before_any_http_call(self): - for model_type in ("cosmos3-edge", "cosmos3-edge-vlm"): - with self.subTest(model_type=model_type), patch.object(Version, "export") as mock_export: - with self.assertRaises(ValueError) as ctx: - self.version.train(model_type=model_type) - - self.assertIn("create_training", str(ctx.exception)) - self.assertIn("describe_train_recipe", str(ctx.exception)) - mock_export.assert_not_called() - self.assertEqual(len(responses.calls), 0) - - @responses.activate - def test_recipe_from_describe_is_forwarded_to_create_training(self): - self.version.exports = ["video-coco"] - responses.add( - responses.GET, - f"{self.TRAININGS_URL}/recipe", - json={"modelType": "cosmos3-edge", "schema": {}, "template": self.RECIPE}, - ) - responses.add( - responses.GET, - f"{API_URL}/Test Workspace Name/Test Dataset/4", - json={"version": {"generating": False, "progress": 1.0, "images": 10}}, - ) - responses.add( - responses.POST, - self.TRAININGS_URL, - json={"trainingId": "t-cosmos", "status": "queued", "jobId": "job-1"}, - ) - - recipe = self.version.describe_train_recipe("cosmos3-edge")["template"] - training = self.version.create_training(model_type="cosmos3-edge", train_recipe=recipe) - - self.assertEqual(training.training_id, "t-cosmos") - self.assertEqual(training.status, "queued") - create_call = next(c for c in responses.calls if c.request.method == "POST") - body = json.loads(create_call.request.body) - self.assertEqual(body["modelType"], "cosmos3-edge") - self.assertEqual(body["trainRecipe"], self.RECIPE) diff --git a/tests/util/test_versions.py b/tests/util/test_versions.py index 6a9d6bef..5a7803f2 100644 --- a/tests/util/test_versions.py +++ b/tests/util/test_versions.py @@ -36,8 +36,6 @@ def test_get_model_format_with_various_ids(self): ("resnet14", "folder"), ("resenet38", "yolov5pytorch"), ("invlid-type", "yolov5pytorch"), - ("cosmos3-edge", "video-coco"), - ("cosmos3-edge-vlm", "jsonl"), ] for model_type, expected_format in cases: From eed00c84baa99e88e5c5b2e32fdce954036e48a7 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 12:00:21 -0300 Subject: [PATCH 6/8] Scope the 1.5.2 changelog entry to the CLI and drop the private link The entry pointed at an issue in a private repository. This changelog is published on a public repo and inside the PyPI sdist, so the link 404s for every reader outside the org. Name the platform requirement instead. The entry also claimed an SDK change. Workspace.create_project forwards project_type verbatim and never validated it, so the SDK already accepted action-recognition. Only the CLI enum was rejecting the type. --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc473227..e385103b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ All notable changes to this project will be documented in this file. ### Added -- `roboflow project create --type action-recognition` creates an action - recognition project. `roboflow.config.TYPE_ACTION_RECOGNITION` names the type. - It needs a Roboflow platform that includes roboflow/roboflow#16234, which - adds the type to the public create-project route. Older platforms reject - it with HTTP 422. +- CLI: accept `action-recognition` as a project type, so + `roboflow project create --type action-recognition` creates an action + recognition project + ([#532](https://github.com/roboflow/roboflow-python/pull/532)). It needs a + Roboflow platform release that accepts the `action-recognition` project type; + older releases reject it with HTTP 422 and the CLI says so. ## 1.5.1 From c9d192391b196ae187f07a7aa4360cd604ac5a11 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 12:00:28 -0300 Subject: [PATCH 7/8] Document the project types and explain a rejected one TYPE_ACTION_RECOGNITION had no callers: the CLI enum owns the literal, so the constant was a second source of truth for the same string. Drop it. CLI-COMMANDS.md showed one --type value in an example and listed none, so action-recognition was discoverable only through --help. List them all. A 422 from the create-project route means the platform rejected the type, which usually means the type is newer than the platform release. Say that in the hint instead of passing the bare platform message through. --- CLI-COMMANDS.md | 4 ++++ roboflow/cli/handlers/project.py | 5 +++++ roboflow/config.py | 1 - tests/cli/test_project_handler.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CLI-COMMANDS.md b/CLI-COMMANDS.md index bd5c619e..50dbc2ce 100644 --- a/CLI-COMMANDS.md +++ b/CLI-COMMANDS.md @@ -34,6 +34,10 @@ roboflow image upload photo.jpg -p my-project roboflow image upload ./dataset-folder/ -p my-project # smart: detects directory ``` +Supported `--type` values: `object-detection`, `single-label-classification`, +`multi-label-classification`, `instance-segmentation`, `semantic-segmentation`, +`keypoint-detection`, `action-recognition`. + ### Download a dataset ```bash diff --git a/roboflow/cli/handlers/project.py b/roboflow/cli/handlers/project.py index 05e3070d..310f7404 100644 --- a/roboflow/cli/handlers/project.py +++ b/roboflow/cli/handlers/project.py @@ -245,6 +245,11 @@ def _create_project(args): # noqa: ANN001 hint = str(body["message"]) except Exception: pass + # A 422 on create means the platform rejected the project type itself, + # which usually means the type is newer than the platform release. + if getattr(getattr(exc, "response", None), "status_code", None) == 422: + type_hint = f"Your Roboflow platform may not support {args.type} projects yet." + hint = f"{hint} {type_hint}" if hint else type_hint output_error(args, msg, hint=hint) return diff --git a/roboflow/config.py b/roboflow/config.py index 60147af6..800ef6ac 100644 --- a/roboflow/config.py +++ b/roboflow/config.py @@ -74,7 +74,6 @@ def get_conditional_configuration_variable(key, default): TYPE_SEMANTIC_SEGMENTATION = "semantic-segmentation" TYPE_KEYPOINT_DETECTION = "keypoint-detection" TYPE_TEXT_IMAGE_PAIRS = "text-image-pairs" -TYPE_ACTION_RECOGNITION = "action-recognition" TASK_DET = "det" TASK_SEG = "seg" diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index e3ab9d7b..29f25597 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -53,7 +53,7 @@ def test_subcommands_visible(self) -> None: class TestProjectCreateHandler(unittest.TestCase): - """project create sends the chosen type in the public create-project request.""" + """project create sends the chosen type, and explains a platform rejection.""" @responses.activate def test_create_sends_action_recognition_type(self) -> None: @@ -98,6 +98,35 @@ def test_create_sends_action_recognition_type(self) -> None: {"name": "Clips", "type": "action-recognition", "license": "Private", "annotation": "Clips"}, ) + @responses.activate + def test_create_hints_when_the_platform_rejects_the_type(self) -> None: + from unittest.mock import patch + + from roboflow.config import API_URL + from roboflow.core.workspace import Workspace + + workspace = Workspace( + {"workspace": {"name": "My WS", "url": "my-ws", "projects": []}}, + api_key="fake-key", + default_workspace="my-ws", + model_format="yolov8", + ) + responses.add( + responses.POST, + f"{API_URL}/my-ws/projects", + json={"error": {"message": "Invalid project type."}}, + status=422, + ) + + with patch("roboflow.Roboflow") as mock_rf: + mock_rf.return_value.workspace.return_value = workspace + result = runner.invoke(app, ["--json", "project", "create", "Clips", "--type", "action-recognition"]) + + self.assertEqual(result.exit_code, 1) + hint = json.loads(result.stderr)["error"]["hint"] + self.assertIn("Invalid project type.", hint) + self.assertIn("may not support action-recognition projects yet", hint) + class TestProjectDeleteHandler(unittest.TestCase): """project delete calls rfapi.delete_project and honors --yes.""" From 43ecfcdc300565d71711a0376fab878a9bbb808f Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Tue, 29 Sep 2026 14:37:37 -0300 Subject: [PATCH 8/8] Address action recognition CLI review feedback --- CHANGELOG.md | 11 ----------- roboflow/__init__.py | 2 +- roboflow/cli/handlers/project.py | 5 ----- tests/cli/test_project_handler.py | 7 +++---- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e385103b..b9f2bee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,6 @@ All notable changes to this project will be documented in this file. -## 1.5.2 - -### Added - -- CLI: accept `action-recognition` as a project type, so - `roboflow project create --type action-recognition` creates an action - recognition project - ([#532](https://github.com/roboflow/roboflow-python/pull/532)). It needs a - Roboflow platform release that accepts the `action-recognition` project type; - older releases reject it with HTTP 422 and the CLI says so. - ## 1.5.1 ### Added diff --git a/roboflow/__init__.py b/roboflow/__init__.py index f829fd0c..6c556500 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -21,7 +21,7 @@ CLIPModel = None # type: ignore[assignment,misc] GazeModel = None # type: ignore[assignment,misc] -__version__ = "1.5.2" +__version__ = "1.5.1" def check_key(api_key, model, notebook, num_retries=0): diff --git a/roboflow/cli/handlers/project.py b/roboflow/cli/handlers/project.py index 310f7404..05e3070d 100644 --- a/roboflow/cli/handlers/project.py +++ b/roboflow/cli/handlers/project.py @@ -245,11 +245,6 @@ def _create_project(args): # noqa: ANN001 hint = str(body["message"]) except Exception: pass - # A 422 on create means the platform rejected the project type itself, - # which usually means the type is newer than the platform release. - if getattr(getattr(exc, "response", None), "status_code", None) == 422: - type_hint = f"Your Roboflow platform may not support {args.type} projects yet." - hint = f"{hint} {type_hint}" if hint else type_hint output_error(args, msg, hint=hint) return diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 29f25597..1950e0f5 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -53,7 +53,7 @@ def test_subcommands_visible(self) -> None: class TestProjectCreateHandler(unittest.TestCase): - """project create sends the chosen type, and explains a platform rejection.""" + """project create sends the chosen type and reports server errors.""" @responses.activate def test_create_sends_action_recognition_type(self) -> None: @@ -99,7 +99,7 @@ def test_create_sends_action_recognition_type(self) -> None: ) @responses.activate - def test_create_hints_when_the_platform_rejects_the_type(self) -> None: + def test_create_preserves_server_error_message(self) -> None: from unittest.mock import patch from roboflow.config import API_URL @@ -124,8 +124,7 @@ def test_create_hints_when_the_platform_rejects_the_type(self) -> None: self.assertEqual(result.exit_code, 1) hint = json.loads(result.stderr)["error"]["hint"] - self.assertIn("Invalid project type.", hint) - self.assertIn("may not support action-recognition projects yet", hint) + self.assertEqual(hint, "Invalid project type.") class TestProjectDeleteHandler(unittest.TestCase):