diff --git a/roboflow/__init__.py b/roboflow/__init__.py index 177bf8fd..699c2b72 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -15,7 +15,7 @@ from roboflow.models import CLIPModel, GazeModel # noqa: F401 from roboflow.util.general import write_line -__version__ = "1.1.52" +__version__ = "1.1.53" def check_key(api_key, model, notebook, num_retries=0): diff --git a/roboflow/core/workspace.py b/roboflow/core/workspace.py index ebd20401..815c494f 100644 --- a/roboflow/core/workspace.py +++ b/roboflow/core/workspace.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import concurrent.futures import glob import json @@ -10,11 +12,12 @@ from roboflow.adapters import rfapi from roboflow.adapters.rfapi import AnnotationSaveError, ImageUploadError, RoboflowError -from roboflow.config import API_URL, CLIP_FEATURIZE_URL, DEMO_KEYS +from roboflow.config import API_URL, APP_URL, CLIP_FEATURIZE_URL, DEMO_KEYS from roboflow.core.project import Project from roboflow.util import folderparser from roboflow.util.active_learning_utils import check_box_size, clip_encode, count_comparisons from roboflow.util.image_utils import load_labelmap +from roboflow.util.model_processor import process from roboflow.util.two_stage_utils import ocr_infer @@ -566,6 +569,73 @@ def active_learning( prediction_results if type(raw_data_location) is not np.ndarray else prediction_results[-1]["predictions"] ) + def deploy_model( + self, + model_type: str, + model_path: str, + project_ids: list[str], + model_name: str, + filename: str = "weights/best.pt", + ): + """Uploads provided weights file to Roboflow. + Args: + model_type (str): The type of the model to be deployed. + model_path (str): File path to the model weights to be uploaded. + project_ids (list[str]): List of project IDs to deploy the model to. + filename (str, optional): The name of the weights file. Defaults to "weights/best.pt". + """ + + if not project_ids: + raise ValueError("At least one project ID must be provided") + + # Validate if provided project URLs belong to user's projects + user_projects = set(project.split("/")[-1] for project in self.projects()) + for project_id in project_ids: + if project_id not in user_projects: + raise ValueError(f"Project {project_id} is not accessible in this workspace") + + zip_file_name = process(model_type, model_path, filename) + + if zip_file_name is None: + raise RuntimeError("Failed to process model") + + self._upload_zip(model_type, model_path, project_ids, model_name, zip_file_name) + + def _upload_zip( + self, + model_type: str, + model_path: str, + project_ids: list[str], + model_name: str, + model_file_name: str, + ): + # This endpoint returns a signed URL to upload the model + res = requests.post( + f"{API_URL}/{self.url}/models/prepareUpload?api_key={self.__api_key}&modelType={model_type}&modelName={model_name}&projectIds={','.join(project_ids)}&nocache=true" + ) + try: + res.raise_for_status() + except Exception as e: + print(f"An error occured when getting the model deployment URL: {e}") + return + + # Upload the model to the signed URL + res = requests.put( + res.json()["url"], + data=open(os.path.join(model_path, model_file_name), "rb"), + ) + try: + res.raise_for_status() + + for project_id in project_ids: + print( + f"View the status of your deployment for project {project_id} at:" + f" {APP_URL}/{self.url}/{project_id}/models" + ) + + except Exception as e: + print(f"An error occured when uploading the model: {e}") + def __str__(self): projects = self.projects() json_value = {"name": self.name, "url": self.url, "projects": projects} diff --git a/roboflow/roboflowpy.py b/roboflow/roboflowpy.py index 3e6929fe..d68bdaa9 100755 --- a/roboflow/roboflowpy.py +++ b/roboflow/roboflowpy.py @@ -79,10 +79,20 @@ def upload_model(args): rf = roboflow.Roboflow(args.api_key) workspace = rf.workspace(args.workspace) - # Deploy to specific version - project = workspace.project(args.project) - version = project.version(args.version_number) - version.deploy(str(args.model_type), str(args.model_path), str(args.filename)) + if args.version_number is not None: + # Deploy to specific version + project = workspace.project(args.project) + version = project.version(args.version_number) + version.deploy(str(args.model_type), str(args.model_path), str(args.filename)) + else: + # Deploy to multiple projects + workspace.deploy_model( + model_type=str(args.model_type), + model_path=str(args.model_path), + project_ids=args.project, + model_name=str(args.model_name), + filename=str(args.filename), + ) def list_projects(args): @@ -479,13 +489,15 @@ def _add_upload_model_parser(subparsers): upload_model_parser.add_argument( "-p", dest="project", - help="project_id to upload the model into", + action="append", # Allow multiple projects + help="project_id to upload the model into (can be specified multiple times)", ) upload_model_parser.add_argument( "-v", dest="version_number", type=int, - help="version number to upload the model to", + help="version number to upload the model to (optional)", + default=None, ) upload_model_parser.add_argument( "-t", @@ -503,6 +515,11 @@ def _add_upload_model_parser(subparsers): default="weights/best.pt", help="name of the model file", ) + upload_model_parser.add_argument( + "-n", + dest="model_name", + help="name of the model", + ) upload_model_parser.set_defaults(func=upload_model)