Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion roboflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
24 changes: 24 additions & 0 deletions roboflow/util/model_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
29 changes: 29 additions & 0 deletions tests/util/test_model_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from roboflow.util.model_processor import (
_detect_rfdetr_task,
_detect_yolo_task,
_validate_pose_kpt_shape,
task_of_model_type,
)

Expand Down Expand Up @@ -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()
Loading