{
  "openapi": "3.0.3",
  "info": {
    "title": "Jumper Public API",
    "version": "1.0",
    "description": "REST API for third-party integrations with Jumper's media analysis engine.\n\nJumper analyzes video, image, and audio files \u2014 enabling semantic visual search,\nspeech transcription, and face detection with clustering.\nThis API exposes those capabilities for use by MAM systems, automation pipelines,\nand custom tooling.\n\n## Authentication\n\nAll endpoints except `/health` require a **Jumper Pro** license key.\nThe OpenAPI contract models the supported `X-License-Key` header flow.\n\nJumper also accepts a `license_key` field in many JSON POST bodies as a\nconvenience, but that is a Jumper-specific request-body convention rather\nthan an OpenAPI security scheme.\n\nInvalid or missing keys return `401`. Non-Pro keys return `403`.\n\n## Key Concepts\n\n- **`cache_dir`** \u2014 The folder where Jumper stores analysis data (visual analysis, transcriptions, face clusters). You choose this path.\n- **`media_paths`** \u2014 Absolute filesystem paths to media files. Jumper needs direct access to these files.\n- **`hash_str`** \u2014 A CRC32-based hash that uniquely identifies a media file. Returned by the metadata endpoint.\n- **`model_key`** \u2014 Internal identifier for a visual or speech analysis model variant (e.g. `v2-medium-256`).\n",
    "contact": {
      "name": "Jumper",
      "url": "https://getjumper.io"
    }
  },
  "servers": [
    {
      "url": "http://localhost:6699/api/v1",
      "description": "Local Jumper backend"
    }
  ],
  "security": [
    {
      "HeaderAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "HeaderAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-License-Key",
        "description": "Jumper Pro license key passed via header"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message"
          }
        },
        "required": [
          "error"
        ]
      },
      "MediaProperties": {
        "type": "object",
        "additionalProperties": {
          "type": "object",
          "properties": {
            "video_cached": {
              "type": "boolean",
              "description": "Whether visual analysis data exists for this file"
            },
            "audio_cached": {
              "type": "boolean",
              "description": "Whether transcription data exists for this file"
            },
            "runtime": {
              "type": "string",
              "nullable": true,
              "description": "Duration as HH:MM:SS",
              "example": "00:02:30"
            },
            "fps": {
              "type": "number",
              "nullable": true,
              "description": "Frames per second (0 for images and audio)"
            },
            "media_path": {
              "type": "string",
              "description": "Absolute path to the file"
            },
            "timecode": {
              "type": "string",
              "description": "Start timecode (HH:MM:SS:FF)",
              "example": "00:00:00:00"
            },
            "hash_str": {
              "type": "string",
              "description": "Unique hash identifying this media file",
              "example": "1e09e4953de0471b"
            }
          }
        }
      },
      "SearchMatch": {
        "type": "object",
        "properties": {
          "frame_idx": {
            "type": "string",
            "description": "Frame number within the media file (1 FPS basis), returned as a string"
          },
          "timestamp": {
            "type": "string",
            "description": "Timestamp as HH:MM:SS"
          },
          "image": {
            "type": "string",
            "format": "byte",
            "description": "Base64-encoded JPEG preview for the matched frame"
          },
          "scene_start_timestamp": {
            "type": "string",
            "description": "Start of the matched scene as HH:MM:SS"
          },
          "scene_end_timestamp": {
            "type": "string",
            "description": "End of the matched scene as HH:MM:SS"
          },
          "original_index": {
            "type": "integer",
            "description": "Position in the underlying ranking before per-video grouping"
          },
          "hash_str": {
            "type": "string",
            "description": "Hash of the media file this match belongs to"
          },
          "video_path": {
            "type": "string",
            "description": "Absolute path to the media file"
          }
        },
        "required": [
          "frame_idx",
          "timestamp",
          "image",
          "scene_start_timestamp",
          "scene_end_timestamp",
          "original_index",
          "hash_str",
          "video_path"
        ]
      },
      "TranscriptionSegment": {
        "type": "array",
        "items": {
          "oneOf": [
            {
              "type": "number"
            },
            {
              "type": "string"
            }
          ]
        },
        "description": "[start_seconds, text, end_seconds, media_path]",
        "example": [
          0.0,
          " One, two, three, testing.",
          2.52,
          "/path/to/audio.mp3"
        ]
      },
      "MetadataFilterRequest": {
        "type": "object",
        "properties": {
          "scope": {
            "$ref": "#/components/schemas/MetadataFilterScope"
          },
          "filters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetadataFilter"
            },
            "default": []
          }
        },
        "description": "Metadata filters to apply to a search. If scope is omitted, Jumper derives it\nfrom the endpoint's `search_all` and `media_paths` values.\n"
      },
      "MetadataFilterScope": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "paths",
              "global"
            ],
            "default": "paths",
            "description": "`paths` scopes filters to the supplied files; `global` scopes to loaded searchable media."
          },
          "media_type": {
            "type": "string",
            "enum": [
              "visual",
              "speech",
              "any"
            ],
            "default": "any",
            "description": "Searchable media type for the scope."
          },
          "media_paths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Absolute media paths. When paired with `hash_strs`, the arrays are index-aligned."
          },
          "hash_strs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Media hashes aligned with `media_paths`, or standalone hashes for already-known media."
          },
          "expected_count": {
            "type": "integer",
            "description": "Optional count used for coverage reporting."
          }
        }
      },
      "MetadataFilter": {
        "type": "object",
        "required": [
          "facet_id",
          "values"
        ],
        "properties": {
          "facet_id": {
            "type": "string",
            "description": "Facet identifier returned by `/metadata/facets`, such as `video.codec`."
          },
          "operator": {
            "type": "string",
            "enum": [
              "is_any_of",
              "is_not",
              "include_any_of",
              "include_all_of",
              "exclude_any_of",
              "exclude_all_of",
              "before",
              "after",
              "greater_than",
              "less_than"
            ],
            "default": "is_any_of"
          },
          "values": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Selected facet option values."
          }
        }
      },
      "MetadataFacetResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "facets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetadataFacet"
            }
          },
          "coverage": {
            "$ref": "#/components/schemas/MetadataCoverage"
          },
          "active_filter_count": {
            "type": "integer"
          },
          "warning": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "MetadataCoverage": {
        "type": "object",
        "properties": {
          "scope_count": {
            "type": "integer",
            "description": "Number of files or hashes in the requested scope."
          },
          "indexed_count": {
            "type": "integer",
            "description": "Number of scoped files that currently have indexed metadata."
          },
          "missing_count": {
            "type": "integer",
            "description": "Number of scoped files without indexed metadata."
          }
        }
      },
      "MetadataFacet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "video.codec"
          },
          "label": {
            "type": "string",
            "example": "Codec"
          },
          "group": {
            "type": "string",
            "example": "Video"
          },
          "path": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "kind": {
            "type": "string",
            "enum": [
              "single",
              "multi",
              "numeric",
              "date"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetadataFacetOption"
            }
          },
          "min": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              }
            ]
          },
          "max": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              }
            ]
          }
        }
      },
      "MetadataFacetOption": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "meta": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "ClusterJob": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the clustering job"
          },
          "total_entries": {
            "type": "integer",
            "description": "Total detected faces"
          },
          "num_clusters": {
            "type": "integer",
            "description": "Number of clusters found"
          },
          "noise_entries": {
            "type": "integer",
            "description": "Faces not assigned to any cluster"
          },
          "cluster_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "List of cluster IDs"
          },
          "face_storage_version": {
            "type": "integer",
            "description": "On-disk face storage format version. Legacy jobs may not be mutable."
          },
          "media_hashes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Hashes of media files included in this job"
          }
        }
      },
      "ClusterSample": {
        "type": "object",
        "properties": {
          "cluster_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "User-assigned name (empty string if unnamed)"
          },
          "size": {
            "type": "integer",
            "description": "Total faces in this cluster"
          },
          "sample_faces": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "byte"
            },
            "description": "Base64-encoded JPEG thumbnails of sample faces"
          }
        }
      },
      "WatchFolder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "folder_path": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "enable_visual_analysis": {
            "type": "boolean"
          },
          "enable_audio_analysis": {
            "type": "boolean"
          },
          "enable_face_analysis": {
            "type": "boolean"
          },
          "audio_language": {
            "type": "string"
          },
          "cluster_job_name": {
            "type": "string",
            "nullable": true
          },
          "face_eps": {
            "type": "number"
          },
          "face_min_samples": {
            "type": "integer"
          },
          "cache_dir": {
            "type": "string",
            "nullable": true
          },
          "excluded_extensions": {
            "type": "array",
            "description": "File extensions to skip, normalized to lowercase with a leading dot",
            "items": {
              "type": "string"
            }
          },
          "excluded_filename_globs": {
            "type": "array",
            "description": "Case-insensitive glob patterns matched against the filename only",
            "items": {
              "type": "string"
            }
          },
          "excluded_folder_globs": {
            "type": "array",
            "description": "Case-insensitive folder glob patterns under the watched folder. Patterns with slashes match relative folder paths segment by segment.",
            "items": {
              "type": "string"
            }
          },
          "created_at": {
            "type": "number"
          },
          "last_poll_time": {
            "type": "number"
          },
          "files_analyzed_count": {
            "type": "integer"
          }
        }
      },
      "WatchFolderServiceStatus": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "stopped",
              "running",
              "stopping",
              "paused",
              "waiting"
            ]
          },
          "current_folder": {
            "type": "string",
            "nullable": true
          },
          "current_file": {
            "type": "string",
            "nullable": true
          },
          "files_pending": {
            "type": "integer"
          },
          "files_processed_this_session": {
            "type": "integer"
          },
          "last_error": {
            "type": "string",
            "nullable": true
          },
          "paused_until": {
            "type": "number",
            "nullable": true
          },
          "folder_stats": {
            "type": "object",
            "additionalProperties": {
              "type": "object"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Returns server status. No authentication required.",
        "tags": [
          "Health"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Server is running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/models/loaded": {
      "get": {
        "summary": "Get currently loaded models",
        "description": "Returns the visual and speech analysis models that are currently loaded in memory.",
        "tags": [
          "Models"
        ],
        "responses": {
          "200": {
            "description": "Current model state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "visual": {
                      "type": "object",
                      "properties": {
                        "model_key": {
                          "type": "string",
                          "nullable": true,
                          "example": "v2-medium-256"
                        },
                        "is_loading": {
                          "type": "boolean"
                        }
                      }
                    },
                    "speech": {
                      "type": "object",
                      "properties": {
                        "model_key": {
                          "type": "string",
                          "nullable": true,
                          "example": "mlx-large-v3-turbo"
                        },
                        "is_loading": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/models/available": {
      "get": {
        "summary": "List available models",
        "description": "Returns all supported models for this platform, which ones are downloaded, and which is currently loaded.",
        "tags": [
          "Models"
        ],
        "responses": {
          "200": {
            "description": "Model information",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "supported_models": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "All model keys supported on this hardware"
                    },
                    "downloaded_models": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Model keys that are downloaded and ready to use"
                    },
                    "loaded_model": {
                      "type": "string",
                      "description": "Currently active model key"
                    },
                    "model_info": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "object"
                        }
                      },
                      "description": "Array of objects, each keyed by model_key with details (accuracy, speed, frame_resolution, etc.)"
                    },
                    "current_system": {
                      "type": "string",
                      "description": "Hardware type (e.g. \"mac-arm\", \"mac-x86\", \"windows\", \"linux\")"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/models/load": {
      "post": {
        "summary": "Switch the active visual model",
        "description": "Loads a different visual analysis model. The model must already be downloaded.\nThis clears loaded analysis data from memory \u2014 you will need to reload it afterwards.\n",
        "tags": [
          "Models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "model_key"
                ],
                "properties": {
                  "model_key": {
                    "type": "string",
                    "description": "The model key to load",
                    "example": "v2-medium-256"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Model switched (or already active, or still loading from previous switch)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Present on success or when model is already active"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present when a previous model switch is still loading"
                    },
                    "loaded_model": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid model key or model not downloaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "loaded_model": {
                      "type": "string",
                      "description": "The currently loaded model key"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Model loading failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "loaded_model": {
                      "type": "string",
                      "description": "The currently loaded model key (may be the previous model)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/media/metadata": {
      "post": {
        "summary": "Get metadata for media files",
        "description": "Returns properties (duration, FPS, timecode) and analysis status for each media file.\nUse this to check which files have been analyzed and to get their `hash_str` identifiers.\n",
        "tags": [
          "Media"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "media_paths"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string",
                    "description": "Path to the analysis data folder"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Absolute paths to media files"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Metadata keyed by file path",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media_properties": {
                      "$ref": "#/components/schemas/MediaProperties"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analyze": {
      "post": {
        "summary": "Start media analysis",
        "description": "Starts an asynchronous analysis pipeline. Returns immediately with a `task_id`\nthat can be used to track progress via SocketIO.\n\nYou can run visual analysis, speech transcription, and face clustering in a single call.\n\nFor throughput, batch many files into one request instead of sending many\nsingle-file requests. Jumper loads ML models per analysis request, so one\n50-file request is much faster than 50 one-file requests on the same node.\n\nOnly one analysis task can run at a time per backend instance. A new analyze\nrequest first asks the current task to stop. If the previous task is still\nunwinding, Jumper returns `409`.\n",
        "tags": [
          "Analysis"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string",
                    "description": "Where to store analysis results"
                  },
                  "detect_speakers": {
                    "type": "boolean",
                    "default": true,
                    "description": "Set to false to skip final speaker diarization for transcription jobs."
                  },
                  "speaker_count": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "auto",
                          "auto-detect",
                          "5+"
                        ]
                      },
                      {
                        "type": "integer",
                        "enum": [
                          1,
                          2,
                          3,
                          4
                        ]
                      }
                    ],
                    "default": "auto",
                    "description": "Optional speaker-count hint for pyannote diarization. Use auto for unconstrained detection, 1-4 for num_speakers, or 5+ for min_speakers=5 and max_speakers=12."
                  },
                  "preferred_qwen_model_key": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "faster",
                      "more accurate",
                      "qwen3-asr-0.6b",
                      "qwen3-asr-1.7b"
                    ],
                    "default": "auto",
                    "description": "Optional Qwen ASR preference for Qwen-supported languages. Use faster for the 0.6B model, more accurate for the 1.7B model, or auto/default to use Jumper's default."
                  },
                  "visual_media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Files to run visual analysis on"
                  },
                  "transcription_jobs": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "path"
                      ],
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "language": {
                          "type": "string",
                          "default": "english"
                        },
                        "detect_speakers": {
                          "type": "boolean",
                          "default": true,
                          "description": "Optional per-job override for speaker diarization."
                        },
                        "speaker_count": {
                          "oneOf": [
                            {
                              "type": "string",
                              "enum": [
                                "auto",
                                "auto-detect",
                                "5+"
                              ]
                            },
                            {
                              "type": "integer",
                              "enum": [
                                1,
                                2,
                                3,
                                4
                              ]
                            }
                          ],
                          "default": "auto",
                          "description": "Optional per-job speaker-count hint."
                        },
                        "preferred_qwen_model_key": {
                          "type": "string",
                          "enum": [
                            "auto",
                            "faster",
                            "more accurate",
                            "qwen3-asr-0.6b",
                            "qwen3-asr-1.7b"
                          ],
                          "default": "auto",
                          "description": "Optional per-job Qwen ASR preference for Qwen-supported languages."
                        },
                        "channels": {
                          "type": "array",
                          "nullable": true,
                          "items": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "default": null,
                          "description": "Omit, set to null, or pass an empty array to use 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."
                        }
                      }
                    },
                    "description": "Files to transcribe, with optional language and channel hints"
                  },
                  "face_clustering_jobs": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "cluster_job_name",
                        "face_media_paths"
                      ],
                      "properties": {
                        "cluster_job_name": {
                          "type": "string",
                          "description": "Name for this clustering job"
                        },
                        "face_media_paths": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Paths to media files for face detection"
                        },
                        "face_eps": {
                          "type": "number",
                          "description": "Clustering sensitivity (default 0.48)"
                        },
                        "face_min_samples": {
                          "type": "integer",
                          "description": "Minimum faces to form a cluster (default 5)"
                        }
                      }
                    },
                    "description": "Face detection and clustering job definitions"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Analysis started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Analysis started"
                    },
                    "task_id": {
                      "type": "string",
                      "description": "Use this ID to join a SocketIO room for progress events"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No valid media supplied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Another analysis task is still stopping or already active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Models still loading",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analyze/cancel": {
      "post": {
        "summary": "Cancel running analysis",
        "description": "Requests cancellation of any currently running analysis task.",
        "tags": [
          "Analysis"
        ],
        "responses": {
          "200": {
            "description": "Cancellation completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Cancellation requested"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Cancellation requested but task is still stopping",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Cancellation requested"
                    },
                    "still_stopping": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/analysis-data/load": {
      "post": {
        "summary": "Load all analysis data into memory",
        "description": "Loads all visual analysis data, people metadata, and transcriptions from the analysis folder\ninto memory. Must be called before searching, unless you use `/analysis-data/load-for-media`\ninstead.\n",
        "tags": [
          "Analysis Data"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data loaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Analysis data loaded in 0.45s"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Visual model not loaded yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analysis-data/load-for-media": {
      "post": {
        "summary": "Load analysis data for specific media files",
        "description": "Selectively loads visual analysis data for the given video or image files only.\nMore efficient than loading everything when you only need to search across a\nsubset of media. Skips files whose visual data is already in memory.\n\nThis endpoint does not load transcription data. Use `/analysis-data/load` to load\neverything, or `/analysis-data/load-transcriptions` for transcript-only workflows.\n",
        "tags": [
          "Analysis Data"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "media_paths"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data loaded (or already in memory)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Visual model not loaded yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analysis-data/load-transcriptions": {
      "post": {
        "summary": "Load transcription data into memory",
        "description": "Loads all transcription data from the analysis folder into memory.\nThis enables the `/search/transcript` endpoint. Transcription data is also\nloaded automatically when using `/analysis-data/load`.\nDoes not require a specific whisper model to be loaded \u2014 transcriptions from\nany supported model are loaded.\n",
        "tags": [
          "Analysis Data"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transcriptions loaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Loaded 123 transcriptions in 0.09s"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/metadata/facets": {
      "post": {
        "summary": "Get metadata filter facets",
        "description": "Returns curated metadata filter facets and coverage for a searchable scope.\nCall this before search to render available filter options, then pass selected\nfilters to visual or transcript search using `metadata_filter_request`.\n\nFor `mode: global`, facets are scoped to loaded searchable media for the\nrequested `media_type`, not every row in the local metadata database.\n",
        "tags": [
          "Metadata Filters"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "scope": {
                    "$ref": "#/components/schemas/MetadataFilterScope"
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "visual",
                      "speech",
                      "any"
                    ],
                    "default": "any",
                    "description": "Used when `scope` is omitted."
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Used to build a path scope when `scope` is omitted."
                  },
                  "hash_strs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Hashes aligned with `media_paths` when `scope` is omitted."
                  },
                  "search_all": {
                    "type": "boolean",
                    "default": false,
                    "description": "Build a global scope when `scope` is omitted."
                  },
                  "expected_count": {
                    "type": "integer",
                    "description": "Optional count used for coverage reporting when `scope` is omitted."
                  },
                  "filters": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/MetadataFilter"
                    },
                    "default": [],
                    "description": "Usually empty for option loading. Included only for active-filter count reporting."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Metadata facets and coverage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetadataFacetResponse"
                }
              }
            }
          }
        }
      }
    },
    "/search/text": {
      "post": {
        "summary": "Search by text query",
        "description": "Semantic visual search \u2014 finds moments in analyzed media that match\na natural-language description. Analysis data must be loaded into memory first.\nResults are ordered best-first, but Jumper does not return similarity scores.\n",
        "tags": [
          "Search"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query",
                  "cache_dir"
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "Natural-language search query",
                    "example": "man pointing a finger"
                  },
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Restrict search to these files (ignored if search_all is true)"
                  },
                  "max_results": {
                    "type": "integer",
                    "default": 50
                  },
                  "text_weight": {
                    "type": "number",
                    "default": 1,
                    "description": "Weight of text similarity (advanced)"
                  },
                  "exclude": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Softly push down results that match these visual concepts."
                  },
                  "search_all": {
                    "type": "boolean",
                    "default": false,
                    "description": "Search across all loaded media instead of just media_paths"
                  },
                  "people_filter": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "cluster_job_name": {
                          "type": "string",
                          "description": "Name of the face clustering job"
                        },
                        "person_name": {
                          "type": "string",
                          "description": "Person name (resolved to cluster IDs via the job's name mapping)"
                        },
                        "cluster_id": {
                          "type": "string",
                          "description": "Direct cluster ID (alternative to person_name)"
                        }
                      },
                      "required": [
                        "cluster_job_name"
                      ]
                    },
                    "description": "Only return matches containing these people. Each item needs cluster_job_name plus either person_name or cluster_id."
                  },
                  "people_filter_mode": {
                    "type": "string",
                    "enum": [
                      "or",
                      "and"
                    ],
                    "default": "and",
                    "description": "Match mode for multiple people. \"and\" requires every selected person in the same frame; \"or\" matches frames containing any selected person."
                  },
                  "metadata_filter_request": {
                    "$ref": "#/components/schemas/MetadataFilterRequest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results ordered by relevance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchMatch"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/search/image": {
      "post": {
        "summary": "Search by reference image",
        "description": "Finds moments visually similar to a reference image. Optionally combine\nwith a text query to refine results. Results are ordered best-first.\n",
        "tags": [
          "Search"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "image_path",
                  "cache_dir"
                ],
                "properties": {
                  "image_path": {
                    "type": "string",
                    "description": "Absolute path to the reference image"
                  },
                  "query": {
                    "type": "string",
                    "description": "Optional text query to combine with the image"
                  },
                  "exclude": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Softly push down results that match these visual concepts. Requires query text."
                  },
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "max_results": {
                    "type": "integer",
                    "default": 50
                  },
                  "search_all": {
                    "type": "boolean",
                    "default": false
                  },
                  "metadata_filter_request": {
                    "$ref": "#/components/schemas/MetadataFilterRequest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results ordered by visual similarity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchMatch"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/search/frame": {
      "post": {
        "summary": "Search by video frame",
        "description": "Extracts a frame at the given timestamp from a video, then finds visually\nsimilar moments across all loaded media. Useful for \"find more like this\" features.\nResults are ordered best-first.\n",
        "tags": [
          "Search"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "media_path",
                  "time_seconds",
                  "cache_dir"
                ],
                "properties": {
                  "media_path": {
                    "type": "string",
                    "description": "Video to extract the reference frame from"
                  },
                  "time_seconds": {
                    "type": "number",
                    "description": "Timestamp in seconds for the reference frame"
                  },
                  "query": {
                    "type": "string",
                    "description": "Optional text query to combine with the frame"
                  },
                  "exclude": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Softly push down results that match these visual concepts. Requires query text."
                  },
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "max_results": {
                    "type": "integer",
                    "default": 50
                  },
                  "search_all": {
                    "type": "boolean",
                    "default": false
                  },
                  "metadata_filter_request": {
                    "$ref": "#/components/schemas/MetadataFilterRequest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results ordered by visual similarity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchMatch"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/search/transcript": {
      "post": {
        "summary": "Search transcriptions by text",
        "description": "Searches through loaded speech transcriptions for segments containing the query string.\nUses case-insensitive substring matching. Transcriptions must be loaded into memory first\n(via `/analysis-data/load` or `/analysis-data/load-transcriptions`).\nOptionally filter to transcript segments assigned to a named speaker by passing\n`speaker` or `speaker_name`. Speaker filtering uses transcript speaker names,\nnot face-clustering person names.\n",
        "tags": [
          "Search"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query",
                  "cache_dir"
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "Text to search for (case-insensitive substring match)",
                    "example": "hello world"
                  },
                  "cache_dir": {
                    "type": "string"
                  },
                  "max_results": {
                    "type": "integer",
                    "default": 50
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Restrict search to these files (ignored if search_all is true)"
                  },
                  "search_all": {
                    "type": "boolean",
                    "default": true,
                    "description": "Search across all loaded transcriptions"
                  },
                  "speaker": {
                    "type": "string",
                    "description": "Optional named transcript speaker to search within. Matches speaker display names assigned in transcript speaker metadata.",
                    "example": "Anna"
                  },
                  "speaker_name": {
                    "type": "string",
                    "description": "Alias for `speaker`.",
                    "example": "Anna"
                  },
                  "metadata_filter_request": {
                    "$ref": "#/components/schemas/MetadataFilterRequest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching transcript segments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "hash_str",
                          "start_seconds",
                          "end_seconds",
                          "text",
                          "start_timestamp",
                          "end_timestamp"
                        ],
                        "properties": {
                          "media_path": {
                            "type": "string",
                            "description": "Absolute path to the media file (may be absent if path could not be resolved)"
                          },
                          "hash_str": {
                            "type": "string"
                          },
                          "start_seconds": {
                            "type": "number"
                          },
                          "end_seconds": {
                            "type": "number"
                          },
                          "text": {
                            "type": "string"
                          },
                          "start_timestamp": {
                            "type": "string",
                            "description": "Formatted as HH:MM:SS"
                          },
                          "end_timestamp": {
                            "type": "string",
                            "description": "Formatted as HH:MM:SS"
                          },
                          "speaker": {
                            "type": "string",
                            "description": "Transcript-local speaker ID for the matched segment, when available.",
                            "example": "SPEAKER_00"
                          },
                          "speaker_name": {
                            "type": "string",
                            "description": "Display name for the matched speaker, when available.",
                            "example": "Anna"
                          },
                          "speakers": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "All transcript-local speaker IDs assigned to the matched segment, when multiple speakers are present."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/transcriptions": {
      "post": {
        "summary": "Get transcriptions",
        "description": "Returns cached speech transcriptions for the given media files.\nFiles must have been transcribed via the `/analyze` endpoint first.\n",
        "tags": [
          "Transcriptions"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "media_paths"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_paths": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Paths to video or audio files"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transcription segments keyed by file path",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transcriptions": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/TranscriptionSegment"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/thumbnails": {
      "post": {
        "summary": "Get thumbnails at specific timestamps",
        "description": "Returns base64-encoded JPEG thumbnails for the requested media/timestamp pairs.",
        "tags": [
          "Thumbnails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "requests"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "requests": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "media_path",
                        "time_seconds"
                      ],
                      "properties": {
                        "media_path": {
                          "type": "string"
                        },
                        "time_seconds": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Thumbnails in request order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "thumbnails": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "thumbnail": {
                            "type": "string",
                            "format": "byte",
                            "nullable": true,
                            "description": "Base64-encoded JPEG, or null for audio files"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/thumbnails/scene": {
      "post": {
        "summary": "Get thumbnails for a time range",
        "description": "Returns a series of thumbnails spanning a time range in a video.\nUseful for building timeline scrubbers or scene overview strips.\n",
        "tags": [
          "Thumbnails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "media_path",
                  "start_time",
                  "end_time"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_path": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string",
                    "description": "Start timestamp (HH:MM:SS)",
                    "example": "00:01:00"
                  },
                  "end_time": {
                    "type": "string",
                    "description": "End timestamp (HH:MM:SS)",
                    "example": "00:02:00"
                  },
                  "hash_str": {
                    "type": "string",
                    "description": "Optional media file hash for faster lookup"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Array of [base64_jpeg, timestamp] pairs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scene_thumbnails": {
                      "type": "array",
                      "nullable": true,
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "[base64_jpeg, timestamp]"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/faces/clusters": {
      "get": {
        "summary": "List face clustering jobs",
        "description": "Returns all clustering jobs and summary statistics for each.",
        "tags": [
          "Face Clustering"
        ],
        "parameters": [
          {
            "name": "cache_dir",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cluster job list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cluster_jobs": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Job names"
                    },
                    "jobs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClusterJob"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/faces/clusters/samples": {
      "post": {
        "summary": "Get sample face images per cluster",
        "description": "Returns a few sample face thumbnail images for each cluster in a job.\nUseful for building a \"who is this person?\" UI.\n",
        "tags": [
          "Face Clustering"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "cluster_job_name"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "limit_per_cluster": {
                    "type": "integer",
                    "default": 12
                  },
                  "min_cluster_size": {
                    "type": "integer",
                    "default": 1
                  },
                  "max_cluster_size": {
                    "type": "integer",
                    "default": 1000000000
                  },
                  "max_clusters": {
                    "type": "integer",
                    "default": 100
                  },
                  "include_noise": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include unassigned faces in response"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cluster samples",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cluster_job_name": {
                      "type": "string"
                    },
                    "num_clusters": {
                      "type": "integer"
                    },
                    "num_clusters_after_filter": {
                      "type": "integer"
                    },
                    "total_entries": {
                      "type": "integer"
                    },
                    "clusters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClusterSample"
                      }
                    },
                    "noise_size": {
                      "type": "integer",
                      "description": "Only present if include_noise was true"
                    },
                    "noise_sample": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "byte"
                      },
                      "description": "Only present if include_noise was true"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/faces/clusters/faces": {
      "post": {
        "summary": "Get paginated cluster faces",
        "description": "Returns face images for specific cluster(s) with pagination support.",
        "tags": [
          "Face Clustering"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "cluster_job_name"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "cluster_id": {
                    "type": "string",
                    "description": "Single cluster ID"
                  },
                  "cluster_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Multiple cluster IDs (overrides cluster_id)"
                  },
                  "limit": {
                    "type": "integer",
                    "default": 500,
                    "maximum": 2000
                  },
                  "offset": {
                    "type": "integer",
                    "default": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paginated face data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/faces/clusters/names": {
      "put": {
        "summary": "Rename clusters",
        "description": "Assign human-readable names to clusters (e.g. \"John\", \"Sarah\").",
        "tags": [
          "Face Clustering"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "cluster_job_name",
                  "assignments"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "assignments": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "cluster_id",
                        "name"
                      ],
                      "properties": {
                        "cluster_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Names updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "integer",
                      "description": "Number of clusters renamed"
                    },
                    "changes": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "Map of cluster_id to new name"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/faces/recluster": {
      "post": {
        "summary": "Re-run face clustering",
        "description": "Re-runs clustering on existing face detections with new parameters.\nRuns asynchronously \u2014 returns a task_id for progress tracking.\n",
        "tags": [
          "Face Clustering"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "cluster_job_name"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "eps": {
                    "type": "number",
                    "default": 0.48,
                    "description": "Clustering sensitivity (lower = stricter grouping)"
                  },
                  "min_samples": {
                    "type": "integer",
                    "default": 5,
                    "description": "Minimum faces needed to form a cluster"
                  },
                  "auto_detect_params": {
                    "type": "boolean",
                    "default": false,
                    "description": "Auto-tune eps and min_samples"
                  },
                  "clear_names": {
                    "type": "boolean",
                    "default": false,
                    "description": "Clear all existing cluster names"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Re-clustering started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "task_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/faces/clusters/modify": {
      "post": {
        "summary": "Merge or move faces between clusters",
        "description": "Merge entire clusters together, or move individual faces between clusters.\nUseful for correcting clustering mistakes.\n",
        "tags": [
          "Face Clustering"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "cluster_job_name"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "merges": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "cluster_ids": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Clusters to merge"
                        },
                        "target_cluster_id": {
                          "type": "string",
                          "description": "Which cluster to merge into"
                        }
                      }
                    },
                    "description": "Cluster merge operations"
                  },
                  "moves": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Individual face move operations"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Modification summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/watch-folders": {
      "get": {
        "summary": "List watch folders",
        "description": "Returns all configured watch folders and the background service status.",
        "tags": [
          "Watch Folders"
        ],
        "responses": {
          "200": {
            "description": "Watch folder list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watch_folders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchFolder"
                      }
                    },
                    "service_status": {
                      "$ref": "#/components/schemas/WatchFolderServiceStatus"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a watch folder",
        "description": "Adds a new folder to be automatically analyzed when new media files appear.\nThe folder must exist on disk. Exclusion glob patterns are matched against\nthe filename only, not the full path.\n",
        "tags": [
          "Watch Folders"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "folder_path"
                ],
                "properties": {
                  "folder_path": {
                    "type": "string",
                    "description": "Absolute path to the folder to watch"
                  },
                  "cache_dir": {
                    "type": "string",
                    "description": "Where to store analysis results (optional, uses default if omitted)"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true
                  },
                  "enable_visual_analysis": {
                    "type": "boolean",
                    "default": true
                  },
                  "enable_audio_analysis": {
                    "type": "boolean",
                    "default": false
                  },
                  "enable_face_analysis": {
                    "type": "boolean",
                    "default": false
                  },
                  "audio_language": {
                    "type": "string",
                    "default": "english"
                  },
                  "cluster_job_name": {
                    "type": "string",
                    "description": "Required when face analysis is enabled"
                  },
                  "face_eps": {
                    "type": "number",
                    "default": 0.48
                  },
                  "face_min_samples": {
                    "type": "integer",
                    "default": 5
                  },
                  "excluded_extensions": {
                    "type": "array",
                    "description": "File extensions to skip, normalized to lowercase with a leading dot",
                    "items": {
                      "type": "string"
                    }
                  },
                  "excluded_filename_globs": {
                    "type": "array",
                    "description": "Case-insensitive glob patterns matched against the filename only",
                    "items": {
                      "type": "string"
                    }
                  },
                  "excluded_folder_globs": {
                    "type": "array",
                    "description": "Case-insensitive folder glob patterns under the watched folder. Patterns with slashes match relative folder paths segment by segment.",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Watch folder created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "watch_folder": {
                      "$ref": "#/components/schemas/WatchFolder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Folder doesn't exist or already registered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/watch-folders/{watch_folder_id}": {
      "put": {
        "summary": "Update a watch folder",
        "description": "Modify settings for an existing watch folder. Only include fields you want to change.\nFilename exclusion glob patterns are matched against the filename only, not the full path.\nFolder exclusion glob patterns without slashes match folder names; patterns with slashes match relative folder paths segment by segment.\n",
        "tags": [
          "Watch Folders"
        ],
        "parameters": [
          {
            "name": "watch_folder_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "enable_visual_analysis": {
                    "type": "boolean"
                  },
                  "enable_audio_analysis": {
                    "type": "boolean"
                  },
                  "enable_face_analysis": {
                    "type": "boolean"
                  },
                  "audio_language": {
                    "type": "string"
                  },
                  "cluster_job_name": {
                    "type": "string"
                  },
                  "face_eps": {
                    "type": "number"
                  },
                  "face_min_samples": {
                    "type": "integer"
                  },
                  "cache_dir": {
                    "type": "string"
                  },
                  "excluded_extensions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "excluded_filename_globs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "excluded_folder_globs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Watch folder updated"
          },
          "404": {
            "description": "Watch folder not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a watch folder",
        "description": "Removes a watch folder from the configuration. Does not delete analysis data.",
        "tags": [
          "Watch Folders"
        ],
        "parameters": [
          {
            "name": "watch_folder_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Watch folder removed"
          },
          "404": {
            "description": "Watch folder not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/watch-folders/service/start": {
      "post": {
        "summary": "Start the watch folder service",
        "description": "Starts the background service that monitors watch folders for new media.\nThe service uses the enabled watch-folder configuration already persisted in settings.\n",
        "tags": [
          "Watch Folders"
        ],
        "responses": {
          "200": {
            "description": "Service started (or already running)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/watch-folders/service/stop": {
      "post": {
        "summary": "Stop the watch folder service",
        "description": "Stops the background watch folder monitoring service.",
        "tags": [
          "Watch Folders"
        ],
        "responses": {
          "200": {
            "description": "Service stopped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "was_running": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/watch-folders/service/status": {
      "get": {
        "summary": "Get watch folder service status",
        "description": "Returns whether the background service is running and its current state.",
        "tags": [
          "Watch Folders"
        ],
        "responses": {
          "200": {
            "description": "Service status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchFolderServiceStatus"
                }
              }
            }
          }
        }
      }
    },
    "/cache-paths": {
      "post": {
        "summary": "Get cache folder paths for a media file",
        "description": "Returns the visual and audio cache folder paths for a specific media file.\nUseful for inspecting or managing analysis data on disk.\n",
        "tags": [
          "Utilities"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cache_dir",
                  "media_path"
                ],
                "properties": {
                  "cache_dir": {
                    "type": "string"
                  },
                  "media_path": {
                    "type": "string",
                    "description": "Absolute path to the media file"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cache paths for the media file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "video_cache_path": {
                      "type": "string",
                      "nullable": true,
                      "description": "Path to the visual analysis cache folder (null if not yet analyzed)"
                    },
                    "audio_cache_path": {
                      "type": "string",
                      "nullable": true,
                      "description": "Path to the audio/transcription cache folder (null if not yet analyzed)"
                    },
                    "hash_str": {
                      "type": "string",
                      "description": "Unique hash identifying this media file"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/export/clips": {
      "post": {
        "summary": "Export trimmed video clips",
        "description": "Exports trimmed video clips to a folder using ffmpeg. Each clip specifies\na source file and in/out points. Supports subfolder organization.\n",
        "tags": [
          "Export"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "clips",
                  "output_dir"
                ],
                "properties": {
                  "clips": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "source_path",
                        "start_seconds",
                        "end_seconds"
                      ],
                      "properties": {
                        "source_path": {
                          "type": "string",
                          "description": "Absolute path to the source media file"
                        },
                        "start_seconds": {
                          "type": "number",
                          "description": "Start time in seconds"
                        },
                        "end_seconds": {
                          "type": "number",
                          "description": "End time in seconds"
                        },
                        "subfolder": {
                          "type": "string",
                          "description": "Optional subfolder within output_dir"
                        }
                      }
                    }
                  },
                  "output_dir": {
                    "type": "string",
                    "description": "Directory to write exported clips to"
                  },
                  "copy_codec": {
                    "type": "boolean",
                    "default": true,
                    "description": "Use stream copy (fast) vs re-encode"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Export results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "success": {
                            "type": "boolean"
                          },
                          "output_path": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "string",
                      "example": "3/3 clips exported successfully"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/export/premiere-xml": {
      "post": {
        "summary": "Generate Premiere Pro XML sequence",
        "description": "Generates a Premiere Pro compatible XML sequence file (XMEML v4) from a list of clips.\nThe XML can be imported into Premiere Pro, DaVinci Resolve, Avid, or other NLEs.\n",
        "tags": [
          "Export"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "clips",
                  "output_path"
                ],
                "properties": {
                  "sequence_name": {
                    "type": "string",
                    "default": "Untitled Sequence",
                    "description": "Name of the sequence in the NLE"
                  },
                  "clips": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "source_path",
                        "start_seconds",
                        "end_seconds"
                      ],
                      "properties": {
                        "source_path": {
                          "type": "string"
                        },
                        "start_seconds": {
                          "type": "number"
                        },
                        "end_seconds": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "output_path": {
                    "type": "string",
                    "description": "Where to write the XML file"
                  },
                  "timebase": {
                    "type": "integer",
                    "default": 24,
                    "description": "Sequence frame rate (e.g. 24, 25, 30)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "XML generated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "output_path": {
                      "type": "string"
                    },
                    "clip_count": {
                      "type": "integer"
                    },
                    "total_duration_seconds": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/export/transcript": {
      "post": {
        "summary": "Export transcript to file",
        "description": "Export transcript segments to a file in TXT, CSV, DOCX, or PDF format.\nIf output_path is omitted, saves to ~/Desktop/{display_name}.{format}.\n",
        "tags": [
          "Export"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "segments"
                ],
                "properties": {
                  "segments": {
                    "type": "array",
                    "description": "List of [start_seconds, text, end_seconds] arrays",
                    "items": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "number"
                          },
                          {
                            "type": "string"
                          }
                        ]
                      }
                    }
                  },
                  "format": {
                    "type": "string",
                    "default": "txt",
                    "enum": [
                      "txt",
                      "csv",
                      "docx",
                      "pdf"
                    ],
                    "description": "Export format"
                  },
                  "display_name": {
                    "type": "string",
                    "default": "Transcript",
                    "description": "Name shown in header and used for filename"
                  },
                  "output_path": {
                    "type": "string",
                    "description": "Destination file path (defaults to ~/Desktop/)"
                  },
                  "include_silences": {
                    "type": "boolean",
                    "default": true,
                    "description": "Include silence gap rows between segments"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transcript exported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "output_path": {
                      "type": "string"
                    },
                    "format": {
                      "type": "string"
                    },
                    "row_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
