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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

All notable changes to this project will be documented in this file.

## 1.4.0

### Added — Support for multiple models per version

A dataset version can now own many trainings, and a training can produce many
models (e.g. a NAS sweep). New object types expose this:

**SDK (`roboflow/core/training.py`, `roboflow/core/version.py`):**
- `Version.trainings()` — list the version's training runs as `Training` objects.
- `Version.models()` — every trained model for the version (the union across its
trainings), as `TrainedModel` objects. This is now the canonical way to get a
version's models.
- `Version.create_training(speed=, model_type=, checkpoint=, epochs=)` — launch a
run without blocking, returning a `Training`.
- `Training` — `.models`, `.refresh()`, `.cancel()`, `.stop()`, plus
`.training_id` / `.status` / `.model_type`.
- `TrainedModel` — `.predict()`, `.predict_video()`, `.download()`, plus
`.model_id` / `.model_type` / `.metrics`. A `TrainedModel` does everything the
old `version.model` could; you just reach it through `version.models()`.

**Adapters (`roboflow/adapters/rfapi.py`):** v2 trainings endpoints —
`list_trainings_for_version`, `get_training`, `create_training_v2`,
`cancel_training_v2`, `stop_training_v2`, `get_model_weights_url`.

### Changed

- Keypoint detection inference now reports its prediction type correctly
(previously mislabeled as classification), fixing rendering/plotting of
keypoint predictions.

### Deprecated

- `version.model` (the singular attribute) is deprecated and emits a
`DeprecationWarning`. It cannot represent a version with multiple models;
use `version.models()` instead.

## 1.3.10

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ version = project.version("VERSION_NUMBER")
# upload model weights - yolov10
version.deploy(model_type="yolov10", model_path=f”{HOME}/runs/detect/train/”, filename="weights.pt")

# run inference
model = version.model
# run inference (a version may own several trained models; models() returns all of them)
model = version.models()[0]

img_url = "https://media.roboflow.com/quickstart/aerial_drone.jpeg"

Expand Down
1 change: 1 addition & 0 deletions docs/core/training.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:::roboflow.core.training
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ version = project.version("VERSION_NUMBER")
# upload model weights - yolov10
version.deploy(model_type="yolov10", model_path=f”{HOME}/runs/detect/train/”, filename="weights.pt")

# run inference
model = version.model
# run inference (a version may own several trained models; models() returns all of them)
model = version.models()[0]

