> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getjumper.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Public api v1

# Jumper Public API v1

Base URL: `http://localhost:6699/api/v1`

Jumper analyzes your media files locally on your machine. This API lets external tools (MAM systems, automation scripts, custom integrations) tap into those capabilities: semantic search across video, speech transcription, face detection and clustering, and more.

***

## Authentication

Every endpoint except `/health` requires a **Jumper Pro** license key. Pass it one of two ways:

| Method                   | Example                                 |
| ------------------------ | --------------------------------------- |
| **Header** (recommended) | `X-License-Key: your-key-here`          |
| **JSON body**            | `{"license_key": "your-key-here", ...}` |

The key is validated on the first request and cached for the session — subsequent requests with the same key skip the network check.

`/auth_by_settings` is an internal bootstrap endpoint and does **not** authenticate later `/api/v1` calls by itself. Public API clients should still send `X-License-Key` or `license_key` on each `/api/v1` request.

**Error responses:**

| Status | Meaning                            |
| ------ | ---------------------------------- |
| `401`  | No license key provided            |
| `403`  | Key is valid but not a Pro license |

```json theme={null}
// 401
{"error": "Authentication required", "message": "Provide license_key in request body or X-License-Key header"}

// 403
{"error": "Jumper Pro license required", "message": "The public API requires a Jumper Pro license"}
```

***

## Typical Workflow

Most integrations follow this pattern:

1. **Check health** — verify the backend is running
2. **Analyze media** — run visual and/or speech analysis on your files
3. **Load analysis data** — load analysis results into memory for searching
4. **Search** — find matching moments by text, image, or frame similarity
5. **Get transcriptions** — retrieve speech-to-text results
6. **Get thumbnails** — fetch preview images for specific timestamps

***

## Endpoints

### Health

#### `GET /health`

No authentication required. Use this to check if the backend is running.

**Response:**

```json theme={null}
{"status": "ok"}
```

***

### Models

#### `GET /models/loaded`

Returns which visual and speech analysis models are currently loaded in memory.

**Example request:**

```bash theme={null}
curl -H "X-License-Key: YOUR_KEY" http://localhost:6699/api/v1/models/loaded
```

**Response:**

```json theme={null}
{
  "visual": {
    "model_key": "v2-medium-256",
    "is_loading": false
  },
  "speech": {
    "model_key": "mlx-large-v3-turbo",
    "is_loading": false
  }
}
```

#### `GET /models/available`

Lists all models supported on this hardware, which are downloaded, and which is active.

**Example request:**

```bash theme={null}
curl -H "X-License-Key: YOUR_KEY" http://localhost:6699/api/v1/models/available
```

**Response:**

```json theme={null}
{
  "supported_models": ["v2-medium-256", "v2-large-384", "v1-multilingual-384"],
  "downloaded_models": ["v2-medium-256", "v2-large-384"],
  "loaded_model": "v2-medium-256",
  "model_info": [
    {"v2-medium-256": {"speed": 5, "accuracy": 3, "frame_resolution": 256, "size_gb": 0.6, "compatible_os": ["mac-arm", "mac-x86", "windows"], "is_multilingual": false}}
  ],
  "current_system": "mac-arm"
}
```

`model_info` is an array of single-key objects, where each key is a `model_key` from `supported_models`. `current_system` is one of `mac-arm`, `mac-x86`, `windows`, or `linux`.

#### `POST /models/load`

Switches to a different visual model. The model must already be downloaded. This clears loaded analysis data from memory — you'll need to reload it afterwards.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/models/load \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model_key": "v2-large-384"}'
```

**Response:**

```json theme={null}
{
  "message": "Model changed from V2 Medium to V2 Large high-res in 3.24s",
  "loaded_model": "v2-large-384"
}
```

| Status | Meaning                                                                 |
| ------ | ----------------------------------------------------------------------- |
| `200`  | Model switched, already active, or still loading from a previous switch |
| `400`  | Model key invalid or not downloaded                                     |
| `500`  | Loading failed                                                          |

***

### Media Metadata

#### `POST /media/metadata`

Returns file properties (duration, FPS, timecode) and analysis status for each file. This is the way to check which files have been analyzed and to get their `hash_str` identifiers.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/media/metadata \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": [
      "/Videos/interview.mp4",
      "/Videos/photo.jpg",
      "/Audio/voiceover.mp3"
    ]
  }'
```

**Response:**

