---
title: Video
description: Upload videos, probe metadata, and extract frames as images into a dataset.
section: API Reference
order: 13
---
Pictograph annotates **frames**, not videos. The video resource handles
upload and frame extraction; once extracted, frames are regular images
you annotate with the standard SAM3 / annotation workflows.

```python
from pictograph import Client
client = Client()
```

## upload

Three-step upload (signed URL → PUT → register), same pattern as
images.

```python
from pathlib import Path

info = client.video.upload(
    file_path=Path("./recording.mp4"),
    dataset_id="proj-uuid",
    folder_path="/raw-footage",
)
print(info.gcs_path, info.video_id)
```

| Arg | Type | Default | Notes |
|---|---|---|---|
| `file_path` | `str \| Path` | required | Local video file |
| `dataset_id` | `str` | required | Destination dataset |
| `folder_path` | `str` | `"/"` | Virtual folder |

Supported codecs: anything ffmpeg can demux (H.264, H.265, VP9, AV1, etc.).

## probe

Inspect a video's metadata without extracting frames. Pass the
`gcs_path` returned by `upload()`.

```python
meta = client.video.probe(info.gcs_path)
print(meta.duration_seconds, meta.fps, meta.width, meta.height)
print(meta.codec, meta.frame_count)
```

Returns `VideoMetadata` from a server-side `ffprobe` invocation.

## extract_frames

Extract frames from a video into the destination dataset as images.

```python
job = client.video.extract_frames(
    gcs_path=info.gcs_path,
    dataset_id="proj-uuid",
    folder_path="/raw-footage/frames",
    fps=2.0,                       # extract 2 frames per second of source video
    start_seconds=10.0,
    end_seconds=120.0,
    max_frames=200,                # cap on output count
    wait=True,
    poll_interval=5.0,
    timeout=1800.0,
)
print(job.status, job.frames_extracted)
```

Frames are written as `{video_basename}_{frame_index:06d}.jpg` in the
target folder. Each becomes a regular `Image` row — ready for
annotation, search, training.

| Arg | Type | Default | Notes |
|---|---|---|---|
| `gcs_path` | `str` | required | Source video |
| `dataset_id` | `str` | required | Destination dataset |
| `folder_path` | `str` | `"/"` | Virtual folder for the extracted frames |
| `fps` | `float` | `1.0` | Frames per source second |
| `start_seconds` | `float \| None` | `None` | Skip the first N seconds |
| `end_seconds` | `float \| None` | `None` | Stop at second N |
| `max_frames` | `int \| None` | `None` | Cap on output count |
| `wait` | `bool` | `True` | Poll until terminal |

`fps=1.0` is the cheapest setting; `fps=30.0` extracts every frame of a
30 fps source. Frame extraction does **not** consume credits — you pay
only for the storage of the resulting images.

## get_extraction / wait_for_extraction

```python
job = client.video.get_extraction(job_id)
job = client.video.wait_for_extraction(job_id, timeout=600.0)
```

## Common errors

| Status | Exception | Cause |
|---|---|---|
| 404 | `NotFoundError` | `gcs_path` missing or `dataset_id` invalid |
| 415 | `ValidationError` | Unsupported codec |
| 408 | `PollTimeoutError` | Long videos may exceed default `timeout` |
| 413 | `ApiError` | Video file exceeds upload limit (10 GB) |