Skip to content
Closed
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
53 changes: 42 additions & 11 deletions roboflow/cli/handlers/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ def _handle_upload_single(args, api_key: str, path: str) -> None: # noqa: ANN00


def _handle_upload_directory(args, api_key: str, path: str) -> None: # noqa: ANN001
import os
import json

import roboflow
from roboflow.cli._output import output, output_error, suppress_sdk_output
Expand Down Expand Up @@ -393,27 +393,58 @@ def _handle_upload_directory(args, api_key: str, path: str) -> None: # noqa: AN
output_error(args, str(exc))
return

if isinstance(result, dict):
if "status" in result:
# Zip flow returns the server's task status; the per-image summary has no "status"
status = result.get("status", "unknown")
task_id = result.get("task_id") or result.get("taskId")
if status == "failed":
output_error(
args,
f"Zip upload of {path} to {args.project} failed (task_id={task_id}): {result.get('error') or result}",
hint="Check the dataset format, then retry; run without --zip-upload to see per-image errors.",
)
return
data = {
"status": status,
"task_id": result.get("task_id") or result.get("taskId"),
"task_id": task_id,
"path": path,
"project": args.project,
"result": result,
}
output(args, data, text=f"Imported {path} to {args.project} (zip upload, status={status})")
return

# Per-image fallback — count files via image extensions
count = 0
image_exts = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
for root, _dirs, files in os.walk(path):
for f in files:
if os.path.splitext(f)[1].lower() in image_exts:
count += 1
failed = result["failed"]
if failed:
total = len(failed) + result["uploaded"] + result["duplicates"]
lines = [f" {f['path']} ({f['stage']}): {f['error']}" for f in failed[:10]]
if len(failed) > 10:
lines.append(f" ... and {len(failed) - 10} more")
message = f"{len(failed)} of {total} images failed to import from {path} to {args.project}:\n" + "\n".join(
lines
)
error = {
"message": message,
"uploaded": result["uploaded"],
"duplicates": result["duplicates"],
"failed_count": len(failed),
"failed": failed,
}
output_error(
args,
json.dumps(error),
hint="Fix the listed files and re-run; images already uploaded are skipped as duplicates.",
)
return

data = {"status": "imported", "path": path, "count": count}
count = result["uploaded"] + result["duplicates"]
data = {
"status": "imported",
"path": path,
"count": count,
"uploaded": result["uploaded"],
"duplicates": result["duplicates"],
}
output(args, data, text=f"Imported {count} images from {path} to {args.project}")


Expand Down
54 changes: 19 additions & 35 deletions roboflow/core/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from tqdm import tqdm