```json theme={null}
{
  "media_properties": {
    "/Videos/interview.mp4": {
      "video_cached": true,
      "audio_cached": true,
      "runtime": "00:02:30",
      "fps": 30,
      "media_path": "/Videos/interview.mp4",
      "timecode": "00:00:00:00",
      "hash_str": "1e09e4953de0471b"
    },
    "/Videos/photo.jpg": {
      "video_cached": true,
      "audio_cached": false,
      "runtime": "00:00:01",
      "fps": 0,
      "media_path": "/Videos/photo.jpg",
      "timecode": "00:00:00:00",
      "hash_str": "fba6b4a2d2cf672c"
    },
    "/Audio/voiceover.mp3": {
      "video_cached": false,
      "audio_cached": true,
      "runtime": "00:00:03",
      "fps": 0,
      "media_path": "/Audio/voiceover.mp3",
      "timecode": "00:00:00:00",
      "hash_str": "485535a3914b2d02"
    }
  }
}
```

**Fields explained:**

* `video_cached` — `true` if visual analysis data exists (for videos/images)
* `audio_cached` — `true` if a transcription exists (for videos/audio)
* `hash_str` — unique identifier for this file, used internally and in other endpoints

***

### Analysis

#### `POST /analyze`

Starts analyzing media files. This is asynchronous — it returns immediately with a `task_id`, and the actual work runs in the background.

You can combine visual analysis, transcription, and face clustering in one call.

Batching matters: one request with 50 files is much faster than 50 one-file requests on the same backend, because Jumper loads the ML models per analysis request.

Only one analysis task can run at a time per backend instance. A new `/analyze` request first tries to stop any current task. If the previous task is still unwinding, the new request returns `409`.

**Example request — visual + transcription + face clustering:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analyze \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "detect_speakers": true,
    "speaker_count": "auto",
    "preferred_qwen_model_key": "more accurate",
    "visual_media_paths": [
      "/Videos/interview.mp4",
      "/Videos/photo.jpg"
    ],
    "transcription_jobs": [
      {"path": "/Videos/interview.mp4", "language": "english", "channels": [1, 3]},
      {"path": "/Audio/voiceover.mp3", "language": "english"}
    ],
    "face_clustering_jobs": [
      {
        "cluster_job_name": "Interview Faces",
        "face_media_paths": [
          "/Videos/interview.mp4"
        ],
        "face_eps": 0.48,
        "face_min_samples": 5
      }
    ]
}'
```

`detect_speakers` is optional and defaults to `true`. Set it to `false` to skip final speaker diarization for the speech batch. `speaker_count` is optional and defaults to `"auto"`; accepted values are `"auto"`, `1`, `2`, `3`, `4`, and `"5+"`. For pyannote, `1`-`4` becomes `num_speakers`, while `"5+"` becomes `min_speakers=5, max_speakers=12`. `preferred_qwen_model_key` is optional and applies to Qwen-supported languages; use `"faster"` for the 0.6B model, `"more accurate"` for the 1.7B model, or `"auto"`/omit it for Jumper's default. `detect_speakers`, `speaker_count`, and `preferred_qwen_model_key` can also be supplied on individual `transcription_jobs` to override the batch setting.

**Example request — with face clustering:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analyze \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "visual_media_paths": ["/Videos/interview.mp4"],
    "transcription_jobs": [],
    "face_clustering_jobs": [
      {
        "cluster_job_name": "Interview Faces",
        "face_media_paths": ["/Videos/interview.mp4"],
        "face_eps": 0.48,
        "face_min_samples": 5
      }
    ]
  }'
```

**Response (202):**

```json theme={null}
{
  "message": "Analysis started",
  "task_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

**Tracking progress:** Connect to the SocketIO server at `http://localhost:6699`, emit `join` with the returned `task_id`, then listen for `progress` events:

```json theme={null}
{
  "progress": 45.5,
  "video_path": "/Videos/interview.mp4",
  "done": false,
  "type": "video"
}
```

| Status | Meaning                                                   |
| ------ | --------------------------------------------------------- |
| `202`  | Analysis started                                          |
| `400`  | No valid media files supplied                             |
| `409`  | Another analysis task is still stopping or already active |
| `503`  | Models still loading, try again shortly                   |

**Transcription job fields:**

| Field                      | Type                                  | Default        | Description                                                                                                                                                                                                                                                                                                     |
| -------------------------- | ------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`                     | string                                | required       | Video or audio file to transcribe                                                                                                                                                                                                                                                                               |
| `language`                 | string                                | `english`      | Spoken language hint                                                                                                                                                                                                                                                                                            |
| `channels`                 | integer array, `null`, or empty array | legacy downmix | Omit, set to `null`, or pass `[]` to use Jumper's legacy downmix-to-mono extraction. Pass a non-empty array to explicitly transcribe 1-based audio channel/track numbers; `[1]` means only channel 1. Multiple selected channels are analyzed separately, then merged with conservative near-duplicate removal. |
| `preferred_qwen_model_key` | string                                | batch setting  | Qwen model preference for Qwen-supported languages. Use `"faster"`, `"more accurate"`, or `"auto"`.                                                                                                                                                                                                             |

#### `POST /analyze/cancel`

Cancels any running analysis task.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analyze/cancel \
  -H "X-License-Key: YOUR_KEY"
```

