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.

    Jumper analyzes video, image, and audio files — enabling semantic visual search,
    speech transcription, and face detection with clustering.
    This API exposes those capabilities for use by MAM systems, automation pipelines,
    and custom tooling.

    ## Authentication

    All endpoints except `/health` require a **Jumper Pro** license key.
    The OpenAPI contract models the supported `X-License-Key` header flow.

    Jumper also accepts a `license_key` field in many JSON POST bodies as a
    convenience, but that is a Jumper-specific request-body convention rather
    than an OpenAPI security scheme.

    Invalid or missing keys return `401`. Non-Pro keys return `403`.

    ## Key Concepts

    - **`cache_dir`** — The folder where Jumper stores analysis data (visual analysis, transcriptions, face clusters). You choose this path.
    - **`media_paths`** — Absolute filesystem paths to media files. Jumper needs direct access to these files.
    - **`hash_str`** — A CRC32-based hash that uniquely identifies a media file. Returned by the metadata endpoint.
    - **`model_key`** — Internal identifier for a visual or speech analysis model variant (e.g. `v2-medium-256`).

  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
        from the endpoint's `search_all` and `media_paths` values.

    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
  # ===========================================================================
  /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
  # ===========================================================================
  /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.
        This clears loaded analysis data from memory — you will need to reload it afterwards.
      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
  # ===========================================================================
  /media/metadata:
    post:
      summary: Get metadata for media files
      description: |
        Returns properties (duration, FPS, timecode) and analysis status for each media file.
        Use this to check which files have been analyzed and to get their `hash_str` identifiers.
      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"

  # ===========================================================================
  # Analysis
  # ===========================================================================
  /analyze:
    post:
      summary: Start media analysis
      description: |
        Starts an asynchronous analysis pipeline. Returns immediately with a `task_id`
        that can be used to track progress via SocketIO.

        You can run visual analysis, speech transcription, and face clustering in a single call.

        For throughput, batch many files into one request instead of sending many
        single-file requests. Jumper loads ML models per analysis request, so one
        50-file request is much faster than 50 one-file requests on the same node.

        Only one analysis task can run at a time per backend instance. A new analyze
        request first asks the current task to stop. If the previous task is still
        unwinding, Jumper returns `409`.
      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 Management
  # ===========================================================================
  /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
        into memory. Must be called before searching, unless you use `/analysis-data/load-for-media`
        instead.
      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.
        More efficient than loading everything when you only need to search across a
        subset of media. Skips files whose visual data is already in memory.

        This endpoint does not load transcription data. Use `/analysis-data/load` to load
        everything, or `/analysis-data/load-transcriptions` for transcript-only workflows.
      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.
        This enables the `/search/transcript` endpoint. Transcription data is also
        loaded automatically when using `/analysis-data/load`.
        Does not require a specific whisper model to be loaded — transcriptions from
        any supported model are loaded.
      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 Filters
  # ===========================================================================
  /metadata/facets:
    post:
      summary: Get metadata filter facets
      description: |
        Returns curated metadata filter facets and coverage for a searchable scope.
        Call this before search to render available filter options, then pass selected
        filters to visual or transcript search using `metadata_filter_request`.

        For `mode: global`, facets are scoped to loaded searchable media for the
        requested `media_type`, not every row in the local metadata database.
      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
  # ===========================================================================
  /search/text:
    post:
      summary: Search by text query
      description: |
        Semantic visual search — finds moments in analyzed media that match
        a natural-language description. Analysis data must be loaded into memory first.
        Results are ordered best-first, but Jumper does not return similarity scores.
      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
        with a text query to refine results. Results are ordered best-first.
      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
        similar moments across all loaded media. Useful for "find more like this" features.
        Results are ordered best-first.
      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.
        Uses case-insensitive substring matching. Transcriptions must be loaded into memory first
        (via `/analysis-data/load` or `/analysis-data/load-transcriptions`).
        Optionally filter to transcript segments assigned to a named speaker by passing
        `speaker` or `speaker_name`. Speaker filtering uses transcript speaker names,
        not face-clustering person names.
      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
  # ===========================================================================
  /transcriptions:
    post:
      summary: Get transcriptions
      description: |
        Returns cached speech transcriptions for the given media files.
        Files must have been transcribed via the `/analyze` endpoint first.
      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
  # ===========================================================================
  /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.
        Useful for building timeline scrubbers or scene overview strips.
      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]"

  # ===========================================================================
  # Face Clustering
  # ===========================================================================
  /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.
        Useful for building a "who is this person?" UI.
      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.
        Runs asynchronously — returns a task_id for progress tracking.
      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.
        Useful for correcting clustering mistakes.
      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
  # ===========================================================================
  /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.
        The folder must exist on disk. Exclusion glob patterns are matched against
        the filename only, not the full path.
      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.
        Filename exclusion glob patterns are matched against the filename only, not the full path.
        Folder exclusion glob patterns without slashes match folder names; patterns with slashes match relative folder paths segment by segment.
      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.
        The service uses the enabled watch-folder configuration already persisted in settings.
      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
  # ===========================================================================
  /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.
        Useful for inspecting or managing analysis data on disk.
      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
  # ===========================================================================
  /export/clips:
    post:
      summary: Export trimmed video clips
      description: |
        Exports trimmed video clips to a folder using ffmpeg. Each clip specifies
        a source file and in/out points. Supports subfolder organization.
      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.
        The XML can be imported into Premiere Pro, DaVinci Resolve, Avid, or other NLEs.
      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.
        If output_path is omitted, saves to ~/Desktop/{display_name}.{format}.
      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