img_url = "https://media.roboflow.com/quickstart/aerial_drone.jpeg"

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ nav:
- Projects: core/project.md
- Workspaces: core/workspace.md
- Versions: core/version.md
- Trainings: core/training.md
- Models:
- Object Detection: models/object-detection.md
- Classification: models/classification.md
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ banned-module-level-imports = [
python_version = "3.10"
exclude = ["^build/"]

# numpy's bundled stubs use PEP 695 `type` statements, which mypy rejects when
# checking against python_version 3.10. Skip following them so the type checker
# doesn't choke on numpy's own stub syntax.
[[tool.mypy.overrides]]
module = ["numpy", "numpy.*"]
follow_imports = "skip"
follow_imports_for_stubs = true

[[tool.mypy.overrides]]
module = [
"_datetime.*",
Expand Down
6 changes: 4 additions & 2 deletions 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.10"
__version__ = "1.4.0"


def check_key(api_key, model, notebook, num_retries=0):
Expand Down Expand Up @@ -168,7 +168,9 @@ def load_model(model_url):

project = operate_workspace.project(project)
version = project.version(version)
model = version.model
# version.model is deprecated; read the underlying legacy model directly so
# load_model keeps its single-model return contract without emitting the warning.
model = getattr(version, "_model", None)
return model


Expand Down
133 changes: 133 additions & 0 deletions roboflow/adapters/rfapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,139 @@ def get_training_results(api_key: str, workspace_url: str, project_url: str, ver
return response.json()


# ---------------------------------------------------------------------------
# DNA v2 trainings surface (MMPV-aware). Mirrors the MCP's rf_api.py 1:1: a
# version owns many trainings, each owning one or more models (a NAS run owns
# many). trainingId rides in the query/body, never the path, because legacy ids
# contain slashes. The legacy-vs-MMPV branch lives entirely on the backend.
# ---------------------------------------------------------------------------


def list_trainings_for_version(api_key: str, workspace_url: str, project_url: str, version: str):
"""List a version's trainings (DNA ``trainings.list``).

GET /{ws}/{proj}/{version}/v2/trainings. MMPV versions return every run;
SMPV versions return a single entry synthesized from ``version.train``.
Returns the raw ``trainings`` array — each entry carries
``{trainingId, status, modelType, modelGroup, modelIds, start}``.
"""
url = f"{API_URL}/{workspace_url}/{project_url}/{version}/v2/trainings?api_key={api_key}"
response = requests.get(url)
if not response.ok:
raise RoboflowError(response.text)
data = response.json()
return data.get("trainings", []) or []


def get_training(api_key: str, workspace_url: str, project_url: str, version: str, training_id=None):
"""A single run's results bundle (DNA ``trainings.get``).

GET /{ws}/{proj}/{version}/v2/trainings/get[?trainingId=]. Omitting
``training_id`` targets the version's sole run; a version that owns several
runs responds 409 (list them and pass a specific id). Returns
``{trainingId, status, modelType, modelGroup, modelCount, models: [...]}``,
each model carrying an inference-style ``modelId`` (``<workspace>/<slug>``).
"""
url = f"{API_URL}/{workspace_url}/{project_url}/{version}/v2/trainings/get?api_key={api_key}"
if training_id:
url += f"&trainingId={quote(str(training_id), safe='')}"
response = requests.get(url)
if not response.ok:
raise RoboflowError(response.text)
return response.json()


def create_training_v2(
api_key: str,
workspace_url: str,
project_url: str,
version: str,
*,
speed: Optional[str] = None,
checkpoint: Optional[str] = None,
model_type: Optional[str] = None,
epochs: Optional[int] = None,
):
"""Create a training on a version (DNA ``trainings.create``).

POST /{ws}/{proj}/{version}/v2/trainings. A version may own many trainings,
so repeated/concurrent runs are allowed; the backend rejects a second run on
a legacy (SMPV) version. Returns ``{trainingId, status, jobId}``.
"""
url = f"{API_URL}/{workspace_url}/{project_url}/{version}/v2/trainings?api_key={api_key}"
data: Dict[str, Union[str, int]] = {}
if speed is not None:
data["speed"] = speed
if checkpoint is not None:
data["checkpoint"] = checkpoint
if model_type is not None:
data["modelType"] = model_type
if epochs is not None:
data["epochs"] = epochs
response = requests.post(url, json=data)
if not response.ok:
raise RoboflowError(response.text)
return response.json() if response.content else {"status": "training_started"}


def cancel_training_v2(
api_key: str,
workspace_url: str,
project_url: str,
version: str,
training_id=None,
continue_if_no_refund: bool = False,
):
"""Cancel an in-flight run (DNA ``trainings.cancel``).

POST /{ws}/{proj}/{version}/v2/trainings/cancel. ``training_id`` selects a
specific run; omit it to target the version's sole run.
"""
url = f"{API_URL}/{workspace_url}/{project_url}/{version}/v2/trainings/cancel?api_key={api_key}"
body: Dict[str, Union[str, bool]] = {}
if training_id:
body["trainingId"] = training_id
if continue_if_no_refund:
body["continueIfNoRefund"] = True
response = requests.post(url, json=body)
if not response.ok:
raise RoboflowError(response.text)
return response.json() if response.content else {"success": True}


def stop_training_v2(api_key: str, workspace_url: str, project_url: str, version: str, training_id=None):
"""Request an early stop on an in-flight run (DNA ``trainings.stop``).

POST /{ws}/{proj}/{version}/v2/trainings/stop. ``training_id`` selects a
specific run; omit it to target the version's sole run.
"""
url = f"{API_URL}/{workspace_url}/{project_url}/{version}/v2/trainings/stop?api_key={api_key}"
body: Dict[str, str] = {}
if training_id:
body["trainingId"] = training_id
response = requests.post(url, json=body)
if not response.ok:
raise RoboflowError(response.text)
return response.json() if response.content else {"success": True}


def get_model_weights_url(api_key: str, workspace_url: str, project_url: str, model_id: str, model_format: str = "pt"):
"""Resolve a signed PyTorch weights URL for a single trained model.

GET /{ws}/{proj}/{model_id}/ptFile, where ``model_id`` is the addressable
segment of an inference-style id — a model slug (MMPV) or a version number
(SMPV). Returns the signed ``weightsUrl``.
"""
if model_format != "pt":
raise RoboflowError(f"Unsupported weights format '{model_format}'. Only 'pt' is supported.")
encoded = quote(str(model_id), safe="")
url = f"{API_URL}/{workspace_url}/{project_url}/{encoded}/ptFile?api_key={api_key}"
response = requests.get(url)
if not response.ok:
raise RoboflowError(response.text)
return response.json()["weightsUrl"]


def list_project_models(
api_key: str,
workspace_url: str,
Expand Down
8 changes: 4 additions & 4 deletions roboflow/cli/handlers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,15 @@ def _list_models(args): # noqa: ANN001

models = []
for v in versions:
if v.model:
# version.model is deprecated; read the underlying legacy model directly.
v_model = getattr(v, "_model", None)
if v_model:
models.append(
{
"version": v.version,
"id": v.id,
"model": getattr(v, "model_format", ""),
"map": getattr(v, "model", {}).get("map", "")
if isinstance(getattr(v, "model", None), dict)
else "",
"map": v_model.get("map", "") if isinstance(v_model, dict) else "",
}
)

Expand Down
10 changes: 9 additions & 1 deletion roboflow/cli/handlers/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ def _video_infer(args) -> None: # noqa: ANN001
rf = roboflow.Roboflow(api_key)
project = rf.workspace().project(args.project)
version = project.version(args.version_number)
model = version.model
model = getattr(version, "_model", None)
if model is None:
output_error(
args,
f"No model found for project '{args.project}' version {args.version_number}.",
hint="Train or deploy a model for this version before running video inference.",
exit_code=3,
)
return

job_id, _signed_url, _expire_time = model.predict_video(
args.video_file,
Expand Down
1 change: 1 addition & 0 deletions roboflow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def get_conditional_configuration_variable(key, default):

CLASSIFICATION_MODEL = os.getenv("CLASSIFICATION_MODEL", "ClassificationModel")
INSTANCE_SEGMENTATION_MODEL = "InstanceSegmentationModel"
KEYPOINT_DETECTION_MODEL = "KeypointDetectionModel"
OBJECT_DETECTION_MODEL = os.getenv("OBJECT_DETECTION_MODEL", "ObjectDetectionModel")
SEMANTIC_SEGMENTATION_MODEL = "SemanticSegmentationModel"
PREDICTION_OBJECT = os.getenv("PREDICTION_OBJECT", "Prediction")
Expand Down
Loading
Loading