**Response:**

```json theme={null}
{"message": "Cancellation requested"}
```

***

### Analysis Data Management

Before you can search, the analysis data needs to be loaded into memory. There are two approaches:

#### `POST /analysis-data/load`

Loads **all** analysis data from the folder into memory — visual embeddings, people metadata, and transcriptions. Best when you want to search across everything.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analysis-data/load \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cache_dir": "/Users/me/JumperAnalysis"}'
```

**Response:**

```json theme={null}
{"message": "Analysis data loaded in 0.45s"}
```

#### `POST /analysis-data/load-for-media`

Loads visual analysis data for **specific video/image files** only. More efficient when working with a subset. Skips files already in memory.

This endpoint does not load transcriptions. Use `/analysis-data/load` to load everything, or `/analysis-data/load-transcriptions` for transcript-only workflows.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analysis-data/load-for-media \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": ["/Videos/interview.mp4"]
  }'
```

**Response:**

```json theme={null}
{"message": "Loaded analysis data for 1 files in 0.12s"}
```

#### `POST /analysis-data/load-transcriptions`

Loads transcription data into memory separately. This is useful if you only need transcript search and don't want to load visual analysis data. Transcriptions are also loaded automatically by `/analysis-data/load`.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/analysis-data/load-transcriptions \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cache_dir": "/Users/me/JumperAnalysis"}'
```

**Response:**

```json theme={null}
{"message": "Loaded 123 transcriptions in 0.09s"}
```

***

### Search

All search endpoints require analysis data to be loaded into memory first (see above).

The visual search endpoints (`/search/text`, `/search/image`, `/search/frame`) return the same match structure and order results best-first. Jumper does not return similarity scores in the response payload. The optional `exclude` field is a soft ranking signal, not a hard filter.

#### `POST /search/text`

Semantic visual search — describe what you're looking for in plain language.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/search/text \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "dog on a beach",
    "exclude": ["water", "swimming pool"],
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": ["/Videos/interview.mp4"],
    "max_results": 10
  }'
```

**Response:**

```json theme={null}
{
  "matches": [
    {
      "frame_idx": "76",
      "timestamp": "00:01:16",
      "image": "/9j/4AAQSkZJRgABAQAAAQABAAD...",
      "scene_start_timestamp": "00:01:10",
      "scene_end_timestamp": "00:01:19",
      "original_index": 0,
      "hash_str": "1e09e4953de0471b",
      "video_path": "/Videos/interview.mp4"
    },
    {
      "frame_idx": "107",
      "timestamp": "00:01:47",
      "image": "/9j/4AAQSkZJRgABAQAAAQABAAD...",
      "scene_start_timestamp": "00:01:43",
      "scene_end_timestamp": "00:01:52",
      "original_index": 1,
      "hash_str": "1e09e4953de0471b",
      "video_path": "/Videos/interview.mp4"
    }
  ]
}
```

`frame_idx` is the frame number on the 1-FPS embedding grid, returned as a string. `original_index` is the result's position in the underlying ranking before per-video grouping.

**Parameters:**

| Parameter            | Type      | Default  | Description                                                                                |
| -------------------- | --------- | -------- | ------------------------------------------------------------------------------------------ |
| `query`              | string    | required | What you're looking for                                                                    |
| `cache_dir`          | string    | required | Analysis data folder                                                                       |
| `media_paths`        | string\[] | `[]`     | Restrict to these files                                                                    |
| `max_results`        | int       | 50       | Maximum matches to return                                                                  |
| `search_all`         | bool      | false    | Search all loaded media (ignores `media_paths`)                                            |
| `text_weight`        | number    | 1        | Text similarity weight (advanced)                                                          |
| `exclude`            | string\[] | `[]`     | Visual concepts to softly push down in the ranking                                         |
| `people_filter`      | object\[] | `[]`     | Only show results containing specific people                                               |
| `people_filter_mode` | string    | `and`    | `"and"` requires all selected people in the same frame; `"or"` matches any selected person |

**Filtering by people:**

If you've run face clustering, you can restrict results to frames containing specific people:

```json theme={null}
{
  "query": "waving hands",
  "cache_dir": "/Users/me/JumperAnalysis",
  "media_paths": ["/Videos/interview.mp4"],
  "people_filter_mode": "and",
  "people_filter": [
    {"cluster_job_name": "Interview Faces", "person_name": "Anna"},
    {"cluster_job_name": "Interview Faces", "person_name": "David"}
  ]
}
```

