From 64b61b6f31079f01ae20183bcfeff024c5fdf766 Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Wed, 13 May 2026 00:14:53 +0200 Subject: [PATCH 1/3] Include keypoints_metadata.json when uploading model --- roboflow/util/model_processor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 674170ae..5d3ac47b 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -295,6 +295,7 @@ def _process_yolo(model_type: str, model_path: str, filename: str) -> tuple[str, "results.png", "model_artifacts.json", "state_dict.pt", + "keypoints_metadata.json", ] zip_file_name = "roboflow_deploy.zip" From 42c8425eb49b4be6f0b8685d9bc386297fd07e4f Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Wed, 13 May 2026 11:27:11 +0200 Subject: [PATCH 2/3] bump version --- roboflow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roboflow/__init__.py b/roboflow/__init__.py index 79822649..560ffbbc 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.3.9" +__version__ = "1.3.10" def check_key(api_key, model, notebook, num_retries=0): From e095c5a65d29795eb07b7538f0e706aa5e40ab6f Mon Sep 17 00:00:00 2001 From: Grzegorz Klimaszewski <166530809+grzegorz-roboflow@users.noreply.github.com> Date: Tue, 26 May 2026 10:06:16 +0200 Subject: [PATCH 3/3] Ensure keypoints data is available when uploading pose models --- roboflow/util/model_processor.py | 25 ++++++++++++++++++++++++- tests/util/test_model_processor.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/roboflow/util/model_processor.py b/roboflow/util/model_processor.py index 5d3ac47b..1d020052 100644 --- a/roboflow/util/model_processor.py +++ b/roboflow/util/model_processor.py @@ -125,6 +125,28 @@ def _detect_yolo_task(model_instance) -> Optional[str]: }.get(type(model_instance).__name__) +def _validate_pose_kpt_shape(model_type: str, model_instance, pt_path: str) -> None: + """Fail fast if a pose model lacks `kpt_shape` in its config. + + Roboflow's converter reads `model_artifacts["yaml"]["kpt_shape"]` to build + keypoints_metadata.json. Without it the conversion crashes and the deployed package + loads as incomplete (CorruptedModelPackageError) — so reject the upload here with an + actionable message rather than shipping a model that can never serve. + """ + if task_of_model_type(model_type) != TASK_POSE: + return + yaml_cfg = getattr(model_instance, "yaml", None) + kpt_shape = yaml_cfg.get("kpt_shape") if isinstance(yaml_cfg, dict) else None + if not kpt_shape: + raise ValueError( + f"model_type '{model_type}' is a keypoint/pose model but the checkpoint at " + f"'{pt_path}' has no 'kpt_shape' in its config, so the number of keypoints is " + "unknown and the deployed model would fail to load. Train/export the model with " + "Ultralytics on a pose dataset whose data.yaml sets " + "'kpt_shape: [num_keypoints, dims]' (e.g. [17, 3]), then redeploy that .pt." + ) + + def _process_yolo(model_type: str, model_path: str, filename: str) -> tuple[str, str]: if "yolov8" in model_type: try: @@ -218,6 +240,8 @@ def _process_yolo(model_type: str, model_path: str, filename: str) -> tuple[str, f".pt file is a '{detected_task}' checkpoint. Use a matching model_type." ) + _validate_pose_kpt_shape(model_type, model_instance, os.path.join(model_path, filename)) + if isinstance(model_instance.names, list): class_names = model_instance.names else: @@ -295,7 +319,6 @@ def _process_yolo(model_type: str, model_path: str, filename: str) -> tuple[str, "results.png", "model_artifacts.json", "state_dict.pt", - "keypoints_metadata.json", ] zip_file_name = "roboflow_deploy.zip" diff --git a/tests/util/test_model_processor.py b/tests/util/test_model_processor.py index 951abe29..59621cb1 100644 --- a/tests/util/test_model_processor.py +++ b/tests/util/test_model_processor.py @@ -5,6 +5,7 @@ from roboflow.util.model_processor import ( _detect_rfdetr_task, _detect_yolo_task, + _validate_pose_kpt_shape, task_of_model_type, ) @@ -84,5 +85,33 @@ def test_unrecognized_returns_none(self): self.assertIsNone(_detect_rfdetr_task({"args": SimpleNamespace(other=1)})) +class ValidatePoseKptShapeTest(unittest.TestCase): + def test_non_pose_is_noop(self): + # Detection model with no yaml at all must not raise. + _validate_pose_kpt_shape("yolov11", SimpleNamespace(yaml=None), "/tmp/best.pt") + _validate_pose_kpt_shape("yolov11-seg", SimpleNamespace(), "/tmp/best.pt") + + def test_pose_with_kpt_shape_ok(self): + inst = SimpleNamespace(yaml={"nc": 1, "kpt_shape": [17, 3]}) + _validate_pose_kpt_shape("yolov11-pose", inst, "/tmp/best.pt") + + def test_pose_missing_kpt_shape_raises(self): + inst = SimpleNamespace(yaml={"nc": 1}) + with self.assertRaises(ValueError) as ctx: + _validate_pose_kpt_shape("yolov11-pose", inst, "/tmp/best.pt") + msg = str(ctx.exception) + self.assertIn("kpt_shape", msg) + self.assertIn("/tmp/best.pt", msg) + + def test_pose_empty_kpt_shape_raises(self): + inst = SimpleNamespace(yaml={"kpt_shape": []}) + with self.assertRaises(ValueError): + _validate_pose_kpt_shape("yolov11-pose", inst, "/tmp/best.pt") + + def test_pose_no_yaml_raises(self): + with self.assertRaises(ValueError): + _validate_pose_kpt_shape("yolo26-pose", SimpleNamespace(yaml=None), "/tmp/best.pt") + + if __name__ == "__main__": unittest.main()