> For the complete documentation index, see [llms.txt](https://docs.unitlab.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.unitlab.ai/api-sdk-cli/sdks/project-uploads.md).

# Project uploads

Upload local files, folders and cloud data with the Unitlab Python SDK. Track Batch Queue processing, handle failures and prepare media for annotation import.

Upload a file or folder directly to a project, then check its Batch Queue before using the data. Use [Assets](https://docs.unitlab.ai/api-sdk-cli/sdks/assets-and-custom-metadata) instead when the files should be curated, grouped or reused across projects.

![Project upload dialog with Local, CLI, SDK, and Storage tabs](https://292810646-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGjVLUz4wthGkGlRKM6rM%2Fuploads%2FYIMKhOzBn8nrF0VZAPxY%2Fproject-upload-modal.png?alt=media\&token=3d793031-28d6-4529-bf7f-bfe87b2e98b8)

*Project upload uses the same project destination as the Local, CLI, SDK and Storage options in the interface.*

## Before you start

Use Unitlab 3.1.3 and Python 3.10 or newer. Complete [installation](https://docs.unitlab.ai/api-sdk-cli/get-started/install-the-python-sdk-and-cli) and [authentication](https://docs.unitlab.ai/api-sdk-cli/get-started/authentication-and-configuration), and copy the ID of your target project.

Prepare two valid images in this structure. Run the examples from `road-scenes/` so `./images` resolves to this folder:

```
road-scenes/
├── images/
│   ├── road.jpg
│   └── crossing.jpg
└── upload.py
```

Use the following steps together in `upload.py`. Replace `PROJECT_ID` with your project UUID. Choose one upload tab; the folder and single-file examples are alternatives.

## Upload and verify your images

{% stepper %}
{% step %}

### Select the project

```python
from unitlab import UnitlabClient

client = UnitlabClient()
project = client.projects.get("PROJECT_ID")
```

To create a project instead, replace the `get()` line with `project = client.projects.create("Road inspection")`. Pass `ontology_hash="ONTOLOGY_ID"` if it should use an existing workspace ontology.
{% endstep %}

{% step %}

### Upload a local file or folder

{% tabs %}
{% tab title="Folder" %}

```python
batch = project.upload("./images")
```

This uploads `road.jpg` and `crossing.jpg`. Supported files in subfolders are discovered automatically.
{% endtab %}

{% tab title="Single file" %}

```python
batch = project.upload("./images/road.jpg")
```

{% endtab %}
{% endtabs %}

```python
print("Batch Queue:", batch.batch_queue_id)
print("Uploaded:", batch.uploaded, "of", batch.total)
for failure in batch.failed:
    print(failure.path, failure.error)
```

One call creates one Batch Queue. Save its ID for subsequent checks. Annotation JSON and ordinary ZIP archives are not imported by this media-upload call.
{% endstep %}

{% step %}

### Check server processing

```python
status = batch.wait(timeout=1800)
print(status.total, status.completed, status.processing, status.failed)

if batch.failed or status.failed:
    raise RuntimeError("Inspect failed uploads or processing results.")
```

`batch.failed` contains transfer failures. `status.failed` contains server-processing failures. `wait()` ends when nothing remains in processing, including when some items failed.
{% endstep %}

{% step %}

### Verify the imported filenames

```python
queue = project.batch_queue(batch.batch_queue_id)
for item in queue.data():
    print(item["id"], item["file_name"], item["status"])
```

For the two-image folder, expect `Uploaded: 2 of 2`, processing counts `2 2 0 0`, and both `road.jpg` and `crossing.jpg` in the item list when both images are valid. Save the item IDs if you will map existing annotations to these files. The single-file tab should produce one item.
{% endstep %}
{% endstepper %}

Run the completed script:

```bash
python3 upload.py
```

## Folder names and supported files

Ordinary upload recursively finds files but sends each file with its basename. It does not mirror your local folder tree. Two different local folders containing `road.jpg` can therefore produce ambiguous names. Use unique filenames or upload subfolders separately into explicit [Assets paths](https://docs.unitlab.ai/api-sdk-cli/sdks/assets-and-custom-metadata#choose-a-folder-and-path).

The uploader selects recognized media extensions and skips unsupported files. If nothing is uploadable, it raises an error. See [supported source files](https://docs.unitlab.ai/documentation/data/data-upload#supported-source-files) for images, video, audio, medical, document, HTML, CSV, geospatial and pathology formats.

## Upload options

| Python argument              | Use                                                                             |
| ---------------------------- | ------------------------------------------------------------------------------- |
| `source`                     | One local file or directory, as a string or `Path`.                             |
| `fps=1.0`                    | Frame sampling rate for video.                                                  |
| `generic_type=None`          | Explicit family when needed, such as `tabular` or `pointcloud`.                 |
| `primary_column=None`        | Preferred text column for tabular CSV row mode.                                 |
| `batch_size=100`             | Local scheduling batch size. This does not create separate server Batch Queues. |
| `split_sensor_folders=False` | In pointcloud mode, interpret matching sensor folders as separate recordings.   |

{% tabs %}
{% tab title="Video" %}

```python
batch = project.upload("./drive.mp4", fps=30.0)
```

Use the source frame rate when importing annotations for original frames. The default 1 FPS can omit frames referenced by those labels. Upload videos with different source frame rates separately.
{% endtab %}

{% tab title="Tabular CSV" %}

```python
batch = project.upload(
    "./records.csv",
    generic_type="tabular",
    primary_column="text",
)
```

CSV without an explicit family waits for configuration in the target project. Choose row-based tabular data or time-series charts intentionally.
{% endtab %}

{% tab title="Large imagery" %}

```python
batch = project.upload("./maps")
status = batch.wait(timeout=25200)
```

GeoTIFF and whole-slide imagery are detected automatically. Transfer can finish well before raster or slide processing. Increase the wait timeout when needed.
{% endtab %}
{% endtabs %}

DICOM without a usable extension can be recognized by its standard file marker. Small medical uploads are finalized together into appropriate volumes. A large medical file sent through direct upload becomes its own volume rather than joining a series of other files.

## Use a local JSON file list

If your pipeline already produces a JSON list of local files, read that list in Python and call the SDK for each path. This is a local script pattern, not a server-side manifest format. Each call creates its own Batch Queue.

Save `upload-list.json` beside the `images/` directory, not inside it:

```
road-scenes/
├── upload-list.json
└── images/
    ├── road.jpg
    └── crossing.jpg
```

The file contains:

```json
[
  "images/road.jpg",
  "images/crossing.jpg"
]
```

```python
import json
from pathlib import Path
from unitlab import UnitlabClient

manifest = Path("upload-list.json").resolve()
entries = json.loads(manifest.read_text(encoding="utf-8"))
if not isinstance(entries, list) or not entries:
    raise ValueError("Expected a nonempty JSON array of local file paths.")
if not all(isinstance(value, str) and value.strip() for value in entries):
    raise ValueError("Every entry must be a nonempty string.")
if any("://" in value for value in entries):
    raise ValueError("This script accepts local files, not URLs.")

paths = [(manifest.parent / Path(value).expanduser()).resolve() for value in entries]
if len(set(paths)) != len(paths):
    raise ValueError("The list contains duplicate files.")
for path in paths:
    if not path.is_file():
        raise ValueError(f"File does not exist: {path}")

client = UnitlabClient()
project = client.projects.get("PROJECT_ID")
for path in paths:
    batch = project.upload(path)
    print(path.name, batch.batch_queue_id)
    status = batch.wait()
    if batch.failed or status.failed:
        raise RuntimeError(f"Check the Batch Queue for {path.name}.")
```

With this list, expect two Batch Queue IDs, one for each image. This is an alternative to the folder example; running both uploads the images again. Paths in this example are relative to the JSON file. The script validates the list before starting uploads. It does not fetch URLs or create Data Groups. For one Batch Queue covering many files, place them in one folder and pass that folder to `project.upload()`.

## Import from a saved cloud connection

Use an existing connection ID and paths returned by its browser. This example assumes the connection contains these objects:

```
<connection-prefix>/
└── images/
    ├── road.jpg
    └── new-collection/
        └── crossing.jpg
```

```python
batch = project.import_cloud(
    "CLOUD_STORAGE_ID",
    ["images/road.jpg", "images/new-collection/"],
)
status = batch.wait()
if status.failed:
    raise RuntimeError("Inspect this cloud import's Batch Queue.")

for item in project.batch_queue(batch.batch_queue_id).data():
    print(item["file_name"], item["status"])
```

Expect both selected images to appear when processing succeeds. Use paths from the connected storage browser. Paths are relative to the connection prefix; directory paths end with `/` and include only that directory's immediate files. Supply `fps=` when the selection includes video. See [Cloud storage](https://docs.unitlab.ai/api-sdk-cli/sdks/cloud-storage) for connection discovery and browsing.

## Upload LiDAR scenes

LiDAR requires explicit pointcloud mode. Preview the source before uploading:

```python
from unitlab import detect_scenes

detection = detect_scenes("./recordings")
for scene in detection.scenes:
    print(scene.name, scene.frame_count, scene.point_sensors, scene.cameras)

batch = project.upload("./recordings", generic_type="pointcloud")
status = batch.wait()
```

Supported sources include PCD, PLY and BIN files, a scene ZIP, scene folders with matching camera files, an explicit `scene.json`, and supported nuScenes layouts. One scene becomes one item. The workspace must have LiDAR scenes enabled.

Matching sensor subfolders are treated as one scene by default. Set `split_sensor_folders=True` only when they represent separate recordings. Unrelated ordinary media beside the scenes is not included in the pointcloud upload. Follow [LiDAR data preparation](https://docs.unitlab.ai/documentation/annotations/lidar-annotation#prepare-lidar-data) for scene structure, calibration and limits.

## Resume monitoring and handle failures

```python
queue = project.batch_queue("BATCH_QUEUE_ID")
print(queue.status())
for item in queue.data():
    print(item["file_name"], item["status"])
```

| Situation                         | Next step                                                                           |
| --------------------------------- | ----------------------------------------------------------------------------------- |
| Some transfers failed             | Keep successful IDs and retry the corrected subset.                                 |
| Processing finished with failures | Inspect the queue's items before using the data.                                    |
| `ProcessingTimeoutError`          | Check the saved queue again. The timeout ends waiting, not necessarily processing.  |
| `AmbiguousUploadCompletionError`  | Check the existing item or queue before reuploading; completion may have succeeded. |
| No uploadable files               | Check the path, supported extensions and explicit pointcloud mode for LiDAR.        |

After media is ready, [import local annotation JSON](/api-sdk-cli/sdks/annotation-imports.md), inspect [project Data Units](https://docs.unitlab.ai/api-sdk-cli/clis/cli-projects-and-data-units), or continue in the [annotation workflow](https://docs.unitlab.ai/documentation/workflows/workflows-overview).

Explore [Unitlab's data curation platform](https://unitlab.ai/en/data-curation).
