diff --git a/CHANGELOG.md b/CHANGELOG.md index 1158884c..64b5811a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ All notable changes to this project will be documented in this file. not visible; omitted, the server default (0.6) applies ([#571](https://github.com/roboflow/roboflow-python/pull/571)). +### Removed + +- The Hosted Video Inference API helpers. The service behind them stopped working on + 2026-07-29 and its endpoints now return 410 Gone. Removed: `predict_video`, + `poll_for_video_results` and `poll_until_video_results` on `InferenceModel` and + `TrainedModel`, `VideoInferenceModel`, `CLIPModel`, `GazeModel`, + `rfapi.get_video_job_status`, the `roboflow video infer` and `roboflow video status` + commands and the hidden `run_video_inference_api` alias. To run a model on stored + videos, use [Batch Processing](https://docs.roboflow.com/deployment/roboflow-cloud/batch-processing) + ([#574](https://github.com/roboflow/roboflow-python/pull/574)). + ## 1.7.3 ### Added diff --git a/CLI-COMMANDS.md b/CLI-COMMANDS.md index dedc4d90..372ca778 100644 --- a/CLI-COMMANDS.md +++ b/CLI-COMMANDS.md @@ -542,9 +542,7 @@ roboflow video upload -p my-ar-project -f clip.mov --json # { "videoId": "aBcD1234", "status": "uploaded", "resolvedBatch": { ... } } ``` -Pass `--no-wait` to return as soon as the bytes are stored, then poll separately. -`video upload-status` reports ingestion state — it is distinct from `video status`, which -checks a legacy video *inference* job. +Pass `--no-wait` to return as soon as the bytes are stored, then poll with `video upload-status`. ```bash # 2. Poll ingestion yourself. @@ -577,16 +575,6 @@ Exit codes follow the CLI contract: `0` success, `1` error, `2` auth, `3` not fo `failed` ingestion state and a `--wait` timeout both exit nonzero; the timeout message names the video ID so you can re-check it with `video upload-status`. -### Video inference - -Separate from native upload: this submits a legacy asynchronous inference job for a trained -model version. - -```bash -roboflow video infer -p my-project -v 3 -f video.mp4 --fps 10 -roboflow video status -``` - ### Shell completion The fastest path: let the CLI install completion for you. Auto-detects your shell from `$SHELL`. @@ -676,7 +664,7 @@ Version numbers are always numeric — that's how `x/y` is disambiguated between | `asynctasks` | Inspect async background tasks (e.g. project forks) | | `trash` | List items in Trash | | `universe` | Search Roboflow Universe | -| `video` | Native video upload/status/annotation, and video inference | +| `video` | Native video upload, upload status and annotation | | `batch` | Batch processing jobs *(coming soon)* | | `completion` | Install or generate shell completion scripts (bash, zsh, fish) | diff --git a/roboflow/__init__.py b/roboflow/__init__.py index 22aec8cb..0fcaf2bd 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -17,11 +17,8 @@ try: from roboflow.core.project import Project - from roboflow.models import CLIPModel, GazeModel # noqa: F401 except ImportError: Project = None # type: ignore[assignment,misc] - CLIPModel = None # type: ignore[assignment,misc] - GazeModel = None # type: ignore[assignment,misc] def check_key(api_key, model, notebook, num_retries=0): diff --git a/roboflow/adapters/rfapi.py b/roboflow/adapters/rfapi.py index f18577ee..c16aacb8 100644 --- a/roboflow/adapters/rfapi.py +++ b/roboflow/adapters/rfapi.py @@ -1968,19 +1968,6 @@ def get_labeling_stats(api_key, workspace_url, *, start_date=None, end_date=None return response.json() -# --------------------------------------------------------------------------- -# Phase 2: Video inference status -# --------------------------------------------------------------------------- - - -def get_video_job_status(api_key, job_id): - """GET /videoinfer?jobId={id} — check video inference job status.""" - response = http.get(f"{API_URL}/videoinfer", params={"api_key": api_key, "job_id": job_id}) - if response.status_code != 200: - raise RoboflowError(response.text) - return response.json() - - # --------------------------------------------------------------------------- # Batch Processing (Asset Library orchestration) # --------------------------------------------------------------------------- diff --git a/roboflow/cli/handlers/_aliases.py b/roboflow/cli/handlers/_aliases.py index b1fcef9d..b4550847 100644 --- a/roboflow/cli/handlers/_aliases.py +++ b/roboflow/cli/handlers/_aliases.py @@ -190,17 +190,3 @@ def get_workspace_info_alias( print("project", proj) # noqa: T201 ver = proj.version(args.version_number) print("version", ver) # noqa: T201 - - @app.command("run_video_inference_api", hidden=True) - def run_video_inference_api_alias( - ctx: typer.Context, - project: Annotated[Optional[str], typer.Option("-p", help="Project ID")] = None, - version_number: Annotated[Optional[int], typer.Option("-v", help="Version")] = None, - video_file: Annotated[Optional[str], typer.Option("-f", help="Video file")] = None, - fps: Annotated[int, typer.Option("-fps", help="FPS")] = 5, - ) -> None: - """Run video inference (hidden legacy alias).""" - from roboflow.cli.handlers.video import _video_infer - - args = ctx_to_args(ctx, project=project, version_number=version_number, video_file=video_file, fps=fps) - _video_infer(args) diff --git a/roboflow/cli/handlers/video.py b/roboflow/cli/handlers/video.py index 7a903a72..96623b24 100644 --- a/roboflow/cli/handlers/video.py +++ b/roboflow/cli/handlers/video.py @@ -1,4 +1,4 @@ -"""Video commands: native video Source ingestion/annotation and legacy video inference.""" +"""Video commands: native video Source ingestion and annotation.""" from __future__ import annotations @@ -10,34 +10,11 @@ video_app = typer.Typer( cls=SortedGroup, - help="Native video upload/annotation and video inference operations", + help="Native video upload and annotation operations", no_args_is_help=True, ) -@video_app.command("infer") -def infer( - ctx: typer.Context, - project: Annotated[str, typer.Option("-p", "--project", help="Project ID")], - version_number: Annotated[int, typer.Option("-v", "--version", help="Model version number")], - video_file: Annotated[str, typer.Option("-f", "--file", help="Path to video file")], - fps: Annotated[int, typer.Option("--fps", help="Frames per second")] = 5, -) -> None: - """Run video inference.""" - args = ctx_to_args(ctx, project=project, version_number=version_number, video_file=video_file, fps=fps) - _video_infer(args) - - -@video_app.command("status") -def status( - ctx: typer.Context, - job_id: Annotated[str, typer.Argument(help="Job ID to check")], -) -> None: - """Check a legacy video inference job's status (not native upload ingestion).""" - args = ctx_to_args(ctx, job_id=job_id) - _video_status(args) - - @video_app.command("upload") def upload( ctx: typer.Context, @@ -142,87 +119,6 @@ def annotate( _video_annotate(args) -# --------------------------------------------------------------------------- -# Business logic (unchanged from argparse version) -# --------------------------------------------------------------------------- - - -def _video_infer(args) -> None: # noqa: ANN001 - import roboflow - from roboflow.cli._output import output, output_error - from roboflow.config import load_roboflow_api_key - - api_key = args.api_key or load_roboflow_api_key(None) - if not api_key: - output_error(args, "No API key found.", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'.", exit_code=2) - return - - try: - from roboflow.cli._output import suppress_sdk_output - - with suppress_sdk_output(): - rf = roboflow.Roboflow(api_key) - project = rf.workspace().project(args.project) - version = project.version(args.version_number) - 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, - args.fps, - prediction_type="batch-video", - ) - except Exception as exc: - output_error(args, str(exc)) - return - - data = {"job_id": job_id, "status": "submitted"} - output(args, data, text=f"Video inference submitted. Job ID: {job_id}") - - -def _video_status(args) -> None: # noqa: ANN001 - from roboflow.adapters import rfapi - from roboflow.cli._output import output, output_error - from roboflow.config import load_roboflow_api_key - - api_key = args.api_key or load_roboflow_api_key(None) - if not api_key: - output_error(args, "No API key found.", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'.", exit_code=2) - return - - try: - data = rfapi.get_video_job_status(api_key, args.job_id) - except rfapi.RoboflowError as exc: - msg = str(exc) - if "NOT FOUND" in msg.upper(): - output_error( - args, - f"Video job '{args.job_id}' not found.", - hint="Check the job ID. You can get job IDs from 'roboflow video infer'.", - exit_code=3, - ) - else: - output_error(args, msg, exit_code=3) - return - - status = data.get("status", "unknown") - progress = data.get("progress", "") - text_lines = [ - f"Job ID: {args.job_id}", - f"Status: {status}", - ] - if progress: - text_lines.append(f"Progress: {progress}") - output(args, data, text="\n".join(text_lines)) - - # --------------------------------------------------------------------------- # Native video Source business logic # --------------------------------------------------------------------------- diff --git a/roboflow/core/training.py b/roboflow/core/training.py index a56f4d1f..ee0fdc7c 100644 --- a/roboflow/core/training.py +++ b/roboflow/core/training.py @@ -72,7 +72,6 @@ def __init__(self, api_key, workspace, project, model_id, model_type=None, metri # The second segment addresses the model on /ptFile: a model slug for # MMPV, a version number for SMPV. self._weights_id = model_id.split("/", 1)[1] if "/" in str(model_id) else model_id - self._video_model_cache = None def predict(self, image_path, hosted=False, confidence=40, overlap=30, format="json", **kwargs): """Run hosted inference on an image by this model's id. @@ -92,72 +91,6 @@ def predict(self, image_path, hosted=False, confidence=40, overlap=30, format="j params.update(kwargs) return model.predict(image_path, prediction_type=prediction_type, **params) - def _video_model(self): - """Build (and cache) the legacy inference model used for video inference. - - Video upload and result polling still flow through the legacy - ``/videoinfer`` endpoints, which the task-specific models implement. - Caching keeps ``predict_video`` and the poll methods on one underlying - object, so a job started here can be polled without re-passing its id. - """ - if self._video_model_cache is not None: - return self._video_model_cache - - from roboflow.models.classification import ClassificationModel - from roboflow.models.instance_segmentation import InstanceSegmentationModel - from roboflow.models.keypoint_detection import KeypointDetectionModel - from roboflow.models.object_detection import ObjectDetectionModel - from roboflow.models.semantic_segmentation import SemanticSegmentationModel - - task = task_of_model_type(self.model_type or "") - legacy_class = { - TASK_CLS: ClassificationModel, - TASK_SEG: InstanceSegmentationModel, - TASK_SEM: SemanticSegmentationModel, - TASK_POSE: KeypointDetectionModel, - }.get(task, ObjectDetectionModel) - - legacy_id = f"{self.workspace}/{self.project}/{self._weights_id}" - self._video_model_cache = legacy_class(self.__api_key, legacy_id) - return self._video_model_cache - - def predict_video(self, video_path, fps=5, additional_models=None, prediction_type="batch-video"): - """Run hosted video inference for this model (DNA-era equivalent of the - legacy ``version.model.predict_video``). - - Delegates to the task-appropriate legacy inference model built from this - model's id, so a ``TrainedModel`` can do everything the old - ``version.model`` could. Returns ``(job_id, signed_url, expires)``; poll - with :meth:`poll_until_video_results` on the same object. - - NOTE: the legacy ``/videoinfer`` payload is keyed by ``/``. - For MMPV models addressed by ``/`` this routes the - slug through as the version segment; verify against staging before relying - on it for slug-addressed models. - """ - return self._video_model().predict_video( - video_path, fps=fps, additional_models=additional_models, prediction_type=prediction_type - ) - - def poll_for_video_results(self, job_id=None) -> dict: - """Check once for this model's video inference results (DNA-era equivalent - of the legacy ``version.model.poll_for_video_results``). - - Returns ``{}`` while the job is still running. Defaults to the job started - by the most recent :meth:`predict_video` call on this object. - """ - return self._video_model().poll_for_video_results(job_id) - - def poll_until_video_results(self, job_id=None) -> dict: - """Block until this model's video inference job completes, returning the - results (DNA-era equivalent of the legacy - ``version.model.poll_until_video_results``). - - Defaults to the job started by the most recent :meth:`predict_video` call - on this object. - """ - return self._video_model().poll_until_video_results(job_id) - def download(self, format="pt", location="."): """Download this model's PyTorch weights to ``location/weights.pt``.""" weights_url = rfapi.get_model_weights_url( diff --git a/roboflow/models/__init__.py b/roboflow/models/__init__.py index 07b4b2f4..e69de29b 100644 --- a/roboflow/models/__init__.py +++ b/roboflow/models/__init__.py @@ -1,2 +0,0 @@ -from .clip import CLIPModel # noqa: F401 -from .gaze import GazeModel # noqa: F401 diff --git a/roboflow/models/clip.py b/roboflow/models/clip.py deleted file mode 100644 index a5cfe256..00000000 --- a/roboflow/models/clip.py +++ /dev/null @@ -1,16 +0,0 @@ -from .inference import InferenceModel - - -class CLIPModel(InferenceModel): - """ - Run inference on CLIP, hosted on Roboflow. - """ - - def __init__(self, api_key: str): - """ - Initialize a CLIP model. - - Args: - api_key: Your Roboflow API key. - """ - super().__init__(api_key=api_key, version_id="BASE_MODEL") diff --git a/roboflow/models/gaze.py b/roboflow/models/gaze.py deleted file mode 100644 index f2897e70..00000000 --- a/roboflow/models/gaze.py +++ /dev/null @@ -1,17 +0,0 @@ -from .inference import InferenceModel - - -class GazeModel(InferenceModel): - """ - Run inference on a gaze detection model, hosted on Roboflow. - """ - - def __init__(self, api_key: str, version_id: str): - """ - Initialize a CLIP model. - - Args: - api_key: Your Roboflow API key. - version_id (str): the ID of the dataset version to use for inference - """ - super().__init__(api_key=api_key, version_id=version_id) diff --git a/roboflow/models/inference.py b/roboflow/models/inference.py index 13ac5521..b2fe93fb 100644 --- a/roboflow/models/inference.py +++ b/roboflow/models/inference.py @@ -1,10 +1,5 @@ import io -import json -import os -import time import urllib -from typing import List, Optional, Tuple -from urllib.parse import urljoin from PIL import Image from requests_toolbelt.multipart.encoder import MultipartEncoder @@ -15,21 +10,6 @@ from roboflow.util.image_utils import validate_image_path from roboflow.util.prediction import PredictionGroup -SUPPORTED_ROBOFLOW_MODELS = ["batch-video"] - -SUPPORTED_ADDITIONAL_MODELS = { - "clip": { - "model_id": "clip", - "model_version": "1", - "inference_type": "clip-embed-image", - }, - "gaze": { - "model_id": "gaze", - "model_version": "1", - "inference_type": "gaze-detection", - }, -} - class InferenceModel: def __init__( @@ -145,236 +125,6 @@ def predict(self, image_path, prediction_type=None, **kwargs): colors=self.colors, ) - def predict_video( - self, - video_path: str, - fps: int = 5, - additional_models: Optional[List[str]] = None, - prediction_type: str = "batch-video", - ) -> Tuple[str, str, Optional[str]]: - """ - Infers detections based on image from specified model and image path. - - Args: - video_path (str): path to the video you'd like to perform prediction on - prediction_type (str): type of the model to run - fps (int): frames per second to run inference - - Returns: - A list of the signed url and job id - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> job_id,signed_url,signed_url_expires = model.predict_video("video.mp4" - ,fps=5, inference_type="object-detection") - """ - - signed_url_expires = None - - url = urljoin(API_URL, "/video_upload_signed_url?api_key=" + self.__api_key) - if fps > 120: - raise Exception("FPS must be less than or equal to 120.") - - if additional_models is None: - additional_models = [] - - for model in additional_models: - if model not in SUPPORTED_ADDITIONAL_MODELS: - raise Exception(f"Model {model} is not supported for video inference.") - - if prediction_type not in SUPPORTED_ROBOFLOW_MODELS: - raise Exception(f"{prediction_type} is not supported for video inference.") - - model_class = self.__class__.__name__ - - if model_class == "ObjectDetectionModel": - self.type = "object-detection" - elif model_class == "ClassificationModel": - self.type = "classification" - elif model_class == "InstanceSegmentationModel": - self.type = "instance-segmentation" - elif model_class == "GazeModel": - self.type = "gaze-detection" - elif model_class == "CLIPModel": - self.type = "clip-embed-image" - elif model_class == "KeypointDetectionModel": - self.type = "keypoint-detection" - else: - raise Exception("Model type not supported for video inference.") - - payload = json.dumps( - { - "file_name": os.path.basename(video_path), - } - ) - - if not video_path.startswith(("http://", "https://")): - headers = {"Content-Type": "application/json"} - - try: - response = http.request("POST", url, headers=headers, data=payload) - except Exception as e: - raise Exception(f"Error uploading video: {e}") - - if not response.ok: - raise Exception(f"Error uploading video: {response.text}") - - signed_url = response.json()["signed_url"] - - signed_url_expires = signed_url.split("&X-Goog-Expires")[1].split("&")[0].strip("=") - - # make a POST request to the signed URL - headers = {"Content-Type": "application/octet-stream"} - - try: - with open(video_path, "rb") as f: - video_data = f.read() - except Exception as e: - raise Exception(f"Error reading video: {e}") - - try: - result = http.put(signed_url, data=video_data, headers=headers) - except Exception as e: - raise Exception(f"There was an error uploading the video: {e}") - - if not result.ok: - raise Exception(f"There was an error uploading the video: {result.text}") - else: - signed_url = video_path - - url = urljoin(API_URL, "/videoinfer/?api_key=" + self.__api_key) - if model_class in ("CLIPModel", "GazeModel"): - if model_class == "CLIPModel": - model = "clip" - else: - model = "gaze" - - models = [ - { - "model_id": SUPPORTED_ADDITIONAL_MODELS[model]["model_id"], - "model_version": SUPPORTED_ADDITIONAL_MODELS[model]["model_version"], - "inference_type": SUPPORTED_ADDITIONAL_MODELS[model]["inference_type"], - } - ] - else: - models = [ - { - "model_id": self.dataset_id, - "model_version": self.version, - "inference_type": self.type, - } - ] - - for model in additional_models: - models.append(SUPPORTED_ADDITIONAL_MODELS[model]) - - payload = json.dumps({"input_url": signed_url, "infer_fps": fps, "models": models}) - - headers = {"Content-Type": "application/json"} - - try: - response = http.request("POST", url, headers=headers, data=payload) - except Exception as e: - raise Exception(f"Error starting video inference: {e}") - - if not response.ok: - raise Exception(f"Error starting video inference: {response.text}") - - job_id = response.json()["job_id"] - - self.job_id = job_id - - return job_id, signed_url, signed_url_expires - - def poll_for_video_results(self, job_id: Optional[str] = None) -> dict: - """ - Polls the Roboflow API to check if video inference is complete. - - Returns: - Inference results as a dict - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> prediction = model.predict("video.mp4") - - >>> results = model.poll_for_video_results() - """ - - if job_id is None: - job_id = self.job_id - - url = urljoin(API_URL, "/videoinfer/?api_key=" + self.__api_key + "&job_id=" + job_id) - try: - response = http.get(url, headers={"Content-Type": "application/json"}) - except Exception as e: - raise Exception(f"Error getting video inference results: {e}") - - if not response.ok: - raise Exception(f"Error getting video inference results: {response.text}") - data = response.json() - if "status" not in data: - return {} # No status available - if data.get("status") > 1: - return data # Error - elif data.get("status") == 1: - return {} # Still running - else: # done - output_signed_url = data["output_signed_url"] - inference_data = http.get(output_signed_url, headers={"Content-Type": "application/json"}) - - # frame_offset and model name are top-level keys - return inference_data.json() - - def poll_until_video_results(self, job_id) -> dict: - """ - Polls the Roboflow API to check if video inference is complete. - - When inference is complete, the results are returned. - - Returns: - Inference results as a dict - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> prediction = model.predict("video.mp4") - - >>> results = model.poll_until_results() - """ - if job_id is None: - job_id = self.job_id - - attempts = 0 - print(f"Checking for video inference results for job {job_id} every 60s") - while True: - time.sleep(60) - print(f"({attempts * 60}s): Checking for inference results") - response = self.poll_for_video_results(job_id) - attempts += 1 - - if response != {}: - return response - def download(self, format="pt", location="."): """ Download the weights associated with a model. diff --git a/roboflow/models/video.py b/roboflow/models/video.py deleted file mode 100644 index 65d6b00d..00000000 --- a/roboflow/models/video.py +++ /dev/null @@ -1,240 +0,0 @@ -import json -import time -from typing import Optional, Tuple -from urllib.parse import urljoin - -import filetype - -from roboflow.config import API_URL -from roboflow.models.inference import InferenceModel -from roboflow.util import http - -SUPPORTED_ROBOFLOW_MODELS = ["object-detection", "classification", "instance-segmentation", "keypoint-detection"] - -SUPPORTED_ADDITIONAL_MODELS = { - "clip": { - "model_id": "clip", - "model_version": "1", - "inference_type": "clip-embed-image", - }, - "gaze": { - "model_id": "gaze", - "model_version": "1", - "inference_type": "gaze-detection", - }, -} - -ACCEPTED_VIDEO_FORMATS = { - "video/mp4", - "video/x-msvideo", # AVI - "video/webm", -} - - -def is_valid_mime(filename): - kind = filetype.guess(filename) - - if kind is None: - return False - - return kind.mime in ACCEPTED_VIDEO_FORMATS - - -def is_valid_video(filename): - # check file type - if not is_valid_mime(filename): - return False - - return True - - -class VideoInferenceModel(InferenceModel): - """ - Run inference on an object detection model hosted on Roboflow or served through Roboflow Inference. - """ # noqa: E501 // docs - - def __init__( - self, - api_key, - ): - """ - Create a VideoDetectionModel object through which you can run inference on videos. - - Args: - api_key (str): Your API key (obtained via your workspace API settings page). - """ # noqa: E501 // docs - self.__api_key = api_key - - def predict( # type: ignore[override] - self, - video_path: str, - inference_type: str, - fps: int = 5, - additional_models: Optional[list] = None, - ) -> Tuple[str, str]: - """ - Infers detections based on image from specified model and image path. - - Args: - video_path (str): path to the video you'd like to perform prediction on - inference_type (str): type of the model to run - fps (int): frames per second to run inference - - Returns: - A list of the signed url and job id - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> prediction = model.predict("video.mp4", fps=5, inference_type="object-detection") - """ # noqa: E501 // docs - - url = urljoin(API_URL, f"/video_upload_signed_url/?api_key={self.__api_key}") - - if fps > 120: - raise Exception("FPS must be less than or equal to 120.") - - if additional_models is None: - additional_models = [] - - for model in additional_models: - if model not in SUPPORTED_ADDITIONAL_MODELS: - raise Exception(f"Model {model} is not supported for video inference.") - - if inference_type not in SUPPORTED_ROBOFLOW_MODELS: - raise Exception(f"Model {inference_type} is not supported for video inference.") - - if not is_valid_video(video_path): - raise Exception("Video path is not valid") - - payload = json.dumps( - { - "file_name": video_path, - } - ) - - headers = {"Content-Type": "application/json"} - - response = http.request("POST", url, headers=headers, data=payload) - - signed_url = response.json()["signed_url"] - - print("Uploaded video to signed url: " + signed_url) - - url = urljoin(API_URL, f"/videoinfer/?api_key={self.__api_key}") - - models = [ - { - "model_id": self.dataset_id, - "model_version": self.version, - "inference_type": inference_type, - } - ] - - for model in additional_models: - models.append(SUPPORTED_ADDITIONAL_MODELS[model]) - - payload = json.dumps({"input_url": signed_url, "infer_fps": fps, "models": models}) - - response = http.request("POST", url, headers=headers, data=payload) - - job_id = response.json()["job_id"] - - self.job_id = job_id - - return job_id, signed_url - - def poll_for_results(self, job_id: Optional[str] = None) -> dict: - """ - Polls the Roboflow API to check if video inference is complete. - - Returns: - Inference results as a dict - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> prediction = model.predict("video.mp4") - - >>> results = model.poll_for_results() - """ # noqa: E501 // docs - - if job_id is None: - job_id = self.job_id - - url = urljoin(API_URL, f"/videoinfer/?api_key={self.__api_key}&job_id={self.job_id}") - - try: - response = http.get(url, headers={"Content-Type": "application/json"}) - except Exception as e: - print(e) - raise Exception("Error polling for results.") - - if not response.ok: - raise Exception("Error polling for results.") - - data = response.json() - - if data["success"] == 0: - output_signed_url = data["output_signed_url"] - - inference_data = http.get(output_signed_url, headers={"Content-Type": "application/json"}) - - # frame_offset and model name are top-level keys - return inference_data.json() - elif data["success"] == 1: - print("Job not complete yet. Check back in a minute.") - return {} - else: - raise Exception("Job failed.") - - def poll_until_results(self, job_id) -> dict: - """ - Polls the Roboflow API to check if video inference is complete. - - When inference is complete, the results are returned. - - Returns: - Inference results as a dict - - Example: - >>> import roboflow - - >>> rf = roboflow.Roboflow(api_key="") - - >>> project = rf.workspace().project("PROJECT_ID") - - >>> model = project.version("1").models()[0] - - >>> prediction = model.predict("video.mp4") - - >>> results = model.poll_until_results() - """ # noqa: E501 // docs - if job_id is None: - job_id = self.job_id - - attempts = 0 - - while True: - response = self.poll_for_results() - - attempts += 1 - - if response != {}: - return response - - print(f"({attempts * 60}s): Checking for inference results") - - time.sleep(60) diff --git a/tests/adapters/test_rfapi_phase2.py b/tests/adapters/test_rfapi_phase2.py index 4d9110ce..c83eb9bb 100644 --- a/tests/adapters/test_rfapi_phase2.py +++ b/tests/adapters/test_rfapi_phase2.py @@ -780,26 +780,6 @@ def test_error(self, mock_get): get_labeling_stats("key", "ws") -class TestGetVideoJobStatus(unittest.TestCase): - @patch("roboflow.util.http.requests.get") - def test_success(self, mock_get): - from roboflow.adapters.rfapi import get_video_job_status - - mock_get.return_value = MagicMock(status_code=200, json=lambda: {"status": "completed", "progress": 1.0}) - result = get_video_job_status("key", "job-123") - self.assertEqual(result["status"], "completed") - call_kwargs = mock_get.call_args[1] - self.assertEqual(call_kwargs["params"]["job_id"], "job-123") - - @patch("roboflow.util.http.requests.get") - def test_error(self, mock_get): - from roboflow.adapters.rfapi import RoboflowError, get_video_job_status - - mock_get.return_value = MagicMock(status_code=404, text="Not found") - with self.assertRaises(RoboflowError): - get_video_job_status("key", "job-123") - - class TestSearchUniverse(unittest.TestCase): @patch("roboflow.util.http.requests.get") def test_success(self, mock_get): diff --git a/tests/cli/test_completion_handler.py b/tests/cli/test_completion_handler.py index d701543e..a24f43fa 100644 --- a/tests/cli/test_completion_handler.py +++ b/tests/cli/test_completion_handler.py @@ -93,7 +93,6 @@ def test_hidden_commands_filtered_from_completion(self) -> None: "search-export", "upload_model", "get_workspace_info", - "run_video_inference_api", "help", } leaked = hidden_examples & visible diff --git a/tests/cli/test_discovery.py b/tests/cli/test_discovery.py index d072f956..b17d4c15 100644 --- a/tests/cli/test_discovery.py +++ b/tests/cli/test_discovery.py @@ -118,7 +118,6 @@ def test_hidden_aliases_not_in_help(self) -> None: output = _strip_ansi(result.output) self.assertNotIn("upload_model", output) self.assertNotIn("get_workspace_info", output) - self.assertNotIn("run_video_inference_api", output) def test_hidden_alias_still_works(self) -> None: result = runner.invoke(app, ["upload_model", "--help"]) diff --git a/tests/cli/test_video_handler.py b/tests/cli/test_video_handler.py index 8419e637..bb2e9eb8 100644 --- a/tests/cli/test_video_handler.py +++ b/tests/cli/test_video_handler.py @@ -20,46 +20,6 @@ def test_video_app_exists(self) -> None: self.assertIsNotNone(video_app) - def test_video_infer_exists(self) -> None: - result = runner.invoke(app, ["video", "infer", "--help"]) - self.assertEqual(result.exit_code, 0) - - def test_video_status_exists(self) -> None: - result = runner.invoke(app, ["video", "status", "--help"]) - self.assertEqual(result.exit_code, 0) - - -class TestVideoStatus(unittest.TestCase): - """Test video status handler.""" - - @patch("roboflow.config.load_roboflow_api_key", return_value=None) - def test_status_no_api_key(self, _mock_key) -> None: - result = runner.invoke(app, ["--json", "video", "status", "job-123"]) - self.assertNotEqual(result.exit_code, 0) - - @patch("roboflow.adapters.rfapi.get_video_job_status") - @patch("roboflow.config.load_roboflow_api_key", return_value="fake-key") - def test_status_success(self, _mock_key, mock_api) -> None: - mock_api.return_value = {"status": "completed", "progress": "100%"} - result = runner.invoke(app, ["video", "status", "job-abc"]) - self.assertIn("job-abc", result.output) - self.assertIn("completed", result.output) - - @patch("roboflow.adapters.rfapi.get_video_job_status") - @patch("roboflow.config.load_roboflow_api_key", return_value="fake-key") - def test_status_json_output(self, _mock_key, mock_api) -> None: - mock_api.return_value = {"status": "processing", "progress": "50%"} - result = runner.invoke(app, ["--json", "video", "status", "job-abc"]) - data = json.loads(result.output) - self.assertEqual(data["status"], "processing") - - @patch("roboflow.adapters.rfapi.get_video_job_status") - @patch("roboflow.config.load_roboflow_api_key", return_value="fake-key") - def test_status_passes_job_id_to_api(self, _mock_key, mock_api) -> None: - mock_api.return_value = {"status": "completed"} - runner.invoke(app, ["video", "status", "my-unique-job-777"]) - mock_api.assert_called_once_with("fake-key", "my-unique-job-777") - AR_PROJECT_PAYLOAD = { "project": { diff --git a/tests/manual/video_inference.sh b/tests/manual/video_inference.sh deleted file mode 100755 index e43854bd..00000000 --- a/tests/manual/video_inference.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/env bash -python ../../roboflow/roboflowpy.py run_video_inference_api -a -p -v -f -fps diff --git a/tests/test_training.py b/tests/test_training.py index b8f32e89..e58cf951 100644 --- a/tests/test_training.py +++ b/tests/test_training.py @@ -9,11 +9,6 @@ SEMANTIC_SEGMENTATION_MODEL, ) from roboflow.core.training import TrainedModel, Training -from roboflow.models.classification import ClassificationModel -from roboflow.models.instance_segmentation import InstanceSegmentationModel -from roboflow.models.keypoint_detection import KeypointDetectionModel -from roboflow.models.object_detection import ObjectDetectionModel -from roboflow.models.semantic_segmentation import SemanticSegmentationModel class TestTrainedModelPredict(unittest.TestCase): @@ -45,49 +40,6 @@ def test_predict_routes_through_shared_inference_model_with_task_prediction_type self.assertEqual(predict.call_args.kwargs["format"], "json") -class TestTrainedModelVideo(unittest.TestCase): - def test_predict_video_routes_through_task_appropriate_legacy_model(self): - cases = [ - ("yolov11", ObjectDetectionModel), - ("yolov11-cls", ClassificationModel), - ("yolov11-seg", InstanceSegmentationModel), - ("yolov11-pose", KeypointDetectionModel), - ("yolo26-sem", SemanticSegmentationModel), - ] - - for model_type, legacy_class in cases: - with self.subTest(model_type=model_type): - model = TrainedModel("key", "ws", "proj", "ws/model-slug", model_type=model_type) - with patch.object( - legacy_class, - "predict_video", - autospec=True, - return_value=("job-1", "signed-url", None), - ) as predict_video: - result = model.predict_video("video.mp4", fps=9) - - legacy_model = predict_video.call_args.args[0] - self.assertIsInstance(legacy_model, legacy_class) - self.assertEqual(legacy_model.id, "ws/proj/model-slug") - self.assertEqual(result, ("job-1", "signed-url", None)) - self.assertEqual(predict_video.call_args.kwargs["fps"], 9) - - def test_poll_reuses_the_predict_video_legacy_model(self): - model = TrainedModel("key", "ws", "proj", "ws/model-slug", model_type="yolov11") - - with ( - patch.object(ObjectDetectionModel, "predict_video", autospec=True, return_value=("job-1", "url", None)), - patch.object( - ObjectDetectionModel, "poll_until_video_results", autospec=True, return_value={"frames": []} - ) as poll, - ): - model.predict_video("video.mp4") - result = model.poll_until_video_results("job-1") - - self.assertEqual(result, {"frames": []}) - self.assertIs(poll.call_args.args[0], model._video_model()) - - class TestTrainingModels(unittest.TestCase): def test_models_are_cached_until_refresh(self): training = Training("key", "ws", "proj", "1", {"trainingId": "training-1"})