#### `POST /search/image`

Find moments visually similar to a reference image. Optionally combine with a text query.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/search/image \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image_path": "/Images/reference.jpg",
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": ["/Videos/interview.mp4"],
    "max_results": 10
  }'
```

**Parameters:**

| Parameter     | Type      | Default  | Description                                                           |
| ------------- | --------- | -------- | --------------------------------------------------------------------- |
| `image_path`  | string    | required | Path to the reference image                                           |
| `query`       | string    | optional | Text query to combine with the image                                  |
| `cache_dir`   | string    | required | Analysis data folder                                                  |
| `media_paths` | string\[] | `[]`     | Restrict to these files                                               |
| `max_results` | int       | 50       | Maximum matches to return                                             |
| `search_all`  | bool      | false    | Search all loaded media                                               |
| `exclude`     | string\[] | `[]`     | Visual concepts to softly push down in the ranking. Requires `query`. |

#### `POST /search/frame`

Find moments similar to a specific frame in a video — a "find more like this" search.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/search/frame \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "media_path": "/Videos/interview.mp4",
    "time_seconds": 38,
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": ["/Videos/interview.mp4"],
    "max_results": 10
  }'
```

**Parameters:**

| Parameter      | Type      | Default  | Description                                                           |
| -------------- | --------- | -------- | --------------------------------------------------------------------- |
| `media_path`   | string    | required | Video to extract the reference frame from                             |
| `time_seconds` | number    | required | Timestamp in seconds                                                  |
| `query`        | string    | optional | Text query to combine with the frame                                  |
| `cache_dir`    | string    | required | Analysis data folder                                                  |
| `media_paths`  | string\[] | `[]`     | Restrict to these files                                               |
| `max_results`  | int       | 50       | Maximum matches to return                                             |
| `search_all`   | bool      | false    | Search all loaded media                                               |
| `exclude`      | string\[] | `[]`     | Visual concepts to softly push down in the ranking. Requires `query`. |

#### `POST /search/transcript`

Search through loaded transcriptions for segments containing a query string. Uses case-insensitive substring matching. To search only within one named transcript speaker, pass `speaker` or `speaker_name`.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/search/transcript \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "hello world",
    "cache_dir": "/Users/me/JumperAnalysis",
    "speaker_name": "Anna",
    "max_results": 10
  }'
```

**Response:**

```json theme={null}
{
  "matches": [
    {
      "media_path": "/Videos/interview.mp4",
      "hash_str": "1e09e4953de0471b",
      "start_seconds": 12.5,
      "end_seconds": 15.2,
      "text": "And she said hello world to the audience",
      "start_timestamp": "00:00:12",
      "end_timestamp": "00:00:15",
      "speaker": "SPEAKER_00",
      "speaker_name": "Anna"
    }
  ]
}
```

Speaker filtering uses transcript speaker display names stored in transcription metadata. It is separate from face-clustering people names and does not use `people_filter`.

**Parameters:**

| Parameter      | Type      | Default  | Description                                           |
| -------------- | --------- | -------- | ----------------------------------------------------- |
| `query`        | string    | required | Text to search for (case-insensitive substring match) |
| `cache_dir`    | string    | required | Analysis data folder                                  |
| `media_paths`  | string\[] | `[]`     | Restrict to these files                               |
| `max_results`  | int       | 50       | Maximum matches to return                             |
| `search_all`   | bool      | true     | Search across all loaded transcriptions               |
| `speaker`      | string    | optional | Restrict results to a named transcript speaker        |
| `speaker_name` | string    | optional | Alias for `speaker`                                   |

***

### Transcriptions

#### `POST /transcriptions`

Returns speech transcriptions for media files that have been transcribed via `/analyze`.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/transcriptions \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_paths": ["/Audio/voiceover.mp3", "/Videos/interview.mp4"]
  }'
```

**Response:**

```json theme={null}
{
  "transcriptions": {
    "/Audio/voiceover.mp3": [
      [0.0, " One, two, three, testing.", 2.52, "/Audio/voiceover.mp3"]
    ],
    "/Videos/interview.mp4": [
      [0.0, " Welcome to today's discussion.", 3.84, "/Videos/interview.mp4"],
      [3.84, " We'll be talking about...", 7.12, "/Videos/interview.mp4"]
    ]
  }
}
```

Each segment is an array: `[start_seconds, text, end_seconds, media_path]`

***

### Thumbnails

#### `POST /thumbnails`