from roboflow.adapters import rfapi
from roboflow.adapters.rfapi import RoboflowError
from roboflow.config import (
API_URL,
APP_URL,
Expand Down Expand Up @@ -662,6 +663,9 @@
model_type (str): The type of the model to be deployed.
model_path (str): File path to the model weights to be uploaded.
filename (str, optional): The name of the weights file. Defaults to "weights/best.pt".

Raises:
RoboflowError: If the upload URL cannot be obtained or the upload fails.
"""
bundle = package_custom_weights_interactive(model_type, model_path, filename, build_dir=model_path)

Expand All @@ -676,41 +680,21 @@
f"{API_URL}/{self.workspace}/{self.project}/{self.version}"
f"/uploadModel?api_key={self.__api_key}&modelType={model_type}&nocache=true"
)
try:
if res.status_code == 429:
raise RuntimeError(
"This version already has a trained model. Please generate and"
" train a new version in order to upload model to Roboflow."
)
else:
res.raise_for_status()
except Exception as e:
print(f"An error occured when getting the model upload URL: {e}")
return

res = requests.put(
res.json()["url"],
data=open(os.path.join(model_path, model_file_name), "rb"),
)
try:
res.raise_for_status()

if self.public:
print(
f"View the status of your deployment at: {APP_URL}/{self.workspace}/{self.project}/{self.version}"
)
print(
"Share your model with the world at:"
f" {UNIVERSE_URL}/{self.workspace}/{self.project}/"
f"model/{self.version}"
)
else:
print(
f"View the status of your deployment at: {APP_URL}/{self.workspace}/{self.project}/{self.version}"
)

except Exception as e:
print(f"An error occured when uploading the model: {e}")
if not res.ok:
raise RoboflowError(f"Failed to get the model upload URL (HTTP {res.status_code}): {res.text}")

with open(os.path.join(model_path, model_file_name), "rb") as f:
res = requests.put(res.json()["url"], data=f)
if not res.ok:
raise RoboflowError(f"Failed to upload the model (HTTP {res.status_code}): {res.text}")

print(f"View the status of your deployment at: {APP_URL}/{self.workspace}/{self.project}/{self.version}")
if self.public:
print(
"Share your model with the world at:"
f" {UNIVERSE_URL}/{self.workspace}/{self.project}/"
f"model/{self.version}"
Comment on lines +694 to +696
)

def __download_zip(self, link, location, format):
"""
Expand Down
58 changes: 29 additions & 29 deletions roboflow/core/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@
wait: bool = True,
poll_interval: float = 5.0,
poll_timeout: float = 3600.0,
) -> Optional[dict]:
) -> dict:
"""
Upload a dataset to Roboflow.

Expand Down Expand Up @@ -549,7 +549,9 @@
poll_timeout (float, optional): zip flow only — total seconds to wait before timing out.

Returns:
dict | None: zip flow returns the final/pending status dict; per-image flow returns None.
dict: zip flow returns the final/pending status dict. Per-image flow returns a summary:
``{"uploaded": int, "duplicates": int, "failed": [{"path", "stage", "error"}]}``,
where ``stage`` is ``"image"`` or ``"annotation"``. Failed images do not raise.
""" # noqa: E501 // docs
if dataset_format != "NOT_USED":
print("Warning: parameter 'dataset_format' is deprecated and will be removed in a future release")
Expand Down Expand Up @@ -703,22 +705,31 @@
image_id = image["id"]
annotation, annotation_time = _save_annotation(image_id, imagedesc)
_log_img_upload(image_path, image, annotation, image_upload_time, image_retry_attempts, annotation_time)
return "duplicate" if image.get("duplicate") else "uploaded", None
except ImageUploadError as e:
retry_attempts = f" (with {e.retries} retries)" if e.retries > 0 else ""
print(f"[ERR]{retry_attempts} {image_path} ({e.message})")
return "failed", {"path": image_path, "stage": "image", "error": e.message}
except AnnotationSaveError as e:
upload_time_str = f"[{image_upload_time:.1f}s]"
retry_attempts = f" (with {image_retry_attempts} retries)" if image_retry_attempts > 0 else ""
image_msg = f"[UPLOADED]{retry_attempts} {image_path} ({image_id}) {upload_time_str}"
annotation_msg = f"annotations = ERR: {e.message}"
print(f"{image_msg} / {annotation_msg}")
return "failed", {"path": image_path, "stage": "annotation", "error": e.message}
except Exception as e:
print(f"[ERR] {image_path} ({e})")
return "failed", {"path": image_path, "stage": "image", "error": str(e)}

with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
list(executor.map(_upload, images))
results = list(executor.map(_upload, images))

return None
statuses = [status for status, _ in results]
return {
"uploaded": statuses.count("uploaded"),
"duplicates": statuses.count("duplicate"),
"failed": [failure for _, failure in results if failure],
}

def _get_or_create_project(self, project_id, license: str = "MIT", type: str = "object-detection"):
try:
Expand Down Expand Up @@ -907,6 +918,9 @@
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".

Raises:
RoboflowError: If the upload URL cannot be obtained or the upload fails.
"""

from roboflow.util.model_processor import (
Expand Down Expand Up @@ -943,34 +957,20 @@
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:
error_message = str(e)
status_code = str(res.status_code)

print("\n\033[91m❌ ERROR\033[0m: Failed to get model deployment URL")
print("\033[93mDetails\033[0m:", error_message)
print("\033[93mStatus\033[0m:", status_code)
print(f"\033[93mResponse\033[0m:\n{res.text}\n")
return
if not res.ok:
raise RoboflowError(f"Failed to get the model upload URL (HTTP {res.status_code}): {res.text}")

# 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"
)
with open(os.path.join(model_path, model_file_name), "rb") as f:
res = requests.put(res.json()["url"], data=f)
if not res.ok:
raise RoboflowError(f"Failed to upload the model (HTTP {res.status_code}): {res.text}")

except Exception as e:
print(f"An error occured when uploading the model: {e}")
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"
Comment on lines +971 to +972
)

def search(
self,
Expand Down
Loading
Loading