Get base64-encoded JPEG thumbnail images for specific media/timestamp pairs.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/thumbnails \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "requests": [
      {"media_path": "/Videos/interview.mp4", "time_seconds": 38},
      {"media_path": "/Videos/interview.mp4", "time_seconds": 76}
    ]
  }'
```

**Response:**

```json theme={null}
{
  "thumbnails": [
    {"thumbnail": "/9j/4AAQSkZJRg..."},
    {"thumbnail": "/9j/4AAQSkZJRg..."}
  ]
}
```

Each `thumbnail` is a base64-encoded JPEG string, or `null` for audio files.

#### `POST /thumbnails/scene`

Get a strip of thumbnails spanning a time range. Useful for timeline scrubbers.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/thumbnails/scene \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_path": "/Videos/interview.mp4",
    "start_time": "00:01:00",
    "end_time": "00:02:00",
    "hash_str": "1e09e4953de0471b"
  }'
```

**Response:**

```json theme={null}
{
  "scene_thumbnails": [
    ["/9j/4AAQSkZJRg...", "00:01:00"],
    ["/9j/4AAQSkZJRg...", "00:01:01"],
    ["/9j/4AAQSkZJRg...", "00:01:02"]
  ]
}
```

Each entry is `[base64_jpeg, timestamp]`. For ranges longer than 100 seconds, Jumper samples 100 evenly-spaced frames instead of one per second.

***

### Face Clustering

Jumper detects faces across video frames and automatically groups them by identity. These endpoints let you inspect, name, and refine those groups.

#### `GET /faces/clusters`

Lists all clustering jobs and their statistics.

**Example request:**

```bash theme={null}
curl -H "X-License-Key: YOUR_KEY" \
  "http://localhost:6699/api/v1/faces/clusters?cache_dir=/Users/me/JumperAnalysis"
```

**Response:**

```json theme={null}
{
  "cluster_jobs": ["Interview Faces"],
  "jobs": [
    {
      "name": "Interview Faces",
      "total_entries": 206,
      "num_clusters": 2,
      "noise_entries": 3,
      "cluster_ids": ["a1b2c3d4-...", "e5f6a7b8-..."],
      "face_storage_version": 2,
      "media_hashes": ["1e09e4953de0471b"]
    }
  ]
}
```

`face_storage_version` reports the on-disk face storage format for the job. Newer jobs use the packed format; older legacy jobs may not be mutable for reclustering or merges.

#### `POST /faces/clusters/samples`

Get sample face thumbnail images for each cluster. Useful for building a "who is this?" UI.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/faces/clusters/samples \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "cluster_job_name": "Interview Faces",
    "limit_per_cluster": 4,
    "max_clusters": 50
  }'
```

**Response:**

```json theme={null}
{
  "cluster_job_name": "Interview Faces",
  "num_clusters": 2,
  "num_clusters_after_filter": 2,
  "total_entries": 206,
  "clusters": [
    {
      "cluster_id": "a1b2c3d4-...",
      "name": "Rodrigo",
      "size": 120,
      "sample_faces": ["/9j/4AAQ...", "/9j/4AAQ...", "/9j/4AAQ...", "/9j/4AAQ..."]
    },
    {
      "cluster_id": "e5f6a7b8-...",
      "name": "Carlos",
      "size": 83,
      "sample_faces": ["/9j/4AAQ...", "/9j/4AAQ...", "/9j/4AAQ...", "/9j/4AAQ..."]
    }
  ]
}
```

**Parameters:**

| Parameter           | Type   | Default    | Description                          |
| ------------------- | ------ | ---------- | ------------------------------------ |
| `cache_dir`         | string | required   | Analysis data folder                 |
| `cluster_job_name`  | string | required   | Which clustering job to query        |
| `limit_per_cluster` | int    | 12         | Max face thumbnails per cluster      |
| `min_cluster_size`  | int    | 1          | Skip clusters smaller than this      |
| `max_cluster_size`  | int    | 1000000000 | Skip clusters larger than this       |
| `max_clusters`      | int    | 100        | Maximum clusters to return           |
| `include_noise`     | bool   | false      | Include unassigned faces in response |

#### `POST /faces/clusters/faces`

Get paginated face images for specific cluster(s). For browsing all faces in a cluster.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/faces/clusters/faces \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "cluster_job_name": "Interview Faces",
    "cluster_id": "a1b2c3d4-...",
    "limit": 100,
    "offset": 0
  }'
```

**Parameters:**

| Parameter     | Type      | Default | Description                                |
| ------------- | --------- | ------- | ------------------------------------------ |
| `cluster_id`  | string    | -       | Single cluster to query                    |
| `cluster_ids` | string\[] | -       | Multiple clusters (overrides `cluster_id`) |
| `limit`       | int       | 500     | Page size (max 2000)                       |
| `offset`      | int       | 0       | Skip this many entries                     |

#### `PUT /faces/clusters/names`

Assign human-readable names to clusters.

**Example request:**

```bash theme={null}
curl -X PUT http://localhost:6699/api/v1/faces/clusters/names \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "cluster_job_name": "Interview Faces",
    "assignments": [
      {"cluster_id": "a1b2c3d4-...", "name": "Rodrigo"},
      {"cluster_id": "e5f6a7b8-...", "name": "Carlos"}
    ]
  }'
```

**Response:**

```json theme={null}
{
  "updated": 2,
  "changes": {
    "a1b2c3d4-...": "Rodrigo",
    "e5f6a7b8-...": "Carlos"
  }
}
```

#### `POST /faces/recluster`

Re-runs face clustering with different parameters. Useful for tuning how aggressively faces are grouped. Runs asynchronously.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/faces/recluster \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "cluster_job_name": "Interview Faces",
    "eps": 0.55,
    "min_samples": 3
  }'
```

**Response (202):**

```json theme={null}
{
  "message": "Re-clustering started",
  "task_id": "f1e2d3c4-..."
}
```

**Parameters:**

| Parameter            | Type  | Default | Description                                                                    |
| -------------------- | ----- | ------- | ------------------------------------------------------------------------------ |
| `eps`                | float | 0.48    | Clustering sensitivity — lower means stricter grouping (fewer, purer clusters) |
| `min_samples`        | int   | 5       | Minimum faces needed to form a cluster                                         |
| `auto_detect_params` | bool  | false   | Let Jumper pick eps/min\_samples automatically                                 |
| `clear_names`        | bool  | false   | Clear all existing cluster names                                               |

#### `POST /faces/clusters/modify`

Merge clusters or move individual faces between clusters. Useful for correcting mistakes.

**Example — merge two clusters:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/faces/clusters/modify \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "cluster_job_name": "Interview Faces",
    "merges": [
      {
        "cluster_ids": ["small-cluster-id", "target-cluster-id"],
        "target_cluster_id": "target-cluster-id"
      }
    ],
    "moves": []
  }'
```

***

### Watch Folders

Watch folders let Jumper automatically analyze new media files as they appear in a directory.

#### `GET /watch-folders`

Lists all configured watch folders and the background service status.

```bash theme={null}
curl -H "X-License-Key: YOUR_KEY" http://localhost:6699/api/v1/watch-folders
```

**Response:**

```json theme={null}
{
  "watch_folders": [
    {
      "id": "wf-abc123",
      "folder_path": "/Volumes/Media/Ingest",
      "enabled": true,
      "enable_visual_analysis": true,
      "enable_audio_analysis": true,
      "enable_face_analysis": false,
      "audio_language": "english",
      "cluster_job_name": null,
      "face_eps": 0.48,
      "face_min_samples": 5,
      "cache_dir": "/Users/me/JumperAnalysis",
      "excluded_extensions": [".jpg", ".jpeg"],
      "excluded_filename_globs": ["*_a04.mxf"],
      "excluded_folder_globs": ["finished", "client_a/finished_*"],
      "created_at": 1707840000,
      "last_poll_time": 1707840060,
      "files_analyzed_count": 24
    }
  ],
  "service_status": {
    "state": "running",
    "current_folder": null,
    "current_file": null,
    "files_pending": 0,
    "files_processed_this_session": 24,
    "last_error": null,
    "paused_until": null,
    "folder_stats": {}
  }
}
```

#### `POST /watch-folders`

Add a new watch folder. The folder must exist on disk.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/watch-folders \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "folder_path": "/Volumes/Media/Ingest",
    "cache_dir": "/Users/me/JumperAnalysis",
    "enabled": true,
    "enable_visual_analysis": true,
    "enable_audio_analysis": true,
    "audio_language": "english",
    "excluded_extensions": [".jpg", ".jpeg"],
    "excluded_filename_globs": ["*_a04.mxf"],
    "excluded_folder_globs": ["finished", "client_a/finished_*"]
  }'
```

**Response (201):**

```json theme={null}
{
  "message": "Watch folder added: /Volumes/Media/Ingest",
  "watch_folder": { ... }
}
```

**Parameters:**

| Parameter                 | Type           | Default   | Description                                                                       |
| ------------------------- | -------------- | --------- | --------------------------------------------------------------------------------- |
| `folder_path`             | string         | required  | Absolute path to watch                                                            |
| `cache_dir`               | string         | optional  | Where to store analysis data                                                      |
| `enabled`                 | bool           | true      | Start watching immediately                                                        |
| `enable_visual_analysis`  | bool           | true      | Analyze visual content for searching                                              |
| `enable_audio_analysis`   | bool           | false     | Transcribe audio                                                                  |
| `enable_face_analysis`    | bool           | false     | Detect and cluster faces                                                          |
| `audio_language`          | string         | "english" | Language hint for transcription                                                   |
| `cluster_job_name`        | string         | -         | Required if face analysis is enabled                                              |
| `face_eps`                | float          | 0.48      | Clustering sensitivity for face grouping                                          |
| `face_min_samples`        | int            | 5         | Minimum faces needed to form a cluster                                            |
| `excluded_extensions`     | array\[string] | `[]`      | File extensions to skip; normalized to lowercase with a leading dot               |
| `excluded_filename_globs` | array\[string] | `[]`      | Filename-only glob patterns to skip, matched case-insensitively                   |
| `excluded_folder_globs`   | array\[string] | `[]`      | Folder glob patterns to skip, matched case-insensitively under the watched folder |

`excluded_filename_globs` apply to the basename only, not the relative or absolute path.
`excluded_folder_globs` entries without a path separator match any folder name; for example, `finished` skips files inside any folder named `finished` or `Finished`. Entries with `/` or `\` match relative folder paths under the watched folder; for example, `client_a/finished_*` skips matching folders under `client_a` only. Backslashes are normalized to `/`. Path patterns match segment by segment, so `*/folder2/finished` matches one parent folder above `folder2`, while `folder2/finished` matches only from the watch-folder root.

#### `PUT /watch-folders/{id}`

Update settings for an existing watch folder. Only include the fields you want to change.

```bash theme={null}
curl -X PUT http://localhost:6699/api/v1/watch-folders/wf-abc123 \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false, "excluded_extensions": [], "excluded_filename_globs": ["*_proxy.mov"], "excluded_folder_globs": ["finished", "client_a/finished_*"]}'
```

#### `DELETE /watch-folders/{id}`

Remove a watch folder. Does not delete any analysis data.

```bash theme={null}
curl -X DELETE http://localhost:6699/api/v1/watch-folders/wf-abc123 \
  -H "X-License-Key: YOUR_KEY"
```

#### `POST /watch-folders/service/start`

Start the background service that monitors watch folders. This uses the enabled watch-folder configuration already stored in settings; it does not require prior UI interaction in the current session.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/watch-folders/service/start \
  -H "X-License-Key: YOUR_KEY"
```

#### `POST /watch-folders/service/stop`

Stop the background service.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/watch-folders/service/stop \
  -H "X-License-Key: YOUR_KEY"
```

#### `GET /watch-folders/service/status`

Check if the service is running.

```bash theme={null}
curl -H "X-License-Key: YOUR_KEY" \
  http://localhost:6699/api/v1/watch-folders/service/status
```

**Response:**

```json theme={null}
{
  "state": "running",
  "current_folder": null,
  "current_file": null,
  "files_pending": 0,
  "files_processed_this_session": 24,
  "last_error": null,
  "paused_until": null,
  "folder_stats": {}
}
```

***

### Cache Paths

#### `POST /cache-paths`

Get the visual and audio cache folder paths for a specific media file. Useful for inspecting or managing analysis data on disk.

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/cache-paths \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cache_dir": "/Users/me/JumperAnalysis",
    "media_path": "/Videos/interview.mp4"
  }'
```

**Response:**

```json theme={null}
{
  "video_cache_path": "/Users/me/JumperAnalysis/1e09e4953de0471b",
  "audio_cache_path": "/Users/me/JumperAnalysis/1e09e4953de0471b_audio",
  "hash_str": "1e09e4953de0471b"
}
```

Paths are `null` if the corresponding analysis hasn't been run yet.

***

### Export

#### `POST /export/clips`

Export trimmed video clips to a folder using ffmpeg.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/export/clips \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "clips": [
      {
        "source_path": "/Videos/interview.mp4",
        "start_seconds": 10,
        "end_seconds": 25
      },
      {
        "source_path": "/Videos/interview.mp4",
        "start_seconds": 60,
        "end_seconds": 90,
        "subfolder": "highlights"
      }
    ],
    "output_dir": "/Users/me/Exports",
    "copy_codec": true
  }'
```

**Response:**

```json theme={null}
{
  "results": [
    {"success": true, "output_path": "/Users/me/Exports/interview_10-25.mp4"},
    {"success": true, "output_path": "/Users/me/Exports/highlights/interview_60-90.mp4"}
  ],
  "summary": "2/2 clips exported successfully"
}
```

**Parameters:**

| Parameter               | Type      | Default  | Description                      |
| ----------------------- | --------- | -------- | -------------------------------- |
| `clips`                 | object\[] | required | Array of clip definitions        |
| `clips[].source_path`   | string    | required | Source media file path           |
| `clips[].start_seconds` | number    | required | Start time in seconds            |
| `clips[].end_seconds`   | number    | required | End time in seconds              |
| `clips[].subfolder`     | string    | optional | Subfolder within output\_dir     |
| `output_dir`            | string    | required | Directory to write clips to      |
| `copy_codec`            | bool      | true     | Stream copy (fast, no re-encode) |

#### `POST /export/premiere-xml`

Generate a Premiere Pro compatible XML sequence file (XMEML v4). Can be imported into Premiere Pro, DaVinci Resolve, Avid, and other NLEs.

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/export/premiere-xml \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sequence_name": "Interview Highlights",
    "clips": [
      {"source_path": "/Videos/interview.mp4", "start_seconds": 10, "end_seconds": 25},
      {"source_path": "/Videos/interview.mp4", "start_seconds": 60, "end_seconds": 90}
    ],
    "output_path": "/Users/me/Exports/highlights.xml",
    "timebase": 24
  }'
```

**Response:**

```json theme={null}
{
  "output_path": "/Users/me/Exports/highlights.xml",
  "clip_count": 2,
  "total_duration_seconds": 45.0
}
```

**Parameters:**

| Parameter               | Type      | Default             | Description                           |
| ----------------------- | --------- | ------------------- | ------------------------------------- |
| `sequence_name`         | string    | "Untitled Sequence" | Name of the sequence in the NLE       |
| `clips`                 | object\[] | required            | Array of clip definitions             |
| `clips[].source_path`   | string    | required            | Source media file path                |
| `clips[].start_seconds` | number    | required            | Start time in seconds                 |
| `clips[].end_seconds`   | number    | required            | End time in seconds                   |
| `output_path`           | string    | required            | Where to write the XML file           |
| `timebase`              | int       | 24                  | Sequence frame rate (e.g. 24, 25, 30) |

#### `POST /export/transcript`

Export transcript segments to a file (TXT, CSV, DOCX, or PDF).

**Example request:**

```bash theme={null}
curl -X POST http://localhost:6699/api/v1/export/transcript \
  -H "X-License-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "segments": [
      [0.32, "Hello world.", 5.84],
      [5.84, "Another line.", 11.20]
    ],
    "format": "txt",
    "display_name": "My Transcript",
    "output_path": "/Users/me/Exports/transcript.txt"
  }'
```

**Response:**

```json theme={null}
{
  "output_path": "/Users/me/Exports/transcript.txt",
  "format": "txt",
  "row_count": 2
}
```

**Parameters:**

| Parameter          | Type   | Default                             | Description                                         |
| ------------------ | ------ | ----------------------------------- | --------------------------------------------------- |
| `segments`         | array  | required                            | List of `[start_seconds, text, end_seconds]` arrays |
| `format`           | string | `"txt"`                             | Export format: `txt`, `csv`, `docx`, `pdf`          |
| `display_name`     | string | `"Transcript"`                      | Name shown in header/filename                       |
| `output_path`      | string | `~/Desktop/{display_name}.{format}` | Destination file path                               |
| `include_silences` | bool   | `true`                              | Include silence gap rows between segments           |

***

## Error Format

All errors follow the same structure:

```json theme={null}
{"error": "Description of what went wrong"}
```

Common status codes:

| Code  | Meaning                                     |
| ----- | ------------------------------------------- |
| `400` | Bad request (missing or invalid parameters) |
| `401` | No license key provided                     |
| `403` | Not a Pro license                           |
| `404` | Resource not found (e.g. watch folder ID)   |
| `500` | Server error                                |
| `503` | Models still loading — retry shortly        |

***

## SocketIO Progress Tracking

For long-running operations (`/analyze`, `/faces/recluster`), connect to the SocketIO server at `http://localhost:6699` and join the room matching the returned `task_id`.

In Socket.IO terms, emit `join` with the returned `task_id`, then listen for `progress`.

**Event: `progress`**

```json theme={null}
{
  "progress": 45.5,
  "video_path": "/Videos/interview.mp4",
  "done": false,
  "type": "video"
}
```

Common fields:

* `progress` — numeric percentage
* `video_path` — the file currently being processed, or `null` for some clustering updates
* `done` — `true` when the task is complete
* `type` — `"video"` for visual analysis, `"speech"` for transcription
* `is_cluster_job` — present for face-clustering progress
* `cluster_job_name` — present for face-clustering progress
* `cluster_media_hashes` — present on some clustering completion events
