{
  "openapi": "3.1.0",
  "info": {
    "title": "MinusPod API",
    "description": "REST API for managing podcast feeds with automatic ad detection and removal.\n\n\nThis API allows you to:\n- Manage podcast feed subscriptions\n- View and manage processed episodes\n- Configure ad detection settings including AI model selection\n- Monitor system status and trigger cleanup operations\n- Manage cross-episode ad patterns with network and podcast scope\n- Submit corrections to improve ad detection accuracy\n",
    "version": "2.101.27",
    "contact": {
      "name": "MinusPod"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Same-origin (current server)"
    },
    {
      "url": "http://localhost:8000/api/v1",
      "description": "Local development server"
    },
    {
      "url": "{baseUrl}/api/v1",
      "description": "Custom server",
      "variables": {
        "baseUrl": {
          "default": "http://localhost:8000",
          "description": "Base URL of the podcast server"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Authentication management. Direct source runs may be passwordless; shipped Compose deployments require first-password setup."
    },
    {
      "name": "Feeds",
      "description": "Podcast feed management"
    },
    {
      "name": "Episodes",
      "description": "Episode data and transcripts"
    },
    {
      "name": "Processing",
      "description": "Processing queue management"
    },
    {
      "name": "Patterns",
      "description": "Cross-episode ad pattern management"
    },
    {
      "name": "Sponsors",
      "description": "Sponsor name normalization"
    },
    {
      "name": "Settings",
      "description": "Application settings and configuration"
    },
    {
      "name": "Status",
      "description": "Real-time processing status"
    },
    {
      "name": "History",
      "description": "Processing history and statistics"
    },
    {
      "name": "System",
      "description": "System status and maintenance"
    },
    {
      "name": "Search",
      "description": "Full-text search across all content"
    },
    {
      "name": "AudioCueTemplates",
      "description": "Per-podcast learned audio-cue (ding/stinger) templates (#350)"
    },
    {
      "name": "LocalFeeds",
      "description": "Local (archive-import) feeds: no upstream RSS, episode upload and edits, and bulk directory/upload import (#625)"
    },
    {
      "name": "YouTube",
      "description": "YouTube playlist and channel feeds: source preview, Data API key test, cookies, and yt-dlp updates. Global settings live under Settings."
    },
    {
      "name": "Failover",
      "description": "Provider failover (#806): state of the LLM A/B and transcriber failover targets, manual trigger/cancel, and on-demand health probes."
    }
  ],
  "paths": {
    "/auth/status": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Check authentication status",
        "description": "Returns whether password protection is enabled and if the current session is authenticated.\nThis endpoint is always accessible without authentication.\n",
        "operationId": "getAuthStatus",
        "security": [],
        "responses": {
          "200": {
            "description": "Authentication status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "passwordSet": {
                      "type": "boolean",
                      "description": "Whether a password is configured"
                    },
                    "authenticated": {
                      "type": "boolean",
                      "description": "Whether current session is authenticated"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Login with password",
        "description": "Authenticate with the configured password. Rate limited to 3 attempts per minute and 10 attempts per hour.",
        "operationId": "login",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password"
                ],
                "properties": {
                  "password": {
                    "type": "string",
                    "description": "The application password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Login successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authenticated": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Authentication is required but the first password has not been set",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Logout and clear session",
        "description": "Clears the current session. An authenticated logout requires the CSRF cookie/header pair; an unauthenticated caller may clear stale state.",
        "operationId": "logout",
        "security": [],
        "responses": {
          "200": {
            "description": "Logout successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authenticated": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Missing or invalid CSRF token for an authenticated session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/auth/password": {
      "put": {
        "tags": [
          "Auth"
        ],
        "summary": "Set or change password",
        "description": "Set or change the application password. Rate limited to 3 attempts per hour.\n\n- Initial setup is allowed from loopback. When required auth is on,\n  remote setup needs the configured value in X-MinusPod-Setup-Token.\n- An existing password requires currentPassword instead of CSRF.\n- A new or changed password rotates the session generation, revokes\n  every older session, and authenticates this request's new session.\n- Empty or null newPassword removes protection and clears this session.\n- A non-empty newPassword must contain at least 12 characters.\n",
        "operationId": "setPassword",
        "security": [],
        "parameters": [
          {
            "name": "X-MinusPod-Setup-Token",
            "in": "header",
            "required": false,
            "description": "Required only for remote initial setup when required auth is enabled.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "newPassword"
                ],
                "properties": {
                  "currentPassword": {
                    "type": "string",
                    "description": "Current password (required if password is already set)"
                  },
                  "newPassword": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "New password (at least 12 characters, or empty/null to remove protection)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message",
                    "passwordSet"
                  ],
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "passwordSet": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (password too short)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Current password incorrect",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Remote initial setup token missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Password changed concurrently; retry with current state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "List all feeds",
        "description": "Returns configured podcast feeds with metadata. Omitting page and limit still returns every feed, with pagination fields describing that unbounded page; passing either one switches to a paginated page. Set includeLatestEpisodes=true to add a bounded latestEpisodes projection to each feed, computed in one query for the whole page rather than one request per feed.",
        "operationId": "listFeeds",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-based). Only takes effect when page or limit is set; otherwise every feed is returned.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Feeds per page. Only takes effect when page or limit is set.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "includeLatestEpisodes",
            "in": "query",
            "description": "When true, add a latestEpisodes projection to each feed.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "episodesPerFeed",
            "in": "query",
            "description": "Maximum episodes per feed in latestEpisodes. Clamped to 1-20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 3
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "description": "Orders the whole feed list before it is sliced into a page, so a page is a slice of the sorted list rather than a sorted slice. recent orders by the feed's newest episode, title alphabetically (case-insensitive). Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "recent",
                "title"
              ]
            }
          },
          {
            "name": "sortDir",
            "in": "query",
            "description": "Sort direction. Defaults to desc for recent and asc for title. Any other value is a 400.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of feeds",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feeds": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Feed"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total feed count."
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "lastRefreshCompletedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When an all-feeds refresh pass last finished (15-minute scheduler or the manual Refresh All action). Null until the first pass completes."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown sortBy or sortDir"
          }
        }
      },
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Add a new feed",
        "description": "Add a new podcast feed. Three shapes: a subscribed feed (default), which requires sourceUrl and fetches/parses it before saving; a local (archive-import) feed, set with feedType: \"local\", which takes no sourceUrl and no upstream is ever fetched: metadata comes entirely from the request body; or the single combined recents feed, feedType: \"recents\", served at /recents with a fixed slug, taking only title and description (409 when one already exists). The local branch still validates slug (or derives one from title) but skips URL/SSRF validation since there is no URL.",
        "operationId": "addFeed",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "downloadUserAgentOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 512,
                    "description": "Per-feed download User-Agent. Printable ASCII only. Null or blank inherits downloadUserAgent."
                  },
                  "feedUserAgentOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 512,
                    "description": "Per-feed RSS User-Agent. Printable ASCII only. Null or blank inherits feedUserAgent."
                  },
                  "sourceUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL of the source RSS feed. Required unless feedType is \"local\".",
                    "example": "https://feeds.example.com/podcast.xml"
                  },
                  "slug": {
                    "type": "string",
                    "description": "Optional custom slug (auto-generated if not provided). Immutable once the feed is created.",
                    "example": "my-podcast"
                  },
                  "feedType": {
                    "type": "string",
                    "enum": [
                      "subscribed",
                      "local",
                      "recents",
                      "youtube"
                    ],
                    "default": "subscribed",
                    "description": "\"local\" creates an archive-import feed with no upstream RSS: title is required, sourceUrl is ignored, and no fetch happens. \"youtube\" creates a feed from a YouTube playlist or channel: sourceKind, sourceId and title are required, sourceUrl is ignored, and the first listing runs before the response. Omit or \"subscribed\" for a normal feed."
                  },
                  "sourceKind": {
                    "type": "string",
                    "enum": [
                      "playlist",
                      "channel"
                    ],
                    "description": "YouTube feeds only. Kind of the source."
                  },
                  "sourceId": {
                    "type": "string",
                    "description": "YouTube feeds only. Canonical playlist id or channel id, as returned by the preview.",
                    "example": "UCxxxxxxxxxxxxxxxxxxxxxx"
                  },
                  "artworkUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "YouTube feeds only. Cover image downloaded once at creation."
                  },
                  "includeShorts": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Whether videos of 180 seconds or less are listed; null follows the default."
                  },
                  "autoProcessOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed auto-process override applied at creation. Null inherits the global setting."
                  },
                  "title": {
                    "type": "string",
                    "description": "Feed title. Required and editable when feedType is \"local\"; ignored for a subscribed feed, which takes its title from the upstream RSS on refresh."
                  },
                  "description": {
                    "type": "string",
                    "description": "Feed description. Local feeds only; ignored for a subscribed feed."
                  },
                  "author": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "itunes:author. Local feeds only."
                  },
                  "explicit": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "itunes:explicit. Local feeds only; null means unset."
                  },
                  "categories": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "itunes:category list. Local feeds only; null clears."
                  },
                  "p20": {
                    "$ref": "#/components/schemas/LocalFeedP20"
                  },
                  "maxEpisodes": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Per-feed cap on served episode count. Subscribed feeds: clamped to 10-500; omit or set null to fall back to the global maxFeedEpisodes setting (default 300). Local feeds: 0, null, or omitted serves every episode (the default); a positive value up to 10000 serves only the newest N."
                  },
                  "onlyExposeProcessedEpisodes": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override. true hides non-processed episodes; false shows everything; null falls back to the global onlyExposeProcessedDefault setting."
                  },
                  "processingMode": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "keep_content",
                      "skip_detection",
                      "passthrough"
                    ],
                    "description": "Processing mode applied before the first refresh. Subscribed and YouTube feeds only; local and recents feeds reject it with a 400. Omitted means standard. cue_only is rejected on create because it needs cue templates; set it from PATCH /feeds/{slug} afterwards."
                  },
                  "requireManualApproval": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override. true keeps newly processed episodes out of the served feed until approved; false publishes them at once; null falls back to the global requireManualApprovalDefault setting."
                  },
                  "retentionDaysOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 3650,
                    "description": "Per-feed retention override. Null falls back to the global retentionDays setting; 0 archives the feed so processed audio is never deleted, including by the force-all cleanup; 1-3650 keeps it for that many days."
                  },
                  "audioReplacementSoundOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Override replacement sound for this feed; null inherits the global choice."
                  },
                  "audioMp3StreamCopyOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Override MP3 stream copy for this feed; null inherits the global choice."
                  },
                  "keepOriginalAudioOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override for retaining the pre-cut original audio. Null falls back to the global keepOriginalAudio setting; true always keeps it; false never does. Applies from the next episode processed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feed created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "slug": {
                      "type": "string"
                    },
                    "sourceUrl": {
                      "type": "string"
                    },
                    "feedUrl": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "Feed with slug already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/import-opml": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Import feeds from OPML file",
        "description": "Import podcast feeds from an OPML file upload. Accepts .opml or .xml files.\nAutomatically generates slugs from feed titles and refreshes the first 5 imported feeds.\n",
        "operationId": "importOpml",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "opml"
                ],
                "properties": {
                  "opml": {
                    "type": "string",
                    "format": "binary",
                    "description": "OPML file (.opml or .xml)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "No feeds were imported (all skipped or failed)"
          },
          "201": {
            "description": "Feeds imported successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imported": {
                      "type": "integer",
                      "description": "Number of feeds successfully imported"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Number of feeds skipped (already exist)"
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Number of feeds that failed to import"
                    },
                    "feeds": {
                      "type": "object",
                      "properties": {
                        "imported": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string"
                              },
                              "slug": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "skipped": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string"
                              },
                              "slug": {
                                "type": "string"
                              },
                              "reason": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "failed": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string"
                              },
                              "error": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid OPML file or no file provided",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/export-opml": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "Export feeds as OPML file",
        "description": "Export all subscribed podcast feeds as an OPML XML file.\nUse mode=original for upstream source URLs (backup/migration).\nUse mode=modified for MinusPod-served ad-free feed URLs (import into podcast apps).\n",
        "operationId": "exportOpml",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "URL mode: \"original\" for upstream source URLs, \"modified\" for MinusPod ad-free feed URLs",
            "schema": {
              "type": "string",
              "enum": [
                "original",
                "modified"
              ],
              "default": "original"
            }
          },
          {
            "name": "slugs",
            "in": "query",
            "description": "Optional comma-separated feed slugs to include (in the usual feed order). Unknown slugs are skipped. Omit to export every feed.",
            "schema": {
              "type": "string"
            },
            "example": "my-podcast,another-show"
          }
        ],
        "responses": {
          "200": {
            "description": "OPML file download",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Attachment filename, e.g. attachment; filename=\"minuspod-feeds.opml\""
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid mode parameter, or slugs matched no feeds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Export selected feeds as an OPML file",
        "description": "Export selected feeds in the usual feed order. Unknown slugs are skipped; the request fails if none match.",
        "operationId": "exportSelectedOpml",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "URL mode: \"original\" for upstream source URLs, \"modified\" for MinusPod ad-free feed URLs",
            "schema": {
              "type": "string",
              "enum": [
                "original",
                "modified"
              ],
              "default": "original"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slugs"
                ],
                "properties": {
                  "slugs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OPML file download",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Attachment filename, e.g. attachment; filename=\"minuspod-feeds.opml\""
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid mode or slug list, or slugs matched no feeds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/episodes/{slug}/{episode_id}/chapter-images/{filename}": {
      "servers": [
        {
          "url": "/",
          "description": "Host root on the public feed domain (not under /api/v1)"
        }
      ],
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get a local embedded chapter image",
        "description": "Serves extracted JPEG or PNG bytes without a remote fetch. When feed\nauthentication is enabled, requires a valid global or scoped subscriber\nkey. Served chapters JSON includes the supplied key on local image URLs.\nHEAD is also supported. Responses prevent content sniffing and allow\ncross-origin podcast players.\n",
        "operationId": "getEmbeddedChapterImage",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}\\.(jpg|png)$"
            }
          },
          {
            "name": "key",
            "in": "query",
            "description": "Required when feed authentication is enabled",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stored chapter image",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid feed key"
          },
          "404": {
            "description": "Missing, invalid or unsupported chapter image"
          }
        }
      }
    },
    "/opml/{mode}.opml": {
      "servers": [
        {
          "url": "/",
          "description": "Host root on the public feed domain (not under /api/v1)"
        }
      ],
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "OPML feed list for podcast apps' import-from-URL",
        "description": "App-level, key-gated OPML export served from the public feed domain so\npodcast apps can import feeds directly by URL. Requires authenticated\nfeeds to be enabled and a valid feed key; returns 404 when feed auth is\ndisabled (the modified list is a bundle of keyed feed URLs and is never\nserved unauthenticated). The copyable URLs are surfaced by GET /settings\nas opmlModifiedUrl / opmlOriginalUrl.\n",
        "operationId": "exportOpmlByUrl",
        "parameters": [
          {
            "name": "mode",
            "in": "path",
            "required": true,
            "description": "modified = MinusPod ad-free feed URLs; original = upstream source URLs",
            "schema": {
              "type": "string",
              "enum": [
                "modified",
                "original"
              ]
            }
          },
          {
            "name": "key",
            "in": "query",
            "required": true,
            "description": "The global feed auth key",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OPML document",
            "content": {
              "text/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid feed key"
          },
          "404": {
            "description": "Feed auth disabled, or unknown mode"
          }
        }
      }
    },
    "/feeds/{slug}": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "Get a single feed",
        "description": "Returns details for a specific podcast feed",
        "operationId": "getFeed",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Feed details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feed"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Feeds"
        ],
        "summary": "Delete a feed",
        "description": "Delete a podcast feed and all associated data",
        "operationId": "deleteFeed",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Feed deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Feeds"
        ],
        "summary": "Update feed settings",
        "description": "Update podcast feed settings like network override, DAI platform, processing overrides, and the source RSS URL. The recents feed accepts only title and description; any other field returns 400.",
        "operationId": "updateFeed",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sourceUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Replacement source RSS URL for this feed. The server fetches and parses the URL before persisting; a URL that does not return a parseable RSS feed is rejected with 400 and nothing is saved. On success the stored ETag/Last-Modified validators are cleared and an immediate forced refresh runs. Existing episodes are kept (matched by GUID). Cannot be null or empty."
                  },
                  "networkIdOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Network to assign this feed to: a known network id, or a custom name that links feeds sharing the same value (null uses the auto-detected network). The effective network is the override when set, else the auto-detected one."
                  },
                  "daiPlatform": {
                    "type": "string",
                    "description": "DAI platform identifier (e.g., megaphone, acast)"
                  },
                  "autoProcessOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Override auto-process setting (null uses global)"
                  },
                  "autoProcessMaxAgeHoursOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 720,
                    "description": "Per-feed auto-process window in hours (null uses the global autoProcessMaxAgeHours)"
                  },
                  "downloadUserAgentOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 512,
                    "description": "Audio, artwork, chapters, and transcript User-Agent for this feed. Printable ASCII only. Null or blank inherits downloadUserAgent. Cross-fetch uses a different podcast client."
                  },
                  "feedUserAgentOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 512,
                    "description": "RSS fetch User-Agent for this feed. Printable ASCII only. Null or blank inherits feedUserAgent."
                  },
                  "languageOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Per-feed transcription language. 'auto' pins auto-detect for this feed; a language code (e.g. 'en', 'de') forces that language; null falls back to the global whisperLanguage setting."
                  },
                  "titleOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Per-feed display title shown to subscribers in the generated RSS feed (issue 375). Survives RSS refreshes. Null or empty clears it and falls back to the source feed title."
                  },
                  "descriptionOverride": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10000,
                    "description": "Per-feed description shown to subscribers in the generated RSS feed. Survives RSS refreshes. Line breaks are kept. Null or empty clears it and falls back to the source feed description."
                  },
                  "detectionNotes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000,
                    "description": "Operator hints about this show appended to the LLM ad-detection and review prompts (issue 709). Survives RSS refreshes. Null or empty clears it."
                  },
                  "chapterNotes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000,
                    "description": "Per-feed notes sent as a final Feed notes paragraph with every chapter generation request. Null or blank clears it."
                  },
                  "detectionMode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "blacklist",
                      "keep_content",
                      null
                    ],
                    "description": "Per-feed ad-detection mode. 'keep_content' (experimental) has the LLM label show content and removes everything else; null or 'blacklist' is the default, which labels and removes ads. Prefer processingMode; kept for per-field compatibility."
                  },
                  "chaptersMode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "auto",
                      "generate",
                      "off",
                      null
                    ],
                    "description": "Per-feed chapters mode. 'auto' preserves publisher-embedded chapters when enough survive the cut, otherwise generates them; 'generate' always generates new chapters; 'off' skips the chapter step entirely; null follows the global chaptersMode setting."
                  },
                  "chaptersInNotes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "on",
                      "off",
                      null
                    ],
                    "description": "Per-feed override for listing generated chapters in served episode descriptions. null follows the global chaptersInNotes setting."
                  },
                  "reviewLinksInNotes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "on",
                      "off",
                      null
                    ],
                    "description": "Per-feed override for review and rate links in served episode descriptions. null follows the global reviewLinksInNotes setting."
                  },
                  "adChaptersEnabled": {
                    "type": [
                      "boolean",
                      "string",
                      "null"
                    ],
                    "enum": [
                      true,
                      false,
                      "on",
                      "off",
                      null
                    ],
                    "deprecated": true,
                    "description": "Deprecated. Translates into segmentCategoryActions; false (or 'off') demotes every mark to keep. true, 'on', and null are accepted and ignored (null is a no-op, matching the old per-feed override contract)."
                  },
                  "adChapterCategories": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": {
                      "type": "boolean"
                    },
                    "deprecated": true,
                    "description": "Deprecated. Translates into segmentCategoryActions; true promotes keep to mark, false demotes mark to keep; remove and beep stay unchanged. null is a no-op."
                  },
                  "queuePriority": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "high",
                      "normal",
                      "low",
                      null
                    ],
                    "description": "Per-feed auto-process queue priority (issue 625). 'high' processes this feed's episodes before other queued episodes; 'low' runs only when nothing else is waiting; 'normal' or null uses the default ordering. Changing it re-stamps this feed's already-queued pending episodes immediately."
                  },
                  "lowAdYieldAction": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "nothing",
                      "redetect",
                      "reprocess",
                      "full",
                      null
                    ],
                    "description": "Per-feed override of the global lowAdYieldAction setting; null uses the global value. Automatic response when a pipeline-initiated run removes far less ad time than the feed's recent average: 'nothing' (default), 'redetect' (LLM-only rerun from the stored transcript), 'reprocess', or 'full'. Fires at most once per episode."
                  },
                  "episodeLogs": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "on",
                      "off",
                      null
                    ],
                    "description": "Per-feed run log storage (issue 660): 'on' stores each run's pipeline log, 'off' never does, null follows the global setting. Nothing is stored while episodeLogRetentionDays is 0."
                  },
                  "titleSkipPatterns": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 200
                    },
                    "maxItems": 50,
                    "description": "Per-feed episode title blacklist. Each pattern is matched against the whole episode title, case-insensitively (fnmatch glob); use '*' as a wildcard, e.g. 'Bonus Episode *' matches any title starting with 'Bonus Episode'. Without a wildcard the whole title must match, so a plain substring needs surrounding '*' wildcards. A matching episode is never queued from RSS refresh or JIT-processed; requests for a skipped episode's audio redirect to the upstream enclosure instead. Null or an empty array clears the blacklist."
                  },
                  "descriptionTrimPattern": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "Chapter title glob for the chapter trim. Matched against the title of each timestamp line in the episode description and against the episode's chapters (upstream or MinusPod-generated), case-insensitively, with '*' as a wildcard. A match cuts from that timestamp per descriptionTrimMode. Null clears the rule."
                  },
                  "descriptionTrimMode": {
                    "type": "string",
                    "enum": [
                      "to_end",
                      "to_next"
                    ],
                    "description": "to_end (default) cuts to the end of the episode; to_next cuts until the next timestamp line."
                  },
                  "descriptionSkipPatterns": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 200
                    },
                    "maxItems": 50,
                    "description": "Per-feed episode description blacklist (issue 835). Each pattern is matched against the plain-text description (HTML tags removed, whitespace collapsed), case-insensitively (fnmatch glob); use '*' as a wildcard, e.g. '*This is a preview. To hear the entire episode*'. Without a wildcard the whole description must match, so a plain substring needs surrounding '*' wildcards. A matching episode is never queued from RSS refresh or JIT-processed; requests for a skipped episode's audio redirect to the upstream enclosure instead. Null or an empty array clears the blacklist."
                  },
                  "titleSkipAction": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "serve_original",
                      "hide",
                      null
                    ],
                    "description": "Served-RSS handling for an episode skipped by title, description, or duration. 'serve_original' (default, null) keeps original audio available; 'hide' removes the entry."
                  },
                  "minDurationSeconds": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "description": "Minimum publisher RSS duration in seconds, inclusive. Null clears this limit; omission preserves it. The effective minimum cannot exceed the maximum. Unknown or zero durations remain eligible; manual reprocessing overrides the filter."
                  },
                  "maxDurationSeconds": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "description": "Maximum publisher RSS duration in seconds, inclusive. Null clears this limit; omission preserves it. The effective maximum cannot be below the minimum. Filtering uses RSS duration before ad removal."
                  },
                  "cueTemplateScoreOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0.3,
                    "maximum": 0.99,
                    "description": "Per-feed ZNCC match threshold for audio cue template matching. Overrides the global audioCueTemplateScore setting for this feed. null or omitted clears the override and falls back to the global setting."
                  },
                  "cueCreateFromPairsOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override for cue-pair ad synthesis. true forces on, false forces off, null inherits the global audio_cue_create_from_pairs setting."
                  },
                  "cueCutMatchesOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override for cutting matched cue audio. true forces on, false forces off, null inherits the global audio_cue_cut_matches setting."
                  },
                  "cuePairMinBreakOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 600,
                    "description": "Per-feed minimum break duration (seconds) for cue-pair synthesis. null inherits the global setting."
                  },
                  "cuePairMaxBreakOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 3600,
                    "description": "Per-feed maximum break duration (seconds) for cue-pair synthesis. null inherits the global setting."
                  },
                  "cuePairMaxBreakFractionOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Per-feed maximum break fraction of episode duration for cue-pair synthesis. null inherits the global setting."
                  },
                  "cueSnapConfidenceOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Per-feed minimum cue confidence to snap an ad edge. null inherits the global audio_cue_snap_confidence setting."
                  },
                  "cueSnapLeadOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0.5,
                    "maximum": 30,
                    "description": "Per-feed snap lead window (seconds) before an ad edge. null inherits the global audio_cue_snap_lead_seconds setting."
                  },
                  "cueSnapLagOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0.5,
                    "maximum": 30,
                    "description": "Per-feed snap lag window (seconds) after an ad edge. null inherits the global audio_cue_snap_lag_seconds setting."
                  },
                  "silenceSnapEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed opt-in for snapping cut boundaries to nearby silence. Simple flag, not tri-state; null and false both read as off."
                  },
                  "transitionSnapEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed opt-in for snapping cut boundaries to content-transition cues. Simple flag, not tri-state; null and false both read as off."
                  },
                  "spliceVetoEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed splice check. Tri-state: null inherits the global spliceVetoEnabled setting, true forces the check on, false lets long cuts through without splice evidence."
                  },
                  "maxAdDurationOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 3600,
                    "description": "Per-feed max ad duration cap in seconds. Ads longer than this are held for review instead of cut. null means no cap (global MAX_AD_DURATION constants apply). Also raises the longest single detection the model may report for this feed (the global Longest single LLM detection, default 7 minutes)."
                  },
                  "adDetectionExcludeStartOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 600,
                    "description": "Per-feed opening window in seconds during which ad markers are ignored. null inherits adDetectionExcludeStartSeconds, 0 disables it for the feed, 1 to 600 sets the feed's own window."
                  },
                  "maxAdDurationRejectOverride": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 30,
                    "maximum": 3600,
                    "description": "Per-feed length past which an ad must name a recognized sponsor to be cut; one that does not is held for review. Must not exceed the global maxAdDurationConfirmedSeconds. null falls back to the global maxAdDurationSeconds setting. Also raises the longest single detection the model may report for this feed (the global maxLlmWindowAdSeconds, default 7 minutes), so set it to the length of the show's longest continuous ad block."
                  },
                  "cueGatedApproval": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed cue-gated approval. When true, only ads with audio cue evidence auto-cut; others are held for review. null and false both read as off."
                  },
                  "differentialFetchEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Legacy per-feed cross-fetch flag. Use differentialFetchMode for new clients; null selects automatic DAI detection."
                  },
                  "processingMode": {
                    "type": "string",
                    "enum": [
                      "passthrough",
                      "skip_detection",
                      "keep_content",
                      "standard",
                      "cue_only"
                    ],
                    "description": "Writable processing-mode preset. Sets passthroughEnabled, skipAdDetection, and detectionMode together. Cannot be combined with those fields in one request. cue_only requires at least one enabled ad_break_start and one enabled ad_break_end template on the feed; rejected with a 400 otherwise, since a boundary-only cue cannot pair safely without the LLM pass."
                  },
                  "passthroughEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "When true, episodes on this feed are downloaded and served exactly as published, with no transcription, detection, or cutting. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility."
                  },
                  "skipAdDetection": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "When true, episodes on this feed are transcribed and get chapters and a transcript, but ad detection is skipped and nothing is cut. For ad-free shows. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility."
                  },
                  "cueOnlySafety": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "hold_new",
                      "auto_cut",
                      null
                    ],
                    "description": "Per-feed safety policy for cue_only mode. 'hold_new' (default) holds a template's synthesized cuts for review until it has matched a paired start/end cue on 3 episodes. 'auto_cut' cuts immediately but still holds any pair scoring below 0.90 confidence. null resets to the default 'hold_new' behavior. Ignored outside cue_only mode."
                  },
                  "skipTranscription": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed opt-out from transcription. Only valid on a feed already in cue_only mode; rejected with a 400 otherwise. Generated chapters stop, and transcript search and VTT subtitles become unavailable for those episodes, but publisher chapters (embedded ID3 or linked Podcasting 2.0 JSON) still get remapped onto the cut audio. Null and false both mean transcription still runs."
                  },
                  "maxEpisodes": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Per-feed cap on served episode count. Subscribed feeds: clamped to 10-500; set null to fall back to the global maxFeedEpisodes setting. Local feeds: 0 or null serves every episode (the default); a positive value up to 10000 serves only the newest N."
                  },
                  "onlyExposeProcessedEpisodes": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override. true hides non-processed episodes; false shows everything; null falls back to the global onlyExposeProcessedDefault setting."
                  },
                  "requireManualApproval": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override. true keeps newly processed episodes out of the served feed until approved; false publishes them at once; null falls back to the global requireManualApprovalDefault setting."
                  },
                  "retentionDaysOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 3650,
                    "description": "Per-feed retention override. Null falls back to the global retentionDays setting; 0 archives the feed so processed audio is never deleted, including by the force-all cleanup; 1-3650 keeps it for that many days."
                  },
                  "audioReplacementSoundOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Override replacement sound for this feed; null inherits the global choice."
                  },
                  "audioMp3StreamCopyOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Override MP3 stream copy for this feed; null inherits the global choice."
                  },
                  "keepOriginalAudioOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override for retaining the pre-cut original audio. Null falls back to the global keepOriginalAudio setting; true always keeps it; false never does. Applies from the next episode processed."
                  },
                  "ytProcessDelayMinutesOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1440,
                    "description": "YouTube feeds only. Minutes to wait after a video is published before processing. Null follows the global setting."
                  },
                  "ytProbeAttemptsOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 48,
                    "description": "YouTube feeds only. SponsorBlock lookup attempts. Null follows the global setting."
                  },
                  "ytProbeIntervalMinutesOverride": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 5,
                    "maximum": 1440,
                    "description": "YouTube feeds only. Minutes between SponsorBlock lookup attempts. Null follows the global setting."
                  },
                  "ytLlmFallbackOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Run LLM detection when SponsorBlock has no data. Null follows the global setting."
                  },
                  "ytLlmOnTopOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Run LLM detection on top of SponsorBlock segments. Null follows the global setting."
                  },
                  "ytCaptionsOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Use YouTube captions as the transcript. Null follows the global setting."
                  },
                  "ytHidePrivateOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Hide private and deleted episodes from the listing. Null follows the global setting."
                  },
                  "ytTranscribeOverride": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Transcribe with the configured transcriber when captions are missing. Null follows the global setting."
                  },
                  "youtubeIncludeShorts": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "YouTube feeds only. Whether videos of 180 seconds or less are listed. Null follows the default."
                  },
                  "segmentCategoryActions": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "remove",
                        "beep",
                        "keep",
                        "mark"
                      ]
                    },
                    "description": "Per-feed segment-category action overrides (issue"
                  },
                  "detectShowSegments": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed opt-in to detect intro, outro, and recap/housekeeping segments in addition to the always-detected ad-side categories. true/false override the global detectShowSegments setting for this feed; null falls back to it."
                  },
                  "skipSecondPass": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "When true, the pass-2 verification scan of the cut audio is skipped. Null follows the global skipSecondPass setting."
                  },
                  "transcriptDifferential": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Per-feed override for upstream-transcript differential. True enables and false disables it for this feed; null follows the global transcriptDifferentialEnabled setting."
                  },
                  "differentialFetchMode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "inherit",
                      "auto",
                      "on",
                      "off",
                      null
                    ],
                    "description": "Per-feed cross-fetch mode. 'inherit' follows the global default, 'auto' follows DAI detection, and 'on' or 'off' forces the stage."
                  },
                  "ownEpisodeGuids": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "When true, the served RSS uses MinusPod episode ids as item GUIDs instead of passing upstream GUIDs through (issue 598). Switching it on a feed with subscribers makes podcast apps treat every episode as new once. Null and false both mean upstream GUIDs. New feeds are created with true."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Feed description, rewritten into the served channel <description>. Editable on any feed, local or subscribed, unlike title and p20 below, which are local-feed only (author, explicit and categories also work on YouTube feeds)."
                  },
                  "title": {
                    "type": "string",
                    "description": "Local feeds only (issue #625); 400 on a subscribed feed, whose title is RSS-managed and would be overwritten on the next refresh anyway; use titleOverride there instead. Must be non-empty."
                  },
                  "author": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "itunes:author. Local and YouTube feeds; 400 on a subscribed feed."
                  },
                  "explicit": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "itunes:explicit. Local and YouTube feeds; 400 on a subscribed feed. Null clears."
                  },
                  "categories": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "itunes:category list. Local and YouTube feeds; 400 on a subscribed feed. Null clears."
                  },
                  "p20": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/LocalFeedP20"
                      }
                    ],
                    "description": "Local feeds only; 400 on a subscribed feed. Merged onto the feed's existing stored p20 object, so an unspecified tag is left untouched, except sending the literal JSON null, which clears the six list tags and any stored locked_owner in one shot (see LocalFeedP20). guid can never be set here."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feed updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feed"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/refresh": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Refresh a single feed",
        "description": "Fetch the latest RSS feed and update the episode list. By default\nthis sends a conditional GET. If upstream returns 304 Not\nModified, the refresh is a no-op. Pass `{\"force\": true}` in the\nbody to bypass the stored ETag / Last-Modified headers and do a\nfull fetch, useful when an upstream CDN is falsely returning 304\nagainst its own new content.\n",
        "operationId": "refreshFeed",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Optional force-refresh flag. Omit for default conditional GET.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "description": "When true, clears the podcast's stored ETag +\nLast-Modified values before fetching so upstream\ncannot short-circuit with 304 Not Modified.\n",
                    "default": false
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feed refreshed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "slug",
                    "message",
                    "outcome",
                    "episodeCount",
                    "lastRefreshed",
                    "lastPodpingAt"
                  ],
                  "properties": {
                    "slug": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "outcome": {
                      "$ref": "#/components/schemas/RefreshOutcome"
                    },
                    "episodeCount": {
                      "type": "integer"
                    },
                    "lastRefreshed": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "lastPodpingAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Feed has no upstream source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "description": "Upstream fetch failed or returned content that could not be parsed as RSS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/refresh": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Refresh all feeds",
        "description": "Refresh RSS feeds for all configured podcasts. By default this\nsends a conditional GET per feed. If upstream returns 304 Not\nModified, that feed is a no-op. Pass `{\"force\": true}` in the\nbody to clear every podcast's stored ETag / Last-Modified before\nfetching so upstream cannot short-circuit with 304.\n",
        "operationId": "refreshAllFeeds",
        "requestBody": {
          "required": false,
          "description": "Optional force-refresh flag. Omit for default conditional GET per feed.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "description": "When true, clears every podcast's stored ETag +\nLast-Modified values before fetching so upstream\ncannot short-circuit with 304 Not Modified.\n",
                    "default": false
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "All feeds refreshed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefreshAllResult"
                }
              }
            }
          },
          "207": {
            "description": "Refresh pass completed with at least one feed failure",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefreshAllResult"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/feeds/refresh-artwork": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Refresh cover art for all feeds",
        "description": "Re-pull each feed's cover art and rebuild its served RSS so the\ncover-art badge setting (artworkWatermarkEnabled, issue #420) takes\neffect. Unlike a feed refresh, this does not re-discover or queue\nepisodes, so it never triggers processing. Podcast apps still re-fetch\nthe feed on their own schedule before the change is visible.\n",
        "operationId": "refreshAllArtwork",
        "responses": {
          "200": {
            "description": "Artwork refreshed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "feedCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/regenerate": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Rebuild every served RSS with current URL settings (feed auth key, cover badge, base URL)",
        "description": "Re-fetch each feed's upstream source and re-render its served RSS so\nURL-affecting settings (feed auth key, cover-art badge, base URL)\ntake effect. Unlike a feed refresh, this never re-discovers or\nqueues episodes, so it cannot trigger processing. Podcast apps\nstill re-fetch the feed on their own schedule before the change\nis visible.\n",
        "operationId": "regenerateAllFeeds",
        "responses": {
          "200": {
            "description": "Feeds regenerated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "feedCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/artwork": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "Get feed artwork",
        "description": "Returns cached podcast artwork without admin authentication. Safe legacy slugs are accepted up to 200 characters.",
        "operationId": "getFeedArtwork",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Artwork image",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "head": {
        "tags": [
          "Feeds"
        ],
        "summary": "Check cached feed artwork",
        "description": "Same public validation and response headers as GET, without a body.",
        "operationId": "headFeedArtwork",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Artwork exists"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Upload cover art for a local, recents or YouTube feed",
        "description": "Local, recents and YouTube feeds only (400 on a subscribed RSS feed, whose artwork comes from the upstream RSS on refresh). JPEG or PNG, detected from the file bytes rather than the client-supplied content type. Bound by the app's default 10 MB request cap. A response under 1400x1400 still succeeds but carries a warning field, since podcast directories generally expect at least that.",
        "operationId": "uploadFeedArtwork",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Artwork uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "artworkUrl": {
                      "type": "string"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present when the image is smaller than 1400x1400 in either dimension."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local, recents or YouTube feed, missing file, or not a JPEG/PNG image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/ad-distribution": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "Get a feed's ad-position distribution",
        "description": "Returns where this feed's ad cuts have historically landed across an episode, as a normalized-position histogram plus the learned prior zones. Setting-independent: returns data even when the positional-prior experiment is disabled.\n",
        "operationId": "getAdDistribution",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Ad distribution for the feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdDistribution"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "List episodes for a feed",
        "description": "Returns paginated list of episodes for a podcast. For the recents feed the rows come from the source feeds (see feedSlug and feedTitle); they are all processed, so a status filter other than processed returns an empty page. Title search applies before pagination. With selection=true, pagination is ignored and selection contains every matching episode's action state, up to 501 rows (truncated is true past that cap); episodes is empty. Bulk actions still accept at most 500 IDs.",
        "operationId": "listEpisodes",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by episode status. `filtered` lists the episodes the feed's title, description, and duration filters match.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "discovered",
                "pending",
                "processing",
                "processed",
                "failed",
                "permanently_failed",
                "deferred",
                "filtered"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of episodes to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of episodes to skip",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Field to sort episodes by",
            "schema": {
              "type": "string",
              "enum": [
                "published_at",
                "created_at",
                "episode_number",
                "title",
                "status"
              ],
              "default": "published_at"
            }
          },
          {
            "name": "sort_dir",
            "in": "query",
            "description": "Sort direction",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive literal title substring, trimmed before matching. Searches the full feed, including Recents; wildcard characters are literal.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "selection",
            "in": "query",
            "description": "Return matching action-state rows in selection and an empty episodes array, capped at 501 rows (see truncated). Ignores limit and offset for selecting rows; limit still reports the normal capped request value and offset is 0.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of episodes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EpisodeSummary"
                      }
                    },
                    "selection": {
                      "type": "array",
                      "description": "Present only when selection=true; every matching episode without pagination, up to 501 rows.",
                      "items": {
                        "$ref": "#/components/schemas/EpisodeSelection"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "Present only when selection=true; true when more than 501 episodes matched and selection was capped."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Upload a single episode into a local feed",
        "description": "Local feeds only. episode_id is minted as s{season:02d}e{episode:02d}; season defaults to 1, episode defaults to one past the highest existing episode number in that season. 409 if that id already exists; use the bulk import commit with overwrite for that. Widened request cap: audio up to 1 GB, streamed to a temp file on the same volume as the episode's final storage path, not buffered fully in memory.",
        "operationId": "uploadLocalEpisode",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "audio"
                ],
                "properties": {
                  "audio": {
                    "type": "string",
                    "format": "binary",
                    "description": ".mp3 only, up to 1 GB."
                  },
                  "season": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                  },
                  "episode": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Defaults to one past the highest existing episode number in this season."
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "publishedAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601, timezone required. Defaults to the upload time."
                  },
                  "artwork": {
                    "type": "string",
                    "format": "binary",
                    "description": "Optional episode cover, JPEG or PNG, up to 10 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Episode created",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/EpisodeDetail"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "episodeNumber": {
                          "type": "integer"
                        },
                        "seasonNumber": {
                          "type": "integer"
                        },
                        "queued": {
                          "type": "boolean",
                          "description": "Whether this upload was queued for auto-process (only possible when the feed already had at least one episode before this upload)."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, no audio field, wrong extension, or unplayable audio",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "An episode with this season/episode already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the configured local upload size limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadTooLarge"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Bulk-edit local episodes",
        "description": "Local feeds only. Every entry is validated before any are applied (atomic): one invalid entry 400s the whole request with nothing written. Up to 500 entries per request. RSS is regenerated once at the end, not per entry.",
        "operationId": "bulkPatchLocalEpisodes",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "minItems": 1,
                "maxItems": 500,
                "items": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "episodeId"
                      ],
                      "properties": {
                        "episodeId": {
                          "type": "string",
                          "description": "sNNeNN local episode id."
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LocalEpisodeEditFields"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Episodes updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, not a list, empty, over 500 entries, duplicate episodeId, or an invalid field on any entry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Feed not found, or an entry names an episodeId that does not exist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get episode details",
        "description": "Returns detailed information about an episode including ad markers",
        "operationId": "getEpisode",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Episode details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EpisodeDetail"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Edit a local episode",
        "description": "Local feeds only. season/episode change only the display season_number/episode_number columns; the episode_id minted at upload time never changes.",
        "operationId": "patchLocalEpisode",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LocalEpisodeEditFields"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Episode updated",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/EpisodeDetail"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "episodeNumber": {
                          "type": "integer"
                        },
                        "seasonNumber": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, or an invalid field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Delete a local episode",
        "description": "Local feeds only. Hard-deletes the row and its files. Unlike a subscribed feed's episode delete, there is no upstream to rediscover it from, so it is not reset to 'discovered' for a future refresh to pick back up.",
        "operationId": "deleteLocalEpisode",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Episode deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "integer"
                    },
                    "episodeId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Episode is currently processing; cancel it first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/artwork": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get episode artwork",
        "description": "Returns the episode's cover, fetching and caching it from the publisher on first request. Publishers block images requested with a cross-site Referer, so the web UI asks here instead of loading them from the publisher directly. The URL comes from the episode record, never from the caller. Redirects to the feed cover when the episode has no cover of its own or the publisher refuses the fetch.\n",
        "operationId": "getEpisodeArtwork",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Artwork image",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "No episode cover available; redirects to the feed cover"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      },
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Upload cover art for a single local episode",
        "description": "Local feeds only. JPEG or PNG, up to 10 MB, detected from the file bytes.",
        "operationId": "uploadLocalEpisodeArtwork",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Artwork uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "episodeId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, missing file, too large, or not a JPEG/PNG image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/transcript": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get episode transcript",
        "description": "Returns the full transcript text for an episode",
        "operationId": "getEpisodeTranscript",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Episode transcript",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "transcript": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/original-transcript": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get original (pre-cut) transcript",
        "description": "Returns the original transcript text captured before ad segments were removed. Only available for episodes processed with v1.0.51+.",
        "operationId": "getOriginalTranscript",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Original transcript",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "originalTranscript": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/original-segments": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get original (pre-cut) transcript segments",
        "description": "Returns the original transcript segments with timestamps, captured before any ads were removed. Only available for episodes processed with v2.0.26+.",
        "operationId": "getOriginalSegments",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Original segments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "text": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/final-segments": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Get final (post-cut) segments",
        "description": "Returns the post-cut transcript segments with adjusted timestamps after ad removal. Only available for episodes processed with v2.0.26+.",
        "operationId": "getFinalSegments",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Final segments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "text": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/runs/{run_number}/log": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Pipeline log for one processing run",
        "description": "Returns the captured pipeline log for a single processing run, either\nas parsed JSONL lines or as the raw file. Logs are stored when the\nfeed opts in and the retention setting is above zero; the 404 body\ndistinguishes a run that stored no log from one whose log the\nretention sweep removed.\n",
        "operationId": "getEpisodeRunLog",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "in": "path",
            "name": "run_number",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Run number, matching processingRuns[].runNumber"
          },
          {
            "in": "query",
            "name": "format",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "raw"
              ]
            },
            "description": "json (default) or raw for the JSONL file as a download"
          },
          {
            "in": "query",
            "name": "level",
            "schema": {
              "type": "string",
              "enum": [
                "debug",
                "info",
                "warning",
                "error",
                "critical"
              ]
            },
            "description": "Minimum level to include; json format only. Levels the server does not know rank above every filter, so they are always returned."
          }
        ],
        "responses": {
          "200": {
            "description": "Run log",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runNumber": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the run hit the size cap"
                    },
                    "bytes": {
                      "type": "integer"
                    },
                    "lines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ts": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "level": {
                            "type": "string"
                          },
                          "logger": {
                            "type": "string"
                          },
                          "msg": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "The raw JSONL file (format=raw)"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "No log for this run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "log_not_stored",
                        "log_pruned"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/peaks": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Waveform peaks for an episode window",
        "description": "Returns ffmpeg-derived waveform peaks for the requested window of an\nepisode's retained original audio. Drives the wavesurfer.js view in\nthe ad editor. The server auto-coarsens the resolution\nfor very long windows so the JSON payload stays bounded; the response\nechoes the effective resolution actually used.\n",
        "operationId": "getEpisodePeaks",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "in": "query",
            "name": "start",
            "schema": {
              "type": "number"
            },
            "description": "Window start in seconds (default 0)"
          },
          {
            "in": "query",
            "name": "end",
            "schema": {
              "type": "number"
            },
            "description": "Window end in seconds (default = full duration)"
          },
          {
            "in": "query",
            "name": "resolution_ms",
            "schema": {
              "type": "integer"
            },
            "description": "Requested peak bucket width in ms (default 50, range 1-1000)"
          }
        ],
        "responses": {
          "200": {
            "description": "Peaks payload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "start": {
                      "type": "number"
                    },
                    "end": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "resolutionMs": {
                      "type": "integer"
                    },
                    "peaks": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/transcript-span": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Transcript text for a [start, end] window",
        "description": "Returns the transcript text that spans the requested window of an\nepisode. Used by the AdEditor create mode to auto-populate the\ntext_template field when the user marks a new ad.\n",
        "operationId": "getEpisodeTranscriptSpan",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "in": "query",
            "name": "start",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "end",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript span",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "start": {
                      "type": "number"
                    },
                    "end": {
                      "type": "number"
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/split-candidates": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Proposed divider points for a multi-sponsor ad marker",
        "description": "Finds ad-transition phrases in the transcript spanning the requested\nwindow and maps each match back to the start of the transcript segment\nit falls in, so the ad editor opens with dividers where a split would\nactually cut (issue #563).\n\nA window with no transition phrase returns no candidates and a single\npiece. That is a valid answer, not an error: the block may genuinely be\none ad, and the user can still add a divider by hand.\n\nCandidates that would leave a piece shorter than the minimum ad\nduration are omitted, so the editor never opens in an invalid state.\n",
        "operationId": "getEpisodeSplitCandidates",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "in": "query",
            "name": "start",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "end",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Divider candidates and the pieces they would produce",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "start": {
                      "type": "number"
                    },
                    "end": {
                      "type": "number"
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "time": {
                            "type": "number"
                          },
                          "phrase": {
                            "type": "string",
                            "description": "The transition phrase that produced this candidate"
                          }
                        }
                      }
                    },
                    "pieces": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "text": {
                            "type": "string"
                          },
                          "sponsor": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/episodes/{slug}/{episode_id}/reprocess": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Force reprocess an episode",
        "description": "Queues an episode for reprocessing. Full and default reprocesses retain\nthe saved transcript until fresh transcription begins.\nSupports four modes:\n- `reprocess` (default): Uses learned patterns from the pattern database plus AI analysis\n- `full`: Skips the pattern database entirely for a fresh AI-only analysis\n- `llm`: Re-runs ad detection and re-cut using the SAVED transcript,\n  skipping re-transcription. Returns 400 if no transcript exists.\n\nReprocessed episodes are prioritized in the processing queue.\n",
        "operationId": "reprocessEpisode",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "reprocess",
                      "full",
                      "llm",
                      "recut"
                    ],
                    "default": "reprocess",
                    "description": "Processing mode:\n- reprocess: Use pattern DB + AI (leverages learned patterns)\n- full: AI only, skip pattern DB (fresh analysis)\n- llm: Re-detect + re-cut using the saved transcript (skips\n  re-transcription); requires an existing transcript\n- recut: Re-cut the retained original from the current ad\n  detections (applying edits) and re-time the saved\n  transcript; no transcription or AI. Requires the retained\n  original audio, saved segments, and existing ad markers\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Reprocessing started, or queued behind the active run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "episodeId": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "queued"
                      ]
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present when status is queued; why it could not start immediately."
                    },
                    "jobState": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "processing"
                      ],
                      "description": "Authoritative queue/run state, derived the same way as the episode's jobState field, so the client reconciles from one field."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Episode is already processing",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "jobState": {
                          "type": "string",
                          "enum": [
                            "processing"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/regenerate-chapters": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Regenerate chapters from transcript",
        "description": "Regenerates chapters for an episode using the existing VTT transcript.\nUses AI topic detection to identify natural chapter boundaries without\nrequiring full reprocessing. The VTT transcript must already exist.\nThe new chapters are also embedded into the processed MP3 as ID3\nframes so players that ignore podcast:chapters pick them up.\nThe work runs in the background because the topic pass can outlast\na reverse proxy's request timeout; the episode reports progress.\nIt starts on demand outside the processing queue. Each finished attempt\nadds a processing-history entry with its own usage and a saved episode\nlog when log storage is enabled, including failed attempts.\n",
        "operationId": "regenerateChapters",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "202": {
            "description": "Regeneration started in the background. Poll the episode; chaptersRegenerating clears when it finishes and chaptersRegenError carries a failure.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Chapter regeneration started"
                    },
                    "episodeId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "started"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No VTT transcript available (full reprocess required)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "A regeneration is already running, the episode is being processed, or an LLM provider rate-limit hold is active"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/sponsorblock/candidates": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "List segments reviewable for SponsorBlock",
        "description": "Lists the cut sponsor and self promotion segments of a YouTube episode that\ncarry evidence beyond the model, with any recorded submission outcome.\nSegments already submitted are returned so they can be shown as sent.\nallowed is false, with a reason, when submission is not possible.\n",
        "operationId": "getSponsorBlockCandidates",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate segments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "allowed",
                    "reason",
                    "segments"
                  ],
                  "properties": {
                    "allowed": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "service_down means SponsorBlock failed its last availability probe.",
                      "enum": [
                        "not_youtube",
                        "not_completed",
                        "disabled",
                        "no_user_id",
                        "community_marks",
                        "no_video_id",
                        "marks_unverified",
                        "service_down",
                        null
                      ]
                    },
                    "segments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "start",
                          "end",
                          "category",
                          "sbCategory",
                          "evidence",
                          "submission"
                        ],
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "category": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sbCategory": {
                            "type": "string",
                            "enum": [
                              "sponsor",
                              "selfpromo"
                            ]
                          },
                          "evidence": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "submission": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/SponsorBlockSubmission"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/sponsorblock/submit": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Submit reviewed segments to SponsorBlock",
        "description": "Sends the segments a person selected to SponsorBlock. SponsorBlock forbids\nautomated submissions, so this is the only way MinusPod sends anything.\nEach segment must match a candidate within 0.05 seconds and must not\nalready be submitted. Outcomes are recorded on the episode. A duplicate\nor rejected result is a recorded outcome and returns 200. Rate limited to\n10 per minute.\n",
        "operationId": "submitSponsorBlockSegments",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "segments"
                ],
                "properties": {
                  "segments": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "start",
                        "end"
                      ],
                      "properties": {
                        "start": {
                          "type": "number"
                        },
                        "end": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded outcomes, one per segment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SponsorBlockSubmitResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "SponsorBlock rate limited the submission, or the endpoint limit was reached. The outcome is recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SponsorBlockSubmitResult"
                }
              }
            }
          },
          "502": {
            "description": "The request to SponsorBlock failed. The outcome is recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SponsorBlockSubmitResult"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/rating": {
      "put": {
        "tags": [
          "Episodes"
        ],
        "summary": "Rate an episode",
        "description": "Stores a 1 to 5 quality rating. Notes are trimmed, limited to 2000\ncharacters and kept only below 5. A rating of 5 confirms every cut ad\nsegment through the same path as a confirm correction, without changing\nthe audio or queuing a recut. A 5 also dismisses every held segment\nwithout recording a correction.\n",
        "operationId": "putEpisodeRating",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rating"
                ],
                "properties": {
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored rating, how many cut segments were newly confirmed and how many held segments were dismissed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rating",
                    "confirmedMarkers",
                    "dismissedMarkers"
                  ],
                  "properties": {
                    "rating": {
                      "$ref": "#/components/schemas/EpisodeRating"
                    },
                    "confirmedMarkers": {
                      "type": "integer"
                    },
                    "dismissedMarkers": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Episodes"
        ],
        "summary": "Clear an episode rating",
        "description": "Clears the rating, notes and rating time. Confirmations already recorded stay.",
        "operationId": "deleteEpisodeRating",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "The rating is now null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rating": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/reprocess-all": {
      "post": {
        "tags": [
          "Processing"
        ],
        "summary": "Reprocess all episodes for a feed",
        "description": "Queue all processed episodes of a podcast for reprocessing. Useful when\nad detection logic has improved and you want to re-detect ads across all episodes.\n",
        "operationId": "reprocessAllEpisodes",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "reprocess",
                      "full",
                      "llm"
                    ],
                    "default": "reprocess",
                    "description": "Processing mode:\n- reprocess: Use pattern DB + AI (leverages learned patterns)\n- full: AI only, skip pattern DB (fresh analysis)\n- llm: Re-detect + re-cut using each saved transcript (skips\n  re-transcription); episodes without a transcript are skipped\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Episodes queued for reprocessing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "queued": {
                      "type": "integer",
                      "description": "Number of episodes queued"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Number of episodes skipped"
                    },
                    "mode": {
                      "type": "string"
                    },
                    "episodes": {
                      "type": "object",
                      "properties": {
                        "queued": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "episodeId": {
                                "type": "string"
                              },
                              "title": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "skipped": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "episodeId": {
                                "type": "string"
                              },
                              "reason": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/retry-ad-detection": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Retry ad detection",
        "description": "Re-runs ad detection using the existing transcript. Faster than full\nreprocess since it skips transcription. Use when ad detection failed\ndue to API errors or when you want to try with updated prompts.\n",
        "operationId": "retryAdDetection",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Ad detection retry completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "adsDetected": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Episode has no transcript (must use reprocess instead)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/reprocess": {
      "post": {
        "tags": [
          "Processing"
        ],
        "summary": "Force reprocess an episode (legacy)",
        "description": "Legacy URL for the mode-aware reprocess endpoint. Accepts the same modes\nas `/episodes/{slug}/{episode_id}/reprocess`; the default is `reprocess`.\n",
        "operationId": "reprocessEpisodeLegacy",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "reprocess",
                      "full",
                      "llm",
                      "recut"
                    ],
                    "default": "reprocess"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Reprocessing started or queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "episodeId": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "queued"
                      ]
                    },
                    "reason": {
                      "type": "string"
                    },
                    "jobState": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "processing"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Episode is currently processing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/original.mp3": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Stream the pre-cut (original) audio",
        "description": "Returns the untouched audio that was downloaded before ad removal.\nUsed by the Ad Editor's Review mode so users can hear what was\ncut. 404 when the episode has no original retained (feature disabled\nin settings, pre-existing episode processed before the feature, or\nretention expired). Supports Range requests.\n",
        "operationId": "serveOriginalAudio",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "$ref": "#/components/parameters/audio_download"
          }
        ],
        "responses": {
          "200": {
            "description": "Original audio stream",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/processed.mp3": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Stream the current cut (ad-free) audio",
        "description": "Returns the current processed version of the episode under the\nsession auth the UI already has. The public feed route serves the\nsame file but needs a feed key when authenticated feeds are on.\n404 when the episode has not been processed or the file is missing.\nSupports Range requests.\n",
        "operationId": "serveProcessedAudio",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          },
          {
            "$ref": "#/components/parameters/audio_download"
          }
        ],
        "responses": {
          "200": {
            "description": "Processed audio stream",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/queue-priority": {
      "post": {
        "tags": [
          "Processing"
        ],
        "summary": "Change a queued episode's priority",
        "description": "Direct per-row write on the pending auto_process_queue row (#696). Send `priority` for an absolute value or `delta` to nudge the stored one; a delta is applied in SQL so a stepper cannot lose a click to a stale read. Exactly one of the two is required. Unlike the monotonic MAX() rule on re-enqueue, either can raise or lower, and both clamp to [-1000, 1000]. A later feed-level queuePriority change restamps the row and overwrites this value.",
        "operationId": "setEpisodeQueuePriority",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "maxProperties": 1,
                "properties": {
                  "priority": {
                    "type": "integer",
                    "minimum": -1000,
                    "maximum": 1000,
                    "description": "New dequeue priority; higher processes sooner."
                  },
                  "delta": {
                    "type": "integer",
                    "minimum": -1000,
                    "maximum": 1000,
                    "description": "Amount to add to the stored priority. Applied server-side, then clamped to the same bounds."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Priority updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "episodeId": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "priority": {
                      "type": "integer",
                      "description": "The priority now stored, after clamping."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not a JSON object, neither or both of priority/delta were given, or the value is not an integer within bounds"
          },
          "404": {
            "description": "Feed not found, or the episode has no pending queue row"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cancel": {
      "post": {
        "tags": [
          "Processing"
        ],
        "summary": "Cancel episode processing",
        "description": "Cancel a running or queued episode. If the episode is processing, the\nworker thread is signalled to stop and the row is reset to pending. If\nthe episode is queued (waiting on the processing lock), it is dropped\nfrom the display queue and its `auto_process_queue` row is closed so\nthe background worker will not pick it up. Returns 400 only when the\nepisode is neither processing nor queued.\n",
        "operationId": "cancelEpisodeProcessing",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Episode canceled (running) or removed from queue (queued)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "\"Episode canceled and reset to pending\" (running) or\n\"Episode removed from queue\" (queued).\n"
                    },
                    "episodeId": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Episode is neither processing nor queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/episodes/pending-recuts": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Episodes with review decisions not yet cut into the audio",
        "description": "Review is bulk work, so a correction that changes the audio stamps its episode instead of re-cutting it. An episode collecting several decisions is listed once and cut once.",
        "operationId": "getPendingRecuts",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Limit the list to one feed's episodes."
          }
        ],
        "responses": {
          "200": {
            "description": "Pending episodes, oldest stamp first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "episodes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "podcast": {
                            "type": "string"
                          },
                          "pendingSince": {
                            "type": "string",
                            "description": "When the episode first went stale."
                          },
                          "recutReady": {
                            "type": "boolean",
                            "description": "An apply would queue this row now. False when the retained original audio, saved segments, or markers are missing (the decision then waits for a full reprocess) or while the episode's own run is queued or underway."
                          },
                          "inFlight": {
                            "type": "boolean",
                            "description": "The episode's own queued or running pass will apply the decisions itself."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/episodes/pending-recuts/apply": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Recut every episode holding unapplied review decisions",
        "description": "Queues one recut per pending episode, using the same recut path and preconditions as the per-feed segment re-render. An episode missing its retained original audio, or whose own run is queued or underway (that run applies the decisions), is skipped and keeps its stamp, so its decisions are not lost.",
        "operationId": "applyPendingRecuts",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "Recut only this feed's pending episodes. Omit to cover every feed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many recuts were queued and how many were skipped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "queued": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (5 per minute)"
          }
        }
      }
    },
    "/episodes/awaiting-approval": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Episodes waiting for manual approval",
        "description": "Processed episodes held out of the served feed and audio until approved, oldest first, across feeds.",
        "operationId": "listAwaitingApproval",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Limit the list to one feed's episodes."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Awaiting episodes and the total count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Total waiting episodes, ignoring limit and offset."
                    },
                    "episodes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "podcast": {
                            "type": "string"
                          },
                          "pendingSince": {
                            "type": "string",
                            "description": "When the episode began waiting for approval. Omitted from the held-detections list."
                          },
                          "adCount": {
                            "type": "integer"
                          },
                          "pendingReviewCount": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/episodes/awaiting-approval/approve": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Approve several waiting episodes",
        "description": "Approves the listed episodes, or every waiting episode of one feed when `slug` is sent instead of `episodes`. Each feed's RSS is rebuilt once. An episode that is not waiting is counted as skipped.",
        "operationId": "approveAwaitingEpisodes",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "episodes": {
                    "type": "array",
                    "maxItems": 500,
                    "items": {
                      "type": "object",
                      "required": [
                        "slug",
                        "episodeId"
                      ],
                      "properties": {
                        "slug": {
                          "type": "string"
                        },
                        "episodeId": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "slug": {
                    "type": "string",
                    "description": "Approve every waiting episode of this feed. Ignored when `episodes` is sent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many episodes were approved and how many were skipped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    },
                    "remaining": {
                      "type": "integer",
                      "description": "Waiting episodes of the feed left after the 500 oldest were approved. 0 for an `episodes` request."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "Rate limited (10 per minute)"
          }
        }
      }
    },
    "/episodes/held": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Episodes with detections held for review",
        "description": "Processed episodes with at least one detection held for human review, newest first, across feeds.",
        "operationId": "listHeldEpisodes",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Held episodes and the total count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Total episodes with held detections, ignoring limit and offset."
                    },
                    "episodes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "podcast": {
                            "type": "string"
                          },
                          "pendingSince": {
                            "type": "string",
                            "description": "When the episode began waiting for approval. Omitted from the held-detections list."
                          },
                          "adCount": {
                            "type": "integer"
                          },
                          "pendingReviewCount": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/approve": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Approve a waiting episode",
        "description": "Clears the approval wait, stamps `approvedAt`, rebuilds the feed RSS and fires the Episode Processed event.",
        "operationId": "approveEpisode",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The approved episode",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "approved": {
                      "type": "boolean"
                    },
                    "episode": {
                      "$ref": "#/components/schemas/EpisodeSummary"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The episode is not awaiting approval, or does not exist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/episodes/{slug}/{episode_id}/corrections": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Submit ad corrections",
        "description": "Submit corrections for detected ads in an episode. This feedback improves\nthe cross-episode pattern database for better future detection.\n\nActions:\n- confirm: Mark detection as correct (creates/updates pattern)\n- reject: Mark as false positive. Stores transcript text for cross-episode matching -\n  similar text in future episodes of the same podcast will be automatically excluded\n  using TF-IDF similarity matching (threshold: 0.75)\n- adjust: Correct ad boundaries. Also creates patterns from the adjusted boundaries\n  (like confirm), ensuring accurate pattern text is learned from the corrected segment\n- create: Mark a brand-new ad on an episode the detector missed. Top-level\n  start/end/sponsor/text_template required (no original_ad). Creates a new\n  pattern with created_by='user' and a pattern_corrections row of type 'create'.\n  If the text contains more than one ad transition phrase (a\n  contaminated multi-sponsor read), it is split at those boundaries into one\n  pattern per sponsor segment instead of a single oversized pattern; the\n  response's patternIds lists every pattern created or reused this way, and\n  pattern_id/sponsor still identify the sponsor-matched primary.\n- split: Replace one marker covering back-to-back ads with N\n  single-sponsor markers (issue #563). Send `original_ad` plus\n  `split_points` (a list of times strictly inside the span) and an\n  optional `pieces` array of `{sponsor}` overrides in piece order.\n  Every resulting piece must be at least the minimum ad duration or the\n  request is rejected and the marker is left untouched. A request whose\n  original_ad matches no persisted marker returns 404. Pieces inherit\n  the original's category, detection stage, confidence, and cut state,\n  so a split never silently changes whether the audio was removed.\n  Correction rows use boundary_adjustment for the first piece and\n  create for the rest: correction_type carries a CHECK constraint\n  SQLite cannot alter in place. Response carries markerCount and\n  patternIds.\n",
        "operationId": "submitCorrections",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          },
          {
            "$ref": "#/components/parameters/episode_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdCorrection"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Corrections submitted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "description": "Human-readable result, returned by every type"
                    },
                    "pattern_id": {
                      "type": "integer",
                      "description": "Pattern created or updated; returned by confirm, adjust, and create"
                    },
                    "sponsor": {
                      "type": "string",
                      "description": "Canonical sponsor name; returned by create"
                    },
                    "patternIds": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "Patterns created or reused; returned by create and split"
                    },
                    "markerCount": {
                      "type": "integer",
                      "description": "Number of markers that replaced the original; returned by split"
                    },
                    "category": {
                      "type": "string",
                      "description": "New segment category; returned by recategorize"
                    },
                    "previousCategory": {
                      "type": "string",
                      "description": "Segment category before the change; returned by recategorize"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Segment is kept for this feed; recategorize instead of confirm, reject, adjust, or split",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/episodes/processing": {
      "get": {
        "tags": [
          "Processing"
        ],
        "summary": "List processing and queued episodes",
        "description": "Returns active jobs, then waiting jobs in dequeue order. offset and limit page the waiting list. Queued rows include queuePosition and admission. queueTotal is the full waiting count and excludes active jobs.",
        "operationId": "listProcessingEpisodes",
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "description": "Waiting-list rows to skip (0-based). Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum waiting-list rows to return. Defaults to 200, capped at 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of processing episodes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProcessingEpisode"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/dashboard": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get dashboard statistics",
        "description": "Returns aggregate stats with avg/min/max for time saved, ads removed, cost, processing time, episode length, and token usage. Optionally filtered by podcast. Episode-scoped fields (totalEpisodesProcessed, episodesWithTimeSaved, and the episode length and time-saved figures) count each surviving episode once; run-scoped fields (ads, audio cues, cost, tokens, processing time, totalRuns) count every completed processing run, including reprocesses.",
        "parameters": [
          {
            "name": "podcast_slug",
            "in": "query",
            "description": "Filter by podcast slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalEpisodesProcessed": {
                      "type": "integer"
                    },
                    "totalRuns": {
                      "type": "integer"
                    },
                    "episodesWithTimeSaved": {
                      "type": "integer"
                    },
                    "avgTimeSavedSeconds": {
                      "type": "number"
                    },
                    "avgAdsRemoved": {
                      "type": "number"
                    },
                    "avgCostPerEpisode": {
                      "type": "number"
                    },
                    "avgProcessingTimeSeconds": {
                      "type": "number"
                    },
                    "avgEpisodeLengthSeconds": {
                      "type": "number"
                    },
                    "totalInputTokens": {
                      "type": "integer"
                    },
                    "totalOutputTokens": {
                      "type": "integer"
                    },
                    "totalLlmCost": {
                      "type": "number"
                    },
                    "avgInputTokens": {
                      "type": "integer"
                    },
                    "avgOutputTokens": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/by-day": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get processing counts by day of week",
        "parameters": [
          {
            "name": "podcast_slug",
            "in": "query",
            "description": "Filter by podcast slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-day stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "days": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string"
                          },
                          "dayIndex": {
                            "type": "integer"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "avgAds": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/by-podcast": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get per-podcast aggregate stats",
        "description": "episodeCount counts distinct surviving episodes with at least one completed run; runCount counts every completed processing run, including reprocesses.",
        "responses": {
          "200": {
            "description": "Per-podcast stats ordered by total ads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "podcasts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "podcastSlug": {
                            "type": "string"
                          },
                          "podcastTitle": {
                            "type": "string"
                          },
                          "episodeCount": {
                            "type": "integer"
                          },
                          "runCount": {
                            "type": "integer"
                          },
                          "totalAds": {
                            "type": "integer"
                          },
                          "avgAds": {
                            "type": "number"
                          },
                          "avgEpisodeLengthSeconds": {
                            "type": "number"
                          },
                          "avgTimeSavedSeconds": {
                            "type": "number"
                          },
                          "totalCost": {
                            "type": "number"
                          },
                          "totalInputTokens": {
                            "type": "integer"
                          },
                          "totalOutputTokens": {
                            "type": "integer"
                          },
                          "avgTokensPerEpisode": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/reviewer": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get ad reviewer aggregate stats",
        "description": "Aggregate counts and averages from the ad_reviewer_log table.\nReturns zero counts when the reviewer has not run.\n",
        "parameters": [
          {
            "name": "podcast_slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reviewer stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalReviews": {
                      "type": "integer"
                    },
                    "verdictCounts": {
                      "type": "object",
                      "properties": {
                        "confirmed": {
                          "type": "integer"
                        },
                        "adjust": {
                          "type": "integer"
                        },
                        "reject": {
                          "type": "integer"
                        },
                        "resurrect": {
                          "type": "integer"
                        },
                        "inconclusive": {
                          "type": "integer"
                        },
                        "failure": {
                          "type": "integer"
                        }
                      }
                    },
                    "pass1AdjustmentCount": {
                      "type": "integer"
                    },
                    "pass2AdjustmentCount": {
                      "type": "integer"
                    },
                    "avgBoundaryShiftSeconds": {
                      "type": "number"
                    },
                    "resurrectionCount": {
                      "type": "integer"
                    },
                    "failureCount": {
                      "type": "integer"
                    },
                    "byMode": {
                      "type": "object",
                      "description": "Verdicts per addressing mode the ad was detected under. Rows logged before 2.100.12 carry no mode and are excluded.",
                      "properties": {
                        "timestamps": {
                          "$ref": "#/components/schemas/ReviewerModeStats"
                        },
                        "segment_ids": {
                          "$ref": "#/components/schemas/ReviewerModeStats"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/addressing": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get per-addressing-mode compliance stats",
        "description": "Aggregate counts from the addressing_log table, grouped by effective\naddressing mode (timestamps / segment_ids). A 'random'-configured run\nrecords under whichever mode was drawn for that pass. Always returns\nboth modes, zeroed when a mode has no rows.\n",
        "parameters": [
          {
            "name": "podcast_slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Addressing-mode compliance stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "modes": {
                      "type": "object",
                      "properties": {
                        "timestamps": {
                          "$ref": "#/components/schemas/AddressingModeStats"
                        },
                        "segment_ids": {
                          "$ref": "#/components/schemas/AddressingModeStats"
                        }
                      }
                    },
                    "byMode": {
                      "type": "object",
                      "description": "Outcomes per addressing mode the marker was detected under. Rates are fractions of corrections that carry a mode; avgSnapDistance covers stored markers from the last 90 days. Null when a mode has no data.",
                      "properties": {
                        "timestamps": {
                          "$ref": "#/components/schemas/AddressingModeOutcomes"
                        },
                        "segment_ids": {
                          "$ref": "#/components/schemas/AddressingModeOutcomes"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/stats/model-usage": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get paginated spend by provider and model",
        "description": "Paginated, sortable, filterable breakdown of the llm_call_usage\nledger grouped by (provider, model). A model id shared by two\nproviders is reported as two separate rows. Spend includes billed\nfailures, not only successes; unknownCostCount counts billable\nrows whose cost could not be resolved so an unpriced model's spend\nstays visibly incomplete instead of reading as $0.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "description": "Unrecognized values fall back to knownCostUsd.",
            "schema": {
              "type": "string",
              "enum": [
                "provider",
                "model",
                "calls",
                "distinctEpisodes",
                "inputTokens",
                "outputTokens",
                "knownCostUsd",
                "unknownCostCount",
                "cacheHitRate"
              ],
              "default": "knownCostUsd"
            }
          },
          {
            "name": "sortDir",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start of the window. Accepts a bare YYYY-MM-DD date or an ISO 8601 timestamp, and selects whole UTC days: the value is truncated to the start of its UTC day, so that whole day is included. An unparseable value is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "End of the window, inclusive of the whole UTC day it names, down to its final fractional second. Same accepted formats as from, and the same 400 on an unparseable value.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated provider/model spend",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "provider": {
                            "type": "string"
                          },
                          "model": {
                            "type": "string"
                          },
                          "calls": {
                            "type": "integer"
                          },
                          "distinctEpisodes": {
                            "type": "integer"
                          },
                          "inputTokens": {
                            "type": "integer"
                          },
                          "outputTokens": {
                            "type": "integer"
                          },
                          "cacheReadTokens": {
                            "type": "integer",
                            "description": "Summed from the ledger; cached tokens are counted inside inputTokens."
                          },
                          "cacheWriteTokens": {
                            "type": "integer",
                            "description": "Summed from the ledger; cached tokens are counted inside inputTokens."
                          },
                          "knownCostUsd": {
                            "type": "string",
                            "description": "Decimal string; sum of rows with a resolved cost."
                          },
                          "unknownCostCount": {
                            "type": "integer",
                            "description": "Billable rows whose cost could not be resolved."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/stats/systemone": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get native System One call statistics",
        "description": "Logical calls include failures before HTTP dispatch. Requests count actual reserved dispatches; logical latency and dispatch latency are measured separately in milliseconds. Known cost is a decimal USD sum; unknown usage and cost counts include failed and cancelled requests. Only allowlisted diagnostic evidence is included; prompts, transcripts, and credentials are excluded. Statistics are retained for the ledger lifetime. Deleting a feed removes its associated diagnostics.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Start of the selected UTC date range; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Inclusive final UTC day; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by exact podcast scope."
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by provider key."
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by configured model ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Native logical-call and request aggregates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SystemOneStats"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/stats/llm": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get LLM call statistics",
        "description": "Counts ledger rows from non-System One providers as calls; requests count actual dispatches. Latencies are in milliseconds, known cost is a decimal USD sum, and unknown usage and cost counts include failed and cancelled requests.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Start of the selected UTC date range; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Inclusive final UTC day; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by exact podcast scope."
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by provider key."
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by configured model ID."
          }
        ],
        "responses": {
          "200": {
            "description": "LLM call and request aggregates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LlmStats"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/stats/cleanup": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get Pattern Cleanup accounting and actions",
        "description": "Attributed usage, checks, and proposals use the cleanup run start date. Separate unattributedUsage uses request time for older unlinked ledger rows and is absent under a podcastSlug filter. Only podcast-scoped patterns have exact feed allocation; global, network, and historical unknown scopes are unallocated. Provider/model filters require exact attempt attribution. Checked counts are attempts, including failed or stale checks. Historical reported reviews are separate from exact checks; historical per-run spend is unavailable. Superseded proposals and compact records of deleted patterns preserve action totals. Applied means approved and not undone, including actions on subsequently deleted patterns; it does not mean the pattern remains active.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Start of the selected UTC date range; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Inclusive final UTC day; accepts a date or ISO 8601 timestamp."
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by exact podcast scope."
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by provider key."
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by configured model ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Exact cleanup cohorts and historical coverage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CleanupStats"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/stats/episode-costs": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get paginated per-episode LLM cost",
        "description": "Paginated, sortable, filterable per-episode breakdown of the\nllm_call_usage ledger. The provider/model filter never reorders\nwhich run counts as latest: latestRunCostUsd and lastActivityAt\nalways reflect the episode's actual latest run, honoring the\nfrom/to window if set. cumulativeCostUsd sums only the runs that\nmatch every filter, including provider/model, so the two cost\nfigures diverge once an episode has been reprocessed.\n",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "description": "Unrecognized values fall back to lastActivityAt.",
            "schema": {
              "type": "string",
              "enum": [
                "podcastSlug",
                "podcastTitle",
                "episodeTitle",
                "runCount",
                "latestRunCostUsd",
                "cumulativeCostUsd",
                "lastActivityAt"
              ],
              "default": "lastActivityAt"
            }
          },
          {
            "name": "sortDir",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start of the window. Accepts a bare YYYY-MM-DD date or an ISO 8601 timestamp, and selects whole UTC days: the value is truncated to the start of its UTC day, so that whole day is included. An unparseable value is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "End of the window, inclusive of the whole UTC day it names, down to its final fractional second. Same accepted formats as from, and the same 400 on an unparseable value.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated per-episode cost",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "podcastSlug": {
                            "type": "string"
                          },
                          "podcastTitle": {
                            "type": "string"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "episodeTitle": {
                            "type": "string"
                          },
                          "modelsUsed": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "runCount": {
                            "type": "integer"
                          },
                          "latestRunCostUsd": {
                            "type": "string",
                            "description": "Decimal string; cost of the episode's actual latest run. Not narrowed by the provider/model filter."
                          },
                          "cumulativeCostUsd": {
                            "type": "string",
                            "description": "Decimal string; cost summed across every run matching all filters, including provider/model."
                          },
                          "lastActivityAt": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/stats/episode-costs/runs": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get per-run cost breakdown for one episode",
        "description": "Per-run, per-phase cost rows for one episode, used by the expandable\nEpisode costs row in Stats. Same run shape as the episode detail\npage's processingRuns.\n",
        "operationId": "getEpisodeCostRuns",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episodeId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Runs for the episode, oldest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EpisodeProcessingRun"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "slug and episodeId are required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Episode not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/stats/spend/attempts": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "List the ledger attempts behind a run's or episode's spend",
        "description": "The individual llm_call_usage rows a spend total was summed from,\nwhich the Stats Incomplete chip links to so an unknown cost can be\ntraced to the attempt carrying no price. Scope it with runId, or with\nslug plus episodeId to cover every run of one episode. Applies the\nsame billable predicate as the totals, so the listed rows are exactly\nthe contributing ones. Capped at 200 rows; truncated says when more\nexist, and the totals still cover them.\n",
        "operationId": "getSpendAttempts",
        "parameters": [
          {
            "name": "runId",
            "in": "query",
            "description": "The run whose attempts to list. Required unless slug and episodeId are given.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "query",
            "description": "Feed slug, paired with episodeId to scope to one episode.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episodeId",
            "in": "query",
            "description": "Episode id, paired with slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider",
            "in": "query",
            "description": "Limit the listing to one provider key.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contributing attempts for the run",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "attempts",
                    "total",
                    "unknownCostCount",
                    "knownCostUsd",
                    "truncated"
                  ],
                  "properties": {
                    "runId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "episodeId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "provider": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "total": {
                      "type": "integer",
                      "description": "Number of attempts returned."
                    },
                    "unknownCostCount": {
                      "type": "integer",
                      "description": "Attempts with no recorded price."
                    },
                    "knownCostUsd": {
                      "type": "string",
                      "description": "Decimal string sum of the priced attempts."
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "attempts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "attemptId",
                          "phase",
                          "provider",
                          "model",
                          "status"
                        ],
                        "properties": {
                          "attemptId": {
                            "type": "string"
                          },
                          "phase": {
                            "type": "string"
                          },
                          "invokingPass": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "provider": {
                            "type": "string"
                          },
                          "credentialSlot": {
                            "type": "string",
                            "description": "Account slot; a null column reads as primary."
                          },
                          "model": {
                            "type": "string"
                          },
                          "returnedModel": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "description": "Ledger state. Inconclusive means the reviewer abstained and the original marker was retained.",
                            "enum": [
                              "success",
                              "failure",
                              "inconclusive"
                            ]
                          },
                          "inputTokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "outputTokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "cacheReadTokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "cacheWriteTokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "costUsd": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Decimal string, or null when the call was never priced."
                          },
                          "costSource": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "finalizedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither runId nor slug plus episodeId was given"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/stats/ledger-filter-options": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "List the provider and model values present in the usage ledger",
        "description": "Distinct providers and models the llm_call_usage ledger holds, for\npopulating the Stats page filter dropdowns. It honors the same\nfrom/to/podcastSlug scope as the two list endpoints, so the options\ndescribe the data actually in view, but it is never paginated: a\nprovider or model that only appears on a later page of a list\nrequest is still selectable. Rows with a null provider or model are\ndropped. providers is sorted and deduplicated; pairs preserves which\nmodels belong to which provider, so a model id served by two\nproviders stays distinguishable.\n",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Same whole-UTC-day semantics as the list endpoints. Bare YYYY-MM-DD or ISO 8601; unparseable is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Inclusive of the whole UTC day it names. Unparseable is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Filter options for the ledger lists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "providers",
                    "pairs"
                  ],
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "pairs": {
                      "type": "array",
                      "description": "Every distinct provider and model combination, ordered by provider then model.",
                      "items": {
                        "type": "object",
                        "required": [
                          "provider",
                          "model"
                        ],
                        "properties": {
                          "provider": {
                            "type": "string"
                          },
                          "model": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "providers": [
                    "anthropic",
                    "openrouter"
                  ],
                  "pairs": [
                    {
                      "provider": "anthropic",
                      "model": "example-model-pro"
                    },
                    {
                      "provider": "openrouter",
                      "model": "example-model-mini"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/LedgerDateBadRequest"
          }
        }
      }
    },
    "/patterns": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "List ad patterns",
        "description": "Returns all ad patterns from the cross-episode pattern database.\nPatterns are organized by scope (global, network, podcast) and can be\nfiltered to show only active patterns.\n",
        "operationId": "listPatterns",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "description": "Filter by pattern scope",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "global",
                "network",
                "podcast"
              ],
              "default": "all"
            }
          },
          {
            "name": "active_only",
            "in": "query",
            "description": "Only return active patterns. Accepts the legacy `active` query param as an alias.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "podcast_id",
            "in": "query",
            "description": "Filter to patterns scoped to this podcast slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "network_id",
            "in": "query",
            "description": "Filter to patterns scoped to this network id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Filter by pattern source",
            "schema": {
              "type": "string",
              "enum": [
                "local",
                "community",
                "imported"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of patterns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "patterns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Pattern"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/corrections/unresolved": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "List unassigned legacy corrections",
        "description": "Candidates are current feed episodes or durable processing-history entries for an active feed. History candidates include their entry count and latest processing timestamp. They do not imply a current episode, transcript, or media file exists.",
        "responses": {
          "200": {
            "description": "Unassigned corrections and safe feed candidates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Total unassigned corrections, which may exceed the page returned"
                    },
                    "corrections": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "episode_id": {
                            "type": "string"
                          },
                          "podcast_title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "episode_title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "correction_type": {
                            "type": "string"
                          },
                          "original_bounds": {
                            "description": "Parsed JSON when valid, otherwise the stored string"
                          },
                          "corrected_bounds": {
                            "description": "Parsed JSON when valid, otherwise the stored string"
                          },
                          "created_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "candidates": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "slug": {
                                  "type": "string"
                                },
                                "podcast_title": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "episode_title": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "episode_available": {
                                  "type": "boolean"
                                },
                                "source": {
                                  "type": "string",
                                  "enum": [
                                    "current",
                                    "history"
                                  ]
                                },
                                "history_run_count": {
                                  "type": [
                                    "integer",
                                    "null"
                                  ]
                                },
                                "history_latest_processed_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/corrections/unresolved/bulk": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Assign or delete selected unassigned corrections",
        "operationId": "bulkUpdateUnresolvedCorrections",
        "description": "Updates up to 500 unique correction IDs in one transaction. Assignment requires one feed with current episode or processing-history evidence for every selected correction. Any stale or invalid row rejects the whole request without changes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action",
                  "correctionIds",
                  "confirm"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "assign",
                      "delete"
                    ]
                  },
                  "correctionIds": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 500,
                    "uniqueItems": true,
                    "items": {
                      "type": "integer",
                      "minimum": 1
                    }
                  },
                  "slug": {
                    "type": "string",
                    "description": "Required when action is assign."
                  },
                  "confirm": {
                    "type": "boolean",
                    "enum": [
                      true
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "All selected corrections updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "action",
                    "correctionIds"
                  ],
                  "properties": {
                    "action": {
                      "type": "string",
                      "enum": [
                        "assign",
                        "delete"
                      ]
                    },
                    "correctionIds": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action, confirmation, feed, or correction IDs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A selected correction changed or lacks feed evidence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/corrections/{correction_id}/assign": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Assign an unassigned correction to a feed",
        "parameters": [
          {
            "$ref": "#/components/parameters/correction_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "confirm"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "confirm": {
                    "type": "boolean",
                    "const": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Correction assigned after explicit confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "assigned",
                    "correctionId",
                    "slug"
                  ],
                  "properties": {
                    "assigned": {
                      "type": "boolean",
                      "const": true
                    },
                    "correctionId": {
                      "type": "integer"
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Correction was already assigned"
          }
        }
      }
    },
    "/patterns/corrections/{correction_id}": {
      "delete": {
        "tags": [
          "Patterns"
        ],
        "summary": "Delete one unassigned correction",
        "description": "Deletes only a correction whose feed is still unassigned. It never changes patterns, processing history, media, or feed records.",
        "parameters": [
          {
            "$ref": "#/components/parameters/correction_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Unassigned correction deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "correctionId"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "correctionId": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Correction not found"
          },
          "409": {
            "description": "Correction is already assigned"
          }
        }
      }
    },
    "/patterns/{pattern_id}": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Get a single pattern",
        "description": "Returns details for a specific ad pattern",
        "operationId": "getPattern",
        "parameters": [
          {
            "$ref": "#/components/parameters/pattern_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Pattern details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pattern"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Patterns"
        ],
        "summary": "Update a pattern",
        "description": "Update pattern properties like active status, sponsor, category, or disabled reason",
        "operationId": "updatePattern",
        "parameters": [
          {
            "$ref": "#/components/parameters/pattern_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "is_active": {
                    "type": "boolean",
                    "description": "Whether the pattern is active. False stamps disabled_at (an already disabled pattern keeps its original time); true clears disabled_at, and disabled_reason too unless the request sends one."
                  },
                  "sponsor": {
                    "type": "string",
                    "description": "Sponsor name for this pattern"
                  },
                  "disabled_reason": {
                    "type": "string",
                    "description": "Reason for disabling the pattern"
                  },
                  "category": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "sponsor",
                      "cross_promo",
                      "self_promo",
                      "interaction",
                      "intro",
                      "outro",
                      "recap",
                      null
                    ],
                    "description": "Segment category for this pattern's matches; each feed's segment actions map the category to remove, beep, keep, or mark. Null clears the category, and an uncategorized pattern resolves as sponsor. An unrecognized value is rejected with 400."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pattern updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "pattern": {
                      "$ref": "#/components/schemas/Pattern"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Patterns"
        ],
        "summary": "Delete a pattern",
        "description": "Permanently delete an ad pattern from the database",
        "operationId": "deletePattern",
        "parameters": [
          {
            "$ref": "#/components/parameters/pattern_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Pattern deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/patterns/backfill-false-positives": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Backfill false positive texts",
        "description": "Backfill transcript text for existing false positive corrections that were\ncreated before cross-episode matching was enabled. This extracts the\ntranscript text from each correction's original boundaries and stores it\nfor future cross-episode matching.\n",
        "operationId": "backfillFalsePositiveTexts",
        "responses": {
          "200": {
            "description": "Backfill completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "updated": {
                      "type": "integer",
                      "description": "Number of corrections updated with transcript text"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Number of corrections skipped (missing transcript, too short)"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/patterns/deduplicate": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Deduplicate patterns",
        "description": "Manually trigger deduplication of similar patterns in the database",
        "operationId": "deduplicatePatterns",
        "responses": {
          "200": {
            "description": "Deduplication completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "removed_count": {
                      "type": "integer",
                      "description": "Number of duplicate patterns removed"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/patterns/export": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Export patterns",
        "description": "Export all patterns as JSON for backup or transfer",
        "operationId": "exportPatterns",
        "parameters": [
          {
            "name": "include_corrections",
            "in": "query",
            "description": "Attach each pattern's pattern_corrections rows as `corrections`. Rows are exported whole, including `origin` (user or auto_pass2).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Exported patterns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "version": {
                      "type": "string"
                    },
                    "patterns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Pattern"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/import": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Import patterns",
        "description": "Import patterns from a JSON export file",
        "operationId": "importPatterns",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "patterns"
                ],
                "properties": {
                  "patterns": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Pattern"
                    }
                  },
                  "merge": {
                    "type": "boolean",
                    "default": true,
                    "description": "Merge with existing patterns (true) or replace all (false)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Patterns imported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "imported": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/stats": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Get pattern statistics",
        "description": "Returns aggregate statistics about the pattern database including\ncounts by scope, stale patterns, patterns without sponsors, and\npatterns with high false positive rates.\n",
        "operationId": "getPatternStats",
        "responses": {
          "200": {
            "description": "Pattern statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    },
                    "active": {
                      "type": "integer"
                    },
                    "inactive": {
                      "type": "integer"
                    },
                    "by_scope": {
                      "type": "object",
                      "properties": {
                        "global": {
                          "type": "integer"
                        },
                        "network": {
                          "type": "integer"
                        },
                        "podcast": {
                          "type": "integer"
                        }
                      }
                    },
                    "no_sponsor": {
                      "type": "integer",
                      "description": "Patterns without a sponsor name"
                    },
                    "never_matched": {
                      "type": "integer",
                      "description": "Patterns that have never been confirmed"
                    },
                    "stale_count": {
                      "type": "integer",
                      "description": "Patterns not matched in 30+ days"
                    },
                    "high_false_positive_count": {
                      "type": "integer",
                      "description": "Patterns with more false positives than confirmations"
                    },
                    "stale_patterns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sponsor": {
                            "type": "string"
                          },
                          "last_matched_at": {
                            "type": "string"
                          },
                          "confirmation_count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "no_sponsor_patterns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "scope": {
                            "type": "string"
                          },
                          "podcast_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "text_preview": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "high_false_positive_patterns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sponsor": {
                            "type": "string"
                          },
                          "confirmation_count": {
                            "type": "integer"
                          },
                          "false_positive_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/health": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Check pattern health",
        "description": "Identify oversized patterns that are too large to match effectively.\nReturns patterns exceeding character thresholds, likely containing\nmultiple merged ads.\n",
        "operationId": "getPatternHealth",
        "responses": {
          "200": {
            "description": "Pattern health report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_patterns": {
                      "type": "integer"
                    },
                    "healthy": {
                      "type": "integer"
                    },
                    "issues_count": {
                      "type": "integer"
                    },
                    "critical_count": {
                      "type": "integer",
                      "description": "Patterns over 3500 chars"
                    },
                    "warning_count": {
                      "type": "integer",
                      "description": "Patterns over 2500 chars"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sponsor": {
                            "type": "string"
                          },
                          "podcast_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "podcast_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "template_len": {
                            "type": "integer"
                          },
                          "confirmation_count": {
                            "type": "integer"
                          },
                          "severity": {
                            "type": "string",
                            "enum": [
                              "warning",
                              "critical"
                            ]
                          },
                          "issue": {
                            "type": "string"
                          },
                          "recommendation": {
                            "type": "string",
                            "enum": [
                              "review",
                              "delete"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/contaminated": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Find contaminated patterns",
        "description": "Scan all active patterns for those containing multiple ad transition phrases,\nindicating they may contain merged multi-sponsor ads that should be split.\n",
        "operationId": "getContaminatedPatterns",
        "responses": {
          "200": {
            "description": "List of contaminated patterns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "description": "Number of contaminated patterns found"
                    },
                    "patterns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sponsor": {
                            "type": "string"
                          },
                          "podcast_id": {
                            "type": "string"
                          },
                          "text_length": {
                            "type": "integer"
                          },
                          "transition_count": {
                            "type": "integer"
                          },
                          "scope": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/{pattern_id}/split": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Split a contaminated pattern",
        "description": "Split a pattern containing multiple ad transition phrases into separate\nsingle-sponsor patterns. The original pattern is disabled and new patterns\nare created for each detected sponsor segment.\n",
        "operationId": "splitPattern",
        "parameters": [
          {
            "name": "pattern_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "ID of the pattern to split"
          }
        ],
        "responses": {
          "200": {
            "description": "Pattern successfully split",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "original_pattern_id": {
                      "type": "integer"
                    },
                    "new_pattern_ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Pattern does not need splitting or not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/merge-suggestions": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Suggested pattern merges",
        "description": "Same-sponsor near-duplicate pattern clusters that could fold into one\nrow. Clusters are precomputed and cached server-side (keyed on each\nsponsor group's text signature). Patterns without a sponsor are never\nclustered.\n",
        "operationId": "getMergeSuggestions",
        "responses": {
          "200": {
            "description": "Cluster suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "sponsor_id": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "sponsor": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "suggested_keep_id": {
                            "type": "integer",
                            "description": "Keep-target chosen by the tiebreaker"
                          },
                          "pattern_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "count": {
                            "type": "integer"
                          },
                          "result_intro_variant_count": {
                            "type": "integer"
                          },
                          "result_outro_variant_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/patterns/merge": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Merge patterns",
        "description": "Fold patterns into a single kept pattern. The kept pattern keeps its own\ntext_template (so its audio fingerprint stays valid) and receives the\ncombined confirmation and false-positive counts plus the unioned\nintro/outro variants (deduped, capped at five per side). Corrections are\nreassigned, and the merged rows and their audio fingerprints are deleted.\nAll patterns must share the kept pattern's sponsor (cross-sponsor merges\nare rejected). Merging reads below 75% similarity is allowed but returns\nan advisory `warning`.\n",
        "operationId": "mergePatterns",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "keep_id",
                  "merge_ids"
                ],
                "properties": {
                  "keep_id": {
                    "type": "integer",
                    "description": "ID of the pattern to keep"
                  },
                  "merge_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "IDs of patterns to merge into the kept pattern"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Patterns merged successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "kept_pattern_id": {
                      "type": "integer"
                    },
                    "merged_count": {
                      "type": "integer"
                    },
                    "total_confirmations": {
                      "type": "integer"
                    },
                    "total_false_positives": {
                      "type": "integer"
                    },
                    "intro_variant_count": {
                      "type": "integer"
                    },
                    "outro_variant_count": {
                      "type": "integer"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present when a merged pair is below 75% similar"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/patterns/cleanup": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "Get pattern cleanup status and settings",
        "description": "Scheduled LLM review of learned ad patterns (Experiments). Combines the six configurable settings with live state: whether a run currently holds the lock, the last run's timestamp/error/summary, and the pending suggestion counts by kind.",
        "operationId": "getPatternCleanupStatus",
        "responses": {
          "200": {
            "description": "Pattern cleanup status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternCleanupStatus"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/run": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Run pattern cleanup now",
        "description": "Starts a cleanup run on a background thread and returns immediately with the new run's id. Each run processes a configured batch. `force` resets review history used for eligibility and replaces pending suggestions for every active learned pattern; approved and rejected decision records remain. Later normal runs continue the remaining batches. Calling force again restarts this process. A missing body means force=false. A supplied body must be a JSON object and force must be a JSON boolean. Rate limited to 6/hour.",
        "operationId": "runPatternCleanup",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Run started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runId": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON, non-object request body, or non-boolean force",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A cleanup run is already in progress (error: cleanup_in_progress)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/runs": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "List recent pattern cleanup runs",
        "operationId": "listPatternCleanupRuns",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent runs, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PatternCleanupRun"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/suggestions": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "List pattern cleanup suggestions",
        "description": "Each suggestion carries the current pattern summary (sponsor, scope, podcast title, counts), the proposed payload, and an immutable before snapshot from review time. Prefer before_id (the smallest id already seen) over offset to page to older decisions, including approved suggestions available for undo: a keyset cursor is not disturbed by rows that leave the result set between pages, where offset can skip or repeat rows. before_id takes precedence over offset when both are given.",
        "operationId": "listPatternCleanupSuggestions",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "undone"
              ]
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "trim",
                "split",
                "rename",
                "retire",
                "flag",
                "category"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "before_id",
            "in": "query",
            "description": "Keyset cursor; only suggestions with id below this are returned.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PatternCleanupSuggestion"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown status or kind filter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/suggestions/{suggestion_id}/approve": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Approve a pattern cleanup suggestion",
        "description": "Applies the change in place; the pattern keeps a `before` snapshot so it can be undone.",
        "operationId": "approvePatternCleanupSuggestion",
        "parameters": [
          {
            "name": "suggestion_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated suggestion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternCleanupSuggestion"
                }
              }
            }
          },
          "404": {
            "description": "Suggestion not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The suggestion or its pattern does not allow this action (error: invalid_transition)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/suggestions/{suggestion_id}/reject": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Reject a pattern cleanup suggestion",
        "description": "Leaves the pattern alone and acknowledges the reviewed version and statistical evidence. Returns a conflict when the pattern changed after the suggestion was made; unseen newer text is not marked reviewed.",
        "operationId": "rejectPatternCleanupSuggestion",
        "parameters": [
          {
            "name": "suggestion_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated suggestion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternCleanupSuggestion"
                }
              }
            }
          },
          "404": {
            "description": "Suggestion not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The suggestion does not allow this action (error: invalid_transition)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/suggestions/{suggestion_id}/undo": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Undo an approved pattern cleanup suggestion",
        "description": "Restores only fields owned by the approval, using its before and post-approval snapshots. Split undo restores the original pattern and disables its unchanged children.",
        "operationId": "undoPatternCleanupSuggestion",
        "parameters": [
          {
            "name": "suggestion_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated suggestion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternCleanupSuggestion"
                }
              }
            }
          },
          "404": {
            "description": "Suggestion not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused when the suggestion is not approved, a later approval on the same pattern has not been undone, owned fields or split children changed afterward, or an older approval lacks enough saved state for safe undo (error: invalid_transition)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/patterns/cleanup/suggestions/bulk": {
      "post": {
        "tags": [
          "Patterns"
        ],
        "summary": "Approve or reject several pattern cleanup suggestions",
        "operationId": "bulkPatternCleanupSuggestions",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids",
                  "action"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "reject"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-id results; a failed id carries an `error` instead of `status`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "approved",
                              "rejected"
                            ]
                          },
                          "error": {
                            "type": "string",
                            "enum": [
                              "not_found",
                              "invalid_transition"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed ids or action",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/youtube/preview": {
      "post": {
        "tags": [
          "YouTube"
        ],
        "summary": "Preview a YouTube source",
        "description": "Resolves a playlist, channel or handle URL and returns its identity plus podcast directory matches that may offer the same show as an RSS feed. Returns enabled false with no lookup when YouTube feeds are disabled.",
        "operationId": "previewYoutubeSource",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "example": "https://www.youtube.com/@example"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Source preview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "source": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "playlist",
                            "channel"
                          ]
                        },
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "author": {
                          "type": "string",
                          "description": "Channel title; for a playlist, the owning channel. Seeds itunes:author when the feed is added."
                        },
                        "artworkUrl": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "videoCount": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "alreadyAdded": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Slug of the existing feed for this source, if any."
                        }
                      }
                    },
                    "rssMatches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "title": {
                            "type": "string"
                          },
                          "feedUrl": {
                            "type": "string"
                          },
                          "artworkUrl": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a YouTube playlist or channel URL"
          },
          "404": {
            "description": "Source not found or private"
          },
          "502": {
            "description": "The lookup failed upstream"
          }
        }
      }
    },
    "/youtube/search": {
      "get": {
        "tags": [
          "YouTube"
        ],
        "summary": "Search YouTube playlists and channels by name",
        "description": "Returns ranked playlist and channel candidates for a show name. Playlists the channel owner marked as a podcast rank first. Uses the Data API when a key is set and yt-dlp otherwise. Results are cached for 10 minutes. Returns enabled false with no lookup when YouTube feeds are disabled.",
        "operationId": "searchYoutubeSources",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Show name to search for."
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked candidates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "playlist",
                              "channel"
                            ]
                          },
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "channelTitle": {
                            "type": "string"
                          },
                          "videoCount": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "podcast": {
                            "type": [
                              "boolean",
                              "null"
                            ],
                            "description": "True when YouTube marks the playlist as a podcast show. Null for channels and unknown status."
                          },
                          "artworkUrl": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "url": {
                            "type": "string",
                            "description": "Public YouTube URL to paste into the Add feed lookup, for example https://www.youtube.com/playlist?list=PL..."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing search query"
          },
          "502": {
            "description": "The search failed upstream"
          }
        }
      }
    },
    "/youtube/test-key": {
      "post": {
        "tags": [
          "YouTube"
        ],
        "summary": "Test a YouTube Data API key",
        "description": "Tests the supplied key, or the stored key when apiKey is omitted, against a public channel.",
        "operationId": "testYoutubeApiKey",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "apiKey": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key works",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "channelTitle": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No key available or YouTube rejected it"
          }
        }
      }
    },
    "/youtube/cookies": {
      "post": {
        "tags": [
          "YouTube"
        ],
        "summary": "Save YouTube cookies",
        "description": "Stores a Netscape-format cookies file used by yt-dlp. The content is never returned.",
        "operationId": "putYoutubeCookies",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cookies"
                ],
                "properties": {
                  "cookies": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cookies status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YoutubeCookiesStatus"
                }
              }
            }
          },
          "400": {
            "description": "The text is not a valid cookies file"
          }
        }
      },
      "delete": {
        "tags": [
          "YouTube"
        ],
        "summary": "Remove YouTube cookies",
        "operationId": "deleteYoutubeCookies",
        "responses": {
          "200": {
            "description": "Cookies status after removal",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YoutubeCookiesStatus"
                }
              }
            }
          }
        }
      }
    },
    "/youtube/ytdlp/update": {
      "post": {
        "tags": [
          "YouTube"
        ],
        "summary": "Update yt-dlp now",
        "description": "Downloads the latest yt-dlp into the runtime copy. Limited to 6 requests per hour.",
        "operationId": "updateYtdlp",
        "responses": {
          "200": {
            "description": "Update result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "yt-dlp is not installed"
          }
        }
      }
    },
    "/podcast-search": {
      "get": {
        "tags": [
          "Podcast Discovery"
        ],
        "summary": "Search podcasts by name",
        "description": "Proxy search via the configured provider. iTunes (the default when\nno PodcastIndex credentials exist) needs no setup; PodcastIndex.org\nrequires API credentials in settings or environment variables. An\nexplicit `podcastSearchProvider` setting always wins. Both providers\nreturn the same result shape; iTunes entries without a feed URL are\ndropped.\n",
        "operationId": "searchPodcasts",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query (podcast name or keywords)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "artworkUrl": {
                            "type": "string"
                          },
                          "feedUrl": {
                            "type": "string"
                          },
                          "author": {
                            "type": "string"
                          },
                          "link": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "itunes",
                        "podcastindex"
                      ],
                      "description": "Provider that served this search"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing query parameter"
          },
          "502": {
            "description": "Search provider unreachable"
          },
          "503": {
            "description": "PodcastIndex selected but credentials not configured"
          }
        }
      }
    },
    "/search": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Unified search",
        "description": "Word-match search across shows, episodes, transcripts, patterns and\nsponsors, returned as five independent groups. Show and episode\ntitles also get a substring match so a mid-word fragment still finds\nthem; descriptions and transcripts match whole words only.\nEvery group honours the limit parameter. Patterns and sponsors are\nAdvanced-page-only groups; the Dashboard box and keyboard palette\nignore them and request only shows, episodes and transcripts via\nthe groups parameter. The former type parameter is gone: a request\nthat still sends it is rejected with 400 rather than served a\ndifferent payload shape, so use groups instead.\n",
        "operationId": "search",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query. A query shorter than two characters after trimming\nreturns all five groups empty without running a search.\n",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum results per group",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "groups",
            "in": "query",
            "description": "Comma-separated subset of groups to compute. An unrequested\ngroup is still present in the response as an empty array\nrather than omitted. A blank or omitted value means all five.\nNames are case-sensitive, empty tokens from a stray comma are\nignored, and duplicates are tolerated.\n",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "shows",
                  "episodes",
                  "transcripts",
                  "patterns",
                  "sponsors"
                ]
              },
              "default": [
                "shows",
                "episodes",
                "transcripts",
                "patterns",
                "sponsors"
              ]
            },
            "example": "shows,episodes,transcripts"
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "shows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchShowResult"
                      }
                    },
                    "episodes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchEpisodeResult"
                      }
                    },
                    "transcripts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchTranscriptResult"
                      }
                    },
                    "patterns": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchPatternResult"
                      }
                    },
                    "sponsors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchSponsorResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/search/rebuild": {
      "post": {
        "tags": [
          "Search"
        ],
        "summary": "Rebuild search index",
        "description": "Rebuild the full-text search index from scratch. Reindexes all\ncontent including podcasts, episodes, patterns, and sponsors.\nRate limited to 1 request per minute.\n",
        "operationId": "rebuildSearchIndex",
        "responses": {
          "200": {
            "description": "Search index rebuilt",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "indexedCount": {
                      "type": "integer",
                      "description": "Total items indexed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/search/stats": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Get search index statistics",
        "description": "Returns statistics about the search index including item counts by type",
        "operationId": "getSearchStats",
        "responses": {
          "200": {
            "description": "Search index statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stats": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/networks": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "List known podcast networks",
        "description": "Returns list of known podcast networks for network-level pattern matching",
        "operationId": "listNetworks",
        "responses": {
          "200": {
            "description": "List of networks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "networks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Network"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sponsors": {
      "get": {
        "tags": [
          "Sponsors"
        ],
        "summary": "List all sponsors",
        "description": "Returns list of all known sponsors with their aliases and metadata",
        "operationId": "listSponsors",
        "parameters": [
          {
            "name": "include_inactive",
            "in": "query",
            "description": "Include inactive (deleted) sponsors",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of sponsors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sponsors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Sponsor"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Add a new sponsor",
        "description": "Create a new sponsor entry with optional aliases and category",
        "operationId": "addSponsor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Canonical sponsor name"
                  },
                  "aliases": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Alternative names for this sponsor"
                  },
                  "category": {
                    "type": "string",
                    "description": "Sponsor category"
                  },
                  "segment_category": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "sponsor",
                      "cross_promo",
                      "self_promo",
                      "interaction",
                      "intro",
                      "outro",
                      "recap",
                      null
                    ],
                    "description": "Segment category applied to every read naming this sponsor"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sponsor created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "Sponsor already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sponsors/{sponsor_id}": {
      "get": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Get a single sponsor",
        "description": "Returns details for a specific sponsor by ID",
        "operationId": "getSponsor",
        "parameters": [
          {
            "$ref": "#/components/parameters/sponsor_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Sponsor details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sponsor"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Update a sponsor",
        "description": "Update sponsor properties. Any subset of the fields may be sent.",
        "operationId": "updateSponsor",
        "parameters": [
          {
            "$ref": "#/components/parameters/sponsor_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "aliases": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "category": {
                    "type": "string"
                  },
                  "segment_category": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "sponsor",
                      "cross_promo",
                      "self_promo",
                      "interaction",
                      "intro",
                      "outro",
                      "recap",
                      null
                    ],
                    "description": "Segment category applied to every read naming this sponsor; null clears it"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Community tags for grouping and filtering"
                  },
                  "common_ctas": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Common calls-to-action associated with this sponsor"
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Set false to deactivate, true to reactivate"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sponsor updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Delete a sponsor",
        "description": "Permanently remove a sponsor. Any ad patterns linked to it are unlinked (their sponsor_id is set to null), not deleted, so no pattern data is lost.",
        "operationId": "deleteSponsor",
        "parameters": [
          {
            "$ref": "#/components/parameters/sponsor_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Sponsor deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "unlinkedPatterns": {
                      "type": "integer",
                      "description": "Number of ad patterns that were unlinked"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/sponsors/normalizations": {
      "get": {
        "tags": [
          "Sponsors"
        ],
        "summary": "List sponsor normalizations",
        "description": "Returns list of sponsor name normalization rules with optional filtering",
        "operationId": "listNormalizations",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Filter by normalization category",
            "schema": {
              "type": "string",
              "enum": [
                "sponsor",
                "url",
                "number",
                "phrase"
              ]
            }
          },
          {
            "name": "include_inactive",
            "in": "query",
            "description": "Include inactive normalizations",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of normalizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "normalizations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Normalization"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Add a normalization rule",
        "description": "Create a sponsor name normalization rule. `terms` is a regex pattern applied case-insensitively; `canonical` is its replacement. The legacy field names `pattern`/`replacement` are still accepted on write. The regex is validated on create and rejected with 400 if invalid.",
        "operationId": "addNormalization",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "terms",
                  "canonical",
                  "category"
                ],
                "properties": {
                  "terms": {
                    "type": "string",
                    "description": "Regex pattern to match (legacy alias - pattern)"
                  },
                  "canonical": {
                    "type": "string",
                    "description": "Replacement text (legacy alias - replacement)"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "sponsor",
                      "url",
                      "number",
                      "phrase"
                    ],
                    "description": "Normalization category"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Normalization created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/sponsors/normalizations/{norm_id}": {
      "put": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Update a normalization rule",
        "description": "Update a normalization rule. Any subset of fields may be sent. The legacy field names `pattern`/`replacement` are still accepted in place of `terms`/`canonical`.",
        "operationId": "updateNormalization",
        "parameters": [
          {
            "$ref": "#/components/parameters/norm_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "terms": {
                    "type": "string",
                    "description": "Regex pattern to match (legacy alias - pattern)"
                  },
                  "canonical": {
                    "type": "string",
                    "description": "Replacement text (legacy alias - replacement)"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "sponsor",
                      "url",
                      "number",
                      "phrase"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Normalization updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Sponsors"
        ],
        "summary": "Delete a normalization rule",
        "description": "Deactivate a normalization rule (soft delete)",
        "operationId": "deleteNormalization",
        "parameters": [
          {
            "$ref": "#/components/parameters/norm_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Normalization deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Get current processing status",
        "description": "Returns shared processing status including active jobs and queue depth. The browser polls this route every 2 seconds and backs off to 30 seconds after failures.",
        "operationId": "getStatus",
        "responses": {
          "200": {
            "description": "Current status",
            "headers": {
              "ETag": {
                "description": "Hash of the returned status payload.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProcessingStatus"
                }
              }
            }
          }
        }
      }
    },
    "/status/stream": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Real-time status stream (SSE)",
        "description": "Compatibility Server-Sent Events stream of processing status updates.\nA stream lasts at most 30 seconds and polls shared state every 2 seconds.\nConcurrent streams are bounded to one fewer than the configured\ngunicorn thread count.\n\nAuthentication is snapshotted once at connect time, not on the\nHTTP handshake. The endpoint is in `AUTH_EXEMPT_PATHS` because\nbrowser `EventSource` clients cannot surface a 401 response.\nAn unauthenticated connect receives a single\n`event: auth-failed` SSE frame and the stream closes; the\nfrontend handler redirects to `/ui/login` on that event. An\nauthenticated connect opens the real data stream.\n\nA password change or removal revokes the stream on its next state\npoll with `event: auth-failed`.\n",
        "operationId": "streamStatus",
        "responses": {
          "200": {
            "description": "SSE stream. Body is either a sequence of `data:` frames\n(authenticated) or a single `event: auth-failed` frame\nfollowed by stream close (unauthenticated).\n",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Authenticated stream: `data:` frames carrying the same\nJSON as `GET /status` (the `ProcessingStatus` schema),\nincluding the `hold` block.\n\nUnauthenticated stream: single frame\n`event: auth-failed\\ndata: {}\\n\\n` then close.\n"
                }
              }
            }
          },
          "410": {
            "description": "SSE compatibility is disabled because no stream slots are configured"
          }
        }
      }
    },
    "/status/processing-admission": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Read whether new processing runs are paused",
        "description": "Pause for new processing runs that survives restarts. While paused,\nactive runs finish and queued episodes stay queued until someone\nresumes.\n",
        "operationId": "getProcessingAdmission",
        "responses": {
          "200": {
            "description": "Current admission state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProcessingAdmission"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Status"
        ],
        "summary": "Pause or resume new processing runs",
        "description": "Pausing does not stop runs already in progress. It only keeps new\nwork from starting.\n",
        "operationId": "setProcessingAdmission",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "paused"
                ],
                "properties": {
                  "paused": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Admission state after the change",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProcessingAdmission"
                }
              }
            }
          },
          "400": {
            "description": "paused must be a boolean",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/detections": {
      "get": {
        "tags": [
          "Patterns"
        ],
        "summary": "List ad detections across all feeds",
        "description": "Flattens every episode's ad markers into one list for the Ad Review tab, with a computed status and resolution per detection. Detections are resolved by POSTing to the existing per-episode corrections endpoint.\n",
        "operationId": "listDetections",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "needs_review returns pending-review and rejected detections that have no matching correction yet.\n",
            "schema": {
              "type": "string",
              "enum": [
                "needs_review",
                "pending",
                "rejected",
                "accepted",
                "all"
              ],
              "default": "needs_review"
            }
          },
          {
            "name": "feed",
            "in": "query",
            "description": "Filter to a single feed by slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Case-insensitive match against sponsor and reason",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "date",
                "confidence",
                "podcast"
              ],
              "default": "date"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by segment category. Use `none` for markers no detection stage classified. Omit for every category.",
            "schema": {
              "type": "string",
              "enum": [
                "sponsor",
                "cross_promo",
                "self_promo",
                "interaction",
                "intro",
                "outro",
                "recap",
                "none"
              ]
            }
          },
          {
            "name": "reviewer",
            "in": "query",
            "description": "Filter by whether the span was moved (see ReviewDetection.reviewerMoved). adjusted keeps moved rows; unadjusted keeps everything else. Omit for no filter. Applies to the cutSummary totals as well.",
            "schema": {
              "type": "string",
              "enum": [
                "adjusted",
                "unadjusted"
              ]
            }
          },
          {
            "name": "holdReason",
            "in": "query",
            "description": "Keep only pending-review detections held for this reason. Omit for no filter.",
            "schema": {
              "type": "string",
              "enum": [
                "max_duration",
                "no_cue_evidence",
                "uncorroborated_tail",
                "reviewer_contradiction",
                "reviewer_boundary_conflict",
                "reviewer_inconclusive_bounds",
                "estimated_pattern_bounds",
                "reviewer_reject_conflict",
                "reviewer_failed",
                "no_splice_evidence",
                "verification_miss",
                "verification_kept_conflict",
                "differential_uncorroborated",
                "large_vad_gap_extension",
                "cue_template_unproven",
                "cue_low_confidence",
                "no_transcript_evidence",
                "transcript_differential_unreviewed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated detection list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "detections": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReviewDetection"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "counts": {
                      "type": "object",
                      "description": "Pre-filter overview counts across all detections",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "needsReview": {
                          "type": "integer"
                        },
                        "pending": {
                          "type": "integer"
                        },
                        "rejected": {
                          "type": "integer"
                        },
                        "accepted": {
                          "type": "integer"
                        },
                        "confirmed": {
                          "type": "integer"
                        },
                        "dismissed": {
                          "type": "integer"
                        },
                        "pendingByHoldReason": {
                          "type": "object",
                          "description": "Pending-review detections per hold reason. Counts follow only the feed and status filters; the q, reviewer, holdReason and category filters do not narrow them. Holds with no recorded reason are not counted.",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "cutSummary": {
                      "type": "object",
                      "description": "Totals over detections that were cut. Reflects the status, feed, and q filters but not the category filter, so the byCategory breakdown stays complete while the podcast selector still narrows it.",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "durationSeconds": {
                          "type": "number"
                        },
                        "byCategory": {
                          "type": "object",
                          "description": "Count per segment category, plus a `none` key for markers with no category set.",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        },
                        "distinctSponsors": {
                          "type": "integer"
                        },
                        "distinctPodcasts": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/history": {
      "get": {
        "tags": [
          "History"
        ],
        "summary": "Get processing history",
        "description": "Returns processing history with pagination and filtering",
        "operationId": "getProcessingHistory",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by processing status",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "completed",
                "failed"
              ],
              "default": "all"
            }
          },
          {
            "name": "podcast_slug",
            "in": "query",
            "description": "Filter by podcast slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of entries to return",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of entries to skip",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-based). Alternative to offset. If both provided, page takes precedence.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "sort_by",
            "in": "query",
            "description": "Column to sort by",
            "schema": {
              "type": "string",
              "enum": [
                "processed_at",
                "podcast_title",
                "episode_title",
                "processing_duration_seconds",
                "ads_detected",
                "reprocess_number",
                "status",
                "llm_cost"
              ],
              "default": "processed_at"
            }
          },
          {
            "name": "sort_dir",
            "in": "query",
            "description": "Sort direction",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Processing history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "history": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProcessingHistoryEntry"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total number of history entries matching the filter"
                    },
                    "totalPages": {
                      "type": "integer",
                      "description": "Total number of pages available"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer",
                      "description": "Current page number (1-based)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/history/stats": {
      "get": {
        "tags": [
          "History"
        ],
        "summary": "Get processing statistics",
        "description": "Returns aggregate statistics from processing history",
        "operationId": "getProcessingHistoryStats",
        "responses": {
          "200": {
            "description": "Processing statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_processed": {
                      "type": "integer"
                    },
                    "completed": {
                      "type": "integer"
                    },
                    "failed": {
                      "type": "integer"
                    },
                    "total_ads_detected": {
                      "type": "integer"
                    },
                    "avg_processing_time": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Average processing time in seconds"
                    },
                    "totalInputTokens": {
                      "type": "integer",
                      "description": "Total LLM input tokens across all completed processing runs"
                    },
                    "totalOutputTokens": {
                      "type": "integer",
                      "description": "Total LLM output tokens across all completed processing runs"
                    },
                    "totalLlmCost": {
                      "type": "number",
                      "description": "Total estimated LLM cost in USD across all completed processing runs"
                    },
                    "podcasts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "title": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/history/downloads": {
      "get": {
        "tags": [
          "History"
        ],
        "summary": "List episode downloads",
        "description": "Client downloads of episode audio, newest first by default. Range requests\nfrom one client within 15 minutes count as one row. Client IPs are stored\nand location is filled only when geo lookup is enabled.\n",
        "operationId": "getDownloadHistory",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "last_seen_at",
                "podcast",
                "client_ip",
                "location",
                "subscriber"
              ],
              "default": "last_seen_at"
            }
          },
          {
            "name": "sortDir",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "podcastSlug",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subscriberKeyId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "First day to include (YYYY-MM-DD, UTC).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Last day to include (YYYY-MM-DD, UTC).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Substring match on client IP or user agent.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of downloads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "downloads",
                    "total",
                    "page",
                    "limit"
                  ],
                  "properties": {
                    "downloads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DownloadHistoryEntry"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/history/export": {
      "get": {
        "tags": [
          "History"
        ],
        "summary": "Export processing history",
        "description": "Export processing history as CSV or JSON file",
        "operationId": "exportProcessingHistory",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "description": "Export format",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "json"
              ],
              "default": "csv"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by processing status",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "completed",
                "failed"
              ],
              "default": "all"
            }
          },
          {
            "name": "podcast_slug",
            "in": "query",
            "description": "Filter by podcast slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Exported history file",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "history": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProcessingHistoryEntry"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/failover": {
      "get": {
        "tags": [
          "Failover"
        ],
        "summary": "Get failover state, probes, policy, and recent events",
        "description": "Snapshot of the three failover targets (llm-a, llm-b, transcriber), the five health probes that drive them (llm-a, llm-b, llm-failover, transcriber, transcriber-failover), the auto-trigger/recovery policy, and the most recent manual/auto/probe events (#806). Reads committed control state without running health probes.",
        "operationId": "getFailover",
        "responses": {
          "200": {
            "description": "Failover overview",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FailoverOverview"
                }
              }
            }
          }
        }
      }
    },
    "/failover/{target}/trigger": {
      "post": {
        "tags": [
          "Failover"
        ],
        "summary": "Manually trigger failover for one target",
        "description": "Switches `target` onto its failover configuration. Fails with 409 when the target's failover config is not set up, so a manual trigger can never land on an endpoint with nowhere to go.",
        "operationId": "triggerFailover",
        "parameters": [
          {
            "in": "path",
            "name": "target",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "llm-a",
                "llm-b",
                "transcriber"
              ]
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Free-text note recorded on the failover event."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Triggered (or already active)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "target": {
                      "type": "string"
                    },
                    "state": {
                      "$ref": "#/components/schemas/FailoverTargetState"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body or non-string reason"
          },
          "404": {
            "description": "Unknown failover target"
          },
          "409": {
            "description": "Target has no failover configuration (error: failover_not_configured), or llm-b is disabled (error: failover_target_disabled)"
          },
          "503": {
            "description": "State could not be saved (error: failover_transition_failed)"
          }
        }
      }
    },
    "/failover/{target}/cancel": {
      "post": {
        "tags": [
          "Failover"
        ],
        "summary": "Manually cancel failover for one target",
        "description": "Switches `target` back onto its original configuration.",
        "operationId": "cancelFailover",
        "parameters": [
          {
            "in": "path",
            "name": "target",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "llm-a",
                "llm-b",
                "transcriber"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled (or already inactive)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "target": {
                      "type": "string"
                    },
                    "state": {
                      "$ref": "#/components/schemas/FailoverTargetState"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown failover target"
          },
          "503": {
            "description": "State could not be saved (error: failover_transition_failed)"
          }
        }
      }
    },
    "/failover/probe": {
      "post": {
        "tags": [
          "Failover"
        ],
        "summary": "Run an on-demand health probe of every enabled target",
        "description": "Probes every currently enabled target (see `enabled_probe_targets`), records the result, and applies the same auto-trigger/auto-recovery rules as the background probe tick. A local transcription diagnostic decode requested from a web request is never run inline: it is handed to the background worker to run on its next tick, and this response carries that target's last stored verdict instead of a fresh one.",
        "operationId": "probeFailover",
        "responses": {
          "200": {
            "description": "Updated probe states",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "probes": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/FailoverProbe"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/providers": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "List provider configuration status",
        "description": "Returns encryption readiness and per-provider status (configured,\nsource, and optional baseUrl/model). Never returns key values.\n",
        "operationId": "listProviders",
        "responses": {
          "200": {
            "description": "Provider status summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "cryptoReady": {
                      "type": "boolean"
                    },
                    "anthropic": {
                      "$ref": "#/components/schemas/ProviderStatus"
                    },
                    "openai": {
                      "$ref": "#/components/schemas/ProviderStatus"
                    },
                    "openrouter": {
                      "$ref": "#/components/schemas/ProviderStatus"
                    },
                    "whisper": {
                      "$ref": "#/components/schemas/ProviderStatus"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/providers/{provider}": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update provider configuration",
        "operationId": "updateProvider",
        "parameters": [
          {
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "anthropic",
                "openai",
                "openrouter",
                "whisper",
                "ollama",
                "typesafe",
                "systemone-compatible"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "apiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Omit to leave unchanged; null or empty to clear"
                  },
                  "baseUrl": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "affectedRunsAction": {
                    "type": "string",
                    "enum": [
                      "requeue",
                      "cancel"
                    ],
                    "default": "requeue",
                    "description": "What to do with runs frozen against the old account when this save moves the account identity. Ignored when the identity is unchanged (a same-account key rotation)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated status. Carries accountChanged and affectedRuns when the save moved the slot's account identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderStatus"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (e.g.",
            "baseUrl failed SSRF validation": null,
            "unknown affectedRunsAction)": null
          },
          "404": {
            "description": "Unknown provider"
          },
          "409": {
            "description": "provider_crypto_unavailable (MINUSPOD_MASTER_PASSPHRASE unset)"
          }
        }
      },
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Clear stored provider key",
        "description": "Removes the DB-stored key so env-var fallback (if any) takes over.",
        "operationId": "clearProvider",
        "parameters": [
          {
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "anthropic",
                "openai",
                "openrouter",
                "whisper",
                "ollama",
                "typesafe",
                "systemone-compatible"
              ]
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "affectedRunsAction": {
                    "type": "string",
                    "enum": [
                      "requeue",
                      "cancel"
                    ],
                    "default": "requeue",
                    "description": "What to do with runs frozen against the cleared endpoint's account. Clearing a base URL moves the account identity."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cleared. Carries accountChanged and affectedRuns when clearing the endpoint moved the slot's account identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderStatus"
                }
              }
            }
          },
          "400": {
            "description": "Unknown affectedRunsAction"
          },
          "404": {
            "description": "Unknown provider"
          }
        }
      }
    },
    "/settings/providers/{slot}/affected-runs": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Runs a change to this account slot would strand",
        "description": "Preflight for a provider or endpoint change: lists the active runs whose frozen routes still name the slot's current account, so the UI can show what a save is about to interrupt before writing anything. A same-account key rotation strands nothing and needs no preflight.",
        "operationId": "getAccountAffectedRuns",
        "parameters": [
          {
            "in": "path",
            "name": "slot",
            "required": true,
            "description": "`a`/`b` are accepted as aliases of `primary`/`secondary`.",
            "schema": {
              "type": "string",
              "enum": [
                "primary",
                "secondary",
                "a",
                "b"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The slot's current account and the runs bound to it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "slot": {
                      "type": "string",
                      "enum": [
                        "primary",
                        "secondary"
                      ]
                    },
                    "accountId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Non-secret account identity (provider type plus endpoint); null when the slot has no provider configured"
                    },
                    "affectedRuns": {
                      "$ref": "#/components/schemas/AffectedRuns"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown provider slot"
          }
        }
      }
    },
    "/settings/providers/rotate-passphrase": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Report that master-passphrase rotation must run offline",
        "description": "Online rotation is refused because sibling workers cannot safely\nreceive the replacement passphrase. Stop all workers and run\n`python scripts/rotate_master_passphrase.py`, update the deployment\nenvironment, then restart every worker together.\n",
        "operationId": "rotateMasterPassphrase",
        "responses": {
          "409": {
            "description": "Rotation requires the offline command"
          }
        }
      }
    },
    "/settings/providers/{provider}/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test a provider connection",
        "description": "Native System One providers check model discovery without a stage model. Success confirms the catalog connection, not inference.",
        "operationId": "testProvider",
        "parameters": [
          {
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "anthropic",
                "openai",
                "openrouter",
                "whisper",
                "ollama",
                "typesafe",
                "systemone-compatible"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "reachable": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "integer"
                    },
                    "validation": {
                      "type": "string",
                      "enum": [
                        "model_catalog"
                      ]
                    },
                    "inferenceChecked": {
                      "type": "boolean",
                      "description": "False for native model-catalog checks"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/providers/{provider}/test-connection": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test a configured endpoint connection",
        "description": "Connection probe for providers with a configurable endpoint.\n`whisper` uploads a one-second generated audio sample through the\nsame request shape and upload format (FLAC by default, WAV when\nSkip FLAC compression is on) as the real transcription path, so the\nresult reflects what an actual episode upload would hit, including\nthe wrong-path case (OpenVINO Model Server only answers under its\nversioned base such as `/v3`). `openai` and `ollama` hit the same\n`/models` route the real LLM client uses for discovery, with the\nsame Ollama `/v1` normalization, and unlike `/test` need no stored\nkey. Accepts unsaved values so a URL can be tested before saving; a\n`baseUrl` key present in the body is authoritative even when empty.\nThe stored API key is sent only when the tested URL points at the\nsame scheme/host/port as the saved base URL, so the key cannot be\nsent to an arbitrary URL. `anthropic` and `openrouter` probe their\nfixed public endpoints with the saved key; the request body is\nignored for them, distinguishing an unreachable network from a\nrejected or missing key.\n`typesafe` and `systemone-compatible` check their `/models` endpoint\nwithout a stage model. Success confirms model discovery, not inference;\npublic catalogs may not validate keys. TypeSafe requires its own saved\nkey. Compatible endpoints can omit a key; a saved key is sent only to\nthe same saved scheme, host and port. Native probes use\nthe bounded connection-probe timeout.\n",
        "operationId": "testProviderConnection",
        "parameters": [
          {
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "whisper",
                "openai",
                "ollama",
                "anthropic",
                "openrouter",
                "typesafe",
                "systemone-compatible"
              ]
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "baseUrl": {
                    "type": "string",
                    "description": "Base URL to test; when the key is absent the saved value is used"
                  },
                  "model": {
                    "type": "string",
                    "description": "Transcription model to test (faster-whisper or remote API); ignored for native System One catalog checks"
                  },
                  "skipFlacCompression": {
                    "type": "boolean",
                    "description": "whisper only: upload WAV (true) or FLAC (false); falls back to the saved setting"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (non-string baseUrl or model, non-boolean skipFlacCompression)"
          },
          "404": {
            "description": "Provider without a testable endpoint"
          }
        }
      }
    },
    "/settings/providers/secondary/test-connection": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test the secondary provider connection",
        "description": "Same staged probe as `/settings/providers/{provider}/test-connection`,\nbut targets the optional secondary provider slot: its key comes from\n`secondaryProviderApiKey`, and its type and endpoint come from\n`secondaryProvider`/`secondaryProviderBaseUrl` unless the request\nbody's `provider`/`baseUrl` override them, to test an unsaved form\nvalue before Save. Returns `ok: false` with a `detail` explaining the\nreason when no secondary provider type is configured yet.\n",
        "operationId": "testSecondaryProviderConnection",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "anthropic",
                      "openrouter",
                      "openai-compatible",
                      "ollama",
                      "typesafe",
                      "systemone-compatible"
                    ],
                    "description": "Provider type to test; when absent the saved secondaryProvider is used"
                  },
                  "baseUrl": {
                    "type": "string",
                    "description": "Base URL to test; when absent the saved secondary value is used"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (non-string baseUrl, non-string provider, or provider not one of the supported types)"
          }
        }
      }
    },
    "/settings/providers/failover/test-connection": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test the LLM failover provider connection end to end",
        "description": "Same staged probe as `/settings/providers/secondary/test-connection`,\nbut targets the shared LLM failover slot (#806): its key comes from\n`failoverLlmApiKey`, and its type and endpoint come from\n`failoverLlmProvider`/`failoverLlmBaseUrl` unless the request body's\n`provider`/`baseUrl` override them, to test an unsaved form value\nbefore Save. Returns `ok: false` with a `detail` explaining the\nreason when no failover provider type is configured yet.\n",
        "operationId": "testFailoverProviderConnection",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "anthropic",
                      "openrouter",
                      "openai-compatible",
                      "ollama",
                      "typesafe",
                      "systemone-compatible"
                    ],
                    "description": "Provider type to test; when absent the saved failoverLlmProvider is used"
                  },
                  "baseUrl": {
                    "type": "string",
                    "description": "Base URL to test; when absent the saved failover value is used"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (non-string baseUrl, non-string provider, or provider not one of the supported types)"
          }
        }
      }
    },
    "/settings/providers/failover-whisper/test-connection": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test the transcriber failover connection end to end",
        "description": "Same staged probe as `/settings/providers/whisper/test-connection`\n(#806), but targets the transcriber failover slot: its key comes\nfrom `failoverWhisperApiKey`, and its base URL/model come from\n`failoverWhisperApiBaseUrl`/`failoverWhisperApiModel` unless the\nrequest body overrides them.\n",
        "operationId": "testFailoverWhisperConnection",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "baseUrl": {
                    "type": "string",
                    "description": "Base URL to test; when the key is absent the saved value is used"
                  },
                  "model": {
                    "type": "string",
                    "description": "Model name to send; falls back to the saved value"
                  },
                  "skipFlacCompression": {
                    "type": "boolean",
                    "description": "Upload WAV (true) or FLAC (false); falls back to the saved setting"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Probe result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTestResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (non-string baseUrl or model, non-boolean skipFlacCompression)"
          }
        }
      }
    },
    "/settings/podcast-index/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test the saved PodcastIndex credentials",
        "description": "Sends the same signed request the podcast search uses (a one-result\nsearch), so a passing test means search will work. Uses only the\nsaved credentials; unsaved form drafts must be saved first.\n",
        "operationId": "testPodcastIndex",
        "responses": {
          "200": {
            "description": "Probe result (always 200; see `ok` field)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTestResult"
                }
              }
            }
          }
        }
      }
    },
    "/settings/feed-auth/regenerate-key": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Rotate the global feed auth key",
        "description": "Generates a new global feed auth key, replacing the current one. The\nold key is rejected immediately, so podcast apps subscribed with the\nprevious key must be re-subscribed using the new URLs.\n",
        "operationId": "regenerateFeedAuthKey",
        "responses": {
          "200": {
            "description": "Key rotated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedAuthKey": {
                      "type": "string",
                      "description": "New global feed auth key (64 hex chars)"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "feed_auth_disabled (enable feed auth before rotating the key)"
          }
        }
      }
    },
    "/settings": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get all settings",
        "description": "Returns current application settings including ad detection configuration",
        "operationId": "getSettings",
        "responses": {
          "200": {
            "description": "Current settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          }
        }
      }
    },
    "/settings/ad-detection": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update ad detection settings",
        "description": "Update AI model, system prompt, or multi-pass detection",
        "operationId": "updateAdDetectionSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "systemPrompt": {
                    "type": "string",
                    "description": "System prompt for AI ad detection (first pass)"
                  },
                  "verificationPrompt": {
                    "type": "string",
                    "description": "System prompt for verification pass (post-cut re-detection of missed ads)"
                  },
                  "chapterPrompt": {
                    "type": "string",
                    "description": "Prompt template for chapter topic detection with placeholders {num_splits}, {segment_start}, {segment_end}, {continuation_block}, {description_block}, {hints_block}, {transcript}"
                  },
                  "chapterPromptOverride": {
                    "type": "string",
                    "description": "Optional override appended to the chapter topic-detection prompt (empty by default)"
                  },
                  "patternCleanupPrompt": {
                    "type": "string",
                    "description": "System prompt for the pattern cleanup review. Blank resets it to the default."
                  },
                  "claudeModel": {
                    "type": "string",
                    "description": "AI model ID to use for first pass ad detection. No model ships by default; unset until configured.",
                    "example": "claude-haiku-4-5"
                  },
                  "verificationModel": {
                    "type": "string",
                    "description": "AI model ID for verification pass (can differ from first pass). No model ships by default; unset until configured.",
                    "example": "claude-haiku-4-5"
                  },
                  "chaptersModel": {
                    "type": "string",
                    "description": "Model for chapter title generation and topic detection (smaller/cheaper models work well). No model ships by default; unset until configured.",
                    "example": "claude-haiku-4-5"
                  },
                  "detectionProvider": {
                    "type": "string",
                    "description": "Routing slot for first-pass ad detection: `primary` runs it on the\nglobal llmProvider, `secondary` on the optional secondary provider\n(falls back to primary if secondary is disabled or unconfigured).\nEmpty string clears the override back to primary.\n",
                    "enum": [
                      "primary",
                      "secondary",
                      ""
                    ],
                    "example": ""
                  },
                  "verificationProvider": {
                    "type": "string",
                    "description": "Routing slot for the verification pass: `primary`, `secondary`, or\n`same_as_detection` to inherit detectionProvider's resolved slot.\nEmpty string clears the override back to same_as_detection.\n",
                    "enum": [
                      "primary",
                      "secondary",
                      "same_as_detection",
                      ""
                    ],
                    "example": ""
                  },
                  "chaptersProvider": {
                    "type": "string",
                    "description": "Routing slot for chapter title generation: `primary`, `secondary`, or\n`same_as_detection` to inherit detectionProvider's resolved slot.\nEmpty string clears the override back to same_as_detection.\n",
                    "enum": [
                      "primary",
                      "secondary",
                      "same_as_detection",
                      ""
                    ],
                    "example": ""
                  },
                  "whisperModel": {
                    "type": "string",
                    "description": "faster-whisper model (tiny, base, small, medium, large-v3); ignored by the parakeet and openai-api backends",
                    "example": "small"
                  },
                  "whisperBackend": {
                    "type": "string",
                    "description": "Whisper backend to use (local uses local GPU/CPU, openai-api uses an external OpenAI-compatible API, parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process)",
                    "enum": [
                      "local",
                      "openai-api",
                      "parakeet"
                    ],
                    "example": "local"
                  },
                  "whisperApiBaseUrl": {
                    "type": "string",
                    "description": "Base URL for OpenAI-compatible remote transcription API endpoint (only used when whisperBackend is openai-api)",
                    "example": "http://localhost:8080/v1"
                  },
                  "whisperApiKey": {
                    "type": "string",
                    "description": "API key for the remote transcription API endpoint (only used when whisperBackend is openai-api)"
                  },
                  "whisperApiModel": {
                    "type": "string",
                    "description": "Model name to use with the remote transcription API (only used when whisperBackend is openai-api)",
                    "example": "whisper-1"
                  },
                  "whisperLanguage": {
                    "type": "string",
                    "description": "ISO 639-1 language code to pin transcription to (e.g. `en`, `fi`, `es`),\nor `auto` to let the backend detect. Empty value resets to the default (`en`).\nApplies to both local (faster-whisper) and remote OpenAI-compatible backends.\n",
                    "example": "en"
                  },
                  "whisperApiTimeoutSeconds": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 3600,
                    "description": "Seconds to wait for each transcription request to the remote\ntranscription backend (issue #593). A slow self-hosted backend that\nreturns 0 segments on larger chunks is hitting this limit;\nraise it or lower transcribeMaxChunkSeconds. A proxy in front\nof the backend may cut the request sooner than this value.\n",
                    "example": 600
                  },
                  "transcribeMaxChunkSeconds": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 7200,
                    "description": "Max duration, in seconds, of each transcription chunk.",
                    "example": 600
                  },
                  "transcribeConcurrentChunks": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 32,
                    "description": "Number of transcription chunks sent to the transcription backend concurrently.",
                    "example": 4
                  },
                  "transcribeChunkOverlapSeconds": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 600,
                    "description": "Overlap, in seconds, between consecutive transcription chunks.",
                    "example": 30
                  },
                  "whisperPoolEnabled": {
                    "type": "boolean",
                    "description": "Bound concurrent requests to the remote transcription API to avoid rate limits."
                  },
                  "whisperPoolMaxRequests": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 64,
                    "description": "Maximum concurrent in-flight requests to the remote transcription API when the pool is enabled.",
                    "example": 4
                  },
                  "whisperPoolMaxEpisodes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 16,
                    "description": "Maximum episodes transcribing concurrently against the remote transcription API when the pool is enabled.",
                    "example": 1
                  },
                  "whisperComputeType": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "float16",
                      "int8_float16",
                      "int8",
                      "float32"
                    ],
                    "description": "Compute precision for the local faster-whisper backend only.\n`auto` resolves to float16 on CUDA and int8 on CPU. On init\nfailure with float16, the server falls back through\nint8_float16 -> int8 -> float32. Older NVIDIA GPUs\n(Pascal GTX 10xx, Maxwell GTX 9xx) need `int8` or `float32`.\nSee the Transcription guide (docs/transcription.md, GPU compute type) for the per-GPU table.\n",
                    "example": "auto"
                  },
                  "audioCueDetectionEnabled": {
                    "type": "boolean",
                    "description": "Master toggle for audio cue detection (#350). Off by default."
                  },
                  "audioCueFreqMinHz": {
                    "type": "number",
                    "description": "Low edge (Hz) of the spectral fallback cue band.",
                    "minimum": 20,
                    "maximum": 20000
                  },
                  "audioCueFreqMaxHz": {
                    "type": "number",
                    "description": "High edge (Hz) of the spectral fallback cue band. Must exceed the low edge.",
                    "minimum": 20,
                    "maximum": 20000
                  },
                  "audioCueProminenceDb": {
                    "type": "number",
                    "description": "dB above the in-band baseline for a burst to count (spectral fallback).",
                    "minimum": 1,
                    "maximum": 40
                  },
                  "audioCueMinConfidence": {
                    "type": "number",
                    "description": "Drop cues weaker than this.",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "audioCueTemplateScore": {
                    "type": "number",
                    "description": "ZNCC score a learned cue template must reach on another episode (default 0.75).",
                    "minimum": 0,
                    "maximum": 0.99
                  },
                  "audioCueFormantAttenDb": {
                    "type": "number",
                    "description": "Voiceover-robust matching (#350). Attenuates the 800-3400 Hz speech-formant band in dB during cue-template matching so a cue keys on its constant music bed despite a varying voiceover. 0 disables it (default); existing/ding/full-spectrum cues are unchanged. A per-template override may supersede this. Typical opt-in ~9-12.",
                    "minimum": 0,
                    "maximum": 24
                  },
                  "audioCueSnapConfidence": {
                    "type": "number",
                    "description": "Min cue confidence before a cue may move an ad edge (default 0.80).",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "audioCueSnapLeadSeconds": {
                    "type": "number",
                    "description": "How far back from the ad edge a cue may sit to be eligible for snapping (default 10.0s).",
                    "example": 10,
                    "minimum": 0.5,
                    "maximum": 30
                  },
                  "audioCueSnapLagSeconds": {
                    "type": "number",
                    "description": "How far past the ad edge a cue may sit to be eligible for snapping (default 4.0s).",
                    "example": 4,
                    "minimum": 0.5,
                    "maximum": 30
                  },
                  "audioCueCaptureMinSeconds": {
                    "type": "number",
                    "description": "Shortest cue a user may bracket (default 0.2s).",
                    "minimum": 0.05,
                    "maximum": 10
                  },
                  "audioCueCaptureMaxSeconds": {
                    "type": "number",
                    "description": "Longest cue a user may bracket (default 4s).",
                    "minimum": 0.05,
                    "maximum": 30
                  },
                  "audioCueCaptureMaxIntroSeconds": {
                    "type": "number",
                    "description": "Longest show-intro stinger a user may bracket (default 60s).",
                    "minimum": 0.05,
                    "maximum": 120
                  },
                  "audioCueCaptureMaxOutroSeconds": {
                    "type": "number",
                    "description": "Longest show-outro stinger a user may bracket (default 60s).",
                    "minimum": 0.05,
                    "maximum": 120
                  },
                  "audioCuePairConfidence": {
                    "type": "number",
                    "description": "Min cue confidence to synthesize an ad from a cue pair (default 0.85).",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "audioCuePairMinBreakSeconds": {
                    "type": "number",
                    "description": "Shortest cue-pair break (default 30s).",
                    "minimum": 1,
                    "maximum": 600
                  },
                  "audioCuePairMaxBreakSeconds": {
                    "type": "number",
                    "description": "Longest cue-pair break (default 480s).",
                    "minimum": 1,
                    "maximum": 3600
                  },
                  "audioCuePairMaxBreakFraction": {
                    "type": "number",
                    "description": "Reject a cue pair spanning more than this fraction of the episode (short-episode backstop; default 0.5, 0 disables).",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "audioCueCreateFromPairs": {
                    "type": "boolean",
                    "description": "Opt-in cue-pair ad synthesis. Off by default."
                  },
                  "audioCueCutMatches": {
                    "type": "boolean",
                    "description": "Cut every matched cue on its own, with no ad next to it. Templates and feeds can override. Off by default."
                  },
                  "silenceSnapNoiseDb": {
                    "type": "number",
                    "description": "Amplitude (dBFS) below which audio counts as silence for silence snap (default -50).",
                    "minimum": -90,
                    "maximum": -20
                  },
                  "silenceSnapMinDurationSeconds": {
                    "type": "number",
                    "description": "Shortest sub-threshold span that counts as a silence for silence snap (default 0.3s).",
                    "minimum": 0.1,
                    "maximum": 5
                  },
                  "silenceSnapMaxDistanceSeconds": {
                    "type": "number",
                    "description": "Farthest an ad edge may move to reach a detected silence (default 2.0s).",
                    "minimum": 0.25,
                    "maximum": 10
                  },
                  "positionalPriorEnabled": {
                    "type": "boolean",
                    "description": "Toggles the learned positional prior experiment (issue\n360). When on, feeds with at least 5 processed episodes\nget per-feed learned ad-break zones, used as a scrutiny\nhint in the detection prompt and as position-based\nconfidence boosts during validation. Off by default.\n",
                    "example": false
                  },
                  "vadGapDetectionEnabled": {
                    "type": "boolean",
                    "description": "Toggles the VAD gap detector, which cuts audio regions\nWhisper's VAD dropped (sped-up disclaimers, distorted ad\ntails). Advanced tuning; intentionally not surfaced in the\nUI. See the Configuration guide (docs/configuration.md, VAD Gap Detector) for full behavior.\n",
                    "example": true
                  },
                  "vadGapStartMinSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "description": "Minimum pre-transcript head gap, in seconds, that the VAD\ndetector will cut. Values below the threshold are left alone.\n",
                    "example": 3
                  },
                  "vadGapMidMinSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "description": "Minimum mid-episode untranscribed gap for the standalone\nemission path. Gaps adjacent to an already-detected ad\nextend that ad in place regardless of this threshold.\n",
                    "example": 8
                  },
                  "vadGapTailMinSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "description": "Minimum post-transcript tail gap that the VAD detector\nwill cut when no postroll marker already covers it.\n",
                    "example": 3
                  },
                  "minContentBetweenAdsSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 60,
                    "description": "Minimum seconds of actual speech content that must appear\nin the gap between two ads to keep them separate. Gaps with\nless content (ad-transition music/silence) are collapsed into\none contiguous cut. Set to 0 to disable. Default 12.0.\n",
                    "example": 12
                  },
                  "adDetectionExcludeStartSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 600,
                    "description": "Seconds from the start of an episode during which ad markers are\nignored, so an intro cue is not mistaken for an ad break. 0 disables it.\nFeeds inherit it or set adDetectionExcludeStartOverride.\n"
                  },
                  "maxAdDurationSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 30,
                    "maximum": 3600,
                    "description": "Length past which an ad must name a sponsor MinusPod\nrecognizes to be cut. One that does not is held for\nreview rather than dropped.\n",
                    "example": 300
                  },
                  "maxAdDurationConfirmedSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 30,
                    "maximum": 3600,
                    "description": "Hard ceiling. Even an ad with a confirmed sponsor is held\nrather than cut past this length.\n",
                    "example": 900
                  },
                  "maxLlmWindowAdSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 60,
                    "maximum": 900,
                    "description": "Longest single ad the model may report within one detection\nwindow; a longer one is discarded. For each feed the cap is the\nlarger of this value, the feed's maxAdDurationOverride and the\nfeed's maxAdDurationRejectOverride. The verification pass uses\nthe same per-feed cap. Out-of-range stored values are clamped\nto 60 to 900 on read.\n",
                    "example": 420
                  },
                  "autoProcessEnabled": {
                    "type": "boolean",
                    "description": "Enable automatic processing of new episodes when feeds refresh"
                  },
                  "autoProcessMaxAgeHours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 720,
                    "description": "Only episodes published within this many hours are auto-queued (default 48). Feeds can override it."
                  },
                  "maxFeedEpisodes": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 500,
                    "description": "Global default cap on episodes returned in served RSS feeds. Overridden by per-feed maxEpisodes when set.",
                    "example": 300
                  },
                  "onlyExposeProcessedDefault": {
                    "type": "boolean",
                    "description": "Global default for hiding upstream episodes whose status is not 'processed' from served RSS feeds. Overridden by per-feed onlyExposeProcessedEpisodes when set."
                  },
                  "feedItunesBlock": {
                    "type": "boolean",
                    "description": "Add itunes:block Yes to every served feed so app directories skip it and Pocket Casts stops matching it to the public show. Default true."
                  },
                  "requireManualApprovalDefault": {
                    "type": "boolean",
                    "description": "Global default for holding newly processed episodes out of served feeds until approved. Overridden by per-feed requireManualApproval when set."
                  },
                  "whatsNewOnUpgrade": {
                    "type": "boolean",
                    "description": "Show the What's new dialog once per browser after the app version changes, or on a browser's first load."
                  },
                  "detectShowSegments": {
                    "type": "boolean",
                    "description": "Global default for detecting intro, outro, and recap/housekeeping segments in addition to the always-detected ad-side categories. Overridden by per-feed detectShowSegments when set."
                  },
                  "seedSponsorsDetection": {
                    "type": "boolean",
                    "description": "Whether the detection pass prompt is rendered with the seed sponsor list. Off renders that prompt with an empty sponsor block. Default true."
                  },
                  "seedSponsorsVerification": {
                    "type": "boolean",
                    "description": "Whether the verification pass prompt is rendered with the seed sponsor list. Off renders that prompt with an empty sponsor block. Default true."
                  },
                  "seedSponsorsReviewer": {
                    "type": "boolean",
                    "description": "Whether the reviewer pass prompt is rendered with the seed sponsor list. Off renders that prompt with an empty sponsor block. Default true."
                  },
                  "seedSponsorsResurrect": {
                    "type": "boolean",
                    "description": "Whether the resurrect pass prompt is rendered with the seed sponsor list. Off renders that prompt with an empty sponsor block. Default true."
                  },
                  "textRecurrenceHints": {
                    "type": "boolean",
                    "description": "Hints the detection prompt with transcript spans whose wording recurs near-verbatim across prior episodes of the same show (intros, disclaimers, credits). Off by default."
                  },
                  "adAddressingMode": {
                    "type": "string",
                    "enum": [
                      "timestamps",
                      "segment_ids",
                      "random"
                    ],
                    "description": "How the LLM addresses detected segments. 'timestamps' (default) reports absolute start/end seconds. 'segment_ids' reports transcript line ids instead, which the server maps to exact segment times. 'random' draws one of the two per detection run and records which, for the /stats/addressing compliance comparison."
                  },
                  "processNewEpisodesFirst": {
                    "type": "boolean",
                    "description": "Whether episodes published within the last 48 hours get an automatic queue-priority boost ahead of the rest of the auto-process backlog. On by default."
                  },
                  "rssRefreshIntervalMinutes": {
                    "type": "integer",
                    "minimum": 5,
                    "maximum": 1440,
                    "description": "Minutes between background RSS refresh passes for every feed. A change applies after the wait already in progress finishes. Default 15.",
                    "example": 15
                  },
                  "queueManualBoost": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Priority added when a user plays an unprocessed episode or reprocesses one by hand. Default 20."
                  },
                  "queueFreshBoost": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Priority added to episodes published within the last 48 hours while processNewEpisodesFirst is on. Default 5."
                  },
                  "queueBulkBoost": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Priority added to bulk work (Reprocess All, segment re-renders). Default 0 keeps backlog runs behind everything else. Default 0."
                  },
                  "podpingEnabled": {
                    "type": "boolean",
                    "description": "Opt-in listener that polls public Hive API nodes for Podping publish notifications and refreshes a matching feed immediately. Off by default."
                  },
                  "segmentCategoryActions": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "remove",
                        "beep",
                        "keep",
                        "mark"
                      ]
                    },
                    "description": "Partial global segment-category action map (issue",
                    "example": {
                      "sponsor": "remove",
                      "cross_promo": "keep",
                      "self_promo": "beep"
                    }
                  },
                  "communitySyncCategories": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "sponsor",
                        "cross_promo",
                        "self_promo",
                        "interaction",
                        "intro",
                        "outro",
                        "recap"
                      ]
                    },
                    "description": "Segment categories accepted from community pattern sync. Default is\nevery category. Replaces the stored list outright.\n"
                  },
                  "jitBlockedUserAgents": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "User-agent patterns barred from triggering just-in-time episode processing. Case-insensitive substring match; a leading caret anchors to the start of the agent."
                  },
                  "artworkWatermarkEnabled": {
                    "type": "boolean",
                    "description": "When true, the served feed's channel cover art gets a MinusPod badge overlaid so the filtered feed is distinct in a podcast app (issue"
                  },
                  "artworkBadgePosition": {
                    "type": "string",
                    "enum": [
                      "bottom-right",
                      "bottom-left",
                      "top-right",
                      "top-left"
                    ],
                    "description": "Corner the MinusPod badge is composited into (issue"
                  },
                  "lowAdYieldAction": {
                    "type": "string",
                    "enum": [
                      "nothing",
                      "redetect",
                      "reprocess",
                      "full"
                    ],
                    "description": "Automatic response when a pipeline-initiated run removes far less ad time than the feed's recent average: 'nothing' (default), 'redetect' (LLM-only rerun from the stored transcript), 'reprocess', or 'full'. Fires at most once per episode."
                  },
                  "episodeLogRetentionDays": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 365,
                    "description": "Days to keep per-run pipeline logs (issue 660). 0 turns run log storage off and sweeps what is already stored."
                  },
                  "episodeLogLevel": {
                    "type": "string",
                    "enum": [
                      "debug",
                      "info"
                    ],
                    "description": "Minimum level kept in a run log (issue 660). Defaults to debug."
                  },
                  "downloadUserAgent": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "User-Agent sent when fetching audio, artwork, and chapters. Printable ASCII on a single line. Send an empty string to restore the default."
                  },
                  "feedUserAgent": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "User-Agent sent when fetching RSS feeds. Printable ASCII on a single line. Send an empty string to restore the default."
                  },
                  "logDownloadQuery": {
                    "type": "boolean",
                    "description": "Include URL query strings in download logs. Off by default: an enclosure query string regularly carries a signed CDN token or a listener tracking id, and logs outlive both. The path and redirect chain are logged either way."
                  },
                  "feedAuthEnabled": {
                    "type": "boolean",
                    "description": "Require the global feed auth key on all public feed and episode URLs. Enabling generates the key if absent."
                  },
                  "downloadHistoryEnabled": {
                    "type": "boolean",
                    "description": "Record episode downloads for the History Downloads tab."
                  },
                  "downloadGeoLookupEnabled": {
                    "type": "boolean",
                    "description": "Resolve public client IPs to a location through ipwho.is. Off by default."
                  },
                  "downloadHistoryRetentionDays": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3650,
                    "description": "Days to keep download history rows."
                  },
                  "minCutConfidence": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0.5,
                    "maximum": 0.95,
                    "description": "Minimum confidence threshold to cut an ad from audio. Lower = more aggressive (removes more potential ads), higher = more conservative",
                    "example": 0.8
                  },
                  "vttTranscriptsEnabled": {
                    "type": "boolean",
                    "description": "Generate a VTT transcript sidecar for each processed episode.",
                    "example": true
                  },
                  "chaptersEnabled": {
                    "type": "boolean",
                    "description": "Generate podcast:chapters JSON for each processed episode.",
                    "example": true
                  },
                  "chaptersMode": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "generate",
                      "off"
                    ],
                    "description": "Default chapter mode for feeds that inherit this setting."
                  },
                  "skipSecondPass": {
                    "type": "boolean",
                    "description": "When true, feeds that inherit this setting skip the pass-2 verification scan."
                  },
                  "transcriptDifferentialEnabled": {
                    "type": "boolean",
                    "description": "Default for the upstream-transcript differential; feeds that inherit this setting follow it. Diffs the upstream podcast:transcript against the transcript to help locate ads.",
                    "example": true
                  },
                  "differentialFetchMode": {
                    "type": "string",
                    "enum": [
                      "auto",
                      "on",
                      "off"
                    ],
                    "description": "Default cross-fetch mode for feeds that inherit this setting."
                  },
                  "chaptersInNotes": {
                    "type": "boolean",
                    "description": "Append the generated chapter list to served episode descriptions and the episode API description. Feeds can override per feed.",
                    "example": false
                  },
                  "reviewLinksInNotes": {
                    "type": "boolean",
                    "description": "Append review and rate links (episode page and a 5-star rating link) to the served description of processed episodes. Feeds can override per feed.",
                    "example": false
                  },
                  "adChaptersEnabled": {
                    "type": [
                      "boolean",
                      "string"
                    ],
                    "enum": [
                      true,
                      false,
                      "on",
                      "off"
                    ],
                    "deprecated": true,
                    "description": "Deprecated. Translates into segmentCategoryActions; false or 'off' demotes every mark to keep, true or 'on' is accepted and ignored.",
                    "example": false
                  },
                  "adChapterCategories": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "boolean"
                    },
                    "deprecated": true,
                    "description": "Deprecated. Translates into segmentCategoryActions; true promotes keep to mark, false demotes mark to keep; remove and beep stay unchanged.",
                    "example": {
                      "sponsor": true,
                      "cross_promo": true
                    }
                  },
                  "adChaptersIncludeHeld": {
                    "type": "boolean",
                    "description": "Also publish held-for-review segments as ad chapters.",
                    "example": false
                  },
                  "adChapterTitleFormat": {
                    "type": "string",
                    "description": "Title template for an ad chapter; {label} is the category name and {category} its id. Send an empty string to restore the default.",
                    "example": "Ad: {label}"
                  },
                  "adChapterHeldTitleFormat": {
                    "type": "string",
                    "description": "Title template for a held segment's ad chapter; {label} is the category name and {category} its id. Send an empty string to restore the default.",
                    "example": "Possible ad: {label}"
                  },
                  "adChapterResumeTitle": {
                    "type": "string",
                    "description": "Title of the chapter that marks the return to show content after an ad chapter. Send an empty string to restore the default.",
                    "example": "Show"
                  },
                  "adChapterMinConfidence": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Minimum marker confidence required before a segment becomes an ad chapter.",
                    "example": 0.9
                  },
                  "audioReplacementSoundEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Insert the replacement clip for Remove actions. Null resets to the default.",
                    "default": true
                  },
                  "audioMp3StreamCopyEnabled": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Try compatible MP3 cuts; otherwise re-encode. Null resets to the default.",
                    "default": false
                  },
                  "audioBitrate": {
                    "type": "string",
                    "enum": [
                      "64k",
                      "96k",
                      "128k",
                      "192k",
                      "256k"
                    ],
                    "description": "Output bitrate used when re-encoding processed audio.",
                    "example": "128k"
                  },
                  "audioEncoderCompressionLevel": {
                    "type": "string",
                    "enum": [
                      "default",
                      "0",
                      "1",
                      "2",
                      "3",
                      "4",
                      "5",
                      "6",
                      "7",
                      "8",
                      "9"
                    ],
                    "description": "libmp3lame -compression_level: 0 is slowest/best, 9 is fastest. 'default' omits the flag, leaving ffmpeg's own encoding behavior unchanged.",
                    "example": "default"
                  },
                  "skipFlacCompression": {
                    "type": "boolean",
                    "description": "When true, skip the FLAC encode pass before uploading audio\nto the remote transcription API. Send the preprocessed WAV directly.\n",
                    "example": false
                  },
                  "audioNormalizeEnabled": {
                    "type": "boolean",
                    "description": "Loudness-normalize processed audio output.",
                    "example": false
                  },
                  "audioNormalizeIntensity": {
                    "type": "string",
                    "enum": [
                      "gentle",
                      "normal",
                      "aggressive",
                      "extreme",
                      "maximum"
                    ],
                    "description": "Loudness normalization intensity, applied when audioNormalizeEnabled is true.",
                    "example": "normal"
                  },
                  "adDetectionParallelWindows": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 32,
                    "description": "Number of transcript windows the ad detector processes\nconcurrently through the LLM.\n",
                    "example": 4
                  },
                  "adReviewerParallelAds": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 32,
                    "description": "Number of ads the reviewer asks the LLM about at the same time.\n",
                    "example": 4
                  },
                  "reviewProvider": {
                    "type": "string",
                    "description": "Routing slot for the reviewer pass. `same_as_pass` (default) inherits\nboth the provider and model of whichever pass is being reviewed.\n`primary` or `secondary` runs the reviewer on that slot instead and\nenables reviewModel; secondary falls back to primary when it is\ndisabled or unconfigured.\n",
                    "enum": [
                      "primary",
                      "secondary",
                      "same_as_pass"
                    ],
                    "example": "same_as_pass"
                  },
                  "maxArtworkBytes": {
                    "type": "integer",
                    "minimum": 65536,
                    "maximum": 52428800,
                    "description": "Cover art download size cap in bytes.",
                    "example": 26214400
                  },
                  "maxRssBytes": {
                    "type": "integer",
                    "minimum": 1048576,
                    "description": "RSS response body size cap in bytes (no ceiling).",
                    "example": 209715200
                  },
                  "maxAudioDownloadMb": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Per-episode audio download cap in MB (no ceiling).",
                    "example": 500
                  },
                  "localUploadMaxMb": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 4096,
                    "default": 1024,
                    "description": "Maximum size in MB of one local-feed upload request.",
                    "example": 1024
                  },
                  "llmProvider": {
                    "type": "string",
                    "description": "LLM provider for detection and verification; System One review is experimental. Chapters and Pattern Cleanup require a chat provider when enabled.",
                    "enum": [
                      "anthropic",
                      "openai-compatible",
                      "ollama",
                      "openrouter",
                      "typesafe",
                      "systemone-compatible"
                    ],
                    "example": "anthropic"
                  },
                  "omitTemperature": {
                    "type": "boolean",
                    "description": "Operator override: when true, omit the temperature parameter from every\nLLM call, for every model, regardless of the built-in list of models known\nto reject it or anything learned at runtime from a prior rejection. Use\nahead of a newly-released model that rejects temperature, without waiting\nfor a failed call or a code change. Off by default.\n"
                  },
                  "llmJsonSchemaEnabled": {
                    "type": "boolean",
                    "description": "Opt-in (#693): send response_format json_schema on detection, review,\ncategory-repair, and trim-recovery calls to OpenAI-compatible endpoints,\nfor each model that passes the probe. Support is tracked per model, not\nper endpoint; unsupported models fall back to json_object. Off by default.\n"
                  },
                  "typesafeApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "writeOnly": true,
                    "description": "Primary TypeSafe key. Omit to preserve; null or empty clears the stored key and restores TYPESAFE_API_KEY fallback. Separate from chat-provider credentials."
                  },
                  "systemoneApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "writeOnly": true,
                    "description": "Optional primary compatible key. Omit to preserve; null or empty clears the stored key and restores SYSTEMONE_API_KEY fallback."
                  },
                  "systemoneBaseUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Primary compatible API base URL; MinusPod appends /systemone. Empty or null clears the saved endpoint and restores SYSTEMONE_BASE_URL fallback. Default empty. TypeSafe always uses its canonical endpoint.",
                    "example": "https://example.com/v1"
                  },
                  "systemOneTunables": {
                    "$ref": "#/components/schemas/SystemOneProfilesPatch"
                  },
                  "openaiBaseUrl": {
                    "type": "string",
                    "description": "Base URL for OpenAI-compatible API endpoint (only used when provider is not anthropic)",
                    "example": "http://localhost:11434/v1"
                  },
                  "pricingSourceMode": {
                    "type": "string",
                    "description": "Pricing source override. 'auto' picks by provider/domain; 'litellm' always uses the\nLiteLLM catalog; 'free' disables cost tracking (use only for local models with no cost).\n",
                    "enum": [
                      "auto",
                      "litellm",
                      "free"
                    ],
                    "example": "auto"
                  },
                  "modelPricingOverrides": {
                    "type": "object",
                    "description": "Partial map of model IDs to operator pricing in USD per 1 million tokens. Operator pricing takes precedence over catalog pricing. Set an entry to null to remove its override. Both rates may be 0 for a free model.",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ModelPricingOverride"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "openrouterApiKey": {
                    "type": "string",
                    "description": "API key for OpenRouter (only used when provider is openrouter)"
                  },
                  "secondaryProviderEnabled": {
                    "type": "boolean",
                    "description": "Turn the optional secondary provider slot on or off. A\nper-stage provider override set to secondary while this is\nfalse falls back to primary. Off by default.\n"
                  },
                  "secondaryProvider": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Secondary provider type. Empty or null clears it.",
                    "enum": [
                      "anthropic",
                      "openai-compatible",
                      "ollama",
                      "openrouter",
                      "typesafe",
                      "systemone-compatible",
                      "",
                      null
                    ],
                    "example": "openai-compatible"
                  },
                  "secondaryProviderBaseUrl": {
                    "type": "string",
                    "description": "Base URL for the secondary provider, used when its type has a\nconfigurable endpoint (openai-compatible, ollama). SSRF-validated\nlike every other operator-configured URL. Empty clears the\noverride back to the default.\n",
                    "example": "http://localhost:11434/v1"
                  },
                  "secondaryProviderApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "API key for the secondary provider. Omit to leave unchanged; null or empty to clear."
                  },
                  "providerRequestsPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual requests-per-minute cap for the primary provider account. 0 means unlimited.",
                    "example": 5
                  },
                  "providerRequestsPerDay": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual requests-per-day cap for the primary provider account. 0 means unlimited.",
                    "example": 20
                  },
                  "secondaryProviderRequestsPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual requests-per-minute cap for the secondary provider account. 0 means unlimited.",
                    "example": 5
                  },
                  "secondaryProviderRequestsPerDay": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual requests-per-day cap for the secondary provider account. 0 means unlimited.",
                    "example": 20
                  },
                  "providerTokensPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual tokens-per-minute cap for the primary provider account. 0 means unlimited. Reservation-based, not recorded-usage-based: each dispatch reserves an estimate (prompt tokens plus max_tokens) before the request goes out and reconciles to actual usage when the call finishes, so a single large request cannot cross the cap unseen. The cost is that an over-estimated call holds more of the window than it spends until it completes.",
                    "example": 250000
                  },
                  "secondaryProviderTokensPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Manual tokens-per-minute cap for the secondary provider account. 0 means unlimited. Reservation-based in the same way as providerTokensPerMin: an estimate is reserved at dispatch and reconciled to actual usage on completion.",
                    "example": 250000
                  },
                  "providerBEnabled": {
                    "type": "boolean",
                    "description": "Alias of secondaryProviderEnabled."
                  },
                  "providerB": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Alias of secondaryProvider.",
                    "enum": [
                      "anthropic",
                      "openai-compatible",
                      "ollama",
                      "openrouter",
                      "",
                      null
                    ]
                  },
                  "providerBBaseUrl": {
                    "type": "string",
                    "description": "Alias of secondaryProviderBaseUrl."
                  },
                  "providerBApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Alias of secondaryProviderApiKey."
                  },
                  "providerBRequestsPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Alias of secondaryProviderRequestsPerMin."
                  },
                  "providerBRequestsPerDay": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Alias of secondaryProviderRequestsPerDay."
                  },
                  "providerBTokensPerMin": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Alias of secondaryProviderTokensPerMin."
                  },
                  "providerATimeoutSeconds": {
                    "anyOf": [
                      {
                        "type": "number",
                        "exclusiveMinimum": 0
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Request timeout for Provider A. Null or blank clears it to the provider-type default; omitted leaves it unchanged. Native providers accept positive finite fractional seconds; chat providers require integer seconds from 10 to 3600."
                  },
                  "providerAMaxRetries": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Max retries for Provider A. Null or blank clears it to the provider-type default; omitted leaves it unchanged."
                  },
                  "providerBTimeoutSeconds": {
                    "anyOf": [
                      {
                        "type": "number",
                        "exclusiveMinimum": 0
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Request timeout for Provider B. Null or blank clears it to the provider-type default; omitted leaves it unchanged. Native providers accept positive finite fractional seconds; chat providers require integer seconds from 10 to 3600."
                  },
                  "providerBMaxRetries": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Max retries for Provider B. Null or blank clears it to the provider-type default; omitted leaves it unchanged."
                  },
                  "failoverLlmEnabled": {
                    "type": "boolean",
                    "description": "Enable or disable the shared LLM failover provider. Off by default."
                  },
                  "failoverLlmProvider": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "anthropic",
                      "openai-compatible",
                      "ollama",
                      "openrouter",
                      "",
                      null
                    ],
                    "description": "LLM failover provider type. Empty or null clears it."
                  },
                  "failoverLlmBaseUrl": {
                    "type": "string",
                    "description": "Base URL for the LLM failover provider; requests are SSRF-validated."
                  },
                  "failoverLlmApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "API key for the LLM failover provider. Omit to leave unchanged; null or empty to clear."
                  },
                  "failoverLlmTimeoutSeconds": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": 10,
                        "maximum": 3600
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Request timeout for the LLM failover provider. Null or blank clears it to the provider-type default; omitted leaves it unchanged."
                  },
                  "failoverLlmMaxRetries": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Max retries for the LLM failover provider. Null or blank clears it to the provider-type default; omitted leaves it unchanged."
                  },
                  "failoverLlmDetectionModel": {
                    "type": "string",
                    "description": "Model the LLM failover provider uses for ad detection."
                  },
                  "failoverLlmReviewModel": {
                    "type": "string",
                    "description": "Model the LLM failover provider uses for review. Blank inherits the detection model."
                  },
                  "failoverLlmVerificationModel": {
                    "type": "string",
                    "description": "Model the LLM failover provider uses for verification. Blank inherits the detection model."
                  },
                  "failoverLlmChaptersModel": {
                    "type": "string",
                    "description": "Model the LLM failover provider uses for chapters. Blank inherits the detection model."
                  },
                  "failoverWhisperEnabled": {
                    "type": "boolean",
                    "description": "Enable or disable the transcriber failover provider. Off by default."
                  },
                  "failoverWhisperBackend": {
                    "type": "string",
                    "enum": [
                      "local",
                      "openai-api",
                      "parakeet"
                    ],
                    "description": "Transcriber failover backend (parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process)."
                  },
                  "failoverWhisperModel": {
                    "type": "string",
                    "description": "Local model name for the transcriber failover, when its backend is local."
                  },
                  "failoverWhisperApiBaseUrl": {
                    "type": "string",
                    "description": "Base URL for the transcriber failover API backend, SSRF-validated."
                  },
                  "failoverWhisperApiKey": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "API key for the transcriber failover API backend. Omit to leave unchanged; null or empty to clear."
                  },
                  "failoverWhisperApiModel": {
                    "type": "string",
                    "description": "Model name for the transcriber failover API backend."
                  },
                  "failoverWhisperApiTimeoutSeconds": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 3600,
                    "description": "Request timeout for the transcriber failover API backend."
                  },
                  "failoverWhisperMaxAttempts": {
                    "anyOf": [
                      {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      },
                      {
                        "type": "string",
                        "const": ""
                      }
                    ],
                    "description": "Upload attempts for the failover transcriber, including the first upload. Null or blank inherits whisperMaxAttempts; omit to leave unchanged."
                  },
                  "failoverWhisperLanguage": {
                    "type": "string",
                    "description": "Language for the transcriber failover. Empty means same as the active transcriber; otherwise 'auto' or a 2-3 letter code."
                  },
                  "failoverProbeIntervalMinutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 60,
                    "description": "Minutes between health probes of a failover target."
                  },
                  "failoverRecoveryProbes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "description": "Consecutive healthy probes needed before an automatic failover recovers."
                  },
                  "whisperMaxAttempts": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "description": "Max attempts for a single whisper API transcription request before giving up."
                  },
                  "detectionTemperature": {
                    "type": "number",
                    "description": "Temperature (0.0-2.0) for the first-pass ad detection LLM call.",
                    "example": 0
                  },
                  "detectionMaxTokens": {
                    "type": "integer",
                    "description": "Maximum response tokens (128-32768) for first-pass ad detection.",
                    "example": 4096
                  },
                  "detectionReasoningBudget": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Manual Anthropic thinking budget (1024-65536 tokens). Used only when the routed model accepts manual budgets; otherwise kept as a dormant setting."
                  },
                  "detectionReasoningLevel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "low",
                      "medium",
                      "high",
                      null
                    ],
                    "description": "Effort for adaptive-only Anthropic models and other providers. Null restores the default. none disables thinking where supported, or uses low effort on models that require thinking."
                  },
                  "verificationTemperature": {
                    "type": "number",
                    "description": "Temperature (0.0-2.0) for the verification (pass 2) LLM call."
                  },
                  "verificationMaxTokens": {
                    "type": "integer",
                    "description": "Maximum response tokens (128-32768) for the verification pass."
                  },
                  "verificationReasoningBudget": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Manual Anthropic thinking budget for verification pass. Dormant when the routed model uses effort or another provider."
                  },
                  "verificationReasoningLevel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "low",
                      "medium",
                      "high",
                      null
                    ],
                    "description": "Reasoning effort for verification pass, including adaptive-only Anthropic models. Dormant when the routed model uses a manual budget."
                  },
                  "reviewerTemperature": {
                    "type": "number",
                    "description": "Temperature (0.0-2.0) for both reviewer passes."
                  },
                  "reviewerMaxTokens": {
                    "type": "integer",
                    "description": "Maximum response tokens (128-32768) for the reviewer."
                  },
                  "reviewerReasoningBudget": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Manual Anthropic thinking budget for the reviewer. Dormant when the routed model uses effort or another provider."
                  },
                  "reviewerReasoningLevel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "low",
                      "medium",
                      "high",
                      null
                    ],
                    "description": "Reasoning effort for the reviewer, including adaptive-only Anthropic models. Dormant when the routed model uses a manual budget."
                  },
                  "chapterBoundaryTemperature": {
                    "type": "number",
                    "description": "Temperature (0.0-2.0) for chapter boundary detection."
                  },
                  "chapterBoundaryMaxTokens": {
                    "type": "integer",
                    "description": "Maximum response tokens (128-32768) for chapter boundary detection."
                  },
                  "chapterBoundaryReasoningBudget": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Manual Anthropic thinking budget for chapter boundary detection. Dormant when the routed model uses effort or another provider."
                  },
                  "chapterBoundaryReasoningLevel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "low",
                      "medium",
                      "high",
                      null
                    ],
                    "description": "Reasoning effort for chapter boundary detection, including adaptive-only Anthropic models. Dormant when the routed model uses a manual budget."
                  },
                  "chapterTargetSeconds": {
                    "type": "integer",
                    "description": "Target chapter length in seconds (120-3600, default 600). Sets how many boundaries each transcript window is asked for. Must not exceed chapterWindowSeconds."
                  },
                  "chapterWindowSeconds": {
                    "type": "integer",
                    "description": "Transcript window per boundary-detection call in seconds (600-10800, default 2700). One LLM call per window."
                  },
                  "chapterMaxBoundaries": {
                    "type": "integer",
                    "description": "Ceiling on chapter boundaries per episode (1-200, default 40)."
                  },
                  "chapterMinDurationSeconds": {
                    "type": "integer",
                    "description": "Shortest chapter in seconds (30-900, default 180). Shorter chapters are absorbed into the previous one. Must not exceed chapterTargetSeconds."
                  },
                  "chapterTitleTemperature": {
                    "type": "number",
                    "description": "Temperature (0.0-2.0) for chapter title generation."
                  },
                  "chapterTitleMaxTokens": {
                    "type": "integer",
                    "description": "Maximum response tokens (128-32768) for chapter title generation."
                  },
                  "chapterTitleReasoningBudget": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Manual Anthropic thinking budget for chapter title generation. Dormant when the routed model uses effort or another provider."
                  },
                  "chapterTitleReasoningLevel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "none",
                      "low",
                      "medium",
                      "high",
                      null
                    ],
                    "description": "Reasoning effort for chapter title generation, including adaptive-only Anthropic models. Dormant when the routed model uses a manual budget."
                  },
                  "ollamaNumCtx": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Ollama context window in tokens (512-131072). Only valid when llmProvider is ollama."
                  },
                  "windowSizeSeconds": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 120,
                    "maximum": 1800,
                    "description": "Detection-window duration in seconds. Must be strictly greater than windowOverlapSeconds."
                  },
                  "windowOverlapSeconds": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1770,
                    "description": "Overlap between consecutive detection windows in seconds. Must be less than windowSizeSeconds."
                  },
                  "verificationMissHoldMinConfidence": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 1,
                    "description": "Confidence floor for a standalone pass-2 verification miss (a catch overlapping no pass-1 marker) to hold for review instead of being dropped (default 0.60).",
                    "example": 0.6
                  },
                  "verificationMissAutocutMinConfidence": {
                    "type": "number",
                    "format": "float",
                    "description": "Confidence floor for a standalone verification miss to cut automatically instead of holding for review. 0 disables autocut (default); otherwise must be between 0.5 and 1.0.",
                    "example": 0
                  },
                  "learningMinConfidence": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0.5,
                    "maximum": 1,
                    "description": "Minimum confidence for a detection to teach the pattern matcher a new sponsor, for ads up to 90 seconds (default 0.85).",
                    "example": 0.85
                  },
                  "learningMinConfidenceLong": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0.5,
                    "maximum": 1,
                    "description": "Same floor as learningMinConfidence for ads longer than 90 seconds (default 0.92).",
                    "example": 0.92
                  },
                  "learningMinPatternDuration": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 600,
                    "description": "Shortest span, in seconds, that can teach the pattern matcher. Below this a span is usually a fragment or a host mention rather than a read (default 15).",
                    "example": 15
                  },
                  "learningMaxPatternDuration": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1800,
                    "description": "Longest span, in seconds, learned as a single pattern. A longer span is split at its ad transitions and each piece is learned separately (default 120).",
                    "example": 120
                  },
                  "daiDifferentialOverridesKeep": {
                    "type": "boolean",
                    "description": "A marker overlapping a measured differential region is cut even when segmentCategoryActions maps its category to keep. Audio that differs between two fetches of the same enclosure was inserted, whatever the category says (default true)."
                  },
                  "spliceVetoEnabled": {
                    "type": "boolean",
                    "description": "Hold a long cut found only in the transcript for review unless the audio shows an edit point at its edges. A feed's own spliceVetoEnabled overrides this (default true)."
                  },
                  "differentialMeasuredCorrMax": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 1,
                    "description": "A cross-fetch region becomes a differential candidate only when its measured correlation is at or below this value (default 0.60).",
                    "example": 0.6
                  },
                  "differentialHoldMinSeconds": {
                    "type": "number",
                    "format": "float",
                    "minimum": 0,
                    "maximum": 120,
                    "description": "Shortest uncorroborated differential candidate that may hold for review; shorter candidates are dropped. 0 disables the floor (default 10).",
                    "example": 10
                  }
                }
              },
              "queueManualBoost": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added when a user plays an unprocessed episode or reprocesses one by hand. Default 20."
              },
              "queueFreshBoost": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added to episodes published within the last 48 hours while processNewEpisodesFirst is on. Default 5."
              },
              "queueBulkBoost": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added to bulk work (Reprocess All, segment re-renders). Default 0 keeps backlog runs behind everything else. Default 0."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/settings/ad-detection/reset": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Reset ad detection settings",
        "description": "Reset all ad detection settings to their default values, including LLM provider and base URL (reverts to environment variable defaults)",
        "operationId": "resetAdDetectionSettings",
        "responses": {
          "200": {
            "description": "Settings reset to defaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/prompts/reset": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Reset prompts only",
        "description": "Reset only the system prompts to defaults without affecting model selection or other settings",
        "operationId": "resetPromptsOnly",
        "responses": {
          "200": {
            "description": "Prompts reset to defaults",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/prompts/{name}/reset": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Reset a single prompt",
        "description": "Reset one prompt (system, verification, review, resurrect, chapter, or pattern_cleanup) and its override, when it has one, to defaults",
        "operationId": "resetSinglePrompt",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "Prompt to reset",
            "schema": {
              "type": "string",
              "enum": [
                "system",
                "verification",
                "review",
                "resurrect",
                "chapter",
                "pattern_cleanup"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Prompt reset to default",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "string"
                    },
                    "isDefault": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/settings/models": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get available AI models",
        "description": "Returns list of available AI models for ad detection. Optionally accepts a provider query param to preview models for a different provider before saving settings.",
        "operationId": "getAvailableModels",
        "parameters": [
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "description": "Preview models for this provider without saving settings. Valid values are anthropic, openrouter, openai-compatible, ollama, typesafe, systemone-compatible.",
            "schema": {
              "type": "string",
              "enum": [
                "anthropic",
                "openrouter",
                "openai-compatible",
                "ollama",
                "typesafe",
                "systemone-compatible"
              ]
            }
          },
          {
            "name": "slot",
            "in": "query",
            "required": false,
            "description": "Which credential slot to preview `provider` with. `secondary` uses the secondary provider slot's own API key and base URL instead of the primary slot's; `failover` likewise uses the LLM failover slot's own key and base URL. `a`/`b` are accepted as aliases of `primary`/`secondary`. Only meaningful together with `provider`.",
            "schema": {
              "type": "string",
              "enum": [
                "primary",
                "secondary",
                "failover",
                "a",
                "b"
              ],
              "default": "primary"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of available models",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Model"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid provider or slot parameter",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/models/refresh": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Refresh available AI models",
        "description": "Force refresh the model list for one credential slot. Recreates that slot's LLM client and fetches a fresh model list. The optional body selects the slot; with no body the primary slot is refreshed.",
        "operationId": "refreshModels",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slot": {
                    "type": "string",
                    "enum": [
                      "primary",
                      "secondary",
                      "failover",
                      "a",
                      "b"
                    ],
                    "default": "primary",
                    "description": "Credential slot to refresh. secondary/failover require a configured secondary/failover provider. `a`/`b` are accepted as aliases of `primary`/`secondary`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refreshed model list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Model"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of models available"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid slot, or secondary requested with no secondary provider configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/whisper-models": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get available Whisper models",
        "description": "Returns list of available Whisper models for transcription with resource requirements",
        "operationId": "getWhisperModels",
        "responses": {
          "200": {
            "description": "List of available Whisper models",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WhisperModel"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Readiness check (database and storage)",
        "description": "Readiness probe used by Docker and Portainer health checks. Returns\n200 when the DB and storage directory are reachable, 503 otherwise.\nDoes not instantiate the processing queue; a busy queue is not an\nill-health signal.\n",
        "operationId": "healthCheck",
        "security": [],
        "responses": {
          "200": {
            "description": "System is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "healthy"
                      ]
                    },
                    "checks": {
                      "type": "object",
                      "properties": {
                        "database": {
                          "type": "boolean"
                        },
                        "storage": {
                          "type": "boolean"
                        },
                        "queue_available": {
                          "type": "boolean"
                        },
                        "llm_model_configured": {
                          "type": "boolean",
                          "description": "Whether a detection model is configured. Informational only; false does not flip overall status to unhealthy, since an instance with no model still serves already-processed feeds."
                        }
                      }
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "System is unhealthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "unhealthy"
                      ]
                    },
                    "checks": {
                      "type": "object"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health/live": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Liveness probe (minimal)",
        "description": "Minimal liveness probe with no side effects. Returns\n``{\"status\": \"ok\"}`` and 200 whenever the process is alive. Safe for\nper-second polling by Kubernetes-style liveness probes.\n",
        "operationId": "healthLive",
        "security": [],
        "responses": {
          "200": {
            "description": "Process is alive",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/changelog": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get changelog sections",
        "description": "Returns released CHANGELOG.md sections, newest first, capped by `limit`. `since` optionally keeps only sections newer than that version. An empty list means the changelog is not bundled in this build.",
        "operationId": "getChangelog",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional X.Y.Z version; only newer sections are returned.",
            "example": "2.101.14"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            },
            "description": "Values above 50 are clamped to 50."
          }
        ],
        "responses": {
          "200": {
            "description": "Changelog sections",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "current",
                    "sections"
                  ],
                  "properties": {
                    "current": {
                      "type": "string",
                      "description": "Running app version"
                    },
                    "sections": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "version",
                          "date",
                          "body"
                        ],
                        "properties": {
                          "version": {
                            "type": "string"
                          },
                          "date": {
                            "type": "string"
                          },
                          "body": {
                            "type": "string",
                            "description": "Markdown section body without its header"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed `since` (`bad_since`) or `limit` below 1 (`bad_limit`)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/system/status": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get system status",
        "description": "Returns system status, statistics, and configuration, plus three non-fatal degraded signals: feed-refresh outage state, Podping all-nodes-down state with per-node health, and the health of the configured transcription backend. None of them gates readiness; /health is the readiness probe.",
        "operationId": "getSystemStatus",
        "responses": {
          "200": {
            "description": "System status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SystemStatus"
                }
              }
            }
          }
        }
      }
    },
    "/system/podping/check": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Check every Podping RPC node",
        "description": "Requests one leader-owned check of every configured node. The check validates the head and a block response without changing the active listener node. An unfinished request is reused. Follow progress through podping.check in GET /system/status. This action also works while the Podping listener is disabled.",
        "operationId": "checkPodpingNodes",
        "responses": {
          "202": {
            "description": "Check accepted or already in progress",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PodpingNodeCheck"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "accepted": {
                          "type": "boolean",
                          "description": "True when this request created the check."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "CSRF validation failed"
          },
          "503": {
            "description": "The background monitor is unavailable"
          }
        }
      }
    },
    "/system/database/checkpoint": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Run a passive SQLite WAL checkpoint",
        "operationId": "checkpointDatabase",
        "responses": {
          "200": {
            "description": "Checkpoint completed"
          },
          "409": {
            "description": "Active readers prevented the checkpoint from completing"
          }
        }
      }
    },
    "/system/updates": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get latest release info from GitHub",
        "description": "Returns the current version plus the latest stable and edge release info from GitHub Releases. Results are cached in-process for 6 hours; pass ?refresh=true to bypass the cache. Never fires the Update Available notification. That only happens on the daily background check.",
        "operationId": "getSystemUpdates",
        "parameters": [
          {
            "name": "refresh",
            "in": "query",
            "required": false,
            "description": "Bypass the 6-hour cache and re-fetch from GitHub",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Update status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "current": {
                      "type": "object",
                      "properties": {
                        "version": {
                          "type": "string"
                        },
                        "releaseDate": {
                          "type": "string",
                          "format": "date"
                        }
                      }
                    },
                    "stable": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/ReleaseInfo"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "edge": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/ReleaseInfo"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "stable",
                        "edge"
                      ]
                    },
                    "updateAvailable": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "GitHub unreachable or fetch failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system/token-usage": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get LLM token usage",
        "description": "Returns cumulative LLM token usage with global totals and per-model breakdown including pricing info",
        "operationId": "getTokenUsage",
        "responses": {
          "200": {
            "description": "Token usage summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalInputTokens": {
                      "type": "integer",
                      "description": "Total input tokens across all models"
                    },
                    "totalOutputTokens": {
                      "type": "integer",
                      "description": "Total output tokens across all models"
                    },
                    "totalCost": {
                      "type": "number",
                      "description": "Total estimated cost in USD"
                    },
                    "models": {
                      "type": "array",
                      "description": "Per-model usage breakdown",
                      "items": {
                        "type": "object",
                        "properties": {
                          "modelId": {
                            "type": "string",
                            "description": "Model identifier"
                          },
                          "displayName": {
                            "type": "string",
                            "description": "Human-readable model name"
                          },
                          "totalInputTokens": {
                            "type": "integer"
                          },
                          "totalOutputTokens": {
                            "type": "integer"
                          },
                          "totalCost": {
                            "type": "number",
                            "description": "Estimated cost in USD for this model"
                          },
                          "callCount": {
                            "type": "integer",
                            "description": "Number of API calls made with this model"
                          },
                          "inputCostPerMtok": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Input cost per million tokens (USD)"
                          },
                          "outputCostPerMtok": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Output cost per million tokens (USD)"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/system/model-pricing": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get model pricing rates",
        "description": "Returns all known LLM model pricing rates from the model_pricing table",
        "operationId": "getModelPricing",
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Filter pricing entries by source",
            "schema": {
              "type": "string",
              "enum": [
                "openrouter_api",
                "pricepertoken",
                "default",
                "legacy"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model pricing list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "models": {
                      "type": "array",
                      "description": "All known model pricing entries",
                      "items": {
                        "type": "object",
                        "properties": {
                          "matchKey": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Normalized model key for pricing lookups"
                          },
                          "rawModelId": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Original model identifier from pricing source"
                          },
                          "displayName": {
                            "type": "string",
                            "description": "Human-readable model name"
                          },
                          "inputCostPerMtok": {
                            "type": "number",
                            "description": "Input cost per million tokens (USD)"
                          },
                          "outputCostPerMtok": {
                            "type": "number",
                            "description": "Output cost per million tokens (USD)"
                          },
                          "cacheReadCostPerMtok": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Cached-input cost per million tokens (USD), when the source publishes one"
                          },
                          "source": {
                            "type": "string",
                            "description": "Pricing data source (openrouter_api, pricepertoken, default, legacy)"
                          },
                          "updatedAt": {
                            "type": "string",
                            "description": "Last updated timestamp"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/system/model-pricing/refresh": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Force refresh model pricing",
        "description": "Force refresh pricing data from the active provider's pricing source (OpenRouter API or pricepertoken.com)",
        "operationId": "refreshModelPricing",
        "responses": {
          "200": {
            "description": "Pricing refreshed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "modelsUpdated": {
                      "type": "integer",
                      "description": "Number of models with pricing data"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Pricing fetch failed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/system/cleanup": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Trigger manual cleanup",
        "description": "Manually trigger cleanup of episodes older than retention period",
        "operationId": "triggerCleanup",
        "responses": {
          "200": {
            "description": "Cleanup completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "episodesRemoved": {
                      "type": "integer"
                    },
                    "spaceFreedMb": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/system/vacuum": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Trigger SQLite VACUUM",
        "description": "Reclaim disk space by compacting the database. Rate limited to 1/hour.",
        "operationId": "triggerVacuum",
        "responses": {
          "200": {
            "description": "VACUUM completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "durationMs": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/docs": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Swagger UI",
        "description": "Renders this OpenAPI spec in an interactive Swagger UI. Assets\nare bundled in the image under `/ui/swagger/*`, no CDN\ndependency, air-gap friendly. Auth-gated; an unauthenticated\nGET returns 401 (the blueprint-level `check_auth` runs first).\n",
        "operationId": "swaggerUi",
        "responses": {
          "200": {
            "description": "HTML page with Swagger UI",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/docs/": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Swagger UI (trailing-slash alias)",
        "description": "Same content as `/docs`; exists so browsers don't get a Werkzeug redirect on the trailing slash variant.",
        "operationId": "swaggerUiSlash",
        "responses": {
          "200": {
            "description": "HTML page with Swagger UI",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.yaml": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "OpenAPI spec",
        "description": "Returns this spec file. Auth-gated. Note that the spec\ndescribes endpoints relative to `/api/v1`; this handler\nserves the spec at `/api/v1/openapi.yaml`.\n",
        "operationId": "openapiSpec",
        "responses": {
          "200": {
            "description": "OpenAPI YAML document",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "string"
                }
              },
              "text/yaml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/subscriber-keys": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "List subscriber keys across all feeds",
        "description": "Labels and timestamps without secret values, sorted by feed title, label, then creation time.",
        "responses": {
          "200": {
            "description": "Subscriber keys with their feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "keys"
                  ],
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedSubscriberKeyWithFeed"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/subscriber-keys": {
      "parameters": [
        {
          "$ref": "#/components/parameters/slug"
        }
      ],
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "List feed subscriber keys",
        "responses": {
          "200": {
            "description": "Labels and timestamps without secret values",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "keys"
                  ],
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedSubscriberKeyRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "Create a feed subscriber key",
        "description": "Returns the scoped token and feed URL once.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Scoped token and feed URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedSubscriberKeyCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/subscriber-keys/{key_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/slug"
        },
        {
          "name": "key_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}$"
          }
        }
      ],
      "delete": {
        "tags": [
          "Feeds"
        ],
        "summary": "Revoke a feed subscriber key",
        "responses": {
          "200": {
            "description": "Key revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "revoked"
                  ],
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/subscriber-keys/{key_id}/record": {
      "parameters": [
        {
          "$ref": "#/components/parameters/slug"
        },
        {
          "name": "key_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}$"
          }
        }
      ],
      "delete": {
        "tags": [
          "Feeds"
        ],
        "summary": "Delete a revoked subscriber key record",
        "description": "Removes only a record that was already revoked. The deleted token stays invalid because no matching credential remains.",
        "responses": {
          "200": {
            "description": "Revoked key record deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Subscriber key not found"
          },
          "409": {
            "description": "Subscriber key is still active"
          }
        }
      }
    },
    "/settings/provider-budget": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Read provider admission settings",
        "responses": {
          "200": {
            "description": "Settings and current reservation totals",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBudget"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update provider admission settings",
        "description": "Provider charges and enforcement remain in USD. A non-USD save uses the current server-fetched reference rate and records its currency and date.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBudgetUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings and reservation totals",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBudget"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "description": "Update limit of 10 requests per hour exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Currency service unavailable for a non-USD update",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/provider-budget/currencies": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "List provider budget display currencies",
        "responses": {
          "200": {
            "description": "Supported ISO currency codes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "currencies"
                  ],
                  "properties": {
                    "currencies": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CurrencyOption"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Currency service unavailable"
          }
        }
      }
    },
    "/settings/provider-budget/rate/{currency}": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Read the current USD reference rate for a display currency",
        "parameters": [
          {
            "name": "currency",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{3}$"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{3}$"
            },
            "description": "Existing display currency for preview conversion."
          },
          {
            "name": "fromRate",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Positive decimal local-per-USD rate for the existing currency."
          },
          {
            "name": "dailyLimit",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Non-negative decimal amount to convert for preview."
          },
          {
            "name": "unknownReserve",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Non-negative decimal amount to convert for preview."
          }
        ],
        "responses": {
          "200": {
            "description": "Current USD reference rate and source date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBudgetRate"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "503": {
            "description": "Currency service unavailable"
          }
        }
      }
    },
    "/system/backup": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Download database backup",
        "description": "Downloads encrypted SQLite by default. A missing master passphrase\nreturns 409, never plaintext. `encrypted=false` requests\nplaintext; treat the file as sensitive. Limited to 6/hour.\n",
        "operationId": "downloadBackup",
        "parameters": [
          {
            "in": "query",
            "name": "encrypted",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "true encrypts without fallback. false explicitly requests a plaintext database file."
          }
        ],
        "responses": {
          "200": {
            "description": "Database backup file download",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Attachment filename ending in .db.enc when encrypted or .db for an explicit plaintext export."
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "409": {
            "description": "Encrypted export unavailable because no master passphrase is configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "backup_encryption_unavailable"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Backup creation or encryption failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system/config-backup": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Download runtime configuration JSON",
        "description": "Exports portable global and feed settings, including configured credentials. Excludes the database, episodes, patterns, history, metrics, media, and administrator sign-in credentials. Keep the downloaded file private. Limited to 5/hour. The separate config-export endpoint produces a redacted diagnostic file.",
        "operationId": "downloadRuntimeConfig",
        "responses": {
          "200": {
            "description": "Private configuration attachment with Cache-Control no-store",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeConfigDocument"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "413": {
            "description": "Complete configuration exceeds the supported import request size"
          },
          "500": {
            "description": "Configuration or credentials could not be exported"
          }
        }
      }
    },
    "/system/config-import/preview": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Preview runtime configuration changes",
        "description": "Validates a configuration document and selected scope without writing settings. Returns setting names and feed slugs, never credential values. Limited to 20/hour.",
        "operationId": "previewRuntimeConfigImport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RuntimeConfigImportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview and destination-state token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RuntimeConfigImportPreview"
                }
              }
            }
          },
          "400": {
            "description": "Invalid configuration, scope, selection, or conflicting feed identity"
          },
          "413": {
            "description": "Request exceeds 10 MiB"
          }
        }
      }
    },
    "/system/config-import": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Apply a previewed runtime configuration import",
        "description": "Merges selected settings atomically and re-encrypts credentials for this installation. Omitted settings and destination-only feeds remain unchanged. Does not restore a database, episodes, history, metrics, patterns, or media. Limited to 5/hour.",
        "operationId": "applyRuntimeConfigImport",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RuntimeConfigImportRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "previewToken"
                    ],
                    "properties": {
                      "previewToken": {
                        "type": "string",
                        "description": "Token from previewing the same document and selection"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied configuration and any feed-refresh warnings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "addedFeeds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "updatedFeeds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid configuration, selection, or destination compatibility"
          },
          "409": {
            "description": "Preview is stale; preview the selected configuration again"
          },
          "413": {
            "description": "Request exceeds 10 MiB"
          }
        }
      }
    },
    "/system/config-export": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Download instance configuration",
        "description": "Downloads settings, feed, and webhook configuration as pretty-printed\nJSON, ready to attach to a bug report. A redaction pass drops\nsecret-shaped keys (API keys, passwords, tokens, the feed auth key,\nwebhook secrets), strips credentials and tracking query params from\nURLs, and reduces each webhook URL to scheme + host. Limited to 6/hour.\n",
        "operationId": "exportConfig",
        "responses": {
          "200": {
            "description": "Redacted instance configuration file download",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Attachment filename: minuspod-config-YYYYMMDD-HHMMSS.json"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "type": "object"
                    },
                    "feeds": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "system": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system/diagnostic-export": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Download bounded operational diagnostics",
        "description": "Downloads payload-free structured diagnostics for an optional UTC ISO 8601 start/end range. The range is limited to 24 hours. For warnings and errors the export includes the static message template as written in the source and the exception class name. It omits logged values, exception text, prompts, transcripts, descriptions, responses, identifiers, secrets, and URLs. Episode ratings and their notes are included for the range. Rate limited to 6/hour.",
        "operationId": "exportDiagnosticLog",
        "parameters": [
          {
            "name": "start",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "end",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Structured diagnostic metadata file download",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events",
                    "truncated",
                    "episodeRatings",
                    "start",
                    "end",
                    "coverage"
                  ],
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "episodeRatings": {
                      "type": "array",
                      "description": "Episodes rated inside the range, newest first, at most 500.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "rating": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 5
                          },
                          "notes": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "ratedAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "markers": {
                            "type": "object",
                            "properties": {
                              "cut": {
                                "type": "integer"
                              },
                              "kept": {
                                "type": "integer"
                              },
                              "held": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    },
                    "start": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "coverage": {
                      "type": "object",
                      "required": [
                        "description",
                        "firstEvent",
                        "lastEvent",
                        "version"
                      ],
                      "properties": {
                        "description": {
                          "type": "string"
                        },
                        "firstEvent": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "lastEvent": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "version": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid or oversized time range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system/db-backup/run": {
      "post": {
        "summary": "Run a scheduled-style DB backup now",
        "description": "Forces a backup to the configured destination with rotation, ignoring\nthe enabled flag. Distinct from GET /system/backup (which streams a\none-off download). Rate limited to 6/hour.\n",
        "tags": [
          "System"
        ],
        "operationId": "runDbBackup",
        "responses": {
          "200": {
            "description": "Backup completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseBackupRunSummary"
                }
              }
            }
          },
          "409": {
            "description": "A backup is already in progress",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Backup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/retention": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get retention configuration",
        "operationId": "getRetention",
        "responses": {
          "200": {
            "description": "Retention settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "retentionDays": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 3650
                    },
                    "originalRetentionDays": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 3650,
                      "description": "Days to retain the pre-cut original copy of each episode. Defaults to retentionDays when never set; the server clamps to retentionDays on save so an original cannot outlive its processed peer."
                    },
                    "enabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update retention configuration",
        "operationId": "updateRetention",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "retentionDays"
                ],
                "properties": {
                  "retentionDays": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 3650,
                    "description": "Days to retain processed audio. 0 disables retention."
                  },
                  "originalRetentionDays": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3650,
                    "description": "Optional. Days to retain the pre-cut original copy. Server clamps to retentionDays. When omitted, the existing value is preserved."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Retention updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "retentionDays": {
                      "type": "integer"
                    },
                    "originalRetentionDays": {
                      "type": "integer"
                    },
                    "enabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/offline-queue": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get offline queue configuration",
        "description": "Offline queue for LLM/Whisper outages. When enabled, an episode that fails because the LLM provider or Whisper API endpoint is unreachable is deferred instead of failed, then processed automatically once the service is reachable again.",
        "operationId": "getOfflineQueueSettings",
        "responses": {
          "200": {
            "description": "Offline queue settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "ttlHours": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 720,
                      "description": "Hours a deferred episode waits before being marked permanently failed."
                    },
                    "deferredCount": {
                      "type": "integer",
                      "description": "Episodes currently waiting in the offline queue."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update offline queue configuration",
        "operationId": "updateOfflineQueueSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Defer episodes when the LLM or Whisper endpoint is unreachable. Off by default."
                  },
                  "ttlHours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 720,
                    "description": "Hours a deferred episode waits before being marked permanently failed. Expired episodes are logged and removed from the queue."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Offline queue settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "ttlHours": {
                      "type": "integer"
                    },
                    "deferredCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/rate-limit-hold": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get rate-limit hold configuration",
        "description": "Rate-limit queue hold (#696). When enabled, a provider 429 carrying a reset time sends the episode back to the queue and pauses new queue claims until the reset instead of failing the job.",
        "operationId": "getRateLimitHoldSettings",
        "responses": {
          "200": {
            "description": "Rate-limit hold settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "holdUntil": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "Until when new queue claims pause; null when no hold is active."
                    },
                    "llmUsageUrl": {
                      "type": "string",
                      "description": "Operator-configured provider usage/limit endpoint the hold probe checks first; empty when unset."
                    },
                    "rateLimitProbeMinutes": {
                      "type": "integer",
                      "description": "Minutes between hold probes; 0 disables probing."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update rate-limit hold configuration",
        "operationId": "updateRateLimitHoldSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Pause the queue on a provider 429 with a reset time longer than five minutes. Off by default; setting false clears an active pause."
                  },
                  "llmUsageUrl": {
                    "type": "string",
                    "description": "Provider usage/limit endpoint the hold probe checks first (tier 1); an empty string clears it. Must pass the same SSRF check as the other operator-configured base URLs."
                  },
                  "rateLimitProbeMinutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 60,
                    "description": "Minutes between hold probes; 0 disables probing."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rate-limit hold settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "holdUntil": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "llmUsageUrl": {
                      "type": "string"
                    },
                    "rateLimitProbeMinutes": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/rate-limit-hold/reset": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Clear active rate-limit holds without disabling the feature",
        "description": "Lifts every active provider rate-limit hold while leaving the feature enabled. Use after switching models or providers, whose limits differ from the endpoint that produced the hold (#747).",
        "operationId": "resetRateLimitHold",
        "responses": {
          "200": {
            "description": "Holds cleared; current rate-limit hold settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "holdUntil": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "llmUsageUrl": {
                      "type": "string"
                    },
                    "rateLimitProbeMinutes": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/whisper/capacity": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Resolved Whisper pool capacity and in-flight counts",
        "description": "What the operator configured, whether the pool is active (enabled and the backend is the remote OpenAI-compatible API), and what is currently in flight against it. Read from this process's own pool instance; the queue leader refreshes its own pool every dispatcher pass, so no cross-process signal is needed.",
        "operationId": "getWhisperCapacity",
        "responses": {
          "200": {
            "description": "Pool state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "backend": {
                      "type": "string"
                    },
                    "active": {
                      "type": "boolean",
                      "description": "enabled and backend is openai-api"
                    },
                    "inactiveReason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "disabled",
                        "local_backend",
                        null
                      ]
                    },
                    "capacity": {
                      "type": "integer"
                    },
                    "inFlight": {
                      "type": "integer"
                    },
                    "transcribingEpisodes": {
                      "type": "integer"
                    },
                    "maxEpisodes": {
                      "type": "object",
                      "properties": {
                        "configured": {
                          "type": "integer"
                        },
                        "effective": {
                          "type": "integer"
                        }
                      }
                    },
                    "chunkWorkers": {
                      "type": "object",
                      "properties": {
                        "configured": {
                          "type": "integer"
                        },
                        "effective": {
                          "type": "integer"
                        }
                      }
                    },
                    "worstCaseInFlight": {
                      "type": "integer"
                    },
                    "exceedsCapacity": {
                      "type": "boolean"
                    },
                    "leader": {
                      "type": "boolean",
                      "description": "Whether this process is the background leader that runs transcriptions. inFlight/transcribingEpisodes are only meaningful when true; other workers always report 0."
                    },
                    "health": {
                      "$ref": "#/components/schemas/WhisperHealthProbe"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/youtube/sb-user-id": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Reveal the stored SponsorBlock private user ID",
        "description": "Returns the stored ID so it can be copied, for example into the SponsorBlock extension. Returns 404 with error \"not_configured\" when none is stored. Limited to 10 requests per minute.",
        "operationId": "getSbUserId",
        "responses": {
          "200": {
            "description": "The stored ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sbUserId"
                  ],
                  "properties": {
                    "sbUserId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No ID is stored (error \"not_configured\")"
          }
        }
      }
    },
    "/settings/youtube/sb-user-id/generate": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Generate a SponsorBlock private user ID",
        "description": "Creates a random 48 character SponsorBlock private user ID and stores it encrypted. Returns 409 with error \"configured\" when an ID is already stored; it cannot be regenerated while one is stored. Returns 409 with error \"provider_crypto_unavailable\" when secret encryption is unavailable, as PUT does. Limited to 10 requests per minute. Read the stored ID with GET /settings/youtube/sb-user-id.",
        "operationId": "generateSbUserId",
        "responses": {
          "200": {
            "description": "The new ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sbUserId"
                  ],
                  "properties": {
                    "sbUserId": {
                      "type": "string",
                      "minLength": 48,
                      "maxLength": 48
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Error \"configured\" when an ID is already stored, or \"provider_crypto_unavailable\" when secret encryption is unavailable"
          }
        }
      }
    },
    "/settings/youtube/reset": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Reset YouTube settings to defaults",
        "description": "Restores the YouTube tunables, proxy and extra yt-dlp arguments to their defaults.",
        "operationId": "resetYoutubeSettings",
        "responses": {
          "200": {
            "description": "YouTube settings after the reset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YoutubeSettings"
                }
              }
            }
          }
        }
      }
    },
    "/settings/youtube": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get YouTube feed settings",
        "description": "Global YouTube feed settings. The Data API key is write-only and reported as apiKeyConfigured.",
        "operationId": "getYoutubeSettings",
        "responses": {
          "200": {
            "description": "YouTube settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YoutubeSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update YouTube feed settings",
        "description": "Accepts any subset of the fields. Every value is validated before any is written. apiKey set to an empty string or null clears the stored key. Returns 409 when a key is supplied but secret encryption is unavailable.",
        "operationId": "updateYoutubeSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "apiKey": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "sbSubmitEnabled": {
                    "type": "boolean",
                    "description": "Show the manual SponsorBlock submission review on YouTube episode pages. Nothing is ever sent automatically."
                  },
                  "sbUserId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "minLength": 32,
                    "maxLength": 200,
                    "description": "Private SponsorBlock user ID, 32 to 200 printable ASCII characters. Empty or null clears it. Returns 409 when secret encryption is unavailable."
                  },
                  "processDelayMinutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1440
                  },
                  "sbProbeAttempts": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 48
                  },
                  "sbProbeIntervalMinutes": {
                    "type": "integer",
                    "minimum": 5,
                    "maximum": 1440
                  },
                  "sbWatchIntervalHours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 48
                  },
                  "sbWatchMaxDays": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 60
                  },
                  "llmFallback": {
                    "type": "boolean"
                  },
                  "captionsAsTranscript": {
                    "type": "boolean"
                  },
                  "hidePrivate": {
                    "type": "boolean"
                  },
                  "transcribeFallback": {
                    "type": "boolean"
                  },
                  "proxyUrl": {
                    "type": "string",
                    "description": "Proxy for yt-dlp and YouTube Data API requests. http, https, socks4, socks5 and socks5h schemes."
                  },
                  "ytdlpExtraArgs": {
                    "type": "string",
                    "description": "Extra command line arguments added before the app's own options on every yt-dlp call. Options that run or load other programs, change where files are written, read local files, alter the JSON output, update the binary or carry passwords are rejected; use long option names."
                  },
                  "ytdlpAutoUpdate": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated YouTube settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YoutubeSettings"
                }
              }
            }
          },
          "400": {
            "description": "A value is out of range or has the wrong type"
          },
          "409": {
            "description": "Secret encryption is unavailable (error \"provider_crypto_unavailable\"), or sbUserId was sent while a generated ID is stored (error \"generated\")"
          }
        }
      }
    },
    "/settings/update-check": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get update-check settings",
        "description": "Controls the daily background check that fires the Update Available notification and the manual GET /system/updates lookup's channel.",
        "operationId": "getUpdateCheckSettings",
        "responses": {
          "200": {
            "description": "Update-check settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean",
                      "description": "Whether the daily background update check runs."
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "stable",
                        "edge"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update update-check settings",
        "operationId": "updateUpdateCheckSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Enable or disable the daily background update check."
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "stable",
                      "edge"
                    ],
                    "description": "Release channel to track for update availability."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update-check settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "stable",
                        "edge"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/audio": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get audio-related settings",
        "operationId": "getAudioSettings",
        "responses": {
          "200": {
            "description": "Audio settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keepOriginalAudio": {
                      "type": "boolean",
                      "description": "When true, the pre-cut audio file is retained alongside the processed output for the Ad Editor's Review mode. Roughly doubles per-episode storage."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update audio-related settings",
        "operationId": "updateAudioSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "keepOriginalAudio"
                ],
                "properties": {
                  "keepOriginalAudio": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audio settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keepOriginalAudio": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/processing-timeouts": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get processing timeout configuration",
        "operationId": "getProcessingTimeouts",
        "responses": {
          "200": {
            "description": "Processing timeouts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "softTimeoutSeconds": {
                      "type": "integer"
                    },
                    "hardTimeoutSeconds": {
                      "type": "integer"
                    },
                    "defaults": {
                      "type": "object",
                      "properties": {
                        "softTimeoutSeconds": {
                          "type": "integer"
                        },
                        "hardTimeoutSeconds": {
                          "type": "integer"
                        }
                      }
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "softMin": {
                          "type": "integer"
                        },
                        "hardMax": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update processing timeout configuration",
        "operationId": "updateProcessingTimeouts",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "softTimeoutSeconds",
                  "hardTimeoutSeconds"
                ],
                "properties": {
                  "softTimeoutSeconds": {
                    "type": "integer",
                    "minimum": 300,
                    "maximum": 86400
                  },
                  "hardTimeoutSeconds": {
                    "type": "integer",
                    "minimum": 301,
                    "maximum": 86400
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Processing timeouts updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "softTimeoutSeconds": {
                      "type": "integer"
                    },
                    "hardTimeoutSeconds": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/webhooks": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "List webhooks",
        "description": "Returns all configured webhooks",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "Webhook list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Create a webhook",
        "description": "Create a new webhook subscription for event notifications",
        "operationId": "createWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events",
                  "enabled"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL to receive webhook POST requests"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "Episode Processed",
                        "Episode Awaiting Approval",
                        "Episode Pending Review",
                        "Episode Failed",
                        "Auth Failure",
                        "Limit Exceeded",
                        "Rate Limit Structural",
                        "Feed Refresh Failed",
                        "Update Available",
                        "Cue Template Quiet",
                        "Queue Held",
                        "Queue Resumed",
                        "Service Offline",
                        "Service Reachable",
                        "Failover Triggered",
                        "Failover Cancelled"
                      ]
                    },
                    "description": "List of event types to subscribe to. `Episode Awaiting Approval` fires instead of `Episode Processed` when a finished episode is held for manual approval. `Episode Pending Review` fires alongside the processed or awaiting-approval event when a finished episode has detections held for review. `Auth Failure` fires on credential errors (invalid or expired API key). `Limit Exceeded` fires when the provider rejects a request because a spend or usage limit is exhausted (monthly key limit, out of credits). `Rate Limit Structural` fires when one detection-window request exceeds the provider's per-minute token cap. `Feed Refresh Failed` fires after several consecutive failures to fetch a podcast's origin RSS feed. `Update Available` fires when the daily update check finds a newer release on the selected channel. `Cue Template Quiet` fires when an enabled cue template on a cue_only feed stops matching recent episodes. `Queue Held` fires when a provider rate limit pauses the queue; `Queue Resumed` when that hold clears. `Service Offline` fires when an episode defers because the LLM or Whisper endpoint is unreachable; `Service Reachable` when the probe finds it back up. `Failover Triggered` fires when a target switches to its failover provider; `Failover Cancelled` when it switches back."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Whether the webhook is active"
                  },
                  "secret": {
                    "type": "string",
                    "description": "Optional shared secret for HMAC signature verification"
                  },
                  "payloadTemplate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional custom payload template"
                  },
                  "contentType": {
                    "type": "string",
                    "description": "Content type for webhook delivery (e.g. application/json)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/settings/webhooks/{webhook_id}": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update a webhook",
        "description": "Update an existing webhook configuration",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhook_id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "secret": {
                    "type": "string"
                  },
                  "payloadTemplate": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "contentType": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Settings"
        ],
        "summary": "Delete a webhook",
        "description": "Remove a webhook subscription",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhook_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/webhooks/{webhook_id}/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test a webhook",
        "description": "Send one test payload per event the webhook is subscribed to, matching each event's real payload shape (`test: true` set on every payload). Falls back to a single Episode Processed sample when the webhook's event list is empty. If a custom payload template cannot render for an event, the default payload is used (a readable line for `text/*` content types, JSON otherwise) and the result is flagged `templateFallback`, regardless of delivery status. Rate limited to 10/min.\n",
        "operationId": "testWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhook_id"
          }
        ],
        "responses": {
          "200": {
            "description": "Test result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "true only when every event delivered"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "event": {
                            "type": "string"
                          },
                          "delivered": {
                            "type": "boolean"
                          },
                          "templateFallback": {
                            "type": "boolean",
                            "description": "true when the custom payload template could not render for this event and the default payload was used, independent of delivery status"
                          }
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "description": "Summary, e.g. \"3 of 3 test payloads delivered\". Names the events whose template fell back, if any."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/webhooks/validate-template": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Validate a webhook payload template",
        "description": "Validate a custom payload template and return a preview of the rendered output. Rate limited to 30/min.\n",
        "operationId": "validateWebhookTemplate",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "template"
                ],
                "properties": {
                  "template": {
                    "type": "string",
                    "description": "The payload template string to validate"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "preview": {
                      "type": "string",
                      "description": "Rendered template preview (when valid)"
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Error message (when invalid)"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/notifications/timezone": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get the notification timezone",
        "description": "Returns the IANA zone used to compute timestamp_local in webhook and email notifications. Defaults to UTC, or the container's TZ env var when it names a valid zone.",
        "operationId": "getNotificationTimezone",
        "responses": {
          "200": {
            "description": "Notification timezone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationTimezoneSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Set the notification timezone",
        "description": "Rejects a timezone name zoneinfo cannot resolve.",
        "operationId": "updateNotificationTimezone",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotificationTimezoneSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated timezone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationTimezoneSettings"
                }
              }
            }
          },
          "400": {
            "description": "Unknown timezone name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/notifications/email": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get email notification settings",
        "description": "Returns the email notification configuration. The SMTP password is never included; only the smtpPasswordConfigured flag.",
        "operationId": "getEmailNotificationSettings",
        "responses": {
          "200": {
            "description": "Email notification settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailNotificationSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Update email notification settings",
        "description": "Partial update. smtpPassword is write-only: send a value to set it, an empty string to clear it, or omit the field to keep the stored password. Setting enabled to true requires an SMTP host, from address, and at least one recipient.\n",
        "operationId": "updateEmailNotificationSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailNotificationSettingsUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailNotificationSettings"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Secret store unavailable (set MINUSPOD_MASTER_PASSPHRASE)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/notifications/email/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Send a test email",
        "description": "Sends a test email using the saved settings. Rate limited to 10/min.",
        "operationId": "sendTestEmail",
        "responses": {
          "200": {
            "description": "Test outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/episodes/bulk": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Bulk episode actions",
        "description": "Apply an action to multiple episodes at once. Rate limited to 5/min.",
        "operationId": "bulkEpisodeAction",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "episodeIds",
                  "action"
                ],
                "properties": {
                  "episodeIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 500
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "process",
                      "reprocess",
                      "reprocess_full",
                      "reprocess_llm",
                      "delete"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bulk action result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "queued": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    },
                    "freedMb": {
                      "type": "number"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "skippedEpisodes": {
                      "type": "array",
                      "description": "Episodes skipped because their state changed or processing already owns them.",
                      "items": {
                        "type": "object",
                        "required": [
                          "episodeId",
                          "reason"
                        ],
                        "properties": {
                          "episodeId": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "jobState": {
                      "type": "string",
                      "enum": [
                        "idle",
                        "queued"
                      ],
                      "description": "Present for process/reprocess/reprocess_full/reprocess_llm actions. Bulk enqueues never start processing synchronously, so this is queued whenever anything was queued, else idle. Not present for delete."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/passthrough": {
      "post": {
        "tags": [
          "Episodes"
        ],
        "summary": "Set per-episode pass-through override",
        "description": "Set or clear the per-episode pass-through override (issue #746) for one or more episodes. An episode flagged this way is relayed with no transcription, ad detection, or cutting, even when the feed's own processing mode is not passthrough; the audio may still be transcoded to MP3 when the publisher's enclosure is not already MP3. Enabling enqueues a normal reprocess for each affected episode so the pass-through branch runs now; disabling only clears the flag. Repeated ids are deduplicated in request order, so each id appears once across accepted and rejected. Rate limited to 5/min.",
        "operationId": "setEpisodesPassthrough",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "episodeIds",
                  "enabled"
                ],
                "properties": {
                  "episodeIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 500
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pass-through override result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updated": {
                      "type": "integer",
                      "description": "Number of episodes whose passthroughEnabled flag was set"
                    },
                    "queued": {
                      "type": "integer",
                      "description": "Episodes that actually hold a queue row after the call, counted from the queue itself rather than from the attempted inserts, so a swallowed enqueue failure is never reported as queued work. Always 0 when disabling."
                    },
                    "accepted": {
                      "type": "array",
                      "description": "Episode ids the override was applied to, in request order.",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rejected": {
                      "type": "array",
                      "description": "Episode ids the override was not applied to, each with its reason.",
                      "items": {
                        "type": "object",
                        "required": [
                          "episodeId",
                          "reason"
                        ],
                        "properties": {
                          "episodeId": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string",
                            "enum": [
                              "not_found",
                              "processing"
                            ],
                            "description": "not_found means no episode with that id belongs to this feed. processing means a run already owns the episode and has resolved its mode, so enabling would not take effect this run; it is returned only when enabling, since disabling an owned episode is accepted."
                          }
                        }
                      }
                    },
                    "jobState": {
                      "type": "string",
                      "enum": [
                        "idle",
                        "queued"
                      ],
                      "description": "Present only when enabled is true. queued when anything reached the queue, else idle. Disabling enqueues nothing, so the field is omitted entirely."
                    }
                  }
                },
                "example": {
                  "updated": 2,
                  "queued": 2,
                  "accepted": [
                    "a1b2c3d4e5f6",
                    "b2c3d4e5f6a1"
                  ],
                  "rejected": [
                    {
                      "episodeId": "c3d4e5f6a1b2",
                      "reason": "processing"
                    }
                  ],
                  "jobState": "queued"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request. Returned when the body is not a JSON object, when episodeIds is missing or is not a non-empty array, when any episodeIds entry is not a non-empty string, when more than 500 ids are sent, or when enabled is missing or not a boolean.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/import/upload": {
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Stage archive-import files ahead of scan/commit",
        "description": "Local feeds only. Multipart, repeated field \"files\", saved under each file's original basename into the feed's staging area (<data>/import-staging/<slug>/) so the sNNeNN naming scheme can be parsed by a later scan. A basename that is empty, a dotfile, over 255 UTF-8 bytes, or contains a path separator/\"..\"/NUL is rejected per-file rather than failing the whole request. Widened request cap: up to 1 GB total, same as the single-episode upload.",
        "operationId": "uploadImportFiles",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "files"
                ],
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Files staged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "staged": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Basenames successfully staged."
                    },
                    "rejected": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "file": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, or no files field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "An import is already running for this feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the configured local upload size limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadTooLarge"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/import/scan": {
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Dry-run a bulk archive import",
        "description": "Local feeds only. Scans the staging area and/or the user-managed import directory (<data>/import/<slug>/), applies the sNNeNN naming scheme and any JSON sidecars, synthesizes publish dates for entries that don't have one, and returns the plan the UI renders for review. Nothing is written to disk or the database by this call.",
        "operationId": "scanImport",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LocalImportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry-run plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalImportPlan"
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, or an invalid source/overwrite value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/import/commit": {
      "post": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Commit a previously scanned import plan",
        "description": "Local feeds only. Re-scans the same source(s) server-side and compares the result's hash against the client-echoed planHash; a mismatch means a file changed since the scan and 409s rather than committing a stale plan. Starts a background job (one import per feed at a time; a second commit while one is running also 409s) and returns immediately; poll import/status for progress. An empty feed's very first commit never queues anything for auto-process regardless of settings; a later commit into a non-empty feed does, skipping the publish-date recency check.",
        "operationId": "commitImport",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "required": [
                      "planHash"
                    ],
                    "properties": {
                      "planHash": {
                        "type": "string",
                        "description": "The planHash returned by the most recent import/scan call for this feed and source/overwrite combination."
                      }
                    }
                  },
                  {
                    "$ref": "#/components/schemas/LocalImportRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Import started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed, missing planHash, or an invalid source/overwrite value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "planHash is stale (files changed since scan), an import is already running for this feed, or there is not enough free disk space for the plan's importable bytes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/import/status": {
      "get": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Poll a local feed's background import job",
        "description": "Local feeds only. A slug with no import ever started reads as idle.",
        "operationId": "getImportStatus",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Import status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalImportStatus"
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/feeds/{slug}/import/staging": {
      "delete": {
        "tags": [
          "LocalFeeds"
        ],
        "summary": "Clear a local feed's upload staging area",
        "description": "Local feeds only. Empties <data>/import-staging/<slug>/. The UI clears staging itself at the start of every new file selection, so this is mainly for wiping an abandoned upload attempt by hand.",
        "operationId": "clearImportStaging",
        "parameters": [
          {
            "$ref": "#/components/parameters/slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Staging cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a local feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "An import is already running for this feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system/queue": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Get auto-process queue status",
        "description": "Returns the current status of the auto-process queue for new episodes",
        "operationId": "getQueueStatus",
        "responses": {
          "200": {
            "description": "Queue status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pending": {
                      "type": "integer",
                      "description": "Episodes waiting to be processed"
                    },
                    "processing": {
                      "type": "integer",
                      "description": "Episodes currently being processed"
                    },
                    "completed": {
                      "type": "integer",
                      "description": "Episodes that completed processing"
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Episodes that failed processing"
                    },
                    "deferred": {
                      "type": "integer",
                      "description": "Episodes waiting in the offline queue for the LLM or Whisper endpoint to come back"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total episodes in queue"
                    },
                    "items": {
                      "type": "array",
                      "description": "Pending/processing rows in dequeue order (priority DESC, createdAt ASC), capped at 100",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "episodeId": {
                            "type": "string"
                          },
                          "podcastSlug": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "priority": {
                            "type": "integer",
                            "description": "Higher runs first; feed base (10/0/-10) plus manual (+20) and fresh-episode (+5) boosts"
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "System"
        ],
        "summary": "Clear auto-process queue",
        "description": "Clear all pending items from the auto-process queue",
        "operationId": "clearQueue",
        "responses": {
          "200": {
            "description": "Queue cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "integer",
                      "description": "Number of pending items removed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/patterns/bulk-delete": {
      "post": {
        "summary": "Bulk-delete patterns",
        "description": "Hard-delete patterns by id or by source filter. Both `confirm: true`\nand `expected_count: N` are required; the call is rejected if the\nactual matched count does not equal `expected_count`.\n",
        "tags": [
          "Patterns"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm",
                  "expected_count"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "local",
                      "community",
                      "imported"
                    ]
                  },
                  "confirm": {
                    "type": "boolean"
                  },
                  "expected_count": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deletion summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "integer"
                    },
                    "ids": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure"
          }
        }
      }
    },
    "/patterns/bulk-disable": {
      "post": {
        "summary": "Bulk-disable patterns",
        "description": "Same shape as bulk-delete; sets is_active=0 instead.",
        "tags": [
          "Patterns"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkPatternOp"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Disable summary"
          },
          "400": {
            "description": "Validation failure"
          }
        }
      }
    },
    "/patterns/preview-export": {
      "post": {
        "summary": "Dry-run the community export pipeline for a list of pattern ids",
        "description": "Returns which patterns would pass the quality gates and which would\nbe rejected, with per-id reasons. Used by the UI to show a preview\nbefore the user builds the actual submission bundle.\n",
        "tags": [
          "Patterns"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "minItems": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ready": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    },
                    "rejected": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "sponsor": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "reasons": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "ready_count": {
                      "type": "integer"
                    },
                    "rejected_count": {
                      "type": "integer"
                    },
                    "pattern_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ids list missing or empty"
          }
        }
      }
    },
    "/patterns/submit-bundle": {
      "post": {
        "summary": "Build a downloadable community-submission bundle",
        "description": "Runs the export pipeline on each id and returns a single JSON file\n(Content-Disposition: attachment) containing every pattern that\npassed quality gates. The file is the artifact the contributor\ncommits into `patterns/community/` to open one PR for all the\nselected patterns. The PR-side validator and the manifest builder\nboth handle the bundle format natively, so the maintainer does\nnot need to split it after merge.\n",
        "tags": [
          "Patterns"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "minItems": 1
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bundle JSON file (downloaded as attachment)",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"minuspod-submission-XXXX.json\""
              },
              "X-Bundle-Pattern-Count": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Bundle-Rejected-Count": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "format": {
                      "type": "string",
                      "example": "minuspod-community-submission"
                    },
                    "bundle_version": {
                      "type": "integer"
                    },
                    "submitted_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "submitted_app_version": {
                      "type": "string"
                    },
                    "pattern_count": {
                      "type": "integer"
                    },
                    "patterns": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "ids missing, or every pattern failed quality gates"
          }
        }
      }
    },
    "/patterns/{pattern_id}/submit-to-community": {
      "post": {
        "summary": "Submit a local pattern to the community repository",
        "description": "Runs the community export pipeline: quality gates, tag validation,\nPII strip, metadata strip, sponsor classification, and returns a\nprefilled GitHub PR URL plus the JSON payload. When `too_large` is\ntrue the URL exceeds GitHub's limit and the client should offer\nthe JSON file as a download instead.\n",
        "tags": [
          "Patterns"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "pattern_id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Export result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "object"
                    },
                    "filename": {
                      "type": "string"
                    },
                    "pr_url": {
                      "type": "string"
                    },
                    "too_large": {
                      "type": "boolean"
                    },
                    "sponsor_match": {
                      "type": "string",
                      "enum": [
                        "exact",
                        "alias",
                        "fuzzy",
                        "unknown"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Pattern failed export gates"
          }
        }
      }
    },
    "/patterns/{pattern_id}/protect": {
      "post": {
        "summary": "Mark a community pattern as protected from sync",
        "tags": [
          "Patterns"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "pattern_id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated protection state"
          },
          "400": {
            "description": "Pattern is not source=community"
          }
        }
      },
      "delete": {
        "summary": "Clear the protected_from_sync flag on a community pattern",
        "tags": [
          "Patterns"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "pattern_id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated protection state"
          }
        }
      }
    },
    "/tags/vocabulary": {
      "get": {
        "summary": "Get the canonical community-pattern tag vocabulary",
        "tags": [
          "Patterns"
        ],
        "description": "Returns the fixed tag vocabulary used by the pattern and sponsor tag pickers: the full tag list plus the podcast-genre, sponsor-industry, and special-tag groupings. The vocabulary ships with the app image and only changes when vocabulary_version is bumped.\n",
        "responses": {
          "200": {
            "description": "Tag vocabulary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "vocabulary_version": {
                      "type": "integer"
                    },
                    "all_tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "podcast_genres": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tag": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "sponsor_industries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tag": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "special_tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "tag": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/podping/nodes": {
      "get": {
        "summary": "Get configured Podping RPC servers",
        "operationId": "getPodpingNodes",
        "tags": [
          "Settings"
        ],
        "responses": {
          "200": {
            "description": "Effective server list and shipped defaults",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PodpingNodeSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Replace the Podping RPC server list",
        "operationId": "updatePodpingNodes",
        "tags": [
          "Settings"
        ],
        "description": "Normalizes and deduplicates endpoint URLs. Changes apply to the running listener without resetting its block position. URLs must use HTTP or HTTPS and cannot contain credentials, query strings, or fragments.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "nodes"
                ],
                "properties": {
                  "nodes": {
                    "$ref": "#/components/schemas/PodpingNodeUrls"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved server list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PodpingNodeSettings"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or empty server list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/podping/nodes/reset": {
      "post": {
        "summary": "Restore the default Podping RPC servers",
        "operationId": "resetPodpingNodes",
        "tags": [
          "Settings"
        ],
        "responses": {
          "200": {
            "description": "Shipped server list restored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PodpingNodeSettings"
                }
              }
            }
          }
        }
      }
    },
    "/podping/hosts": {
      "get": {
        "summary": "List domains seen sending Podpings on the Hive chain",
        "tags": [
          "Feeds"
        ],
        "description": "Diagnostic view of what the Podping listener has observed. The listener records the domain of every feed URL announced on the chain, including ones that match no local feed, which is what lets a feed report whether its host sends Podpings at all. Counts are aggregated per domain as the listener runs, so there is no per-notification history to return. Rows recorded earlier stay visible when the listener is disabled.\n",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "description": "Rows to return, clamped to 1-500. Does not affect the totals.",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Observed Podping host domains, most recently active first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "hosts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string",
                            "example": "feeds.transistor.fm"
                          },
                          "firstSeenAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "lastSeenAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "pingCount": {
                            "type": "integer"
                          },
                          "active": {
                            "type": "boolean",
                            "description": "Seen within activeWindowDays."
                          }
                        }
                      }
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "totalDomains": {
                      "type": "integer"
                    },
                    "activeDomains": {
                      "type": "integer"
                    },
                    "activeWindowDays": {
                      "type": "integer",
                      "example": 30
                    },
                    "listenerEnabled": {
                      "type": "boolean"
                    },
                    "allNodesDown": {
                      "type": "boolean",
                      "description": "Non-fatal: every configured Hive RPC node is currently failing. RSS polling remains the fallback, so this never gates /health or feed refresh."
                    },
                    "degradedSince": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When allNodesDown last became true, or null."
                    },
                    "nodes": {
                      "type": "array",
                      "description": "Durable per-node health, one entry per configured RPC node.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "node": {
                            "type": "string",
                            "example": "https://api.hive.blog"
                          },
                          "selected": {
                            "type": "boolean",
                            "description": "Last node to complete a successful RPC call."
                          },
                          "consecutiveFailures": {
                            "type": "integer"
                          },
                          "lastSuccessAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Last successful RPC response, shown as Last seen; null before success."
                          },
                          "nextRetryAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "lastFailureReason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "httpStatus": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Latest HTTP status, including 200 with invalid_response; null if none arrived."
                          },
                          "outcome": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "healthy",
                              "http_error",
                              "invalid_response",
                              "unreachable",
                              null
                            ],
                            "description": "Latest RPC outcome; null before the first check."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/feeds/{slug}/tags": {
      "get": {
        "summary": "Get a feed's tag breakdown (rss / episode / user / effective)",
        "tags": [
          "Feeds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tag breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "effective": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rss": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "episode": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "user": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "put": {
        "summary": "Update a feed's user-added tags",
        "tags": [
          "Feeds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_tags"
                ],
                "properties": {
                  "user_tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tag breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "effective": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rss": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "episode": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "user": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown or invalid tag(s)"
          }
        }
      }
    },
    "/feeds/{slug}/rerender-segments": {
      "post": {
        "summary": "Re-cut every processed episode of a feed against the current segment-category action maps",
        "description": "Queues the existing per-episode recut (reprocess_mode=recut) for every processed episode of the feed that still has a retained original, saved transcript segments, and ad detections. Each recut re-resolves segment-category actions against the current per-feed/global maps, so this is the bulk trigger for applying a changed segment-category setting to already-processed episodes.\n",
        "tags": [
          "Feeds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recut queued for qualifying episodes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "queued": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/sponsors/{sponsor_id}/tags": {
      "put": {
        "summary": "Update a sponsor's tags",
        "tags": [
          "Sponsors"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "sponsor_id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tags"
                ],
                "properties": {
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated sponsor tags"
          },
          "404": {
            "description": "Sponsor not found"
          }
        }
      }
    },
    "/settings/reviewer": {
      "get": {
        "summary": "Read ad-reviewer auto-update settings",
        "tags": [
          "Settings"
        ],
        "responses": {
          "200": {
            "description": "Current settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updatePatternsFromReviewerAdjustments": {
                      "type": "boolean"
                    },
                    "minTrimThreshold": {
                      "type": "number"
                    },
                    "parallelAds": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 32,
                      "description": "Number of ads the reviewer reviews concurrently via the LLM.\n1 reviews them one at a time.\n"
                    },
                    "parallelAdsDefault": {
                      "type": "integer",
                      "description": "Default value of parallelAds (env-backed registry)."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update ad-reviewer auto-update settings",
        "tags": [
          "Settings"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "updatePatternsFromReviewerAdjustments": {
                    "type": "boolean"
                  },
                  "minTrimThreshold": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 120
                  },
                  "parallelAds": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 32,
                    "description": "Number of ads the reviewer reviews concurrently. Range 1-32,\ndefault 4.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updatePatternsFromReviewerAdjustments": {
                      "type": "boolean"
                    },
                    "minTrimThreshold": {
                      "type": "number"
                    },
                    "parallelAds": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 32
                    },
                    "parallelAdsDefault": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/settings/community-sync": {
      "get": {
        "summary": "Read community-pattern sync settings",
        "tags": [
          "Settings"
        ],
        "responses": {
          "200": {
            "description": "Current settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommunitySyncSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update community-pattern sync settings",
        "tags": [
          "Settings"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "cron": {
                    "type": "string"
                  },
                  "categories": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "sponsor",
                        "cross_promo",
                        "self_promo",
                        "interaction",
                        "intro",
                        "outro",
                        "recap"
                      ]
                    },
                    "description": "Segment categories to accept from community sync. Unchecking a\ncategory deactivates (not deletes) its already-synced community\npatterns; re-checking it reactivates them on the next sync.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommunitySyncSettings"
                }
              }
            }
          }
        }
      }
    },
    "/settings/replacement-audio": {
      "get": {
        "summary": "Read the replacement audio currently in use",
        "description": "Metadata for the audio spliced in wherever an ad was cut. `source` is\n`uploaded` when an operator has supplied their own file, `default`\notherwise.\n",
        "tags": [
          "Settings"
        ],
        "operationId": "getReplacementAudio",
        "responses": {
          "200": {
            "description": "Current replacement audio",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplacementAudio"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Upload a replacement audio file",
        "description": "Accepts a single audio file as multipart field `file`, transcodes it to\nMP3 and installs it on the data volume so it survives a redeploy. Takes\neffect on the next episode without a restart. Rejects files over 5 MB or\n30 seconds; every cut becomes as long as this file.\n",
        "tags": [
          "Settings"
        ],
        "operationId": "uploadReplacementAudio",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "MP3, WAV, M4A, OGG or FLAC. Up to 5 MB and 30 seconds."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Installed, returning the new metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplacementAudio"
                }
              }
            }
          },
          "400": {
            "description": "Rejected, with an operator-facing reason",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Restore the default replacement audio",
        "description": "Removes an uploaded file so the shipped default applies again.",
        "tags": [
          "Settings"
        ],
        "operationId": "revertReplacementAudio",
        "responses": {
          "200": {
            "description": "Current replacement audio after the revert",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ReplacementAudio"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reverted": {
                          "type": "boolean",
                          "description": "False when no uploaded file was present."
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/settings/replacement-audio/file": {
      "get": {
        "summary": "Download the replacement audio for preview",
        "tags": [
          "Settings"
        ],
        "operationId": "getReplacementAudioFile",
        "responses": {
          "200": {
            "description": "The audio file",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No replacement audio is installed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/db-backup": {
      "get": {
        "summary": "Read scheduled DB backup settings",
        "tags": [
          "Settings"
        ],
        "operationId": "getDbBackupSettings",
        "responses": {
          "200": {
            "description": "Current settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseBackupSettings"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Update scheduled DB backup settings",
        "tags": [
          "Settings"
        ],
        "operationId": "updateDbBackupSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "cron": {
                    "type": "string",
                    "description": "Standard 5-field cron expression. Must be a valid cron string."
                  },
                  "dest": {
                    "type": "string",
                    "description": "Absolute path to the backup directory. Empty string resets to the\ndefault location (<data_dir>/backups).\n"
                  },
                  "keepCount": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365,
                    "description": "Number of backup files to retain. 1 uses overwrite mode (fixed filename);\n2 or more uses rotate mode (timestamped filenames, oldest pruned).\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatabaseBackupSettings"
                }
              }
            }
          },
          "400": {
            "description": "Invalid cron expression, keepCount out of range, or invalid dest path",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/settings/pattern-cleanup": {
      "put": {
        "summary": "Update pattern cleanup (Experiments) settings",
        "tags": [
          "Settings"
        ],
        "operationId": "updatePatternCleanupSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "cron": {
                    "type": "string",
                    "description": "Valid 5-field cron expression in UTC."
                  },
                  "batchSize": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200
                  },
                  "unusedDays": {
                    "type": "integer",
                    "minimum": 7,
                    "maximum": 3650
                  },
                  "provider": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "'primary'/'secondary' (or the 'a'/'b' aliases) or 'same_as_detection'; blank or null clears the override so detection's slot is inherited."
                  },
                  "model": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Blank or null inherits the detection model."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatternCleanupSettings"
                }
              }
            }
          },
          "400": {
            "description": "Malformed or empty request object, invalid field type, invalid cron, out-of-range value, unknown provider slot, or model too long",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/community-patterns/sync": {
      "post": {
        "summary": "Force a community-pattern sync now",
        "description": "Rate limited to 6/hour.",
        "tags": [
          "CommunityPatterns"
        ],
        "responses": {
          "200": {
            "description": "Sync summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommunitySyncSummary"
                }
              }
            }
          },
          "429": {
            "description": "Rate-limited"
          },
          "502": {
            "description": "Manifest fetch or apply failed"
          }
        }
      }
    },
    "/community-patterns/sync-status": {
      "get": {
        "summary": "Get community-pattern sync status",
        "tags": [
          "CommunityPatterns"
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommunitySyncSettings"
                }
              }
            }
          }
        }
      }
    },
    "/community-patterns/all": {
      "delete": {
        "summary": "Remove every community pattern on this instance",
        "description": "Hard-deletes every row where ``source='community'``. Ignores\n``protected_from_sync``; the flag guards sync reconciliation,\nnot an explicit operator purge. Local and imported patterns are\nuntouched. If sync is enabled, the next tick repopulates from\nthe manifest.\n\nRequires ``{\"confirm\": true}`` in the body as a fat-finger guard\n(matches the ``/patterns/bulk-delete`` convention).\n",
        "tags": [
          "CommunityPatterns"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "boolean",
                    "const": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deletion count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "integer",
                      "example": 47
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "confirm flag missing or not true"
          }
        }
      }
    },
    "/feeds/{slug}/cue-templates": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "List a feed's audio-cue templates",
        "description": "Returns the feed's own templates plus any network-scope templates shared from sibling feeds on the same network. Shared templates carry `owned: false` and are read-only in the UI (managed on the feed that created them).\n",
        "operationId": "listCueTemplates",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Templates for the feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CueTemplate"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      },
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Mark a new cue template from a window of an episode",
        "description": "Requires the episode's original (un-cut) audio to still be retained. Decodes the marked window, extracts an MFCC fingerprint, and stores the raw PCM as the source of truth. Selection must respect the configured capture minimum and the selected cue type's maximum.\n",
        "operationId": "createCueTemplate",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "episodeId",
                  "startS",
                  "endS"
                ],
                "properties": {
                  "episodeId": {
                    "type": "string"
                  },
                  "startS": {
                    "type": "number"
                  },
                  "endS": {
                    "type": "number"
                  },
                  "cueType": {
                    "type": "string",
                    "enum": [
                      "ad_break_boundary",
                      "ad_break_start",
                      "ad_break_end",
                      "show_intro",
                      "show_outro",
                      "content_transition"
                    ],
                    "default": "ad_break_boundary",
                    "description": "Fixed cue type chosen from a dropdown (not freeform). The type sets the consistent label fed to the LLM and the matching role: ad_break_start/end snap that edge, ad_break_boundary snaps both, show_intro/show_outro never snap or pair and only tell the model the sound is the show open/close, not an ad, and content_transition never pairs and hints that a segment transition is there. It can snap an existing ad edge when the feed opts in, while retaining its audio."
                  },
                  "removeWithAd": {
                    "type": "boolean",
                    "default": true,
                    "description": "Include a matched ad-break cue in the cut. Programme cues, including content transitions, keep their audio regardless of this option."
                  },
                  "cutMatches": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Cut every match of this cue on its own. null inherits the feed or global setting. true is rejected for content_transition."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "podcast",
                      "network"
                    ],
                    "default": "podcast"
                  },
                  "networkId": {
                    "type": "string",
                    "description": "Required when scope is network"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/CueTemplate"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid selection or cueType"
          },
          "404": {
            "description": "Feed or original audio not found"
          }
        }
      }
    },
    "/cue-templates/{templateId}": {
      "patch": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Update cue template options, capture window, or scope",
        "operationId": "updateCueTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cueType": {
                    "type": "string",
                    "enum": [
                      "ad_break_boundary",
                      "ad_break_start",
                      "ad_break_end",
                      "show_intro",
                      "show_outro",
                      "content_transition"
                    ],
                    "description": "Change the cue type (also resets the derived label)"
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "removeWithAd": {
                    "type": "boolean",
                    "description": "Include this ad-break cue in cuts. Omit to preserve the saved value; programme cues retain their audio."
                  },
                  "cutMatches": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Cut every match of this cue on its own. null inherits; omit to keep the saved value. true is rejected for content_transition."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "podcast",
                      "network"
                    ]
                  },
                  "networkId": {
                    "type": "string",
                    "description": "Required when promoting to network scope"
                  },
                  "scoreThreshold": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0.3,
                    "maximum": 0.99,
                    "description": "Per-template match-score threshold. Null clears the override (falls back to per-feed or global). Omit to leave unchanged. Minimum 0.30 (noise floor; sub-floor values pass noise as matches).\n"
                  },
                  "sourceOffsetS": {
                    "type": "number",
                    "minimum": 0,
                    "description": "New window start (seconds into source episode). Supplying either sourceOffsetS or durationS triggers blob re-extraction from the retained original audio. Returns 409 when the original has been aged out.\n"
                  },
                  "durationS": {
                    "type": "number",
                    "description": "New window duration (seconds). Must respect the feed's capture bounds (min capture seconds up to the cue type's ceiling). Supplying either sourceOffsetS or durationS triggers blob re-extraction. Returns 409 when the original audio has been aged out.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/CueTemplate"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid scoreThreshold value, non-numeric or negative window field, window duration outside the capture bounds, or a window whose decoded selection is too short to match.\n"
          },
          "404": {
            "description": "Template not found"
          },
          "409": {
            "description": "sourceOffsetS or durationS supplied but the template has no source episode, or the source episode original audio has been aged out (re-extraction is not possible); or the owning feed is in cue_only mode and this enabled/cueType change would leave it without an enabled ad_break_start or ad_break_end template.\n"
          }
        }
      },
      "delete": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Delete a cue template",
        "operationId": "deleteCueTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          },
          "409": {
            "description": "The owning feed is in cue_only mode and deleting this template would leave it without an enabled ad_break_start or ad_break_end template.\n"
          }
        }
      }
    },
    "/cue-templates/{templateId}/export": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Export a template as a portable zip (lossless FLAC + JSON manifest)",
        "description": "The zip manifest preserves removeWithAd alongside cue metadata and lossless audio.",
        "operationId": "exportCueTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Zip archive",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          },
          "422": {
            "description": "Template has no raw audio to export"
          }
        }
      }
    },
    "/cue-templates/{templateId}/audio": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Stream raw audio for a cue template (inline WAV)",
        "operationId": "getCueTemplateAudio",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inline WAV audio for the template",
            "content": {
              "audio/wav": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          },
          "422": {
            "description": "Template has no raw audio stored"
          }
        }
      }
    },
    "/feeds/{slug}/cue-templates/import": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Import a cue template zip into a feed",
        "description": "Accepts a v2 zip (cue.flac) or a legacy v1 zip (cue.wav). The MFCC is recomputed from the decoded audio (a foreign blob is never trusted). Imports land as podcast scope. Sample-rate and channel mismatches are hard-rejected with no resampling. The manifest's removeWithAd must be boolean; omission defaults to true.\n",
        "operationId": "importCueTemplate",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template imported",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "template": {
                      "$ref": "#/components/schemas/CueTemplate"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid zip, manifest, or audio"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cue-scan": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Run every enabled template against an episode (diagnostic)",
        "operationId": "cueScanEpisode",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scoreThreshold": {
                    "type": "number",
                    "description": "Override the global threshold for this run only (clamped to 0-0.99)",
                    "minimum": 0,
                    "maximum": 0.99
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-template peak scores and match times",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "thresholdUsed": {
                      "type": "number"
                    },
                    "thresholdSource": {
                      "type": "string",
                      "enum": [
                        "override",
                        "global",
                        "request"
                      ],
                      "description": "Source of the threshold value. 'override' = per-feed column; 'global' = global setting; 'request' = scoreThreshold body field."
                    },
                    "elapsedSeconds": {
                      "type": "number"
                    },
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "label": {
                            "type": "string"
                          },
                          "durationS": {
                            "type": "number"
                          },
                          "peakScore": {
                            "type": "number"
                          },
                          "matchCount": {
                            "type": "integer"
                          },
                          "matches": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/CueMatch"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Feed has no enabled templates"
          },
          "404": {
            "description": "Feed or original audio not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cue-template-preview": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Run a single template against an episode",
        "operationId": "previewCueTemplate",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "templateId"
                ],
                "properties": {
                  "templateId": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Match times for the template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templateId": {
                      "type": "integer"
                    },
                    "thresholdUsed": {
                      "type": "number"
                    },
                    "peakScore": {
                      "type": "number"
                    },
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CueMatch"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Template, feed, or original audio not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cue-loud-spots": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Template-free energy \"loud spots\" for the capture UI",
        "description": "Runs the band-passed energy detector over an episode's original audio and returns candidate loud spots (not detected cues) as jump-to markers so the user can find a cue to bracket.\n",
        "operationId": "episodeLoudSpots",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate loud spots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "loudSpots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "prominenceDb": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed or original audio not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/detected-cues": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Audio cues already found on an episode, as template candidates",
        "description": "Merges the episode's persisted cue signals (template + spectral) with a\ntemplate-free loud-spot pass over the original audio, labeled by source.\nEach is a candidate the user can promote into a per-feed cue template.\nAdvisory only; nothing here changes the cut.\n",
        "operationId": "episodeDetectedCues",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detected cue candidates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "hasOriginalAudio": {
                      "type": "boolean",
                      "description": "False if the original audio was not retained (then loud spots are omitted and templates cannot be made)"
                    },
                    "detectedCues": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "template",
                              "spectral",
                              "loud_spot"
                            ]
                          },
                          "label": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "cueType": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "score": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "prominenceDb": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cue-candidates": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Find cue-template candidates in an episode",
        "description": "On-demand scan combining within-episode recurring sounds (fingerprint\nself-repeat, for ad-break stings) with cross-episode intro/outro segments\n(head/tail audio shared across recent completed sibling episodes), so\ncandidates cover all cue types. Each candidate carries a `kind` and a\n`suggestedType` hint. The decode is slow (90s+ on a long episode), so the\nscan runs in a background thread and the result is cached. Poll this\nendpoint until `status` is `ready` or `error`. Pass `rescan=1` to force a\nfresh scan after an error.\n",
        "operationId": "episodeCueCandidates",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rescan",
            "in": "query",
            "required": false,
            "description": "Force a fresh scan, ignoring any cached result",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "yes"
              ]
            }
          },
          {
            "name": "peek",
            "in": "query",
            "required": false,
            "description": "Read-only. Returns a cached result or 'idle' without starting a scan",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "yes"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scan status, with candidates once ready (intro/outro first, then recurring by count)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "scanning",
                        "ready",
                        "error",
                        "idle"
                      ],
                      "description": "`scanning` (poll again shortly), `ready` (candidates\nattached), `error` (scan failed; see `error`), or `idle`\n(peek found no cached scan; none was started).\n"
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when status is error"
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "recurring",
                              "intro",
                              "outro"
                            ],
                            "description": "recurring (repeats within the episode), or intro/outro (shared across sibling episodes)"
                          },
                          "count": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Recurring candidates only. Times the sound recurs within the episode"
                          },
                          "episodeMatches": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Intro/outro candidates only. How many sibling episodes share the segment"
                          },
                          "suggestedType": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "show_intro",
                              "show_outro",
                              "ad_break_boundary",
                              "ad_break_start",
                              "ad_break_end",
                              "content_transition",
                              null
                            ],
                            "description": "Capture-type hint; intro/outro get show_intro/show_outro; recurring gets ad-affinity type or null"
                          },
                          "adBoundaryHits": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Recurring candidates only. How many occurrences fell near a known ad boundary"
                          },
                          "boundaryAffinity": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "format": "float",
                            "minimum": 0,
                            "maximum": 1,
                            "description": "adBoundaryHits / count; null when no ad history was available"
                          },
                          "affinitySource": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "episode",
                              "siblings",
                              null
                            ],
                            "description": "Whether affinity was computed from the scanned episode's own ad history or from sibling episodes"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed or original audio not found"
          }
        }
      }
    },
    "/feeds/{slug}/episodes/{episode_id}/cue-candidates/dismiss": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Dismiss a candidate span feed-wide",
        "description": "Fingerprints the given audio span and records a feed-wide dismissal so\nthe sound is suppressed from future candidate scans across all episodes.\nThe episode's cached candidate scan is stamped immediately; other episodes\nself-correct on their next rescan.\n",
        "operationId": "dismissCueCandidate",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "episode_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "start_s",
                  "end_s"
                ],
                "properties": {
                  "start_s": {
                    "type": "number",
                    "description": "Start of the span to dismiss (seconds)"
                  },
                  "end_s": {
                    "type": "number",
                    "description": "End of the span to dismiss (seconds)"
                  },
                  "label": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional human-readable label for the dismissal"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Dismissal created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "label": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "startS": {
                      "type": "number"
                    },
                    "endS": {
                      "type": "number"
                    },
                    "sourceEpisodeId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid span, span exceeds 120 s maximum, or missing required fields"
          },
          "404": {
            "description": "Feed not found or original audio unavailable"
          },
          "503": {
            "description": "Audio fingerprinting failed"
          }
        }
      }
    },
    "/feeds/{slug}/cue-candidate-dismissals": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "List dismissed candidate sounds for a feed",
        "description": "Returns all feed-wide dismissals, newest first. These are the undo\ntargets exposed in the UI.\n",
        "operationId": "listCueCandidateDismissals",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of dismissals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dismissals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "label": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sourceEpisodeId": {
                            "type": "string"
                          },
                          "startS": {
                            "type": "number"
                          },
                          "endS": {
                            "type": "number"
                          },
                          "createdAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/cue-candidate-dismissals/{id}": {
      "delete": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Undo a cue candidate dismissal",
        "description": "Removes the dismissal record so the sound becomes suggestible again.\nThe source episode's cached candidate scan is unstamped immediately.\n",
        "operationId": "deleteCueCandidateDismissal",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dismissal removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Dismissal not found"
          }
        }
      }
    },
    "/cue-detections/{detectionId}/verdict": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Confirm, reject, or reset a cue detection's review verdict",
        "description": "Advisory template-quality signal only. A verdict never changes an\nepisode's cut list; it records whether a template cue matched a real ad\nboundary so a feed's cues can be judged and thresholds tuned.\n",
        "operationId": "setCueDetectionVerdict",
        "parameters": [
          {
            "name": "detectionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "verdict"
                ],
                "properties": {
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "confirmed",
                      "rejected"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated verdict",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "verdict": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "confirmed",
                        "rejected"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid verdict"
          },
          "404": {
            "description": "Cue detection not found"
          }
        }
      }
    },
    "/feeds/{slug}/cue-detections/advisory": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Per-feed cue health summary",
        "description": "Outcome and verdict counts, match-score range, and confirm rate across the feed's processed episodes.",
        "operationId": "getCueFeedAdvisory",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-feed cue advisory",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CueAdvisory"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "templateHints": {
                          "type": "array",
                          "description": "Per-template verdict hints. Only templates with enough rejections to classify are included.",
                          "items": {
                            "type": "object",
                            "required": [
                              "templateId",
                              "hint",
                              "rejected",
                              "confirmed"
                            ],
                            "properties": {
                              "templateId": {
                                "type": "integer"
                              },
                              "label": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "hint": {
                                "type": "string",
                                "enum": [
                                  "raise_threshold",
                                  "recapture"
                                ],
                                "description": "raise_threshold when rejections cluster just above the threshold; recapture when they span the score range or overlap confirmed scores."
                              },
                              "rejected": {
                                "type": "integer",
                                "description": "Number of rejected detections for this template."
                              },
                              "confirmed": {
                                "type": "integer",
                                "description": "Number of confirmed detections for this template."
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{slug}/cue-threshold-suggest": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Suggest a global cue match threshold for a feed",
        "description": "Sweeps a sample of episodes for one or more templates and proposes a global cue match threshold based on the gap between noise and real matches. The suggestion is advisory; a cue below 0.80 confidence will not change cuts regardless of this value.\n",
        "operationId": "suggestCueThreshold",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "episodeId"
                ],
                "properties": {
                  "episodeId": {
                    "type": "string",
                    "description": "Anchor episode to include in the sweep."
                  },
                  "rescan": {
                    "type": "boolean",
                    "description": "Force a fresh scan even if results are cached."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Threshold suggestion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "episodeId",
                    "status"
                  ],
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "scanning",
                        "ready",
                        "error"
                      ],
                      "description": "scanning while results are being gathered, ready when a suggestion is available, error on failure."
                    },
                    "suggestion": {
                      "type": "object",
                      "description": "Suggestion object. Present when status is ready.",
                      "properties": {
                        "confidence": {
                          "type": "string",
                          "enum": [
                            "high",
                            "partial",
                            "low"
                          ],
                          "description": "high when signal is clearly above the effect floor; partial when signal is below the effect floor; low when no clear gap was found."
                        },
                        "suggested": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "Proposed threshold (0-1), or null on low confidence."
                        },
                        "noiseCeiling": {
                          "type": "number",
                          "description": "Top of the noise cluster."
                        },
                        "signalFloor": {
                          "type": "number",
                          "description": "Bottom of the signal cluster."
                        },
                        "gapWidth": {
                          "type": "number",
                          "description": "Width of the gap between noise and signal."
                        },
                        "signalCount": {
                          "type": "integer",
                          "description": "Number of occurrences in the signal cluster."
                        },
                        "effectFloor": {
                          "type": "number",
                          "description": "Lowest confidence at which a cue can affect a cut on this install."
                        },
                        "effectFloorWarning": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "signal-below-floor",
                            null
                          ],
                          "description": "signal-below-floor when the signal cluster sits below the effect floor; null otherwise."
                        },
                        "scoresN": {
                          "type": "integer",
                          "description": "Total number of occurrence scores used."
                        },
                        "reason": {
                          "type": "string",
                          "description": "Human-readable reason. Present on low confidence results."
                        },
                        "labeledCounts": {
                          "type": "object",
                          "description": "Breakdown of labeled scores used in the suggestion (confirmed and rejected counts). Present when labeled scores were available.",
                          "properties": {
                            "confirmed": {
                              "type": "integer"
                            },
                            "rejected": {
                              "type": "integer"
                            }
                          }
                        },
                        "labeledOverlap": {
                          "type": "boolean",
                          "description": "True when the rejected and confirmed labeled score ranges overlap (no clean labeled placement is possible). Set when at least AUDIO_CUE_SUGGEST_MIN_LABELED total reviewed detections exist with both confirmed and rejected present, and the labeled score ranges overlap."
                        }
                      }
                    },
                    "sampleEpisodes": {
                      "type": "integer",
                      "description": "Number of episodes included in the sweep."
                    },
                    "floorUsed": {
                      "type": "number",
                      "description": "Low-floor threshold the sweep used to gather scores."
                    },
                    "currentThreshold": {
                      "type": "number",
                      "description": "The threshold in effect for this feed at scan time (per-feed override if set, else global setting)."
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "feed",
                        "global"
                      ],
                      "description": "Whether currentThreshold came from a per-feed override ('feed') or the global setting ('global')."
                    },
                    "perTemplate": {
                      "type": "object",
                      "description": "Map of templateId to peak score across the sampled episodes. Present when status is ready.",
                      "additionalProperties": {
                        "type": "number"
                      }
                    },
                    "error": {
                      "type": "string",
                      "description": "Error message. Present when status is error."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No cue templates on feed, or invalid request"
          },
          "404": {
            "description": "Feed or episode not found"
          }
        }
      }
    },
    "/feeds/{slug}/cue-cross-episode-scan": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Find recurring audio segments across a set of episodes",
        "description": "Full-body cross-episode fingerprint scan. Finds audio segments that recur anywhere (not just intro/outro) across the supplied episode set. Between 2 and 5 episode IDs must be supplied; all must belong to this feed and have retained original audio. The scan runs in a background thread (full-duration fingerprinting is slow); poll this endpoint with the same body to check progress. rescan=true forces a fresh run even when a cached result exists.\nThe result coordinate frame is the first supplied episode (targetEpisodeId). Candidates can be fed directly into the Make-template flow (CueMarkModal expects bounds + episode).\n",
        "operationId": "cueCrossEpisodeScan",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "episodeIds"
                ],
                "properties": {
                  "episodeIds": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 5,
                    "items": {
                      "type": "string"
                    },
                    "description": "2 to 5 episode IDs. All must belong to this feed and have retained original audio. The first entry defines the coordinate frame for returned candidate times.\n"
                  },
                  "rescan": {
                    "type": "boolean",
                    "description": "Force a fresh scan even if a cached result exists."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scan status, with candidates once ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "scanning",
                        "ready",
                        "error"
                      ],
                      "description": "scanning (poll again shortly), ready (candidates attached), error (scan failed; see error field).\n"
                    },
                    "episodeIds": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Echo of the requested episode IDs."
                    },
                    "targetEpisodeId": {
                      "type": "string",
                      "description": "First episode ID; all candidate times are in this episode's coordinate frame. Present when status is ready.\n"
                    },
                    "candidates": {
                      "type": "array",
                      "description": "Present when status is ready.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "start": {
                            "type": "number"
                          },
                          "end": {
                            "type": "number"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "recurring"
                            ]
                          },
                          "episodeMatches": {
                            "type": "integer",
                            "description": "How many SIBLING episodes share this segment (the target episode always matches, so the total number of episodes containing it is episodeMatches + 1).\n"
                          },
                          "episodes": {
                            "type": "array",
                            "description": "Per-episode breakdown: every occurrence of this candidate in each scanned episode (target first, then the remaining episodes in request order). Match times are in each episode's own timeline. Absent on results cached by versions before 2.42.0; rescan to populate.\n",
                            "items": {
                              "type": "object",
                              "properties": {
                                "episodeId": {
                                  "type": "string"
                                },
                                "matchCount": {
                                  "type": "integer",
                                  "description": "Occurrences in this episode (0 = not found)."
                                },
                                "matches": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "start": {
                                        "type": "number"
                                      },
                                      "end": {
                                        "type": "number"
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when status is error."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Fewer than 2 or more than 5 episode IDs; an ID not in this feed; or an episode without retained original audio.\n"
          },
          "404": {
            "description": "Feed not found"
          }
        }
      }
    },
    "/feeds/{slug}/cue-templates/{templateId}/optimize-window": {
      "post": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Find the window trim that maximises match score across sibling episodes",
        "description": "Grid-sweeps start/end deltas in -0.5..+0.5s (0.1s steps) to find the window geometry that yields the highest mean peak-correlation score across the source episode and up to 4 sibling episodes. Only the grid region of the source is decoded; each sibling is scanned once and all candidates are scored against a small neighborhood of its best baseline match.\nThe scan runs in a background thread; poll this endpoint with the same body to check progress. rescan=true forces a fresh run.\nReturns 409 when the template has no source episode or the source episode original audio has been aged out (re-decoding the source window is required to score candidates).\n",
        "operationId": "cueWindowOptimize",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rescan": {
                    "type": "boolean",
                    "description": "Force a fresh scan even if a cached result exists."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scan status, with proposal once ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "templateId"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "scanning",
                        "ready",
                        "error"
                      ]
                    },
                    "templateId": {
                      "type": "integer"
                    },
                    "proposedStartS": {
                      "type": "number",
                      "description": "Proposed window start (seconds). Present when ready."
                    },
                    "proposedEndS": {
                      "type": "number",
                      "description": "Proposed window end (seconds). Present when ready."
                    },
                    "meanPeakScore": {
                      "type": "number",
                      "description": "Mean peak ZNCC score of the proposed window. Present when ready."
                    },
                    "baselineMeanPeakScore": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Mean peak score of the original window. Present when ready; null when the original window falls outside the live capture bounds and could not be scored.\n"
                    },
                    "perEpisode": {
                      "type": "array",
                      "description": "Per-episode peak scores for the proposed window. Present when ready.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "episodeId": {
                            "type": "string"
                          },
                          "peakScore": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "baselineWindow": {
                      "type": "object",
                      "description": "Original window bounds. Present when ready.",
                      "properties": {
                        "startS": {
                          "type": "number"
                        },
                        "endS": {
                          "type": "number"
                        }
                      }
                    },
                    "error": {
                      "type": "string",
                      "description": "Present when status is error."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Feed not found or template does not belong to this feed."
          },
          "409": {
            "description": "Template has no source episode, or the source episode original audio has been aged out; optimizer cannot re-decode the source window.\n"
          }
        }
      }
    },
    "/cue-detections/aggregate": {
      "get": {
        "tags": [
          "AudioCueTemplates"
        ],
        "summary": "Global cue telemetry for threshold tuning",
        "description": "The per-feed advisory shape across all feeds, plus a match-score histogram.",
        "operationId": "getCueDetectionsAggregate",
        "responses": {
          "200": {
            "description": "Aggregate cue telemetry",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CueAdvisory"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "scoreHistogram": {
                          "type": "array",
                          "description": "Match-score buckets for above-threshold cues.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "scoreFrom": {
                                "type": "number"
                              },
                              "count": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "nearMissHistogram": {
                          "type": "array",
                          "description": "Score buckets for below_threshold near-misses (same bucketing as scoreHistogram). These never affected cuts.\n",
                          "items": {
                            "type": "object",
                            "properties": {
                              "scoreFrom": {
                                "type": "number"
                              },
                              "count": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "nearMissTotal": {
                          "type": "integer",
                          "description": "Count of below_threshold advisory rows."
                        },
                        "unusedReasons": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Count per unused_reason across outcome='none' cues (e.g. covered, out_of_reach, unpaired).\n"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "sessionCookie": []
    }
  ],
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "Flask session cookie issued by ``POST /auth/login``. Every mutating\nendpoint normally requires a matching ``X-CSRF-Token`` header\n(double-submit). ``/auth/password`` instead verifies the current\npassword, or the remote setup token during first setup. Auth routes,\nhealth probes, and public feed-artwork reads declare ``security: []``;\nlogout still enforces CSRF when its session is authenticated.\n"
      }
    },
    "parameters": {
      "correction_id": {
        "name": "correction_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "Podcast feed slug identifier",
        "schema": {
          "type": "string",
          "example": "my-podcast"
        }
      },
      "audio_download": {
        "name": "download",
        "in": "query",
        "required": false,
        "description": "When `1`, the file is sent as an attachment named after the episode title (`<Title>-original.mp3` or `<Title>-cut.mp3`) instead of streamed inline.",
        "schema": {
          "type": "string",
          "enum": [
            "1"
          ]
        }
      },
      "episode_id": {
        "name": "episode_id",
        "in": "path",
        "required": true,
        "description": "Episode identifier: a 12-character MD5-hex string for a subscribed feed's episode, or an sNNeNN id (e.g. s01e05) for a local feed's.",
        "schema": {
          "type": "string",
          "example": "abc123def456"
        }
      },
      "pattern_id": {
        "name": "pattern_id",
        "in": "path",
        "required": true,
        "description": "Pattern identifier (integer)",
        "schema": {
          "type": "integer",
          "example": 42
        }
      },
      "sponsor_id": {
        "name": "sponsor_id",
        "in": "path",
        "required": true,
        "description": "Sponsor identifier (integer)",
        "schema": {
          "type": "integer",
          "example": 1
        }
      },
      "norm_id": {
        "name": "norm_id",
        "in": "path",
        "required": true,
        "description": "Normalization rule identifier (integer)",
        "schema": {
          "type": "integer",
          "example": 1
        }
      },
      "webhook_id": {
        "name": "webhook_id",
        "in": "path",
        "required": true,
        "description": "Webhook identifier",
        "schema": {
          "type": "string",
          "example": "550e8400-e29b-41d4-a716-446655440000"
        }
      }
    },
    "schemas": {
      "YoutubeCookiesStatus": {
        "type": "object",
        "properties": {
          "set": {
            "type": "boolean"
          },
          "lines": {
            "type": "integer"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "YoutubeSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "apiKeyConfigured": {
            "type": "boolean"
          },
          "sbSubmitEnabled": {
            "type": "boolean"
          },
          "sbUserIdConfigured": {
            "type": "boolean",
            "description": "Whether a SponsorBlock user ID is stored. Read it with GET /settings/youtube/sb-user-id."
          },
          "sbUserIdGenerated": {
            "type": "boolean",
            "description": "Whether the stored ID was generated by MinusPod. A generated ID cannot be cleared or replaced."
          },
          "processDelayMinutes": {
            "type": "integer"
          },
          "sbProbeAttempts": {
            "type": "integer"
          },
          "sbProbeIntervalMinutes": {
            "type": "integer"
          },
          "sbWatchIntervalHours": {
            "type": "integer"
          },
          "sbWatchMaxDays": {
            "type": "integer"
          },
          "llmFallback": {
            "type": "boolean"
          },
          "captionsAsTranscript": {
            "type": "boolean"
          },
          "hidePrivate": {
            "type": "boolean"
          },
          "transcribeFallback": {
            "type": "boolean"
          },
          "proxyUrl": {
            "type": "string",
            "description": "Proxy for yt-dlp and YouTube Data API requests. http, https, socks4, socks5 and socks5h schemes."
          },
          "ytdlpExtraArgs": {
            "type": "string",
            "description": "Extra command line arguments added before the app's own options on every yt-dlp call. Options that run or load other programs, change where files are written, read local files, alter the JSON output, update the binary or carry passwords are rejected; use long option names."
          },
          "ytdlp": {
            "type": "object",
            "properties": {
              "autoUpdate": {
                "type": "boolean"
              },
              "version": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastUpdateAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastCheckAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastError": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "bakedVersion": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "cookies": {
            "type": "object",
            "properties": {
              "set": {
                "type": "boolean"
              },
              "lines": {
                "type": "integer"
              },
              "updatedAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "bounds": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "integer"
              }
            }
          }
        }
      },
      "SystemOneTuning": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "maxConcurrentOperations": {
            "type": "integer",
            "minimum": 1,
            "default": 4,
            "description": "Whole native operations per process. Profiles with the same endpoint and credential share the strictest active or queued limit; queue waits consume the operation deadline."
          },
          "retryAfterMaxSeconds": {
            "type": "number",
            "minimum": 0,
            "default": 5,
            "description": "Maximum first-retry upstream hint wait in seconds, including jitter. Zero permits no hint wait; the operation deadline also bounds waits."
          },
          "detectionEnter": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.95
          },
          "detectionStay": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.4,
            "description": "Must not exceed detectionEnter."
          },
          "categoryPass": {
            "type": "boolean",
            "default": true
          },
          "categoryContext": {
            "type": "integer",
            "minimum": 0,
            "default": 2,
            "description": "Transcript segments on each side of a classified span."
          },
          "defaultCategory": {
            "type": "string",
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap"
            ],
            "default": "sponsor"
          },
          "refineBoundaries": {
            "type": "boolean",
            "default": false
          },
          "reviewEvidenceEnter": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "default": null,
            "description": "Null selects the model evidence threshold."
          },
          "reviewChoiceEnter": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "default": null,
            "description": "Null selects the model choice threshold."
          },
          "reviewProgrammeVeto": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.85
          },
          "reviewBoundaryCapSeconds": {
            "type": "number",
            "exclusiveMinimum": 0,
            "default": 60,
            "description": "Maximum boundary movement in seconds."
          },
          "reviewContextSeconds": {
            "type": "number",
            "exclusiveMinimum": 0,
            "default": 30,
            "description": "Focused review context window in seconds."
          },
          "requestDeadlineSeconds": {
            "type": "number",
            "exclusiveMinimum": 0,
            "default": 75,
            "description": "Absolute logical-operation deadline in seconds, shared by requests and retry waits."
          },
          "maxQuestionsPerRequest": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "default": null,
            "description": "Null disables the configured question-count cap."
          },
          "maxRequestBytes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "default": null,
            "description": "Serialized UTF-8 request byte cap; null disables the configured byte cap. This is not a token limit."
          },
          "maxChoiceOptions": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Compatible backend Choice option cap, or null. TypeSafe uses a fixed cap of 255 options."
          },
          "maxPromptTokens": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "default": null,
            "description": "Estimated request token budget (4 characters per token). A detection window above it is split into equal parts before sending; null sends each window whole."
          }
        }
      },
      "SystemOneTypeSafeTuning": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SystemOneTuning"
          },
          {
            "type": "object",
            "properties": {
              "maxChoiceOptions": {
                "type": "integer",
                "const": 255
              }
            }
          }
        ]
      },
      "SystemOneProfiles": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "primary": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "typesafe": {
                "$ref": "#/components/schemas/SystemOneTypeSafeTuning"
              },
              "systemone-compatible": {
                "$ref": "#/components/schemas/SystemOneTuning"
              }
            }
          },
          "secondary": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "typesafe": {
                "$ref": "#/components/schemas/SystemOneTypeSafeTuning"
              },
              "systemone-compatible": {
                "$ref": "#/components/schemas/SystemOneTuning"
              }
            }
          }
        }
      },
      "SystemOneProfileDefaults": {
        "type": "object",
        "properties": {
          "primary": {
            "type": "object",
            "properties": {
              "typesafe": {
                "type": "boolean"
              },
              "systemone-compatible": {
                "type": "boolean"
              }
            }
          },
          "secondary": {
            "type": "object",
            "properties": {
              "typesafe": {
                "type": "boolean"
              },
              "systemone-compatible": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "SystemOneProfilesPatch": {
        "type": "object",
        "additionalProperties": false,
        "description": "Partial nested merge by credential slot and provider type. Unspecified slots, providers, and fields retain saved values. Null resets one complete profile to shipped defaults. Values survive provider type switches. TypeSafe and compatible profiles are independent for both slots. Unsupported keys and invalid cross-field combinations return 400. Bulk ad-detection reset restores all profiles.",
        "properties": {
          "primary": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "typesafe": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SystemOneTypeSafeTuning"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "systemone-compatible": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SystemOneTuning"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "secondary": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "typesafe": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SystemOneTypeSafeTuning"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "systemone-compatible": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SystemOneTuning"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        },
        "example": {
          "primary": {
            "typesafe": {
              "detectionEnter": 0.98
            }
          },
          "secondary": {
            "systemone-compatible": null
          }
        }
      },
      "CleanupUsage": {
        "type": "object",
        "required": [
          "requests",
          "inputTokens",
          "outputTokens",
          "knownCostUsd",
          "unknownUsageRequestCount",
          "unknownCostRequestCount"
        ],
        "properties": {
          "requests": {
            "type": "integer",
            "minimum": 0
          },
          "inputTokens": {
            "type": "integer",
            "minimum": 0
          },
          "outputTokens": {
            "type": "integer",
            "minimum": 0
          },
          "knownCostUsd": {
            "type": "string",
            "description": "Exact decimal USD sum of known attempt costs; partial known spend remains visible."
          },
          "unknownUsageRequestCount": {
            "type": "integer",
            "minimum": 0
          },
          "unknownCostRequestCount": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "CleanupActionCounts": {
        "type": "object",
        "required": [
          "proposed",
          "accepted",
          "applied",
          "reverted"
        ],
        "properties": {
          "proposed": {
            "type": "integer",
            "minimum": 0,
            "description": "Distinct run/pattern/kind proposal count, including superseded records."
          },
          "accepted": {
            "type": "integer",
            "minimum": 0,
            "description": "Approved or undone actions."
          },
          "applied": {
            "type": "integer",
            "minimum": 0,
            "description": "Approved actions not undone; includes deleted patterns."
          },
          "reverted": {
            "type": "integer",
            "minimum": 0,
            "description": "Undone actions."
          }
        }
      },
      "SystemOneStats": {
        "type": "object",
        "required": [
          "calls",
          "requests",
          "dispatchLatencyMsTotal",
          "dispatchLatencyMsAverage",
          "logicalLatencyMsTotal",
          "logicalLatencyMsAverage",
          "tokens",
          "costUsd",
          "unknownCostRequestCount",
          "outcomes",
          "reviewReasons",
          "refinements",
          "refinementSkipReasons"
        ],
        "properties": {
          "calls": {
            "type": "integer",
            "minimum": 0
          },
          "requests": {
            "type": "integer",
            "minimum": 0
          },
          "dispatchLatencyMsTotal": {
            "type": "integer",
            "minimum": 0
          },
          "dispatchLatencyMsAverage": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Mean dispatch latency per HTTP request in milliseconds, null without requests"
          },
          "logicalLatencyMsTotal": {
            "type": "integer",
            "minimum": 0
          },
          "logicalLatencyMsAverage": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean of measured logical-call durations in milliseconds; null when no duration is available."
          },
          "tokens": {
            "type": "object",
            "properties": {
              "input": {
                "type": "integer",
                "minimum": 0
              },
              "output": {
                "type": "integer",
                "minimum": 0
              },
              "unknownRequestCount": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "costUsd": {
            "type": "string",
            "description": "Exact decimal sum in USD, including reported or provider-scoped estimated costs."
          },
          "unknownCostRequestCount": {
            "type": "integer",
            "minimum": 0
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            },
            "properties": {
              "completed": {
                "type": "integer",
                "minimum": 0
              },
              "failed": {
                "type": "integer",
                "minimum": 0
              },
              "inconclusive": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "reviewReasons": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            }
          },
          "refinements": {
            "type": "object",
            "properties": {
              "attempted": {
                "type": "integer",
                "minimum": 0
              },
              "completed": {
                "type": "integer",
                "minimum": 0
              },
              "skipped": {
                "type": "integer",
                "minimum": 0
              },
              "inconclusive": {
                "type": "integer",
                "minimum": 0
              },
              "upstream_error": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "refinementSkipReasons": {
            "type": "object",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            }
          }
        }
      },
      "LlmStats": {
        "type": "object",
        "required": [
          "calls",
          "requests",
          "outcomes",
          "callLatencyMsAverage",
          "dispatchLatencyMsAverage",
          "tokens",
          "costUsd",
          "unknownCostRequestCount",
          "byPhase"
        ],
        "properties": {
          "calls": {
            "type": "integer",
            "minimum": 0
          },
          "requests": {
            "type": "integer",
            "minimum": 0
          },
          "outcomes": {
            "type": "object",
            "required": [
              "success",
              "failure",
              "inconclusive",
              "inFlight"
            ],
            "properties": {
              "success": {
                "type": "integer",
                "minimum": 0
              },
              "failure": {
                "type": "integer",
                "minimum": 0
              },
              "inconclusive": {
                "type": "integer",
                "minimum": 0
              },
              "inFlight": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "callLatencyMsAverage": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Mean measured call duration in milliseconds, null when no duration is available"
          },
          "dispatchLatencyMsAverage": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Mean dispatch latency per HTTP request in milliseconds, null without requests"
          },
          "tokens": {
            "type": "object",
            "required": [
              "input",
              "output",
              "cacheRead",
              "cacheWrite",
              "reasoning",
              "unknownRequestCount"
            ],
            "properties": {
              "input": {
                "type": "integer",
                "minimum": 0
              },
              "output": {
                "type": "integer",
                "minimum": 0
              },
              "cacheRead": {
                "type": "integer",
                "minimum": 0
              },
              "cacheWrite": {
                "type": "integer",
                "minimum": 0
              },
              "reasoning": {
                "type": "integer",
                "minimum": 0
              },
              "unknownRequestCount": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "costUsd": {
            "type": "string",
            "description": "Exact decimal sum in USD of known call costs."
          },
          "unknownCostRequestCount": {
            "type": "integer",
            "minimum": 0
          },
          "byPhase": {
            "type": "object",
            "description": "Calls and known cost per pipeline phase, ordered by calls descending.",
            "additionalProperties": {
              "type": "object",
              "required": [
                "calls",
                "costUsd"
              ],
              "properties": {
                "calls": {
                  "type": "integer",
                  "minimum": 0
                },
                "costUsd": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CleanupStats": {
        "type": "object",
        "required": [
          "runs",
          "patterns",
          "actions",
          "usage",
          "unattributedUsage",
          "coverage"
        ],
        "properties": {
          "runs": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "minimum": 0
              },
              "running": {
                "type": "integer",
                "minimum": 0
              },
              "completed": {
                "type": "integer",
                "minimum": 0
              },
              "failed": {
                "type": "integer",
                "minimum": 0
              },
              "exactAccounting": {
                "type": "integer",
                "minimum": 0
              },
              "legacy": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "patterns": {
            "type": "object",
            "properties": {
              "checked": {
                "type": "integer",
                "minimum": 0
              },
              "distinctChecked": {
                "type": "integer",
                "minimum": 0
              },
              "modelReviewed": {
                "type": "integer",
                "minimum": 0
              },
              "proposed": {
                "type": "integer",
                "minimum": 0
              },
              "changed": {
                "type": "integer",
                "minimum": 0
              },
              "legacyReportedReviews": {
                "type": "integer",
                "minimum": 0
              }
            },
            "description": "checked is one attempt per pattern per run; distinctChecked/proposed/changed count distinct pattern IDs. modelReviewed includes completed and stale reviews. legacyReportedReviews preserves old run counters without claiming exact checks."
          },
          "actions": {
            "type": "object",
            "properties": {
              "trim": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              },
              "split": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              },
              "rename": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              },
              "retire": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              },
              "flag": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              },
              "category": {
                "$ref": "#/components/schemas/CleanupActionCounts"
              }
            },
            "description": "Stored action kinds. Rename means a sponsor rename. Composite trim/split/flag actions may also change sponsors or categories; these are counted once under their actual kind."
          },
          "usage": {
            "$ref": "#/components/schemas/CleanupUsage"
          },
          "unattributedUsage": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CleanupUsage"
              },
              {
                "type": "null"
              }
            ]
          },
          "coverage": {
            "type": "object",
            "properties": {
              "historicalRunCountWithUnknownSpend": {
                "type": "integer",
                "minimum": 0
              },
              "unattributedProposalCount": {
                "type": "integer",
                "minimum": 0
              },
              "proposalHistoryCompleteSince": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Start of durable proposal tracking; older deleted proposals cannot be reconstructed."
              }
            }
          }
        }
      },
      "RuntimeConfigDocument": {
        "type": "object",
        "required": [
          "format",
          "formatVersion",
          "settings",
          "feeds"
        ],
        "properties": {
          "format": {
            "type": "string",
            "const": "minuspod-runtime-config"
          },
          "formatVersion": {
            "type": "integer",
            "const": 1
          },
          "appVersion": {
            "type": "string"
          },
          "exportedAt": {
            "type": "string",
            "format": "date-time"
          },
          "containsSecrets": {
            "type": "boolean"
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Portable runtime setting names and values. Null is allowed only for credentials, supported nullable overrides, and pattern_cleanup_model."
          },
          "feeds": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "slug",
                "feedType",
                "settings"
              ],
              "properties": {
                "slug": {
                  "type": "string"
                },
                "feedType": {
                  "type": "string",
                  "enum": [
                    "subscribed",
                    "local",
                    "recents",
                    "youtube"
                  ]
                },
                "settings": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Feed configuration only; no episodes or history."
                }
              }
            }
          }
        }
      },
      "RuntimeConfigImportRequest": {
        "type": "object",
        "required": [
          "document",
          "scope"
        ],
        "properties": {
          "document": {
            "$ref": "#/components/schemas/RuntimeConfigDocument"
          },
          "scope": {
            "type": "string",
            "enum": [
              "everything",
              "global",
              "feeds"
            ],
            "description": "Everything means global and feed configuration only."
          },
          "selectedFeeds": {
            "type": "array",
            "uniqueItems": true,
            "items": {
              "type": "string"
            },
            "description": "Feed slugs for feeds scope; omission selects all feeds in the document."
          }
        }
      },
      "RuntimeConfigImportPreview": {
        "type": "object",
        "properties": {
          "previewToken": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "everything",
              "global",
              "feeds"
            ]
          },
          "settings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "changedSettings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "const": "setting"
                },
                "key": {
                  "type": "string"
                },
                "secret": {
                  "type": "boolean"
                }
              }
            }
          },
          "addedFeeds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "updatedFeeds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "skippedUnknownSettings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "preservesTargetOnlyFeeds": {
            "type": "boolean"
          },
          "warning": {
            "type": "string"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "selectedFeedSlugs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ReviewerModeStats": {
        "type": "object",
        "properties": {
          "reviews": {
            "type": "integer"
          },
          "confirmed": {
            "type": "integer"
          },
          "adjusted": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          },
          "resurrected": {
            "type": "integer"
          },
          "inconclusive": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "avgBoundaryShiftSeconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean absolute boundary shift over adjust verdicts, in seconds; null when the mode has none"
          }
        }
      },
      "AddressingModeOutcomes": {
        "type": "object",
        "properties": {
          "heldRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Share of this mode's corrections filed against a held marker"
          },
          "approveRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Share of this mode's corrections that confirmed or adjusted"
          },
          "rejectRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Share of this mode's corrections that rejected"
          },
          "avgSnapDistance": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean of |start - llm_start| + |end - llm_end| in seconds between the model's raw bounds and the stored marker",
            "all correction stages included": null
          }
        }
      },
      "AddressingModeStats": {
        "type": "object",
        "description": "Compliance aggregate for one addressing mode (from addressing_log)",
        "properties": {
          "runs": {
            "type": "integer",
            "description": "Number of detection/verification passes recorded for this effective mode"
          },
          "windowsJudged": {
            "type": "integer",
            "description": "Windows with an LLM response usable enough to judge contract compliance (excludes windows whose LLM call failed outright)"
          },
          "windowsCompliant": {
            "type": "integer",
            "description": "Of windowsJudged, how many honored the addressing contract for this mode"
          },
          "compliancePct": {
            "type": "number",
            "description": "100 * windowsCompliant / windowsJudged, rounded to 1 decimal. 0.0 when windowsJudged is 0."
          },
          "yieldRuns": {
            "type": "integer",
            "description": "Passes that recorded yield data. Rows from before 2.92.0 carry none and are excluded from every yield number below, so this denominator can lag runs."
          },
          "adsProposed": {
            "type": "integer",
            "description": "Ads the model returned across judged windows, before any filtering. Yield rows only."
          },
          "adsKept": {
            "type": "integer",
            "description": "Proposed ads that survived reference resolution and the window/duration filter into the pipeline."
          },
          "adsDroppedInvalidRef": {
            "type": "integer",
            "description": "Ads dropped for referencing a segment id that does not exist. Structurally zero for timestamps mode; an invented timestamp is not detectable at this stage."
          },
          "adsDroppedOutOfWindow": {
            "type": "integer",
            "description": "Ads dropped for starting outside their detection window."
          },
          "adsDroppedTooLong": {
            "type": "integer",
            "description": "Ads dropped for exceeding the per-window maximum ad duration."
          },
          "keptPct": {
            "type": "number",
            "description": "100 * adsKept / adsProposed over yield rows, 0.0 when nothing was proposed."
          }
        }
      },
      "FailoverTargetState": {
        "type": "object",
        "description": "Current failover state of one target (#806).",
        "properties": {
          "active": {
            "type": "boolean",
            "description": "True while this target is running on its failover configuration."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "auto",
              "manual",
              "probe",
              null
            ],
            "description": "What last changed the active state; null while inactive."
          },
          "since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the current state began; null while inactive."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text reason recorded with the last trigger; null while inactive."
          },
          "configured": {
            "type": "boolean",
            "description": "Whether this target has a failover configuration to switch to."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the original target is in use. Always true for llm-a and transcriber; llm-b is true only while Provider B is enabled with a provider type."
          }
        }
      },
      "FailoverProbe": {
        "type": "object",
        "description": "Most recent health probe of one failover-related endpoint (#806).",
        "properties": {
          "reachable": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Null before the first probe."
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status code of the last probe, when a response was received."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable outcome of the last probe."
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the last probe ran; null before the first probe."
          },
          "healthyStreak": {
            "type": "integer",
            "description": "Consecutive healthy probes; resets to 0 on any unhealthy probe."
          },
          "failedStreak": {
            "type": "integer",
            "description": "Consecutive unhealthy probes; resets to 0 on any healthy probe."
          }
        }
      },
      "FailoverEvent": {
        "type": "object",
        "description": "One manual, automatic, or probe-driven failover state change (#806).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "target": {
            "type": "string",
            "enum": [
              "llm-a",
              "llm-b",
              "transcriber"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "trigger",
              "cancel"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "auto",
              "manual",
              "probe"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FailoverOverview": {
        "type": "object",
        "description": "GET /failover response body (#806).",
        "properties": {
          "targets": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/FailoverTargetState"
            },
            "description": "Keyed by target name (llm-a, llm-b, transcriber)."
          },
          "probes": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/FailoverProbe"
            },
            "description": "Keyed by probe name (llm-a, llm-b, llm-failover, transcriber, transcriber-failover)."
          },
          "policy": {
            "type": "object",
            "properties": {
              "probeIntervalMinutes": {
                "type": "integer"
              },
              "recoveryProbes": {
                "type": "integer",
                "description": "Consecutive healthy probes needed before an automatic failover recovers."
              }
            }
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FailoverEvent"
            }
          }
        }
      },
      "ConnectionTestResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "The endpoint accepted the probe"
          },
          "reachable": {
            "type": "boolean",
            "description": "The server returned any HTTP response"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, when a response was received"
          },
          "detail": {
            "type": "string",
            "description": "Human-readable outcome"
          },
          "validation": {
            "type": "string",
            "enum": [
              "model_catalog"
            ],
            "description": "Native System One checks validate model discovery only"
          },
          "inferenceChecked": {
            "type": "boolean",
            "description": "False for native model-catalog checks"
          },
          "health": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WhisperHealthProbe"
              }
            ],
            "description": "whisper only, present when ok is true: result of sampling the backend's optional /health endpoint"
          }
        }
      },
      "WhisperHealthProbe": {
        "type": "object",
        "description": "Result of sampling a self-hosted Whisper backend's optional /health endpoint. Behind a load balancer, repeated calls round-robin across replicas, so sampling reveals the replica count and total concurrency. Not necessarily live: a result is cached for 120 seconds, and a request arriving while a probe for that backend is already running is answered from the last successful probe, which may be up to 6 minutes old.",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "The backend exposes a working /health endpoint"
          },
          "instances": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "instance": {
                  "type": "string"
                },
                "model": {
                  "type": "string"
                },
                "device": {
                  "type": "string"
                },
                "compute_type": {
                  "type": "string"
                },
                "batch_size": {
                  "type": "integer"
                },
                "max_concurrent": {
                  "type": "integer"
                },
                "vad_filter": {
                  "type": "boolean"
                }
              }
            }
          },
          "suggested_max_requests": {
            "type": "integer",
            "description": "Sum of each distinct instance's max_concurrent, floored at 1 per instance"
          },
          "mismatch": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Fields (model, compute_type, device) whose values differ across instances"
          },
          "sampled_floor": {
            "type": "boolean",
            "description": "Sampling ran out before any instance repeated three times in a row, so it never demonstrably wrapped the replica set. The instance count and suggested_max_requests are lower bounds, not confirmed totals."
          }
        }
      },
      "CueMatch": {
        "type": "object",
        "description": "One cue match within an episode",
        "properties": {
          "start": {
            "type": "number"
          },
          "end": {
            "type": "number"
          },
          "confidence": {
            "type": "number"
          },
          "score": {
            "type": "number"
          }
        }
      },
      "CueTemplate": {
        "type": "object",
        "description": "A learned per-podcast audio-cue template (metadata; blobs omitted)",
        "properties": {
          "id": {
            "type": "integer"
          },
          "podcastId": {
            "type": "integer"
          },
          "label": {
            "type": "string",
            "description": "Canonical phrase derived from cueType (shown to the LLM and UI)"
          },
          "cueType": {
            "type": "string",
            "enum": [
              "ad_break_boundary",
              "ad_break_start",
              "ad_break_end",
              "show_intro",
              "show_outro",
              "content_transition"
            ],
            "default": "ad_break_boundary"
          },
          "sourceEpisodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceOffsetS": {
            "type": "number"
          },
          "durationS": {
            "type": "number"
          },
          "sampleRate": {
            "type": "integer"
          },
          "nCoeffs": {
            "type": "integer"
          },
          "scope": {
            "type": "string",
            "enum": [
              "podcast",
              "network"
            ],
            "default": "podcast"
          },
          "networkId": {
            "type": [
              "string",
              "null"
            ]
          },
          "removeWithAd": {
            "type": "boolean",
            "default": true,
            "description": "Include matched ad-break cues in cuts. Existing templates and imports without this field default to true. Programme cues retain their audio."
          },
          "cutMatches": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Cut every match of this cue on its own. null inherits the feed or global setting. Included in exports; imports without it default to null."
          },
          "enabled": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string"
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "owned": {
            "type": "boolean",
            "description": "Listing only. False for a network template shared from another feed in this network (read-only here, managed on the feed that created it).\n"
          },
          "hasAudio": {
            "type": "boolean",
            "description": "True when the template has a stored PCM blob (play button is shown only when true or absent for older server rows). Present on both listing and create responses.\n"
          },
          "selfMatchCount": {
            "type": "integer",
            "description": "Create-response only. How many times the captured cue recurs in its source episode (counted via audio fingerprint). 0 means the fingerprint failed or returned no matches (treated as unknown, no warn). Absent on list rows.\n"
          },
          "weakCue": {
            "type": "boolean",
            "description": "Create-response only. True when selfMatchCount is exactly 1: the cue appears only once in its source episode and therefore cannot bracket an ad break reliably. The UI warns the user and requires a second Save to proceed. Absent on list rows.\n"
          },
          "longCapture": {
            "type": "boolean",
            "description": "Create-response only. True when the captured duration exceeds captureWarnSeconds for ad-break-role cue types (ad_break_boundary, ad_break_start, ad_break_end). A capture near 10 s matches far worse than a 1.5-2.5 s clip of the same cue. Non-ad types (show_intro, show_outro, content_transition) are always false. Absent on list rows.\n"
          },
          "captureWarnSeconds": {
            "type": "number",
            "description": "Create-response only. The threshold used for longCapture. Present whenever the create route returns so the UI can display a consistent hint without a separate config fetch. Absent on list rows.\n"
          },
          "scoreThreshold": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0.3,
            "maximum": 0.99,
            "description": "Optional per-template match-score threshold. When set, overrides the per-feed and global thresholds for this template only. Null means fall back to the per-feed override or global setting. Minimum 0.30 (noise floor; sub-floor values pass noise as matches).\n"
          },
          "lastMatchAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Timestamp of this template's most recent above-threshold match. Null when it has never matched."
          },
          "matchedEpisodes": {
            "type": "integer",
            "description": "Distinct episodes with an above-threshold match for this template."
          },
          "quiet": {
            "type": "boolean",
            "description": "True when the template has matched before but had zero above-threshold matches across the feed's last 5 telemetry-recorded episodes. Drives the \"quiet\" badge and the Cue Template Quiet notification."
          }
        }
      },
      "BulkPatternOp": {
        "type": "object",
        "required": [
          "confirm",
          "expected_count"
        ],
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "source": {
            "type": "string",
            "enum": [
              "local",
              "community",
              "imported"
            ]
          },
          "confirm": {
            "type": "boolean"
          },
          "expected_count": {
            "type": "integer"
          }
        }
      },
      "CommunitySyncSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "cron": {
            "type": "string"
          },
          "lastRun": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ]
          },
          "manifestVersion": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastSummary": {
            "type": [
              "string",
              "null"
            ],
            "description": "JSON-encoded summary of the last sync (counts + manifest version)"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sponsor",
                "cross_promo",
                "self_promo",
                "interaction",
                "intro",
                "outro",
                "recap"
              ]
            },
            "description": "Segment categories this install accepts from community sync\n(community_sync_categories setting). Default is every category.\n"
          },
          "categoryBreakdown": {
            "type": "object",
            "description": "Count of currently-active (synced) community patterns per category.",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "CommunitySyncSummary": {
        "type": "object",
        "properties": {
          "inserted": {
            "type": "integer"
          },
          "updated": {
            "type": "integer"
          },
          "deleted": {
            "type": "integer"
          },
          "skipped": {
            "type": "integer"
          },
          "errors": {
            "type": "integer"
          },
          "filtered": {
            "type": "integer",
            "description": "Entries not inserted/updated because their category is not in community_sync_categories."
          },
          "manifest_version": {
            "type": "string"
          },
          "fetched_at": {
            "type": "string"
          }
        }
      },
      "ReplacementAudio": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "default",
              "uploaded"
            ]
          },
          "canRevert": {
            "type": "boolean",
            "description": "True when an uploaded file is in use and can be removed."
          },
          "exists": {
            "type": "boolean",
            "description": "False when no file is on disk, in which case cuts render as silence."
          },
          "sizeBytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "updatedAt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix mtime, used by the UI to bust the preview cache."
          },
          "durationSeconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Every cut becomes this long."
          },
          "channels": {
            "type": [
              "integer",
              "null"
            ]
          },
          "sampleRateHz": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "DatabaseBackupSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "cron": {
            "type": "string"
          },
          "dest": {
            "type": "string",
            "description": "Configured backup directory path. Empty string means default is used."
          },
          "effectiveDest": {
            "type": "string",
            "description": "Resolved absolute path that will be written to (empty on validation error)."
          },
          "destWritable": {
            "type": "boolean",
            "description": "Whether the effective destination directory is currently writable."
          },
          "keepCount": {
            "type": "integer",
            "description": "Number of backup files to keep (1 = overwrite mode, >1 = rotate mode)."
          },
          "lastRun": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 timestamp of the last completed backup run, or null."
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ],
            "description": "Error message from the most recent failed backup run, or null."
          },
          "lastSummary": {
            "type": [
              "string",
              "null"
            ],
            "description": "JSON-encoded DatabaseBackupRunSummary from the last successful run, or null."
          }
        }
      },
      "DatabaseBackupRunSummary": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Absolute path of the backup file written."
          },
          "sizeBytes": {
            "type": "integer",
            "description": "Size of the written backup file in bytes."
          },
          "durationMs": {
            "type": "integer",
            "description": "Wall-clock duration of the backup operation in milliseconds."
          },
          "mode": {
            "type": "string",
            "enum": [
              "overwrite",
              "rotate"
            ],
            "description": "Backup mode used (overwrite when keepCount is 1, rotate otherwise)."
          },
          "keepCount": {
            "type": "integer",
            "description": "Retention limit in effect at the time of the run."
          },
          "prunedCount": {
            "type": "integer",
            "description": "Number of old backup files removed during this run."
          },
          "finishedAt": {
            "type": "string",
            "description": "ISO 8601 UTC timestamp when the backup completed."
          }
        }
      },
      "PodpingNodeUrls": {
        "type": "array",
        "minItems": 1,
        "maxItems": 20,
        "items": {
          "type": "string",
          "format": "uri",
          "maxLength": 2048
        }
      },
      "PodpingNodeSettings": {
        "type": "object",
        "required": [
          "nodes",
          "defaults"
        ],
        "properties": {
          "nodes": {
            "$ref": "#/components/schemas/PodpingNodeUrls"
          },
          "defaults": {
            "$ref": "#/components/schemas/PodpingNodeUrls"
          }
        }
      },
      "PatternCleanupSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "cron": {
            "type": "string",
            "description": "Five-field cron expression in UTC."
          },
          "batchSize": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          },
          "unusedDays": {
            "type": "integer",
            "minimum": 7,
            "maximum": 3650
          },
          "provider": {
            "type": "string",
            "description": "Credential slot ('primary', 'secondary', or 'same_as_detection'), or blank to inherit detection's slot. 'a'/'b' are accepted as aliases for primary/secondary on write."
          },
          "model": {
            "type": "string",
            "maxLength": 200,
            "description": "Blank inherits the detection model."
          }
        }
      },
      "PatternCleanupStatus": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PatternCleanupSettings"
          },
          {
            "type": "object",
            "properties": {
              "modelMissing": {
                "type": "boolean",
                "description": "True when cleanup has an explicitly cleared model and needs a selection before a run. If false and model is blank, cleanup inherits from detection."
              },
              "inProgress": {
                "type": "boolean",
                "description": "Whether another run currently holds the cleanup lock."
              },
              "lastRun": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO 8601 UTC start of the newest run row, including one still running. Enabling the schedule does not change it."
              },
              "lastError": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Error of the newest finished run, if it failed."
              },
              "lastSummary": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/PatternCleanupRun"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The newest finished run (completed or failed)."
              },
              "pending": {
                "type": "object",
                "properties": {
                  "total": {
                    "type": "integer"
                  },
                  "byKind": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "PatternCleanupRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "startedAt": {
            "type": "string"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "completed",
              "failed"
            ]
          },
          "forced": {
            "type": "boolean"
          },
          "trigger": {
            "type": "string",
            "enum": [
              "schedule",
              "manual"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "description": "Configured cleanup model captured at run start, before failover overrides."
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Configured cleanup provider captured at run start, not actual request usage."
          },
          "credentialSlot": {
            "type": [
              "string",
              "null"
            ],
            "description": "Configured credential slot captured at run start, before failover overrides."
          },
          "reviewedCount": {
            "type": "integer"
          },
          "suggestedCount": {
            "type": "integer"
          },
          "skippedCount": {
            "type": "integer"
          },
          "errorCount": {
            "type": "integer",
            "description": "Number of patterns whose review or storage failed. Repeated call failures can stop the run."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set only when the whole run failed (e.g. rate limited)."
          }
        }
      },
      "PatternCleanupSuggestion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "runId": {
            "type": [
              "integer",
              "null"
            ]
          },
          "patternId": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "trim",
              "split",
              "rename",
              "retire",
              "flag",
              "category"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "undone"
            ]
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "payload": {
            "type": "object",
            "description": "Shape depends on kind: trim {text, sponsor?, category?}; split {pieces: [{text, sponsor, category?}]}; rename {sponsor, category?}; category {category}; retire {unusedDays, lastMatchedAt, confirmationCount}; flag {falsePositiveCount, confirmationCount, contaminated, contaminationReason, recommended, trimText?, sponsor?, category?}. Combined text, sponsor, and category edits apply in one approval. Categories are sponsor, cross_promo, self_promo, interaction, intro, outro, and recap."
          },
          "before": {
            "type": [
              "object",
              "null"
            ],
            "description": "Snapshot of the pattern at suggestion time, used to undo an approval.",
            "properties": {
              "textTemplate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "category": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "sponsor",
                  "cross_promo",
                  "self_promo",
                  "interaction",
                  "intro",
                  "outro",
                  "recap",
                  null
                ],
                "description": "Original category; absent in older suggestions."
              },
              "sponsor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "sponsorId": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "isActive": {
                "type": "boolean"
              },
              "introVariants": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "outroVariants": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "sourceContext": {
                "type": "string",
                "description": "Optional retained source transcript around the reviewed pattern, with its span marked by double brackets. Absent when unavailable."
              }
            }
          },
          "applied": {
            "type": [
              "object",
              "null"
            ],
            "description": "Saved approval result, including the post-approval state used to refuse undo after later edits. Older approvals may lack snapshots.",
            "properties": {
              "newPatternIds": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              },
              "disabledPatternId": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "appliedAt": {
                "type": "string",
                "format": "date-time"
              },
              "appliedSeq": {
                "type": "integer",
                "format": "int64",
                "description": "Orders approvals on one pattern for undo conflict checks."
              },
              "text": {
                "type": "string"
              },
              "sponsorId": {
                "type": "integer"
              },
              "after": {
                "type": "object",
                "description": "Post-approval values of fields owned by this change.",
                "properties": {
                  "isActive": {
                    "type": "boolean"
                  }
                }
              },
              "newPatternStates": {
                "type": "array",
                "description": "Post-approval snapshots of split children.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer"
                    },
                    "fields": {
                      "type": "object",
                      "properties": {
                        "isActive": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "createdAt": {
            "type": "string"
          },
          "reviewedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "pattern": {
            "type": "object",
            "description": "Present on the suggestions list; omitted on approve/reject/undo responses.",
            "properties": {
              "id": {
                "type": "integer"
              },
              "sponsor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "scope": {
                "type": "string"
              },
              "networkId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "podcastTitle": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isActive": {
                "type": "boolean"
              },
              "confirmationCount": {
                "type": "integer"
              },
              "falsePositiveCount": {
                "type": "integer"
              },
              "lastMatchedAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "createdAt": {
                "type": "string"
              }
            }
          }
        }
      },
      "AdDistribution": {
        "type": "object",
        "description": "A feed's historical ad-cut position distribution (issue 360)",
        "properties": {
          "slug": {
            "type": "string"
          },
          "episodesConsidered": {
            "type": "integer",
            "description": "Episodes with usable cut history that fed the distribution"
          },
          "medianDurationSeconds": {
            "type": "number",
            "description": "Median original episode duration across those episodes"
          },
          "bucketCount": {
            "type": "integer",
            "description": "Number of normalized-position histogram bins"
          },
          "buckets": {
            "type": "array",
            "description": "Cut-start counts per bin, ordered start to end of episode",
            "items": {
              "type": "integer"
            }
          },
          "totalEvents": {
            "type": "integer",
            "description": "Total cut starts counted (equals the sum of buckets)"
          },
          "zones": {
            "type": "array",
            "description": "Learned prior zones (empty below 5 episodes / 60% support)",
            "items": {
              "type": "object",
              "properties": {
                "center": {
                  "type": "number",
                  "description": "Median normalized position of the zone (0.0-1.0)"
                },
                "low": {
                  "type": "number"
                },
                "high": {
                  "type": "number"
                },
                "support": {
                  "type": "integer",
                  "description": "Distinct episodes with a cut in this zone"
                },
                "boost": {
                  "type": "number",
                  "description": "Confidence boost applied to cuts in this zone"
                }
              }
            }
          }
        }
      },
      "LocalFeedP20": {
        "type": [
          "object",
          "null"
        ],
        "description": "Podcasting 2.0 channel-level extras for a local feed. Sending null clears the six list tags (funding/person/license/location/txt/ podroll) and any stored locked_owner, but leaves guid/medium/locked alone. guid is minted once at feed creation from the feed URL and can never be set through this field, at create or PATCH.",
        "properties": {
          "funding": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "text": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "person": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "text": {
                  "type": "string",
                  "description": "Person's name"
                },
                "role": {
                  "type": "string"
                },
                "group": {
                  "type": "string"
                },
                "img": {
                  "type": "string",
                  "format": "uri"
                },
                "href": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "license": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "location": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "geo": {
                  "type": "string",
                  "description": "geo: URI, e.g. geo:41.0,-71.0"
                },
                "osm": {
                  "type": "string",
                  "description": "OpenStreetMap feature id"
                }
              }
            }
          },
          "txt": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "purpose": {
                  "type": "string"
                }
              }
            }
          },
          "podroll": {
            "type": "array",
            "description": "Other shows to recommend, rendered as a podcast:podroll container of remoteItem children. Max 50 entries.",
            "items": {
              "type": "object",
              "required": [
                "feedGuid"
              ],
              "properties": {
                "feedGuid": {
                  "type": "string",
                  "format": "uuid",
                  "description": "UUID of the recommended feed."
                },
                "feedUrl": {
                  "type": "string",
                  "format": "uri"
                },
                "itemGuid": {
                  "type": "string"
                },
                "medium": {
                  "type": "string",
                  "enum": [
                    "podcast",
                    "music",
                    "video",
                    "film",
                    "audiobook",
                    "newsletter",
                    "blog"
                  ]
                }
              }
            }
          },
          "medium": {
            "type": "string",
            "enum": [
              "podcast",
              "music",
              "video",
              "film",
              "audiobook",
              "newsletter",
              "blog"
            ],
            "default": "podcast"
          },
          "locked": {
            "type": "string",
            "enum": [
              "yes",
              "no"
            ],
            "default": "yes"
          },
          "locked_owner": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contact email for podcast:locked. Blank or null clears the stored owner."
          },
          "guid": {
            "type": "string",
            "readOnly": true,
            "description": "podcast:guid, a UUIDv5 minted from the feed URL at creation. Read-only; rejected with 400 if sent on a write."
          }
        }
      },
      "LocalEpisodeP20": {
        "type": [
          "object",
          "null"
        ],
        "description": "Podcasting 2.0 item-level extras for a local feed episode. Sending null clears both tags.",
        "properties": {
          "person": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "text"
              ],
              "properties": {
                "text": {
                  "type": "string"
                },
                "role": {
                  "type": "string"
                },
                "group": {
                  "type": "string"
                },
                "img": {
                  "type": "string",
                  "format": "uri"
                },
                "href": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "location": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                },
                "geo": {
                  "type": "string"
                },
                "osm": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Feed": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "URL-safe identifier for the feed"
          },
          "feedType": {
            "type": "string",
            "enum": [
              "subscribed",
              "local",
              "recents",
              "youtube"
            ],
            "description": "\"subscribed\" (default) mirrors an upstream RSS feed; \"local\" is an archive-import feed MinusPod builds and serves itself, with no upstream; \"recents\" is the single combined feed of every processed episode published on or after its creation, which accepts only title and description on PATCH. Several fields and actions (refresh, Podping, differential fetch, sourceUrl) are meaningless for local and recents feeds; see the Local Feeds guide."
          },
          "title": {
            "type": "string",
            "description": "Podcast title. Mirrors the upstream RSS <title> for a subscribed feed (overwritten on refresh); user-set and PATCHable for a local feed."
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "description": "Original RSS feed URL. For a local feed this is the internal local://<slug> sentinel, never a real URL; never surfaced as the feed's subscribe URL."
          },
          "feedUrl": {
            "type": "string",
            "format": "uri",
            "description": "Modified RSS feed URL served by this server"
          },
          "artworkUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL to cached artwork image"
          },
          "hasArtwork": {
            "type": "boolean",
            "description": "Whether MinusPod currently holds an artwork file for this feed, regardless of what artworkUrl falls back to when it does not (issue"
          },
          "author": {
            "type": [
              "string",
              "null"
            ],
            "description": "itunes:author. Local and YouTube feeds; always null for a subscribed feed (taken from the upstream RSS at render time, not stored per-feed)."
          },
          "explicit": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "itunes:explicit. Local feeds only; null means unset."
          },
          "categories": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "itunes:category list. Local feeds only; null when unset."
          },
          "p20": {
            "$ref": "#/components/schemas/LocalFeedP20"
          },
          "episodeCount": {
            "type": "integer",
            "description": "Total number of episodes"
          },
          "processedCount": {
            "type": "integer",
            "description": "Number of processed episodes"
          },
          "statusCounts": {
            "type": "object",
            "description": "Per-status episode counts for this feed. The database status 'processed' is exposed under its API alias 'completed', matching episode responses. 'deferred' counts episodes waiting in the offline queue for the LLM or Whisper endpoint to come back.",
            "properties": {
              "discovered": {
                "type": "integer"
              },
              "pending": {
                "type": "integer"
              },
              "processing": {
                "type": "integer"
              },
              "completed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "permanently_failed": {
                "type": "integer"
              },
              "deferred": {
                "type": "integer"
              }
            }
          },
          "lastRefreshed": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last RSS refresh timestamp"
          },
          "lastRefreshError": {
            "type": [
              "string",
              "null"
            ],
            "description": "Error from the current run of consecutive refresh failures. Only set once the run reaches the alert threshold (3 counted failures); null while the feed refreshes cleanly or during a short blip."
          },
          "lastRefreshErrorAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the current run of refresh failures started. Cleared on the next successful refresh."
          },
          "lastPodpingAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Stamped when a Podping publish notification matches this feed; null if the feed has never been matched by the Podping listener. See podpingEnabled in Settings."
          },
          "podpingCoverage": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "received",
              "declared",
              "host_active",
              "unseen",
              "declined",
              null
            ],
            "description": "Why this feed is or is not covered by Podping, most specific first. declined means the feed's own podcast:podping tag carries usesPodping=\"false\", so MinusPod polls it and ignores pings for it. received means a podping arrived for this feed and lastPodpingAt carries the time. declared means the feed's tag opts in but no ping has arrived yet. host_active means the feed host has been seen sending podpings in the last 30 days but not for this feed. unseen means none of the above. Null when the Podping listener is disabled instance-wide, which is a global setting rather than a fact about this feed. The MinusPod UI collapses everything except received into a single \"none\", so consumers wanting the detail should read this field rather than mirror the UI."
          },
          "podpingUses": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "The upstream podcast:podping usesPodping value. Null when the feed carries no such tag."
          },
          "podpingHiveAccounts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Hive accounts the feed declares as allowed to podping it, from podcast:hiveAccount. Empty when the feed names none, in which case any sender is accepted."
          },
          "podpingCheckedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the podcast:podping declaration was last read from the upstream feed body. Null means never, which makes a feed with no tag distinguishable from one whose body has not been read yet."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "autoProcessOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Override auto-process setting for this feed (null uses global setting)"
          },
          "autoProcessMaxAgeHoursOverride": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 720,
            "description": "Per-feed auto-process window in hours (null uses the global autoProcessMaxAgeHours)"
          },
          "downloadUserAgentOverride": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 512,
            "description": "Audio, artwork, chapters, and transcript User-Agent for this feed. Printable ASCII only. Null or blank inherits downloadUserAgent. Cross-fetch uses a different podcast client."
          },
          "feedUserAgentOverride": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 512,
            "description": "RSS fetch User-Agent for this feed. Printable ASCII only. Null or blank inherits feedUserAgent."
          },
          "languageOverride": {
            "type": [
              "string",
              "null"
            ],
            "description": "Per-feed transcription language ('auto' or a language code like 'en'/'de'); null uses the global whisperLanguage setting."
          },
          "titleOverride": {
            "type": [
              "string",
              "null"
            ],
            "description": "Per-feed display title shown to subscribers in the generated RSS feed (issue 375); null falls back to the source feed title."
          },
          "descriptionOverride": {
            "type": [
              "string",
              "null"
            ],
            "description": "Per-feed description shown to subscribers in the generated RSS feed; null falls back to the source feed description."
          },
          "detectionNotes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Operator hints about this show appended to the LLM ad-detection and review prompts (issue 709); null when unset."
          },
          "chapterNotes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Per-feed notes sent with every chapter generation request; null when unset."
          },
          "detectionMode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "blacklist",
              "keep_content",
              null
            ],
            "description": "Per-feed ad-detection mode. 'keep_content' (experimental) labels show content and removes everything else; null or 'blacklist' is the default ad-removal behavior. Prefer processingMode; kept for per-field compatibility."
          },
          "chaptersMode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "auto",
              "generate",
              "off",
              null
            ],
            "description": "Per-feed chapters mode. 'auto' preserves publisher-embedded chapters when enough survive the cut, otherwise generates them; 'generate' always generates new chapters; 'off' skips the chapter step entirely; null follows the global chaptersMode setting."
          },
          "differentialFetchMode": {
            "type": "string",
            "enum": [
              "inherit",
              "auto",
              "on",
              "off"
            ],
            "description": "Configured per-feed cross-fetch mode. 'inherit' follows the global default; other values select auto, on, or off."
          },
          "chaptersInNotes": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "on",
              "off",
              null
            ],
            "description": "Per-feed override for listing generated chapters in served episode descriptions. null follows the global chaptersInNotes setting."
          },
          "reviewLinksInNotes": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "on",
              "off",
              null
            ],
            "description": "Per-feed override for review and rate links in served episode descriptions. null follows the global reviewLinksInNotes setting."
          },
          "adChaptersEnabled": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated. Derived from the feed's resolved segment actions; true when any category resolves to mark."
          },
          "adChapterCategories": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            },
            "deprecated": true,
            "description": "Deprecated. Derived from the feed's resolved segment actions; true for each category that resolves to mark."
          },
          "queuePriority": {
            "type": "string",
            "enum": [
              "high",
              "normal",
              "low"
            ],
            "description": "Per-feed auto-process queue priority (issue 625), always resolved to one of the three values ('normal' covers both an explicit normal setting and no override). 'high' processes this feed's episodes before other queued episodes; 'low' runs only when nothing else is waiting."
          },
          "lowAdYieldAction": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "nothing",
              "redetect",
              "reprocess",
              "full",
              null
            ],
            "description": "Per-feed override of the global lowAdYieldAction setting; null means the global value applies."
          },
          "episodeLogs": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "on",
              "off",
              null
            ],
            "description": "Per-feed run log storage (issue 660); null follows the global setting."
          },
          "titleSkipPatterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Per-feed episode title blacklist, always an array (empty when unset). Each pattern is matched against the whole episode title, case-insensitively (fnmatch glob); use '*' as a wildcard, e.g. 'Bonus Episode *' matches any title starting with 'Bonus Episode'. Without a wildcard the whole title must match, so a plain substring needs surrounding '*' wildcards. A matching episode is never queued or JIT-processed."
          },
          "descriptionSkipPatterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Per-feed episode description blacklist (issue 835), always an array (empty when unset). Each pattern is matched against the plain-text description (HTML tags removed, whitespace collapsed), case-insensitively (fnmatch glob); use '*' as a wildcard, e.g. '*This is a preview. To hear the entire episode*'. A matching episode is never queued or JIT-processed."
          },
          "descriptionTrimPattern": {
            "type": [
              "string",
              "null"
            ],
            "description": "Chapter title glob for the description chapter trim, or null when unset."
          },
          "descriptionTrimMode": {
            "type": "string",
            "enum": [
              "to_end",
              "to_next"
            ],
            "description": "Where a description chapter cut ends. Defaults to to_end."
          },
          "minDurationSeconds": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Inclusive minimum publisher RSS duration in seconds. Null means no minimum; unknown durations remain eligible."
          },
          "maxDurationSeconds": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Inclusive maximum publisher RSS duration in seconds. Null means no maximum. Uses RSS duration before ad removal."
          },
          "titleSkipAction": {
            "type": "string",
            "enum": [
              "serve_original",
              "hide"
            ],
            "description": "Served-RSS handling for episodes skipped by title, description, or duration. Always resolves to serve_original or hide; serve_original is the default."
          },
          "processingMode": {
            "type": "string",
            "enum": [
              "passthrough",
              "skip_detection",
              "keep_content",
              "standard",
              "cue_only"
            ],
            "description": "Effective processing mode resolved from the per-feed flags, with precedence passthrough > skip_detection > keep_content > cue_only > standard. Writable. PATCHing it sets the underlying flags canonically. Cannot be combined with passthroughEnabled, skipAdDetection, or detectionMode in one request. cue_only requires at least one enabled ad_break_start and one enabled ad_break_end template on the feed."
          },
          "cueTemplateScoreOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0.3,
            "maximum": 0.99,
            "description": "Per-feed ZNCC match threshold override for audio cue template matching. null means the global audioCueTemplateScore setting applies."
          },
          "cueCreateFromPairsOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for cue-pair ad synthesis. true/false override the global setting; null inherits."
          },
          "cueCutMatchesOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for cutting matched cue audio. true/false override the global setting; null inherits."
          },
          "cuePairMinBreakOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 1,
            "maximum": 600,
            "description": "Per-feed minimum break duration (seconds) for cue-pair synthesis. null inherits the global setting."
          },
          "cuePairMaxBreakOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 1,
            "maximum": 3600,
            "description": "Per-feed maximum break duration (seconds) for cue-pair synthesis. null inherits the global setting."
          },
          "cuePairMaxBreakFractionOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Per-feed maximum break fraction of episode duration for cue-pair synthesis. null inherits the global setting."
          },
          "cueSnapConfidenceOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Per-feed minimum cue confidence to snap an ad edge. null inherits the global audio_cue_snap_confidence setting."
          },
          "cueSnapLeadOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0.5,
            "maximum": 30,
            "description": "Per-feed snap lead window (seconds) before an ad edge. null inherits the global audio_cue_snap_lead_seconds setting."
          },
          "cueSnapLagOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0.5,
            "maximum": 30,
            "description": "Per-feed snap lag window (seconds) after an ad edge. null inherits the global audio_cue_snap_lag_seconds setting."
          },
          "silenceSnapEnabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed opt-in for snapping cut boundaries to nearby silence. null and false both read as off."
          },
          "transitionSnapEnabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed opt-in for snapping cut boundaries to content-transition cues. null and false both read as off."
          },
          "spliceVetoEnabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed splice check. null inherits the global spliceVetoEnabled setting, true forces the check on, false lets long cuts through without splice evidence."
          },
          "maxAdDurationOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 1,
            "maximum": 3600,
            "description": "Per-feed max ad duration cap in seconds. Ads longer than this are held for review instead of cut. null means no cap. Also raises the longest single detection the model may report for this feed (the global Longest single LLM detection, default 7 minutes)."
          },
          "adDetectionExcludeStartOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 600,
            "description": "Per-feed opening window in seconds during which ad markers are ignored. null inherits adDetectionExcludeStartSeconds, 0 disables it for the feed, 1 to 600 sets the feed's own window."
          },
          "maxAdDurationRejectOverride": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 30,
            "maximum": 3600,
            "description": "Per-feed length past which an ad must name a recognized sponsor to be cut. Must not exceed the global maxAdDurationConfirmedSeconds. null falls back to the global maxAdDurationSeconds setting. Also raises the longest single detection the model may report for this feed (the global maxLlmWindowAdSeconds, default 7 minutes), so set it to the length of the show's longest continuous ad block."
          },
          "cueGatedApproval": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed cue-gated approval. When true, only ads with audio cue evidence auto-cut; others are held for review. null and false both read as off."
          },
          "differentialFetchEnabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Legacy per-feed cross-fetch flag. Use differentialFetchMode for new clients; null selects automatic DAI detection."
          },
          "ratingAverage": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average episode quality rating for the feed. Null when no episode is rated. Feed detail only."
          },
          "ratingCount": {
            "type": "integer",
            "description": "Number of rated episodes in the feed. Feed detail only."
          },
          "daiLikely": {
            "type": "boolean",
            "description": "Hint that this feed likely uses dynamic ad insertion, based on recent enclosure URLs matching known DAI or prefix domains. Returned on the feed detail response."
          },
          "differentialFetchEffective": {
            "type": "boolean",
            "description": "Whether the cross-fetch differential stage will run for this feed, resolved from the per-feed mode, global default, and DAI signals. Feed detail only."
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The show's website from the channel-level RSS link element. Feed detail only; null when the feed does not declare one."
          },
          "passthroughEnabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "When true, episodes are downloaded and served exactly as published, with no transcription, detection, or cutting. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility."
          },
          "skipAdDetection": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "When true, episodes are transcribed and get chapters and a transcript, but ad detection is skipped and nothing is cut. Null and false both mean normal processing. Prefer processingMode; kept for per-field compatibility."
          },
          "cueOnlySafety": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "hold_new",
              "auto_cut",
              null
            ],
            "description": "Per-feed safety policy for cue_only mode. 'hold_new' (default) holds a template's synthesized cuts for review until it has matched a paired start/end cue on 3 episodes. 'auto_cut' cuts immediately but still holds any pair scoring below 0.90 confidence. null means the default 'hold_new' behavior. Ignored outside cue_only mode."
          },
          "skipTranscription": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed opt-out from transcription. Only valid on a feed in cue_only mode. Generated chapters stop, and transcript search and VTT subtitles become unavailable for those episodes, but publisher chapters (embedded ID3 or linked Podcasting 2.0 JSON) still get remapped onto the cut audio. Null and false both mean transcription still runs."
          },
          "maxEpisodes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Per-feed cap on episodes served via the feed's RSS. Subscribed feeds: 10-500; null means use the global maxFeedEpisodes setting (default 300). Local feeds: 0 or null serves every episode (the default); a positive value up to 10000 serves only the newest N."
          },
          "onlyExposeProcessedEpisodes": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for hiding upstream episodes whose status is not 'processed' from the served RSS feed. Null means use the global onlyExposeProcessedDefault setting. true/false override the global for this feed (issue"
          },
          "requireManualApproval": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for holding newly processed episodes out of the served feed until approved. Null means use the global requireManualApprovalDefault setting."
          },
          "awaitingApprovalCount": {
            "type": "integer",
            "description": "Number of this feed's episodes waiting for manual approval."
          },
          "retentionDaysOverride": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 3650,
            "description": "Per-feed retention override. Null falls back to the global retentionDays setting; 0 archives the feed so processed audio is never deleted, including by the force-all cleanup; 1-3650 keeps it for that many days."
          },
          "audioReplacementSoundOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Override replacement sound for this feed; null inherits the global choice."
          },
          "audioMp3StreamCopyOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Override MP3 stream copy for this feed; null inherits the global choice."
          },
          "keepOriginalAudioOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for retaining the pre-cut original audio. Null falls back to the global keepOriginalAudio setting; true always keeps it; false never does. Applies from the next episode processed."
          },
          "ytProcessDelayMinutesOverride": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 1440,
            "description": "YouTube feeds only. Minutes to wait after a video is published before processing. Null follows the global setting."
          },
          "ytProbeAttemptsOverride": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 48,
            "description": "YouTube feeds only. SponsorBlock lookup attempts. Null follows the global setting."
          },
          "ytProbeIntervalMinutesOverride": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 5,
            "maximum": 1440,
            "description": "YouTube feeds only. Minutes between SponsorBlock lookup attempts. Null follows the global setting."
          },
          "ytLlmFallbackOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Run LLM detection when SponsorBlock has no data. Null follows the global setting."
          },
          "ytLlmOnTopOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Run LLM detection on top of SponsorBlock segments. Null follows the global setting."
          },
          "ytCaptionsOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Use YouTube captions as the transcript. Null follows the global setting."
          },
          "ytHidePrivateOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Hide private and deleted episodes from the listing. Null follows the global setting."
          },
          "ytTranscribeOverride": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Transcribe with the configured transcriber when captions are missing. Null follows the global setting."
          },
          "youtubeIncludeShorts": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "YouTube feeds only. Whether videos of 180 seconds or less are listed. Null follows the default."
          },
          "youtubeSourceKind": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "playlist",
              "channel",
              null
            ],
            "description": "YouTube feeds only. Kind of the source."
          },
          "youtubeSourceId": {
            "type": [
              "string",
              "null"
            ],
            "description": "YouTube feeds only. Playlist id or channel id."
          },
          "youtubeSourceUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "YouTube feeds only. Public YouTube page of the source."
          },
          "segmentCategoryActions": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "string",
              "enum": [
                "remove",
                "beep",
                "keep",
                "mark"
              ]
            },
            "description": "Per-feed segment-category action overrides (issue"
          },
          "detectShowSegments": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether this feed detects intro, outro, and recap/housekeeping segments in addition to the always-detected ad-side categories. Null means use the global detectShowSegments setting. true/false override the global for this feed."
          },
          "ownEpisodeGuids": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the served RSS uses MinusPod episode ids as item GUIDs (issue 598). Null and false both mean upstream GUIDs pass through. New feeds are created with true."
          },
          "skipSecondPass": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the pass-2 verification scan of the cut audio is skipped for this feed. Null follows the global skipSecondPass setting."
          },
          "transcriptDifferential": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Per-feed override for upstream-transcript differential. True enables and false disables it for this feed; null follows the global transcriptDifferentialEnabled setting."
          },
          "latestEpisodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeedLatestEpisode"
            },
            "description": "Present only when the request set includeLatestEpisodes=true. Up to episodesPerFeed episodes for this feed, newest first."
          }
        }
      },
      "FeedLatestEpisode": {
        "type": "object",
        "description": "Bounded per-feed episode projection returned by GET /feeds when includeLatestEpisodes=true, for rendering a grouped episodes view without a separate request per feed.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Episode id."
          },
          "title": {
            "type": "string"
          },
          "published": {
            "type": "string",
            "format": "date-time",
            "description": "publishedAt if the feed declared one, else the episode's createdAt."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "processedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "duration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Original duration in seconds."
          },
          "status": {
            "type": "string",
            "enum": [
              "discovered",
              "pending",
              "processing",
              "completed",
              "failed",
              "permanently_failed",
              "deferred"
            ]
          },
          "jobState": {
            "type": "string",
            "enum": [
              "idle",
              "queued",
              "processing"
            ],
            "description": "Authoritative queue/run state, derived from live queue rows rather than the stored status."
          },
          "hasBeenProcessed": {
            "type": "boolean",
            "description": "Same meaning as EpisodeSummary.hasBeenProcessed, so a grouped dashboard row can label its action button without a second request."
          },
          "artworkUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Same fallback rules as EpisodeSummary.artworkUrl."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Failure reason from the most recent processing attempt, present when status is failed or permanently_failed."
          },
          "pendingReviewCount": {
            "type": "integer",
            "description": "Ads held for human review on this episode, so a dashboard row can show the hold chip."
          },
          "awaitingApproval": {
            "type": "boolean",
            "description": "True while the processed episode is held out of the served feed and audio until approved."
          },
          "approvalPendingAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the episode began waiting for manual approval."
          },
          "approvedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When a person or a recut approved the episode. Null for episodes that never needed approval."
          },
          "titleSkipped": {
            "type": "boolean",
            "description": "Whether the episode title matches this feed's title-skip patterns and is ineligible for the bulk Process action."
          },
          "descriptionSkipped": {
            "type": "boolean",
            "description": "Whether the episode description matches this feed's description-skip patterns (issue 835) and is ineligible for the bulk Process action."
          },
          "durationSkipped": {
            "type": "boolean",
            "description": "Publisher RSS duration is outside the feed's inclusive limits, making the episode ineligible for the bulk Process action. Unknown or zero durations return false."
          },
          "passthroughEnabled": {
            "type": "boolean",
            "description": "Per-episode pass-through override, same meaning as EpisodeSummary.passthroughEnabled."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Episode description, truncated to 200 characters."
          }
        }
      },
      "EpisodeSelection": {
        "type": "object",
        "required": [
          "id",
          "status",
          "jobState",
          "titleSkipped",
          "descriptionSkipped",
          "durationSkipped"
        ],
        "description": "Lightweight action state returned for every matching episode when selection=true.",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "discovered",
              "pending",
              "processing",
              "completed",
              "failed",
              "permanently_failed",
              "deferred"
            ]
          },
          "jobState": {
            "type": "string",
            "enum": [
              "idle",
              "queued",
              "processing"
            ]
          },
          "titleSkipped": {
            "type": "boolean"
          },
          "descriptionSkipped": {
            "type": "boolean"
          },
          "durationSkipped": {
            "type": "boolean"
          }
        }
      },
      "EpisodeSummary": {
        "type": "object",
        "properties": {
          "feedSlug": {
            "type": "string",
            "description": "Set on rows of the recents feed; the feed the episode belongs to."
          },
          "feedTitle": {
            "type": "string",
            "description": "Set on rows of the recents feed; the source feed's title."
          },
          "episodeId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "sponsorBlock": {
            "type": [
              "object",
              "null"
            ],
            "description": "SponsorBlock lookup progress for a YouTube episode. Null on other feed types.",
            "properties": {
              "state": {
                "type": "string"
              },
              "attempts": {
                "type": "integer"
              },
              "nextCheckAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "segmentCount": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "note": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastError": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "submitAllowed": {
                "type": "boolean",
                "description": "True when the episode detail may offer manual SponsorBlock submission. Absent in list payloads."
              },
              "submissions": {
                "type": "array",
                "description": "Outcomes of segments a person submitted to SponsorBlock for this episode.",
                "items": {
                  "$ref": "#/components/schemas/SponsorBlockSubmission"
                }
              }
            }
          },
          "rating": {
            "type": [
              "object",
              "null"
            ],
            "description": "The person's quality rating for the episode. Null when unrated.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5
              },
              "notes": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ratedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "discovered",
              "pending",
              "processing",
              "completed",
              "failed",
              "permanently_failed",
              "deferred"
            ]
          },
          "jobState": {
            "type": "string",
            "enum": [
              "idle",
              "submitting",
              "queued",
              "processing"
            ],
            "description": "Authoritative queue/run state, derived from live queue rows rather than the stored status. A pending episode with no queue row reports idle, not queued. The server never returns submitting; that value is a client-only placeholder used right after a request is sent, before the server confirms queued or processing."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "processedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "originalDuration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Original duration in seconds"
          },
          "newDuration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Duration after ad removal in seconds"
          },
          "adsRemoved": {
            "type": "integer",
            "description": "Rendered cuts in the output audio. Markers merged into one cut count once."
          },
          "timeSaved": {
            "type": "number",
            "description": "Seconds saved by removing ads"
          },
          "pendingReviewCount": {
            "type": "integer",
            "description": "Number of ads held for human review (not yet approved or dismissed)"
          },
          "awaitingApproval": {
            "type": "boolean",
            "description": "True while the processed episode is held out of the served feed and audio until approved."
          },
          "approvalPendingAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the episode began waiting for manual approval."
          },
          "approvedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When a person or a recut approved the episode. Null for episodes that never needed approval."
          },
          "titleSkipped": {
            "type": "boolean",
            "description": "Whether the episode title matches this feed's title-skip patterns and is ineligible for the bulk Process action."
          },
          "descriptionSkipped": {
            "type": "boolean",
            "description": "Whether the episode description matches this feed's description-skip patterns (issue 835) and is ineligible for the bulk Process action."
          },
          "durationSkipped": {
            "type": "boolean",
            "description": "Publisher RSS duration is outside the feed's inclusive limits, making the episode ineligible for the bulk Process action. Unknown or zero durations return false."
          },
          "passthroughEnabled": {
            "type": "boolean",
            "description": "Per-episode pass-through override (issue"
          },
          "hasBeenProcessed": {
            "type": "boolean",
            "description": "True when this episode has a processed output on record, independent of its current status. A completed episode queued again reverts to status pending, so clients use this rather than status to choose between a Process and a Reprocess label."
          },
          "artworkUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Episode cover declared by the publisher. Null when the episode declares none, or when the URL is not https and would be blocked as mixed content; clients fall back to the feed artwork. A local feed has no publisher, so this instead falls back to /api/v1/feeds/{slug}/episodes/{episode_id}/artwork whenever the episode has a cached cover (an upload or embedded-artwork extraction), and stays null only when it has none."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Failure reason from the most recent processing attempt, present when status is failed or permanently_failed"
          }
        }
      },
      "EpisodeDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EpisodeSummary"
          },
          {
            "type": "object",
            "properties": {
              "originalUrl": {
                "type": "string",
                "format": "uri"
              },
              "processedUrl": {
                "type": "string",
                "format": "uri-reference",
                "description": "Relative, same-origin path to the processed audio for the in-app player, carrying the feed auth key as a query param when authenticated feeds is enabled. Not the public feed-domain enclosure URL."
              },
              "navigation": {
                "type": "object",
                "description": "Adjacent episodes in the same feed, newest-first order. previous is the newer episode, next the older; either is null at a feed boundary (issue",
                "properties": {
                  "previous": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      }
                    }
                  },
                  "next": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "adMarkers": {
                "type": "array",
                "description": "Ads that were cut and not held for review (was_cut=true, held_for_review not set)",
                "items": {
                  "$ref": "#/components/schemas/AdMarker"
                }
              },
              "rejectedAdMarkers": {
                "type": "array",
                "description": "Ads flagged but kept in audio (REJECT decision or not was_cut, not held)",
                "items": {
                  "$ref": "#/components/schemas/AdMarker"
                }
              },
              "pendingReviewMarkers": {
                "type": "array",
                "description": "Ads held for human review (held_for_review=true, not yet cut or dismissed)",
                "items": {
                  "$ref": "#/components/schemas/AdMarker"
                }
              },
              "keptMarkers": {
                "type": "array",
                "description": "Segments deliberately left in the audio by a Keep or Mark action, not held for review. actionApplied distinguishes keep from mark; rejectedAdMarkers contains detections rejected by the pipeline.",
                "items": {
                  "$ref": "#/components/schemas/AdMarker"
                }
              },
              "cueDetections": {
                "type": "array",
                "description": "Template cue detections for this episode (advisory telemetry; never changes the cut)",
                "items": {
                  "$ref": "#/components/schemas/CueDetection"
                }
              },
              "corrections": {
                "type": "array",
                "description": "Corrections recorded for this episode, newest first.",
                "items": {
                  "$ref": "#/components/schemas/EpisodeCorrection"
                }
              },
              "daiDifferential": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Cross-fetch differential result for this episode. null when the feed is not opted in or the episode predates the feature. status no_differential means no differential was found, not proof the feed has no DAI. status unreliable_reencode means alignment failed wholesale (the CDN re-encoded the whole file on the refetch), so the differential was discarded and no regions are reported.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "no_differential",
                      "unreliable_reencode",
                      "error"
                    ]
                  },
                  "regions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "start_s": {
                          "type": "number"
                        },
                        "end_s": {
                          "type": "number"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "differential",
                            "identical",
                            "unknown"
                          ]
                        },
                        "corr": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      }
                    }
                  },
                  "refetch_meta": {
                    "type": "object"
                  },
                  "error": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              "upstreamTranscript": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Diff of this episode's transcript against the publisher's podcast:transcript. null until the stage has stored a result. status none means the stage did not run (no transcript link, or the feed or global toggle is off); unreliable means the two transcripts matched too little to trust, so no spans are reported. Each span is speech the publisher transcript omits.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "none",
                      "unreliable",
                      "empty",
                      "error"
                    ]
                  },
                  "coverage": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "Fraction of transcript words matched in the publisher transcript."
                  },
                  "sourceType": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "MIME type of the publisher transcript that was compared."
                  },
                  "spans": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "start": {
                          "type": "number"
                        },
                        "end": {
                          "type": "number"
                        },
                        "offsetConfirmed": {
                          "type": "boolean",
                          "description": "The publisher transcript's timestamps jump by the span's length across it."
                        }
                      }
                    }
                  }
                }
              },
              "spliceCalibration": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The feed's splice calibration when this episode's audio was analyzed, or null when no audio analysis is stored. status host_read means most of the feed's recent long transcript-only cuts showed no edit point in the audio, so the splice check does not hold them. long_cut_corroboration counts those cuts over recent episodes.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "cold_start",
                      "calibrated",
                      "host_read"
                    ]
                  },
                  "episodes_considered": {
                    "type": "integer"
                  },
                  "long_cut_corroboration": {
                    "type": "object",
                    "properties": {
                      "episodes": {
                        "type": "integer"
                      },
                      "cuts": {
                        "type": "integer"
                      },
                      "corroborated": {
                        "type": "integer"
                      },
                      "fraction": {
                        "type": [
                          "number",
                          "null"
                        ]
                      }
                    }
                  }
                }
              },
              "transcriptAvailable": {
                "type": "boolean"
              },
              "transcriptSource": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "whisper",
                  "upstream",
                  null
                ],
                "description": "Where the transcript came from; null when there is none"
              },
              "chaptersRegenerating": {
                "type": "boolean",
                "description": "A chapter regeneration started by regenerate-chapters is still running"
              },
              "chaptersRegenError": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Why the last chapter regeneration failed; null after a successful one"
              },
              "originalTranscriptAvailable": {
                "type": "boolean",
                "description": "Whether an original (pre-cut) transcript is available"
              },
              "adsRemovedFirstPass": {
                "type": "integer",
                "description": "Number of ads detected by first pass (includes merged)"
              },
              "adsRemovedVerification": {
                "type": "integer",
                "description": "Rendered cuts in the output audio that hold a verification-pass marker"
              },
              "inputTokens": {
                "type": "integer",
                "description": "Total LLM input tokens used to process this episode"
              },
              "outputTokens": {
                "type": "integer",
                "description": "Total LLM output tokens used to process this episode"
              },
              "llmCost": {
                "type": "number",
                "description": "Estimated LLM cost in USD for processing this episode"
              },
              "rssDuration": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Duration the feed declares for this episode (itunes:duration), in seconds. Null when the feed omits it or the episode was discovered before 2.53.0."
              },
              "processingRuns": {
                "type": "array",
                "description": "One entry per processing run, oldest first.",
                "items": {
                  "$ref": "#/components/schemas/EpisodeProcessingRun"
                }
              },
              "activeRunSpend": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/EpisodeRunSpend"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Live ledger spend of the run that currently owns this episode, read straight from the ledger while the run is still in flight. Null when no run owns the episode, so a client can use its presence as the \"a run is spending right now\" signal."
              },
              "latestRunSpend": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/EpisodeRunSpend"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Spend of the latest attempted run, which is a different question from activeRunSpend: failures and runs that made no LLM call are included, and the run need not be the one that produced the served audio. Null when the episode has no processing history."
              },
              "cumulativeSpend": {
                "$ref": "#/components/schemas/EpisodeCumulativeSpend"
              },
              "lowAdYield": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Set when this episode removed far less ad time than the feed's recent average. Either the downloaded copy arrived with unfilled ad slots, or ads were missed.",
                "properties": {
                  "removedSeconds": {
                    "type": "number"
                  },
                  "feedAverageSeconds": {
                    "type": "number"
                  },
                  "sampleSize": {
                    "type": "integer"
                  }
                }
              },
              "incompleteCoverage": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Windows the latest completed run lost, per pass. Null when both passes answered every window. Set independently of partialDetection: a run that answers most windows still completes, so the skipped stretches were never examined for ads. A pass key is present only when that pass lost at least one window.\n",
                "properties": {
                  "detection": {
                    "$ref": "#/components/schemas/IncompleteCoveragePass"
                  },
                  "verification": {
                    "$ref": "#/components/schemas/IncompleteCoveragePass"
                  }
                }
              }
            }
          }
        ]
      },
      "IncompleteCoveragePass": {
        "type": "object",
        "description": "Windows one pass of the latest completed run lost, with the causes recorded.",
        "properties": {
          "failed": {
            "type": "integer"
          },
          "total": {
            "type": [
              "integer",
              "null"
            ]
          },
          "failureClasses": {
            "type": [
              "object",
              "null"
            ],
            "description": "Lost windows by cause: rate_limit, server_error, connectivity, reasoning_exhausted, output_truncated, empty_completion, other. Absent on runs recorded before the classes were tracked.\n",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "LocalEpisodeEditFields": {
        "type": "object",
        "description": "Shared editable fields for a local episode, used by both the single-episode PATCH and each entry of the bulk PATCH.",
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "season": {
            "type": "integer",
            "minimum": 1,
            "description": "Changes only the display season_number; the episode_id minted at upload/import time never changes."
          },
          "episode": {
            "type": "integer",
            "minimum": 1,
            "description": "Changes only the display episode_number; the episode_id minted at upload/import time never changes."
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601, timezone required. Sending null clears the stored publish date."
          },
          "p20": {
            "$ref": "#/components/schemas/LocalEpisodeP20"
          }
        }
      },
      "LocalImportRequest": {
        "type": "object",
        "description": "Shared body for import/scan and import/commit.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "staging",
              "directory",
              "both"
            ],
            "default": "both",
            "description": "Which source(s) to scan: the upload staging area, the user-managed import directory, or both."
          },
          "overwrite": {
            "type": "boolean",
            "default": false,
            "description": "When true, a file whose sNNeNN id matches an existing episode replaces it (full reset: new audio and metadata, prior transcripts/chapters/processing state discarded) instead of erroring as a collision."
          }
        }
      },
      "LocalImportEntry": {
        "type": "object",
        "description": "One matched file in an import plan.",
        "properties": {
          "episodeId": {
            "type": "string",
            "description": "sNNeNN id, from the filename token or a sidecar override."
          },
          "season": {
            "type": "integer"
          },
          "episode": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "audioFile": {
            "type": "string",
            "description": "Basename of the matched audio file."
          },
          "descriptionFile": {
            "type": [
              "string",
              "null"
            ]
          },
          "artworkFile": {
            "type": [
              "string",
              "null"
            ]
          },
          "sidecarFile": {
            "type": [
              "string",
              "null"
            ],
            "description": "Basename of the matched .json sidecar, if any."
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "publishedAtSource": {
            "type": "string",
            "enum": [
              "explicit",
              "synthesized"
            ],
            "description": "explicit: from a sidecar or already-set value. synthesized: filled in from episode order (see the Local Feeds guide's Publish dates section)."
          },
          "bytes": {
            "type": "integer",
            "description": "Audio file size at scan time; commit re-checks this and mtimeNs against the live file and errors the entry if either changed (TOCTOU guard)."
          },
          "mtimeNs": {
            "type": "integer"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Non-empty means this entry will not be committed (e.g. duplicate id in the batch, collision with an existing episode without overwrite, invalid sidecar, out-of-order explicit date)."
          },
          "replacesExisting": {
            "type": "boolean",
            "description": "True whenever episodeId already exists in the feed, regardless of overwrite. A collision marker, not an outcome; whether it errors or cleanly overwrites depends on the overwrite flag and whether errors is empty."
          },
          "replacesExistingId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The actual existing row's episode_id when replacesExisting is true, else null. Usually identical to episodeId, but can be the row's original wide-zero-padded id (e.g. s01e0006) for an episode imported before id canonicalization, which an overwrite replaces rather than duplicates."
          }
        }
      },
      "LocalImportPlan": {
        "type": "object",
        "description": "The dry-run result from import/scan, and the shape import/commit re-derives server-side to check against the client-echoed planHash. Entries carrying server filesystem paths are stripped from the client-facing response; only basenames are exposed.",
        "properties": {
          "slug": {
            "type": "string"
          },
          "overwrite": {
            "type": "boolean"
          },
          "source": {
            "type": "string",
            "enum": [
              "staging",
              "directory",
              "both"
            ],
            "description": "Echoes the source this plan was scanned with; commit re-derives from this same value and gates the post-commit staging sweep on it."
          },
          "planHash": {
            "type": "string",
            "description": "sha256 over the sorted (name, size, mtime) of every scanned source file, plus overwrite. Echo this back to import/commit; a mismatch there means the source directory changed since this scan, or overwrite was flipped after scanning."
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocalImportEntry"
            }
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "file": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              }
            },
            "description": "Files that did not match the naming scheme at all (or are hidden/incomplete/empty/a non-mp3 audio format), never imported."
          },
          "batchErrors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Errors that block the whole batch rather than one entry (currently just an out-of-order explicit-publish-date pair). Commit refuses the plan while this is non-empty, even though only the two offending entries carry the message in their own errors."
          },
          "totals": {
            "type": "object",
            "properties": {
              "importable": {
                "type": "integer"
              },
              "rejected": {
                "type": "integer"
              },
              "errors": {
                "type": "integer"
              },
              "bytes": {
                "type": "integer"
              }
            }
          }
        }
      },
      "LocalImportStatus": {
        "type": "object",
        "description": "Background progress/result of the most recent import/commit for a feed.",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "idle",
              "running",
              "done",
              "error"
            ],
            "description": "idle means no import has ever been started for this feed. State is file-backed and shared across worker processes, so it survives a worker restart; a worker that crashed mid-commit self-heals to error with an \"import interrupted\" report the next time this is polled."
          },
          "processed": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "report": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present once state is done or error.",
            "properties": {
              "committed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "audioFile": {
                      "type": "string"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "skipped": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "audioFile": {
                      "type": "string"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "failed": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "episodeId": {
                      "type": "string"
                    },
                    "audioFile": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              },
              "queued": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "episodeIds queued for auto-process (only possible when the feed already had episodes before this commit)."
              },
              "error": {
                "type": "string",
                "description": "Present only when state is error. Either an exception escaped the whole batch (a per-file failure lands in failed above instead), or the value \"import interrupted\", meaning a worker crashed mid-commit and this state was self-healed from a stuck running job whose lock nobody still holds."
              }
            }
          }
        }
      },
      "EpisodeRunSpend": {
        "type": "object",
        "description": "LLM spend attributed to one processing run. Used for both activeRunSpend (in-flight, read live from the ledger) and latestRunSpend (the latest attempted run).",
        "required": [
          "runId",
          "inputTokens",
          "outputTokens",
          "costUsd",
          "breakdownAvailable",
          "hasUnknownCost"
        ],
        "properties": {
          "runId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ledger run identifier. Null on a run recorded before the usage ledger existed and on recuts, which have no ledger rows; those runs report only their stored processing-history total."
          },
          "inputTokens": {
            "type": "integer"
          },
          "outputTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": "string",
            "description": "Decimal string, not a number, so no precision is lost in JSON. Sums only the calls whose cost could be resolved, so it understates the true figure whenever hasUnknownCost is true."
          },
          "breakdownAvailable": {
            "type": "boolean",
            "description": "True when this run has ledger rows, which is also when processingRuns[].phases carries its per-phase breakdown. False for a pre-ledger run or a recut, whose figures come from the stored processing-history total instead."
          },
          "hasUnknownCost": {
            "type": "boolean",
            "description": "True when at least one billable call in this run has no resolved cost, usually an unpriced or custom model. Clients should label costUsd as a partial figure rather than a total."
          }
        },
        "examples": [
          {
            "summary": "Fully priced run",
            "value": {
              "runId": "9f2c1b7a4d6e",
              "inputTokens": 184320,
              "outputTokens": 9210,
              "costUsd": "0.412300",
              "breakdownAvailable": true,
              "hasUnknownCost": false
            }
          },
          {
            "summary": "Partial, some calls have no resolved price",
            "value": {
              "runId": "3a7d0e55c118",
              "inputTokens": 210500,
              "outputTokens": 11040,
              "costUsd": "0.087500",
              "breakdownAvailable": true,
              "hasUnknownCost": true
            }
          },
          {
            "summary": "Legacy run recorded before the ledger",
            "value": {
              "runId": null,
              "inputTokens": 120000,
              "outputTokens": 6400,
              "costUsd": "0.245000",
              "breakdownAvailable": false,
              "hasUnknownCost": false
            }
          }
        ]
      },
      "EpisodeCumulativeSpend": {
        "type": "object",
        "description": "Lifetime LLM spend for this episode, summed over every processing attempt rather than one run. Always present; an episode with no ledger rows reports zeros.",
        "required": [
          "inputTokens",
          "outputTokens",
          "costUsd",
          "hasUnknownCost"
        ],
        "properties": {
          "inputTokens": {
            "type": "integer"
          },
          "outputTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": "string",
            "description": "Decimal string. Sums only calls with a resolved cost."
          },
          "hasUnknownCost": {
            "type": "boolean",
            "description": "True when any contributing call has no resolved cost, so costUsd is a floor rather than the real total."
          }
        },
        "example": {
          "inputTokens": 512000,
          "outputTokens": 24600,
          "costUsd": "1.104200",
          "hasUnknownCost": false
        }
      },
      "EpisodeRunPhaseUsage": {
        "type": "object",
        "description": "One pipeline phase's ledger usage inside a run, grouped by phase, invoking pass, provider and configured model. A phase that retried onto a different model contributes more than one entry.",
        "properties": {
          "phaseKey": {
            "type": "string",
            "enum": [
              "detection",
              "verification",
              "review",
              "chapters",
              "probe"
            ]
          },
          "invokingPass": {
            "type": [
              "integer",
              "null"
            ],
            "description": "1 or 2 for a pass-scoped call, null otherwise."
          },
          "provider": {
            "type": "string",
            "description": "Provider the call routed to. Endpoint URLs are never stored here."
          },
          "configuredModel": {
            "type": "string"
          },
          "returnedModel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Model id the provider reported serving, when it differs from the configured one."
          },
          "inputTokens": {
            "type": "integer",
            "description": "Total input tokens, including Anthropic cache reads and writes."
          },
          "outputTokens": {
            "type": "integer"
          },
          "cacheReadTokens": {
            "type": "integer",
            "description": "Input tokens read from a provider prompt cache. Included in Anthropic inputTokens."
          },
          "cacheWriteTokens": {
            "type": "integer",
            "description": "Input tokens written to a provider prompt cache. Included in Anthropic inputTokens."
          },
          "reasoningTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": [
              "string",
              "null"
            ],
            "description": "Decimal string, or null when any call in the group has no resolved cost. Null is the signal to render this phase as unpriced rather than as zero."
          },
          "costSource": {
            "type": "string",
            "enum": [
              "provider_reported",
              "estimated",
              "explicit_zero",
              "mixed",
              "unknown"
            ],
            "description": "How the cost was arrived at. mixed means the grouped calls did not agree on one source; unknown pairs with a null costUsd."
          }
        }
      },
      "EpisodeCorrection": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "correction_type": {
            "type": "string",
            "enum": [
              "false_positive",
              "boundary_adjustment",
              "confirm",
              "promotion",
              "auto_promotion",
              "create"
            ]
          },
          "original_bounds": {
            "type": [
              "object",
              "null"
            ],
            "description": "Bounds of the marker the correction applies to; null for create.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              }
            }
          },
          "corrected_bounds": {
            "type": [
              "object",
              "null"
            ],
            "description": "User-set bounds for create, boundary_adjustment and trimmed confirms.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "origin": {
            "type": "string",
            "enum": [
              "user",
              "auto_pass2"
            ],
            "description": "user for a decision made in the UI or API; auto_pass2 for a confirm the verification pass filed when it re-detected a held ad."
          }
        }
      },
      "ProcessingAdmission": {
        "type": "object",
        "properties": {
          "paused": {
            "type": "boolean"
          },
          "activeRuns": {
            "type": "integer",
            "description": "Runs currently holding a processing slot"
          },
          "queuedEpisodes": {
            "type": "integer",
            "description": "Episodes waiting in the auto-process queue"
          }
        }
      },
      "EpisodeRating": {
        "type": "object",
        "properties": {
          "value": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "ratedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SponsorBlockSubmission": {
        "type": "object",
        "description": "Recorded outcome of one segment sent to SponsorBlock.",
        "properties": {
          "start": {
            "type": "number"
          },
          "end": {
            "type": "number"
          },
          "category": {
            "type": "string",
            "enum": [
              "sponsor",
              "selfpromo"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "duplicate",
              "rejected",
              "rate_limited",
              "error"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "uuid": {
            "type": [
              "string",
              "null"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "SponsorBlockSubmitResult": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SponsorBlockSubmission"
            }
          }
        }
      },
      "EpisodeProcessingRun": {
        "type": "object",
        "description": "One processing run of an episode, with its pipeline stats.",
        "properties": {
          "runNumber": {
            "type": "integer"
          },
          "runId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ledger run identifier. Null for a pre-ledger run or a recut."
          },
          "processedAt": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "completed",
              "failed"
            ]
          },
          "adsDetected": {
            "type": "integer"
          },
          "processingDurationSeconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Total run wall-clock seconds; null when no duration was recorded."
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "inputTokens": {
            "type": "integer"
          },
          "outputTokens": {
            "type": "integer"
          },
          "cacheReadTokens": {
            "type": "integer",
            "description": "Run total from the ledger; 0 for a run without ledger rows."
          },
          "cacheWriteTokens": {
            "type": "integer",
            "description": "Run total from the ledger; 0 for a run without ledger rows."
          },
          "llmCost": {
            "type": "number"
          },
          "hasLog": {
            "type": "boolean",
            "description": "True when this run stored a pipeline log the run-log endpoint can serve."
          },
          "breakdownAvailable": {
            "type": "boolean",
            "description": "True when this run has usage-ledger rows, which is when phases is non-empty."
          },
          "phases": {
            "type": "array",
            "description": "Per-phase provider, model, token and cost breakdown from the usage ledger. Empty for a pre-ledger run or a recut, whose figures come from the stored run total instead.",
            "items": {
              "$ref": "#/components/schemas/EpisodeRunPhaseUsage"
            }
          },
          "stats": {
            "type": [
              "object",
              "null"
            ],
            "description": "Per-run pipeline stats. Null for runs recorded before 2.53.0 and for recuts; a failed run keeps whatever was gathered before the failure.",
            "properties": {
              "mode": {
                "type": "string",
                "description": "auto, reprocess, full, or llm for processing runs; chapters for chapter-only regeneration"
              },
              "detectionSkipped": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True when the run skipped ad detection because the feed has skipAdDetection set"
              },
              "chaptersDegraded": {
                "type": "boolean",
                "description": "Chapter generation used a fallback; absent on runs without this diagnostic."
              },
              "chaptersDegradedReason": {
                "type": "string",
                "description": "Reason chapter generation used a fallback."
              },
              "verificationSkipped": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True when the verification pass was skipped by the per-feed setting or by cue-only mode. Absent when detection was skipped outright, which already implies no verification."
              },
              "cueOnly": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True when the run cut only from cue and learned-pattern evidence with no LLM call."
              },
              "transcriptionSkipped": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True when the run skipped transcription; no transcript, generated chapters, or subtitles."
              },
              "normalizationSkipped": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True when the run did not apply the optional loudness normalization pass."
              },
              "downloadedDuration": {
                "type": "number",
                "description": "ffprobe duration of the downloaded copy, in seconds"
              },
              "transcriptSegments": {
                "type": "integer"
              },
              "windows": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Null when the run failed before detection or the detection mode does not report window counts",
                "properties": {
                  "total": {
                    "type": "integer"
                  },
                  "failed": {
                    "type": "integer"
                  },
                  "failureClasses": {
                    "type": "object",
                    "description": "Lost windows by cause: rate_limit, server_error, connectivity, reasoning_exhausted, output_truncated, empty_completion, other. Absent when no window was lost.\n",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  }
                }
              },
              "verificationWindows": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Verification-pass window counts. Null when the verification pass was skipped or ended before its detection call.",
                "properties": {
                  "total": {
                    "type": "integer"
                  },
                  "failed": {
                    "type": "integer"
                  },
                  "failureClasses": {
                    "type": "object",
                    "description": "Lost verification windows by cause; same classes as windows.failureClasses. Absent when no window was lost.\n",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  }
                }
              },
              "stageHits": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Detections per stage before validation. Null when the run failed before detection.",
                "properties": {
                  "fingerprint": {
                    "type": "integer"
                  },
                  "textPattern": {
                    "type": "integer"
                  },
                  "differential": {
                    "type": "integer"
                  },
                  "transcriptDifferential": {
                    "type": "integer"
                  },
                  "llm": {
                    "type": "integer"
                  }
                }
              },
              "detected": {
                "type": "integer",
                "description": "First-pass detections after dedup"
              },
              "markers": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Final marker buckets after validation and review. Null when the run failed before validation.",
                "properties": {
                  "cut": {
                    "type": "integer"
                  },
                  "held": {
                    "type": "integer"
                  },
                  "notCut": {
                    "type": "integer"
                  }
                }
              },
              "verificationAdsCut": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Rendered cuts holding an ad the second scan of the output audio found. 0 means the scan ran and came back clean. Absent or null means the scan did not run, either because it was skipped (see verificationSkipped and detectionSkipped) or because it failed."
              },
              "secondsRemoved": {
                "type": "number",
                "description": "Net seconds removed, original duration minus output duration. Replacement beeps count against it. When the output duration cannot be measured this is estimated as sourceSecondsRemoved minus replacementSecondsAdded."
              },
              "sourceSecondsRemoved": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Seconds of source audio inside the rendered cuts. Absent on runs recorded before this field existed."
              },
              "replacementSecondsAdded": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Seconds of replacement audio (beeps) inserted in place of the rendered cuts. secondsRemoved is about sourceSecondsRemoved minus this value."
              },
              "timings": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Wall-clock seconds by stage. Concurrent stages and FFmpeg can overlap, so fields do not sum to processingDurationSeconds. ffmpegSeconds includes every FFmpeg task and retry. finalizeSeconds stops before history persistence and notifications. Null means no measurement was recorded for the object or field.",
                "properties": {
                  "downloadSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "transcriptionSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "transcriptDiffSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "differentialSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "audioAnalysisSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "detectionSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "refineValidateSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "cutSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "verificationSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "normalizationSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "assetsSeconds": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "chaptersSeconds": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "Chapter generation. Included in assetsSeconds."
                  },
                  "finalizeSeconds": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "Finalization work before history persistence and notifications."
                  },
                  "ffmpegSeconds": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "Wall-clock seconds across FFmpeg tasks and retries; zero means none were observed."
                  }
                }
              },
              "transcription": {
                "type": "object",
                "description": "Local transcription outcome for this run. The whole block is absent when the run did not transcribe locally, so a run on the remote API backend or one that skipped transcription has no entry at all.",
                "properties": {
                  "outcome": {
                    "type": "string",
                    "enum": [
                      "success",
                      "failed"
                    ]
                  },
                  "batchSize": {
                    "type": "integer",
                    "description": "Batch size the run finished on, lower than the configured tier after a GPU-memory downshift."
                  },
                  "retryCount": {
                    "type": "integer",
                    "description": "Retries the run consumed, each one halving the batch size after an out-of-memory error."
                  },
                  "retrySucceeded": {
                    "type": "boolean",
                    "description": "True when the run retried and then completed. Always false on a failed outcome."
                  },
                  "device": {
                    "type": "string",
                    "enum": [
                      "cpu",
                      "cuda"
                    ]
                  },
                  "gpuDeviceName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Reported GPU name. Null whenever device is cpu."
                  },
                  "model": {
                    "type": "string",
                    "description": "Local model the run loaded."
                  },
                  "error": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Failure reason, truncated to 500 characters. Null on a successful outcome."
                  }
                },
                "example": {
                  "outcome": "success",
                  "batchSize": 4,
                  "retryCount": 2,
                  "retrySucceeded": true,
                  "device": "cuda",
                  "gpuDeviceName": "NVIDIA GPU",
                  "model": "large-v3",
                  "error": null
                }
              },
              "transcriptDiff": {
                "type": "object",
                "description": "Upstream transcript diff outcome for this run. Absent on runs that did not reach the stage.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "none",
                      "unreliable",
                      "empty",
                      "error"
                    ]
                  },
                  "coverage": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "spans": {
                    "type": "integer",
                    "description": "Number of spans the publisher transcript omits."
                  }
                }
              },
              "thinkingNotices": {
                "type": "array",
                "description": "Reasoning and thinking compatibility notices recorded by this run. Present only when a provider rejected a requested setting and the client retried with pass defaults. Provider error text is never stored.",
                "items": {
                  "$ref": "#/components/schemas/ThinkingCompatibilityNotice"
                }
              },
              "failover": {
                "type": "object",
                "description": "Present only when this run actually sent requests to a failover configuration. llm lists the original targets whose calls ran on the failover LLM provider; whisper is true when transcription ran on the failover transcriber.",
                "required": [
                  "llm",
                  "whisper"
                ],
                "properties": {
                  "llm": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "llm-a",
                        "llm-b"
                      ]
                    }
                  },
                  "whisper": {
                    "type": "boolean"
                  }
                },
                "example": {
                  "llm": [
                    "llm-a"
                  ],
                  "whisper": false
                }
              }
            }
          }
        }
      },
      "ThinkingCompatibilityNotice": {
        "type": "object",
        "required": [
          "pass",
          "provider",
          "model",
          "requested",
          "compatibility",
          "fallback"
        ],
        "properties": {
          "pass": {
            "type": "string",
            "enum": [
              "ad_detection_pass_1",
              "reviewer_pass_1",
              "ad_detection_pass_2",
              "reviewer_pass_2",
              "chapter_generation"
            ],
            "description": "Pipeline pass that made the rejected request."
          },
          "provider": {
            "type": "string",
            "enum": [
              "anthropic",
              "openrouter",
              "openai-compatible",
              "ollama",
              "unknown"
            ],
            "description": "Configured provider identifier. Endpoint URLs are not stored."
          },
          "model": {
            "type": "string",
            "description": "Model identifier, or redacted when the value was not safe to retain."
          },
          "requested": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string",
                "enum": [
                  "none",
                  "low",
                  "medium",
                  "high"
                ]
              }
            ],
            "description": "Requested reasoning effort or Anthropic thinking token budget."
          },
          "compatibility": {
            "type": "string",
            "enum": [
              "required",
              "unsupported",
              "incompatible"
            ],
            "description": "What the provider response proved. incompatible means the response identified the setting but did not reliably say whether thinking is required or unsupported."
          },
          "fallback": {
            "type": "object",
            "required": [
              "maxTokens",
              "temperature",
              "reasoningEffort"
            ],
            "description": "Pass defaults used for the retry.",
            "properties": {
              "maxTokens": {
                "type": "integer"
              },
              "temperature": {
                "type": "number"
              },
              "reasoningEffort": {
                "type": [
                  "integer",
                  "string",
                  "null"
                ],
                "description": "Null means the retry omitted the reasoning setting."
              }
            }
          }
        }
      },
      "CueDetection": {
        "type": "object",
        "description": "One template cue the matcher surfaced for an episode (advisory telemetry).",
        "properties": {
          "id": {
            "type": "integer"
          },
          "template_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "cue_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "ad_break_boundary",
              "ad_break_start",
              "ad_break_end",
              "show_intro",
              "show_outro",
              "content_transition",
              null
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "description": "Always 'template'; spectral cues are not recorded"
          },
          "start_s": {
            "type": "number"
          },
          "end_s": {
            "type": "number"
          },
          "match_score": {
            "type": [
              "number",
              "null"
            ]
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ]
          },
          "outcome": {
            "type": "string",
            "enum": [
              "snap",
              "pair",
              "none",
              "below_threshold"
            ],
            "description": "How detection used the cue. 'below_threshold' is an advisory near-miss the matcher scored just under the feed threshold; it was never a signal and never affected a cut.\n"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "rejected"
            ]
          },
          "edge_distance_s": {
            "type": [
              "number",
              "null"
            ],
            "description": "Signed distance (edge - cue) from an above-threshold cue to the nearest pre-snap LLM ad edge on its eligible side. NULL for advisory (non_ad) cues and for below_threshold rows.\n"
          },
          "unused_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "covered",
              "out_of_reach",
              "below_snap_confidence",
              "advisory_role",
              "unpaired",
              "below_pair_confidence",
              "covered_by_existing_ad",
              "no_partner_in_band",
              "phase_mismatch",
              null
            ],
            "description": "For an outcome='none' cue, why it did nothing. NULL for snap / pair / below_threshold outcomes.\n"
          }
        }
      },
      "CueAdvisory": {
        "type": "object",
        "description": "Aggregate cue telemetry for a feed (or globally). Totals, score range, and confirmRate count above-threshold cues only; below_threshold near-misses are reported separately (see the aggregate endpoint's nearMissTotal / nearMissHistogram).\n",
        "properties": {
          "total": {
            "type": "integer"
          },
          "snapped": {
            "type": "integer"
          },
          "paired": {
            "type": "integer"
          },
          "unused": {
            "type": "integer"
          },
          "confirmed": {
            "type": "integer"
          },
          "rejected": {
            "type": "integer"
          },
          "pending": {
            "type": "integer"
          },
          "avgScore": {
            "type": [
              "number",
              "null"
            ]
          },
          "minScore": {
            "type": [
              "number",
              "null"
            ]
          },
          "maxScore": {
            "type": [
              "number",
              "null"
            ]
          },
          "confirmRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "confirmed / (confirmed + rejected), or null if none reviewed"
          }
        }
      },
      "AdMarker": {
        "type": "object",
        "properties": {
          "start": {
            "type": "number",
            "description": "Start time in seconds"
          },
          "end": {
            "type": "number",
            "description": "End time in seconds"
          },
          "confidence": {
            "type": "number",
            "description": "Detection confidence (0.0 to 1.0)",
            "example": 0.95
          },
          "reason": {
            "type": "string",
            "description": "Reason for marking as ad"
          },
          "sponsor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sponsor or advertiser name extracted from the ad"
          },
          "detection_stage": {
            "type": "string",
            "enum": [
              "first_pass",
              "claude",
              "fingerprint",
              "text_pattern",
              "language",
              "audio_enforced",
              "verification",
              "dai_differential",
              "transcript_differential",
              "vad_gap",
              "cue_pair",
              "heuristic_preroll",
              "heuristic_postroll",
              "keep_content",
              "manual",
              "description_chapter"
            ],
            "description": "Which detection stage found this ad",
            "example": "first_pass"
          },
          "corroborated_by": {
            "type": "string",
            "enum": [
              "transition_pair",
              "template_cue",
              "volume_anomaly",
              "splice_evidence",
              "dai_differential",
              "transcript_differential"
            ],
            "description": "Independent evidence the validator found for this ad. transcript_differential means the publisher's transcript omits most of the span. Absent when none was found."
          },
          "category": {
            "type": "string",
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap"
            ],
            "description": "What kind of content this span is. LLM detection stages emit it directly; fingerprint, text-pattern, and differential stages default to sponsor. An unknown or missing value falls back to sponsor at marker-save time.",
            "example": "sponsor"
          },
          "actionApplied": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "remove",
              "beep",
              "keep",
              "mark",
              null
            ],
            "description": "The action resolved for this marker's category (per-feed override, else global default, else remove). Stored Keep/Mark values reflect current switches between Keep and Mark without recutting; changes involving remove or beep require a recut. Null before action resolution.",
            "example": "remove"
          },
          "approved": {
            "type": "boolean",
            "description": "Set on a held-for-review marker once its confirm correction is recorded; the approval is applied by the next recut."
          },
          "held_for_review": {
            "type": "boolean",
            "description": "True when this marker remains in the audio pending a human decision."
          },
          "carved_from": {
            "type": "object",
            "description": "Set on a fragment of a marker the render removed only in part; names the detected span.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              }
            }
          },
          "silent_absorbed_spans": {
            "type": "array",
            "description": "Measured silence cut with this accepted ad; excluded from its duration checks.",
            "items": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "number"
                },
                "end": {
                  "type": "number"
                }
              }
            }
          },
          "hold_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "max_duration",
              "no_cue_evidence",
              "uncorroborated_tail",
              "reviewer_contradiction",
              "reviewer_boundary_conflict",
              "reviewer_inconclusive_bounds",
              "estimated_pattern_bounds",
              "reviewer_reject_conflict",
              "reviewer_failed",
              "no_splice_evidence",
              "verification_miss",
              "verification_kept_conflict",
              "differential_uncorroborated",
              "large_vad_gap_extension",
              "cue_template_unproven",
              "cue_low_confidence",
              "no_transcript_evidence",
              "transcript_differential_unreviewed",
              null
            ],
            "description": "Why the marker requires review. reviewer_boundary_conflict means a reviewer proposal crossed protected merged-ad evidence; reviewer_reject_conflict means the reviewer rejected a span backed by measured evidence or a confirmed sponsor; reviewer_failed means the review call failed and no independent evidence backs the bounds."
          },
          "reviewer_reasoning": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reviewer explanation when the reviewer evaluated this marker."
          },
          "reviewer_verdict": {
            "type": "string",
            "enum": [
              "confirmed",
              "adjust",
              "reject",
              "resurrect",
              "inconclusive",
              "failure"
            ],
            "description": "Ad reviewer verdict on this marker; absent when the reviewer did not review it."
          },
          "reviewer_locked_start": {
            "type": "number",
            "description": "Start the reviewer validated; later steps may move the start inward but not before it."
          },
          "reviewer_locked_end": {
            "type": "number",
            "description": "End the reviewer validated; later steps may move the end inward but not past it."
          },
          "hold_id": {
            "type": "string",
            "description": "Identity a held marker gets when first held. Copies, carved fragments and recuts keep it, and pass-2 auto-approval confirms record it so releases of one hold stay grouped."
          },
          "pass2_corroborated": {
            "type": "boolean",
            "description": "Set on a held marker when pass 2 independently detected an ad inside it."
          },
          "pass2_corroborated_span": {
            "type": "object",
            "description": "Part of the hold pass 2 attested as ad; the auto-approval confirm is trimmed to it.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              }
            }
          },
          "pass2_reviewed_release": {
            "type": "object",
            "description": "First pass-2 span inside the hold that review released.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              }
            }
          },
          "pass2_released_spans": {
            "type": "array",
            "description": "Every pass-2 span inside the hold that review released, in start order. Filed as separate confirms when there are several; a single release files the hold's own confirm span; spans overlapping a standing reviewer reject are skipped.",
            "items": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "number"
                },
                "end": {
                  "type": "number"
                }
              }
            }
          },
          "pass2_outcome": {
            "type": "string",
            "description": "Set on a marker pass 2 ended with: cut, kept, or held:<hold_reason>. Spans pass 2 discarded are only logged and counted in the stored run stats, which the API does not return.",
            "example": "cut"
          },
          "pass2_hold_review": {
            "type": [
              "object",
              "null"
            ],
            "description": "Set on a held marker when review of a pass-2 span inside it did not release the hold. The hold stays pending; cleared when a later review releases it. Diagnostic only, separate from reviewer_reasoning.",
            "properties": {
              "span": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "Reviewed pass-2 span [start, end] in original-audio seconds."
              },
              "verdict": {
                "type": "string",
                "enum": [
                  "confirmed",
                  "adjust",
                  "reject",
                  "resurrect",
                  "failure",
                  "inconclusive"
                ]
              },
              "reason": {
                "type": "string",
                "description": "Reviewer reasoning, or the abstain reason for an inconclusive review."
              }
            }
          },
          "reviewer_proposed_start": {
            "type": [
              "number",
              "null"
            ],
            "description": "Informational reviewer-proposed start on a held marker. It is not applied until a human confirms it."
          },
          "reviewer_proposed_end": {
            "type": [
              "number",
              "null"
            ],
            "description": "Informational reviewer-proposed end on a held marker. It is not applied until a human confirms it."
          },
          "addressing_mode": {
            "type": "string",
            "enum": [
              "timestamps",
              "segment_ids"
            ],
            "description": "Addressing mode the LLM detection window was rendered under. Absent on markers from before 2.100.12 and on markers that did not come from an LLM window."
          },
          "llm_start": {
            "type": "number",
            "description": "Start in seconds exactly as the model reported it, before boundary snapping and review. Set with addressing_mode."
          },
          "llm_end": {
            "type": "number",
            "description": "End in seconds exactly as the model reported it, before boundary snapping and review. Set with addressing_mode."
          },
          "validation": {
            "type": "object",
            "description": "Validation results from post-detection validation layer",
            "properties": {
              "decision": {
                "type": "string",
                "enum": [
                  "ACCEPT",
                  "REVIEW",
                  "REJECT"
                ],
                "description": "Validation decision - ACCEPT/REVIEW are removed, REJECT stays in audio"
              },
              "adjusted_confidence": {
                "type": "number",
                "description": "Confidence after position/content adjustments (0.0 to 1.0)"
              },
              "original_confidence": {
                "type": "number",
                "description": "Original confidence from AI detection"
              },
              "flags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Validation warnings or errors"
              },
              "corrections": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Auto-corrections applied (merged ads, clamped boundaries)"
              },
              "audio_corroboration": {
                "type": "string",
                "enum": [
                  "transition_pair",
                  "template_cue",
                  "volume_anomaly",
                  "splice_evidence",
                  "dai_differential",
                  "none"
                ],
                "description": "Audio evidence found at the edges of a long cut detected from the transcript, or none. Set only on those cuts, in the first pass."
              }
            }
          }
        }
      },
      "ReviewDetection": {
        "type": "object",
        "properties": {
          "feedSlug": {
            "type": "string"
          },
          "feedTitle": {
            "type": "string"
          },
          "episodeId": {
            "type": "string"
          },
          "episodeTitle": {
            "type": "string"
          },
          "publishDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "hasOriginalAudio": {
            "type": "boolean"
          },
          "processedUrl": {
            "type": "string",
            "description": "Same-origin processed-audio URL (carries the feed auth key when enabled)"
          },
          "start": {
            "type": "number"
          },
          "end": {
            "type": "number"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ]
          },
          "sponsor": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "patternId": {
            "type": [
              "integer",
              "null"
            ]
          },
          "detectionStage": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Segment category, or null when no stage classified the marker",
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap",
              null
            ]
          },
          "actionApplied": {
            "type": [
              "string",
              "null"
            ],
            "description": "Action resolved for this marker. Stored Keep/Mark values reflect current switches between Keep and Mark without recutting; changes involving remove or beep require a recut.",
            "enum": [
              "remove",
              "beep",
              "keep",
              "mark",
              null
            ]
          },
          "reviewerVerdict": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ad reviewer verdict on this marker, or null when the reviewer did not run. inconclusive means the reviewer abstained: the marker keeps its bounds when measured evidence supports them and is otherwise held as reviewer_inconclusive_bounds. adjust alone does not mean the span moved: a held contradiction keeps the pass-1 bounds and a split piece drops the originals, and neither records reviewerOriginalStart/End.",
            "enum": [
              "confirmed",
              "adjust",
              "reject",
              "resurrect",
              "inconclusive",
              "failure",
              null
            ]
          },
          "reviewerOriginalStart": {
            "type": [
              "number",
              "null"
            ],
            "description": "Start of the span before the reviewer moved it; present only when the reviewer moved the span"
          },
          "reviewerOriginalEnd": {
            "type": [
              "number",
              "null"
            ],
            "description": "End of the span before the reviewer moved it; present only when the reviewer moved the span"
          },
          "reviewerMoved": {
            "type": "boolean",
            "description": "Whether the span was moved from its pass-1 bounds, by the reviewer or by a human approving a trimmed boundary. Use this instead of inferring a move from reviewerVerdict/reviewerOriginalStart/End."
          },
          "holdReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the detection is held for review; null unless status is pending",
            "enum": [
              "max_duration",
              "no_cue_evidence",
              "uncorroborated_tail",
              "reviewer_contradiction",
              "reviewer_boundary_conflict",
              "reviewer_inconclusive_bounds",
              "estimated_pattern_bounds",
              "reviewer_reject_conflict",
              "reviewer_failed",
              "no_splice_evidence",
              "verification_miss",
              "verification_kept_conflict",
              "differential_uncorroborated",
              "large_vad_gap_extension",
              "cue_template_unproven",
              "cue_low_confidence",
              "no_transcript_evidence",
              "transcript_differential_unreviewed",
              null
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "accepted",
              "rejected",
              "pending"
            ]
          },
          "resolution": {
            "type": "string",
            "enum": [
              "unresolved",
              "confirmed",
              "dismissed"
            ]
          }
        }
      },
      "Model": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Model identifier",
            "example": "claude-sonnet-4-5-20250929"
          },
          "name": {
            "type": "string",
            "description": "Human-readable model name",
            "example": "Claude Opus 4.5"
          },
          "created": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "inputCostPerMtok": {
            "type": [
              "number",
              "null"
            ],
            "description": "Input cost per million tokens (USD), present when pricing is known"
          },
          "outputCostPerMtok": {
            "type": [
              "number",
              "null"
            ],
            "description": "Output cost per million tokens (USD), present when pricing is known"
          },
          "cacheReadCostPerMtok": {
            "type": [
              "number",
              "null"
            ],
            "description": "Cached-input cost per million tokens (USD), present when pricing is known and a cache rate exists"
          },
          "pricingSource": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pricing source, including operator when a configured override applies"
          }
        }
      },
      "ModelPricingOverride": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "inputCostPerMtok",
          "outputCostPerMtok"
        ],
        "properties": {
          "inputCostPerMtok": {
            "type": "number",
            "minimum": 0,
            "description": "Input price in USD per 1 million tokens"
          },
          "outputCostPerMtok": {
            "type": "number",
            "minimum": 0,
            "description": "Output price in USD per 1 million tokens"
          },
          "cacheReadCostPerMtok": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Cached-input price in USD per 1 million tokens. Optional."
          }
        }
      },
      "WhisperModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Model identifier (tiny, base, small, medium, large-v3)",
            "example": "small"
          },
          "name": {
            "type": "string",
            "description": "Human-readable model name",
            "example": "Small (Default)"
          },
          "vram": {
            "type": "string",
            "description": "Estimated GPU VRAM requirement",
            "example": "~2GB"
          },
          "speed": {
            "type": "string",
            "description": "Approximate transcription speed",
            "example": "~2-3 min/60min"
          },
          "quality": {
            "type": "string",
            "description": "Relative quality compared to other models",
            "example": "Better"
          }
        }
      },
      "ProviderStatus": {
        "type": "object",
        "properties": {
          "configured": {
            "type": "boolean"
          },
          "source": {
            "type": "string",
            "enum": [
              "db",
              "env",
              "none"
            ]
          },
          "baseUrl": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "accountChanged": {
            "type": "boolean",
            "description": "Present and true only when this save moved the slot's account identity (provider type or endpoint), which strands every run whose routes were frozen against the old account"
          },
          "affectedRuns": {
            "$ref": "#/components/schemas/AffectedRuns"
          }
        }
      },
      "AffectedRuns": {
        "type": "object",
        "description": "Active runs whose frozen routes name a provider account. Returned by the preflight, and by a save that moved the account identity.",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of affected runs"
          },
          "action": {
            "type": "string",
            "enum": [
              "requeue",
              "cancel"
            ],
            "description": "What the save did with them (absent from the preflight). requeue drops their frozen routes so a resumed run re-resolves against the new configuration; cancel also asks each owner to stop now."
          },
          "runs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "runId": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "episodeId": {
                  "type": "string"
                },
                "state": {
                  "type": "string",
                  "enum": [
                    "running",
                    "cancel_requested"
                  ]
                },
                "phases": {
                  "type": "array",
                  "description": "Pipeline phases of this run that name the account",
                  "items": {
                    "type": "string",
                    "enum": [
                      "detection",
                      "review",
                      "verification",
                      "chapters"
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "Settings": {
        "type": "object",
        "properties": {
          "systemPrompt": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationPrompt": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "enableAdReview": {
            "type": "object",
            "description": "Opt-in LLM ad reviewer. Default false.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewModel": {
            "type": "object",
            "description": "Model used for the reviewer pass. Sentinel `same_as_pass` reuses\nthe corresponding pass model (claude_model for pass 1,\nverification_model for pass 2). Otherwise a concrete model id.\n",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewProvider": {
            "type": "object",
            "description": "Routing slot used for the reviewer pass: `primary`, `secondary`,\nor the sentinel `same_as_pass` (the default), which inherits\nboth the provider and model of the pass being reviewed and\nignores reviewModel. `primary`/`secondary` enable reviewModel.\n",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewMaxBoundaryShift": {
            "type": "object",
            "description": "Cap on boundary shift in seconds for adjust verdicts.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewPrompt": {
            "type": "object",
            "description": "System prompt for the confirm/adjust/reject reviewer.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "resurrectPrompt": {
            "type": "object",
            "description": "System prompt for the resurrect/reject reviewer over rejected ads.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chapterPrompt": {
            "type": "object",
            "description": "Prompt template for chapter topic detection with placeholders {num_splits}, {segment_start}, {segment_end}, {continuation_block}, {description_block}, {hints_block}, {transcript}.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupPrompt": {
            "type": "object",
            "description": "System prompt for the pattern cleanup review of learned patterns.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "systemPromptOverride": {
            "type": "object",
            "description": "Optional per-pass override appended to the first-pass prompt at run\ntime (empty by default, leaving the default prompt unchanged). If the\nprompt contains an {override} placeholder it is substituted there.\n",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationPromptOverride": {
            "type": "object",
            "description": "Optional per-pass override appended to the verification prompt (empty by default).",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewPromptOverride": {
            "type": "object",
            "description": "Optional per-pass override appended to the reviewer prompt (empty by default).",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "resurrectPromptOverride": {
            "type": "object",
            "description": "Optional per-pass override appended to the resurrect prompt (empty by default).",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chapterPromptOverride": {
            "type": "object",
            "description": "Optional override appended to the chapter topic-detection prompt (empty by default).",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "claudeModel": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationModel": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "detectionProvider": {
            "type": "object",
            "description": "Routing slot for first-pass ad detection: `primary` or\n`secondary`. Empty when unset, which resolves to primary.\n",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationProvider": {
            "type": "object",
            "description": "Routing slot for the verification pass: `primary`, `secondary`,\nor `same_as_detection`. Empty when unset, which resolves to\nsame_as_detection.\n",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chaptersModel": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Model for chapter title generation and topic detection"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chaptersProvider": {
            "type": "object",
            "description": "Routing slot for chapter title generation: `primary`,\n`secondary`, or `same_as_detection`. Empty when unset, which\nresolves to same_as_detection.\n",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupEnabled": {
            "type": "object",
            "description": "Whether the scheduled pattern cleanup review runs (Experiments).",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupCron": {
            "type": "object",
            "description": "Five-field cron expression (UTC) for the pattern cleanup schedule. Default `0 4 * * 0`.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupBatchSize": {
            "type": "object",
            "description": "Learned patterns reviewed per cleanup run, 1 to 200. Default 25.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupUnusedDays": {
            "type": "object",
            "description": "Days without a match before cleanup suggests retiring a pattern, 7 to 3650. Default 90.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupProvider": {
            "type": "object",
            "description": "Routing slot for pattern cleanup, one of `primary`, `secondary`, or `same_as_detection`. Null when unset, which inherits detectionProvider.",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "patternCleanupModel": {
            "type": "object",
            "description": "Model for pattern cleanup. Null when unset, which inherits the detection model.",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperModel": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "faster-whisper model for transcription; ignored by the parakeet and openai-api backends"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperBackend": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Whisper backend to use (local, openai-api, parakeet; parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperApiBaseUrl": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Base URL for OpenAI-compatible remote transcription API endpoint"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperApiKeyConfigured": {
            "type": "boolean",
            "description": "Whether a remote transcription API key is configured (read-only)"
          },
          "whisperApiModel": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Model name to use with the remote transcription API"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperLanguage": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "ISO 639-1 code (e.g. `en`, `fi`) or `auto` for backend auto-detect"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperApiTimeoutSeconds": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "description": "Seconds to wait for each remote transcription request."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "transcribeMaxChunkSeconds": {
            "type": "object",
            "description": "Max duration, in seconds, of each transcription chunk.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "transcribeConcurrentChunks": {
            "type": "object",
            "description": "Number of transcription chunks sent to the transcription backend concurrently.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "transcribeChunkOverlapSeconds": {
            "type": "object",
            "description": "Overlap, in seconds, between consecutive transcription chunks.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperPoolEnabled": {
            "type": "object",
            "description": "Bound concurrent requests to the remote transcription API to avoid rate limits.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperPoolMaxRequests": {
            "type": "object",
            "description": "Maximum concurrent in-flight requests to the remote transcription API when the pool is enabled.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperPoolMaxEpisodes": {
            "type": "object",
            "description": "Maximum episodes transcribing concurrently against the remote transcription API when the pool is enabled.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperComputeType": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "auto",
                  "float16",
                  "int8_float16",
                  "int8",
                  "float32"
                ],
                "description": "Compute precision for the local faster-whisper backend only. `auto` resolves to float16\non CUDA and int8 on CPU; older GPUs automatically fall back through the chain\nint8_float16 -> int8 -> float32 when float16 init fails.\n"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueDetectionEnabled": {
            "type": "object",
            "description": "Master toggle for audio cue detection (templates + spectral fallback). Off by default.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueFreqMinHz": {
            "type": "object",
            "description": "Low edge (Hz) of the spectral fallback's cue band.",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueFreqMaxHz": {
            "type": "object",
            "description": "High edge (Hz) of the spectral fallback's cue band.",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueProminenceDb": {
            "type": "object",
            "description": "dB above the in-band baseline a burst must rise to count as a cue (spectral fallback).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueMinConfidence": {
            "type": "object",
            "description": "Drop cues weaker than this. The model never sees a cue below 0.80.",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueTemplateScore": {
            "type": "object",
            "description": "ZNCC score (0-0.99, default 0.75) a learned cue template must reach to register on another episode.",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueFormantAttenDb": {
            "type": "object",
            "description": "Speech-formant-band attenuation in dB for voiceover-robust cue matching (#350). 0 = off (default).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueSnapConfidence": {
            "type": "object",
            "description": "Min cue confidence before a cue may move an ad edge (default 0.80).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueSnapLeadSeconds": {
            "type": "object",
            "description": "How far back from the ad edge a cue may sit to be eligible for snapping, seconds (default 10.0).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueSnapLagSeconds": {
            "type": "object",
            "description": "How far past the ad edge a cue may sit to be eligible for snapping, seconds (default 4.0).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCaptureMinSeconds": {
            "type": "object",
            "description": "Shortest cue a user may bracket, seconds (default 0.2).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCaptureMaxSeconds": {
            "type": "object",
            "description": "Longest cue a user may bracket, seconds (default 4).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCaptureMaxIntroSeconds": {
            "type": "object",
            "description": "Longest show-intro stinger a user may bracket, seconds (default 60).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCaptureMaxOutroSeconds": {
            "type": "object",
            "description": "Longest show-outro stinger a user may bracket, seconds (default 60).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCuePairConfidence": {
            "type": "object",
            "description": "Min cue confidence to synthesize an ad from a cue pair (default 0.85).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCuePairMinBreakSeconds": {
            "type": "object",
            "description": "Shortest cue-pair break, seconds (default 30).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCuePairMaxBreakSeconds": {
            "type": "object",
            "description": "Longest cue-pair break, seconds (default 480).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCuePairMaxBreakFraction": {
            "type": "object",
            "description": "Reject a cue pair spanning more than this fraction of the episode (short-episode backstop; default 0.5, 0 disables).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCreateFromPairs": {
            "type": "object",
            "description": "Opt-in. Synthesize a cue-only ad when two high-confidence cues bracket a break the LLM missed (still reviewed). Off by default.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioCueCutMatches": {
            "type": "object",
            "description": "Opt-in. Cut every matched cue on its own; the feed's category action for the cue type decides the outcome. Off by default.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "silenceSnapNoiseDb": {
            "type": "object",
            "description": "Amplitude (dBFS) below which audio counts as silence for silence snap (default -50).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "silenceSnapMinDurationSeconds": {
            "type": "object",
            "description": "Shortest sub-threshold span that counts as a silence for silence snap, seconds (default 0.3).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "silenceSnapMaxDistanceSeconds": {
            "type": "object",
            "description": "Farthest an ad edge may move to reach a detected silence, seconds (default 2.0).",
            "properties": {
              "value": {
                "type": "number"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationMissHoldMinConfidence": {
            "type": "object",
            "description": "Confidence floor for a standalone pass-2 verification miss to hold for review instead of being dropped (default 0.60).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "verificationMissAutocutMinConfidence": {
            "type": "object",
            "description": "Confidence floor for a standalone verification miss to cut automatically instead of holding for review. 0 disables autocut (default).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "learningMinConfidence": {
            "type": "object",
            "description": "Minimum confidence for a detection to teach the pattern matcher a new sponsor, for ads up to 90 seconds (default 0.85).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "learningMinConfidenceLong": {
            "type": "object",
            "description": "Same floor as learningMinConfidence for ads longer than 90 seconds (default 0.92).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "learningMinPatternDuration": {
            "type": "object",
            "description": "Shortest span in seconds that can teach the pattern matcher (default 15).",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "learningMaxPatternDuration": {
            "type": "object",
            "description": "Longest span in seconds learned as one pattern; longer spans are split (default 120).",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "daiDifferentialOverridesKeep": {
            "type": "object",
            "description": "A marker overlapping a measured differential region is cut even when segmentCategoryActions maps its category to keep (default true).",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "spliceVetoEnabled": {
            "type": "object",
            "description": "Hold long transcript-only cuts that show no edit point in the audio. A feed's own spliceVetoEnabled overrides this (default true).",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "differentialMeasuredCorrMax": {
            "type": "object",
            "description": "A cross-fetch region becomes a differential candidate only when its measured correlation is at or below this value (default 0.60).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "differentialHoldMinSeconds": {
            "type": "object",
            "description": "Shortest uncorroborated differential candidate that may hold for review; shorter candidates are dropped. 0 disables the floor (default 10).",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "positionalPriorEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Toggles the learned positional prior experiment (off by default)."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "vadGapDetectionEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Toggles the VAD gap detector."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "vadGapStartMinSeconds": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "vadGapMidMinSeconds": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "vadGapTailMinSeconds": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioReplacementSoundEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioMp3StreamCopyEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioBitrate": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "64k",
                  "96k",
                  "128k",
                  "192k",
                  "256k"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioEncoderCompressionLevel": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "default",
                  "0",
                  "1",
                  "2",
                  "3",
                  "4",
                  "5",
                  "6",
                  "7",
                  "8",
                  "9"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "skipFlacCompression": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adDetectionParallelWindows": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1,
                "maximum": 32
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adReviewerParallelAds": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1,
                "maximum": 32
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "maxArtworkBytes": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 65536,
                "maximum": 52428800
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "maxRssBytes": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1048576
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "maxAudioDownloadMb": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "localUploadMaxMb": {
            "type": "object",
            "description": "Maximum size in MB of one local-feed upload request (1 to 4096, default 1024).",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1,
                "maximum": 4096
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "youtubeEnabled": {
            "type": "object",
            "description": "Whether YouTube feeds are enabled. Written through PUT /settings/youtube. Default false.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "youtubeApiKeyConfigured": {
            "type": "boolean",
            "description": "True when a YouTube Data API key is stored. The key itself is never returned."
          },
          "autoProcessEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether auto-processing of new episodes is enabled"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "autoProcessMaxAgeHours": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 1,
                "maximum": 720,
                "description": "Only episodes published within this many hours are auto-queued. Default 48."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "maxFeedEpisodes": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 10,
                "maximum": 500,
                "description": "Global default cap on episodes returned in served RSS feeds (per-feed maxEpisodes overrides when set)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "onlyExposeProcessedDefault": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Global default for hiding non-processed episodes from served feeds (per-feed onlyExposeProcessedEpisodes overrides when set)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "feedItunesBlock": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether every served feed carries itunes:block Yes (default true)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whatsNewOnUpgrade": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Show the What's new dialog once per browser after the app version changes, or on a browser's first load"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "requireManualApprovalDefault": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Global default for holding newly processed episodes out of served feeds until approved (per-feed requireManualApproval overrides when set)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "detectShowSegments": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Global default for detecting intro, outro, and recap/housekeeping segments (per-feed detectShowSegments overrides when set)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "seedSponsorsDetection": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether the detection pass prompt is rendered with the seed sponsor list. Default true."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "seedSponsorsVerification": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether the verification pass prompt is rendered with the seed sponsor list. Default true."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "seedSponsorsReviewer": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether the reviewer pass prompt is rendered with the seed sponsor list. Default true."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "seedSponsorsResurrect": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether the resurrect pass prompt is rendered with the seed sponsor list. Default true."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "textRecurrenceHints": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Hints the detection prompt with transcript spans whose wording recurs near-verbatim across prior episodes of the same show. Off by default."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adAddressingMode": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "timestamps",
                  "segment_ids",
                  "random"
                ],
                "description": "How the LLM addresses detected segments. 'timestamps' (default), 'segment_ids', or 'random' (draws one of the two per run)."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "processNewEpisodesFirst": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether episodes published within the last 48 hours get an automatic queue-priority boost ahead of the rest of the auto-process backlog. On by default."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "rssRefreshIntervalMinutes": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 5,
                "maximum": 1440,
                "description": "Minutes between background RSS refresh passes for every feed. Default 15."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "queueManualBoost": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added when a user plays an unprocessed episode or reprocesses one by hand. Default 20."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "queueFreshBoost": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added to episodes published within the last 48 hours while processNewEpisodesFirst is on. Default 5."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "queueBulkBoost": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "description": "Priority added to bulk work (Reprocess All, segment re-renders). Default 0 keeps backlog runs behind everything else. Default 0."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "podpingEnabled": {
            "type": "object",
            "description": "Opt-in Podping listener. Default false.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "segmentCategoryActions": {
            "type": "object",
            "description": "Global segment-category action map (issue",
            "properties": {
              "value": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "enum": [
                    "remove",
                    "beep",
                    "keep",
                    "mark"
                  ]
                }
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "communitySyncCategories": {
            "type": "object",
            "description": "Segment categories accepted from community pattern sync. Default is\nevery category.\n",
            "properties": {
              "value": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "sponsor",
                    "cross_promo",
                    "self_promo",
                    "interaction",
                    "intro",
                    "outro",
                    "recap"
                  ]
                }
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "jitBlockedUserAgents": {
            "type": "object",
            "description": "User-agent patterns barred from triggering just-in-time episode processing. Case-insensitive substring match; a leading caret anchors to the start of the agent.",
            "properties": {
              "value": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "artworkWatermarkEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "When true, overlays a MinusPod badge on the served feed's channel cover art (issue"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "artworkBadgePosition": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "bottom-right",
                  "bottom-left",
                  "top-right",
                  "top-left"
                ],
                "description": "Corner the MinusPod badge is composited into (issue"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "lowAdYieldAction": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "nothing",
                  "redetect",
                  "reprocess",
                  "full"
                ],
                "description": "Automatic response when a pipeline-initiated run removes far less ad time than the feed's recent average: 'nothing' (default), 'redetect' (LLM-only rerun from the stored transcript), 'reprocess', or 'full'. Fires at most once per episode."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "episodeLogRetentionDays": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "description": "Days to keep per-run pipeline logs (issue 660); 0 turns storage off."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "episodeLogLevel": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "debug",
                  "info"
                ],
                "description": "Minimum level kept in a run log (issue 660)."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "downloadUserAgent": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "User-Agent sent when fetching audio, artwork, and chapters."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "feedUserAgent": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "User-Agent sent when fetching RSS feeds."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "logDownloadQuery": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether download logs include URL query strings."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "feedAuthEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether the global feed auth key is required on all public feed and episode URLs. Off by default."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "downloadHistoryEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether episode downloads are recorded."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "downloadGeoLookupEnabled": {
            "type": "object",
            "properties": {
              "value": {
                "type": "boolean",
                "description": "Whether public client IPs are sent to ipwho.is for a location. Off by default."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "downloadHistoryRetentionDays": {
            "type": "object",
            "properties": {
              "value": {
                "type": "integer",
                "description": "Days download history rows are kept."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "feedAuthKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "Global feed auth key (64 hex chars) carried as ?key= on feed/episode URLs and embedded in the badged cover path token; null until first enable."
          },
          "opmlModifiedUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Copyable public URL for importing modified (ad-free) feeds by URL; null when feed auth is disabled."
          },
          "opmlOriginalUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Copyable public URL for importing original (upstream) feeds by URL; null when feed auth is disabled."
          },
          "minCutConfidence": {
            "type": "object",
            "properties": {
              "value": {
                "type": "number",
                "format": "float",
                "description": "Minimum confidence threshold to cut an ad from audio (0.50-0.95)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "vttTranscriptsEnabled": {
            "type": "object",
            "description": "Generate a VTT transcript sidecar for each processed episode.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chaptersEnabled": {
            "type": "object",
            "description": "Generate podcast:chapters JSON for each processed episode.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chaptersMode": {
            "type": "object",
            "description": "Default chapter mode for feeds that inherit the global setting.",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "auto",
                  "generate",
                  "off"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "skipSecondPass": {
            "type": "object",
            "description": "Skip the pass-2 verification scan by default for feeds that inherit this setting.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "transcriptDifferentialEnabled": {
            "type": "object",
            "description": "Default for the upstream-transcript differential for feeds that inherit this setting.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "differentialFetchMode": {
            "type": "object",
            "description": "Default cross-fetch mode for feeds that inherit the global setting.",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "auto",
                  "on",
                  "off"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "chaptersInNotes": {
            "type": "object",
            "description": "Append the generated chapter list to served episode descriptions and the episode API description.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "reviewLinksInNotes": {
            "type": "object",
            "description": "Append review and rate links to the served description of processed episodes. Feeds can override per feed.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChaptersEnabled": {
            "type": "object",
            "deprecated": true,
            "description": "Deprecated. Derived from segmentCategoryActions; true when any category resolves to mark.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChapterCategories": {
            "type": "object",
            "deprecated": true,
            "description": "Deprecated. Derived from segmentCategoryActions; true for each category that resolves to mark.",
            "properties": {
              "value": {
                "type": "object",
                "additionalProperties": {
                  "type": "boolean"
                }
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChaptersIncludeHeld": {
            "type": "object",
            "description": "Also publish held-for-review segments as ad chapters.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChapterTitleFormat": {
            "type": "object",
            "description": "Title template for an ad chapter; {label} is the category name and {category} its id, both optional.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChapterHeldTitleFormat": {
            "type": "object",
            "description": "Title template for a held segment's ad chapter; {label} is the category name and {category} its id, both optional.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChapterResumeTitle": {
            "type": "object",
            "description": "Title of the chapter that marks the return to show content after an ad chapter.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "adChapterMinConfidence": {
            "type": "object",
            "description": "Minimum marker confidence required before a segment becomes an ad chapter.",
            "properties": {
              "value": {
                "type": "number",
                "format": "float"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioNormalizeEnabled": {
            "type": "object",
            "description": "Loudness-normalize processed audio output.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "audioNormalizeIntensity": {
            "type": "object",
            "description": "Loudness normalization intensity, applied when audioNormalizeEnabled is true.",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "gentle",
                  "normal",
                  "aggressive",
                  "extreme",
                  "maximum"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "llmProvider": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Active LLM provider (anthropic, openrouter, openai-compatible, ollama, typesafe, systemone-compatible)"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "omitTemperature": {
            "type": "object",
            "description": "Operator override: when true, temperature is omitted from every LLM call for\nevery model, regardless of the built-in list of models known to reject it or\nanything learned at runtime from a prior rejection. Off by default.\n",
            "properties": {
              "value": {
                "type": "boolean"
              }
            }
          },
          "llmJsonSchemaEnabled": {
            "type": "object",
            "description": "Opt-in structured output (#693): json_schema response format for\nOpenAI-compatible endpoints, per model that passes the probe. Off by default.\n",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "typesafeApiKeyConfigured": {
            "type": "boolean",
            "readOnly": true,
            "description": "Effective primary stored or environment key exists; no key material is returned."
          },
          "systemoneApiKeyConfigured": {
            "type": "boolean",
            "readOnly": true,
            "description": "Effective primary compatible stored or environment key exists; no key material is returned."
          },
          "systemoneBaseUrl": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "systemOneTunables": {
            "$ref": "#/components/schemas/SystemOneProfiles"
          },
          "systemOneTunablesIsDefault": {
            "$ref": "#/components/schemas/SystemOneProfileDefaults"
          },
          "systemOneTunableDefaults": {
            "$ref": "#/components/schemas/SystemOneProfiles"
          },
          "openaiBaseUrl": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Base URL for OpenAI-compatible API endpoint"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "pricingSourceMode": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string",
                "description": "Pricing source mode (auto, litellm, free)",
                "enum": [
                  "auto",
                  "litellm",
                  "free"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "modelPricingOverrides": {
            "type": "object",
            "properties": {
              "value": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/ModelPricingOverride"
                }
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "apiKeyConfigured": {
            "type": "boolean",
            "description": "Whether an API key is configured for the current provider (read-only)"
          },
          "openrouterApiKeyConfigured": {
            "type": "boolean",
            "description": "Whether an OpenRouter API key is configured (read-only)"
          },
          "secondaryProviderEnabled": {
            "type": "object",
            "description": "Optional second full provider config. Per-stage provider\noverrides (detectionProvider, verificationProvider,\nchaptersProvider, reviewProvider) pick a routing slot, primary or\nsecondary, rather than a provider type; a stage routed to\nsecondary while this is false falls back to primary. Off by\ndefault.\n",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProvider": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Secondary provider type (anthropic, openrouter, openai-compatible, ollama), or null when unset"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProviderBaseUrl": {
            "type": "object",
            "description": "Base URL for the secondary provider, when its type uses a configurable endpoint (openai-compatible, ollama).",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProviderApiKeyConfigured": {
            "type": "boolean",
            "description": "Whether a secondary provider API key is configured (read-only; the key itself is never returned)"
          },
          "providerRequestsPerMin": {
            "type": "object",
            "description": "Manual requests-per-minute cap for the primary provider account. 0 means unlimited.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerRequestsPerDay": {
            "type": "object",
            "description": "Manual requests-per-day cap for the primary provider account. 0 means unlimited.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProviderRequestsPerMin": {
            "type": "object",
            "description": "Manual requests-per-minute cap for the secondary provider account. 0 means unlimited.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProviderRequestsPerDay": {
            "type": "object",
            "description": "Manual requests-per-day cap for the secondary provider account. 0 means unlimited.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerTokensPerMin": {
            "type": "object",
            "description": "Manual tokens-per-minute cap for the primary provider account. 0 means unlimited. Reservation-based, not recorded-usage-based: each dispatch reserves an estimate (prompt tokens plus max_tokens) before the request goes out and reconciles to actual usage when the call finishes, so a single large request cannot cross the cap unseen. The cost is that an over-estimated call holds more of the window than it spends until it completes.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "secondaryProviderTokensPerMin": {
            "type": "object",
            "description": "Manual tokens-per-minute cap for the secondary provider account. 0 means unlimited. Reservation-based in the same way as providerTokensPerMin: an estimate is reserved at dispatch and reconciled to actual usage on completion.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBEnabled": {
            "type": "object",
            "description": "Alias of secondaryProviderEnabled.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerB": {
            "type": "object",
            "description": "Alias of secondaryProvider.",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBBaseUrl": {
            "type": "object",
            "description": "Alias of secondaryProviderBaseUrl.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBApiKeyConfigured": {
            "type": "boolean",
            "description": "Alias of secondaryProviderApiKeyConfigured (read-only)."
          },
          "providerBRequestsPerMin": {
            "type": "object",
            "description": "Alias of secondaryProviderRequestsPerMin.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBRequestsPerDay": {
            "type": "object",
            "description": "Alias of secondaryProviderRequestsPerDay.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBTokensPerMin": {
            "type": "object",
            "description": "Alias of secondaryProviderTokensPerMin.",
            "properties": {
              "value": {
                "type": "integer",
                "minimum": 0
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerATimeoutSeconds": {
            "type": "object",
            "description": "Request timeout for Provider A. Null value means the provider-type default. Native providers accept positive finite fractional seconds; chat providers require integer seconds from 10 to 3600.",
            "properties": {
              "value": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerAMaxRetries": {
            "type": "object",
            "description": "Max retries for Provider A. Null value means the provider-type default.",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBTimeoutSeconds": {
            "type": "object",
            "description": "Request timeout for Provider B. Null value means the provider-type default. Native providers accept positive finite fractional seconds; chat providers require integer seconds from 10 to 3600.",
            "properties": {
              "value": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "providerBMaxRetries": {
            "type": "object",
            "description": "Max retries for Provider B. Null value means the provider-type default.",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmEnabled": {
            "type": "object",
            "description": "Whether the shared LLM failover provider (#806) is on.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmProvider": {
            "type": "object",
            "description": "LLM failover provider type, or null when unset.",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmBaseUrl": {
            "type": "object",
            "description": "Base URL for the LLM failover provider.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmApiKeyConfigured": {
            "type": "boolean",
            "description": "Whether an LLM failover API key is configured (read-only; the key itself is never returned)"
          },
          "failoverLlmTimeoutSeconds": {
            "type": "object",
            "description": "Request timeout for the LLM failover provider. Null value means the provider-type default.",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmMaxRetries": {
            "type": "object",
            "description": "Max retries for the LLM failover provider. Null value means the provider-type default.",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmDetectionModel": {
            "type": "object",
            "description": "Model the LLM failover provider uses for ad detection.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmReviewModel": {
            "type": "object",
            "description": "Model the LLM failover provider uses for review. Blank inherits the detection model.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmVerificationModel": {
            "type": "object",
            "description": "Model the LLM failover provider uses for verification. Blank inherits the detection model.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverLlmChaptersModel": {
            "type": "object",
            "description": "Model the LLM failover provider uses for chapters. Blank inherits the detection model.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperEnabled": {
            "type": "object",
            "description": "Whether the transcriber failover provider (#806) is on.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperBackend": {
            "type": "object",
            "description": "Transcriber failover backend.",
            "properties": {
              "value": {
                "type": "string",
                "enum": [
                  "local",
                  "openai-api",
                  "parakeet"
                ],
                "description": "Backend value; parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process."
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperModel": {
            "type": "object",
            "description": "Local model name for the transcriber failover, when its backend is local.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperApiBaseUrl": {
            "type": "object",
            "description": "Base URL for the transcriber failover API backend.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperApiKeyConfigured": {
            "type": "boolean",
            "description": "Whether a transcriber failover API key is configured (read-only; the key itself is never returned)"
          },
          "failoverWhisperApiModel": {
            "type": "object",
            "description": "Model name for the transcriber failover API backend.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperApiTimeoutSeconds": {
            "type": "object",
            "description": "Request timeout for the transcriber failover API backend.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperMaxAttempts": {
            "type": "object",
            "description": "Failover transcriber upload attempts; null inherits whisperMaxAttempts.",
            "properties": {
              "value": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 1,
                "maximum": 10
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverWhisperLanguage": {
            "type": "object",
            "description": "Language for the transcriber failover. Empty means same as the active transcriber.",
            "properties": {
              "value": {
                "type": "string"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverProbeIntervalMinutes": {
            "type": "object",
            "description": "Minutes between health probes of a failover target.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "failoverRecoveryProbes": {
            "type": "object",
            "description": "Consecutive healthy probes needed before an automatic failover recovers.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "whisperMaxAttempts": {
            "type": "object",
            "description": "Max attempts for a single whisper API transcription request before giving up.",
            "properties": {
              "value": {
                "type": "integer"
              },
              "isDefault": {
                "type": "boolean"
              }
            }
          },
          "podcastSearchReady": {
            "type": "boolean",
            "description": "Whether the resolved podcast search provider (podcastSearchProvider) can run right now: iTunes always is; PodcastIndex needs both API credentials saved (read-only)."
          },
          "openrouterBaseUrl": {
            "type": "string",
            "description": "OpenRouter API base URL (read-only)"
          },
          "retentionDays": {
            "type": "integer",
            "description": "Days to retain processed audio files. 0 = disabled."
          },
          "stageTunables": {
            "type": "object",
            "description": "Per-stage LLM tunables (temperature, max_tokens, reasoning), chapter\ndensity, detection window geometry and the global Ollama context\nwindow, keyed by the same names as the PUT\n/settings/ad-detection body. Each field carries the resolved\nvalue (env > DB > default), an isDefault flag, and the name of\nany environment variable that is overriding the stored value.\n",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "value": {
                  "description": "Resolved value (env > DB > default). Type depends on field; nullable for reasoning budgets/levels and ollamaNumCtx."
                },
                "isDefault": {
                  "type": "boolean"
                },
                "envOverride": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Environment variable currently overriding this setting (read-only in the UI), or null."
                }
              }
            }
          },
          "stageTunableDefaults": {
            "type": "object",
            "description": "Built-in defaults for each stage tunable, used as fallback when a provider rejects a user value.",
            "additionalProperties": true
          },
          "defaults": {
            "type": "object",
            "properties": {
              "systemPrompt": {
                "type": "string"
              },
              "verificationPrompt": {
                "type": "string"
              },
              "chapterPrompt": {
                "type": "string"
              },
              "patternCleanupPrompt": {
                "type": "string"
              },
              "systemPromptOverride": {
                "type": "string",
                "description": "Per-pass override for the first pass; empty clears it."
              },
              "verificationPromptOverride": {
                "type": "string",
                "description": "Per-pass override for the verification pass; empty clears it."
              },
              "reviewPromptOverride": {
                "type": "string",
                "description": "Per-pass override for the reviewer pass; empty clears it."
              },
              "resurrectPromptOverride": {
                "type": "string",
                "description": "Per-pass override for the resurrect pass; empty clears it."
              },
              "claudeModel": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "verificationModel": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "chaptersModel": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "whisperModel": {
                "type": "string"
              },
              "whisperBackend": {
                "type": "string"
              },
              "whisperApiBaseUrl": {
                "type": "string"
              },
              "whisperApiModel": {
                "type": "string"
              },
              "whisperLanguage": {
                "type": "string"
              },
              "whisperComputeType": {
                "type": "string",
                "enum": [
                  "auto",
                  "float16",
                  "int8_float16",
                  "int8",
                  "float32"
                ]
              },
              "positionalPriorEnabled": {
                "type": "boolean"
              },
              "spliceVetoEnabled": {
                "type": "boolean"
              },
              "vadGapDetectionEnabled": {
                "type": "boolean"
              },
              "vadGapStartMinSeconds": {
                "type": "number",
                "format": "float"
              },
              "vadGapMidMinSeconds": {
                "type": "number",
                "format": "float"
              },
              "vadGapTailMinSeconds": {
                "type": "number",
                "format": "float"
              },
              "audioReplacementSoundEnabled": {
                "type": "boolean",
                "default": true
              },
              "audioMp3StreamCopyEnabled": {
                "type": "boolean",
                "default": false
              },
              "audioBitrate": {
                "type": "string",
                "enum": [
                  "64k",
                  "96k",
                  "128k",
                  "192k",
                  "256k"
                ]
              },
              "audioEncoderCompressionLevel": {
                "type": "string",
                "enum": [
                  "default",
                  "0",
                  "1",
                  "2",
                  "3",
                  "4",
                  "5",
                  "6",
                  "7",
                  "8",
                  "9"
                ]
              },
              "skipFlacCompression": {
                "type": "boolean"
              },
              "adDetectionParallelWindows": {
                "type": "integer",
                "minimum": 1,
                "maximum": 32
              },
              "adReviewerParallelAds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 32
              },
              "maxArtworkBytes": {
                "type": "integer"
              },
              "maxRssBytes": {
                "type": "integer"
              },
              "maxAudioDownloadMb": {
                "type": "integer"
              },
              "localUploadMaxMb": {
                "type": "integer"
              },
              "providerRequestsPerMin": {
                "type": "integer"
              },
              "providerRequestsPerDay": {
                "type": "integer"
              },
              "secondaryProviderRequestsPerMin": {
                "type": "integer"
              },
              "secondaryProviderRequestsPerDay": {
                "type": "integer"
              },
              "providerTokensPerMin": {
                "type": "integer"
              },
              "secondaryProviderTokensPerMin": {
                "type": "integer"
              },
              "providerATimeoutSeconds": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "providerAMaxRetries": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "providerBTimeoutSeconds": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "providerBMaxRetries": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "failoverLlmEnabled": {
                "type": "boolean"
              },
              "failoverLlmProvider": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "failoverLlmBaseUrl": {
                "type": "string"
              },
              "failoverLlmTimeoutSeconds": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "failoverLlmMaxRetries": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "failoverLlmDetectionModel": {
                "type": "string"
              },
              "failoverLlmReviewModel": {
                "type": "string"
              },
              "failoverLlmVerificationModel": {
                "type": "string"
              },
              "failoverLlmChaptersModel": {
                "type": "string"
              },
              "failoverWhisperEnabled": {
                "type": "boolean"
              },
              "failoverWhisperBackend": {
                "type": "string",
                "enum": [
                  "local",
                  "openai-api",
                  "parakeet"
                ],
                "description": "Failover transcriber backend; parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process."
              },
              "failoverWhisperModel": {
                "type": "string"
              },
              "failoverWhisperApiBaseUrl": {
                "type": "string"
              },
              "failoverWhisperApiModel": {
                "type": "string"
              },
              "failoverWhisperApiTimeoutSeconds": {
                "type": "integer"
              },
              "failoverWhisperMaxAttempts": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "failoverWhisperLanguage": {
                "type": "string"
              },
              "failoverProbeIntervalMinutes": {
                "type": "integer",
                "example": 5
              },
              "failoverRecoveryProbes": {
                "type": "integer",
                "example": 3
              },
              "whisperMaxAttempts": {
                "type": "integer",
                "example": 2
              },
              "autoProcessEnabled": {
                "type": "boolean"
              },
              "autoProcessMaxAgeHours": {
                "type": "integer"
              },
              "maxFeedEpisodes": {
                "type": "integer"
              },
              "onlyExposeProcessedDefault": {
                "type": "boolean"
              },
              "feedItunesBlock": {
                "type": "boolean"
              },
              "requireManualApprovalDefault": {
                "type": "boolean"
              },
              "whatsNewOnUpgrade": {
                "type": "boolean"
              },
              "detectShowSegments": {
                "type": "boolean"
              },
              "seedSponsorsDetection": {
                "type": "boolean"
              },
              "seedSponsorsVerification": {
                "type": "boolean"
              },
              "seedSponsorsReviewer": {
                "type": "boolean"
              },
              "seedSponsorsResurrect": {
                "type": "boolean"
              },
              "textRecurrenceHints": {
                "type": "boolean"
              },
              "adAddressingMode": {
                "type": "string",
                "enum": [
                  "timestamps",
                  "segment_ids",
                  "random"
                ]
              },
              "processNewEpisodesFirst": {
                "type": "boolean"
              },
              "rssRefreshIntervalMinutes": {
                "type": "integer"
              },
              "podpingEnabled": {
                "type": "boolean"
              },
              "omitTemperature": {
                "type": "boolean"
              },
              "llmJsonSchemaEnabled": {
                "type": "boolean"
              },
              "segmentCategoryActions": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "enum": [
                    "remove",
                    "beep",
                    "keep",
                    "mark"
                  ]
                }
              },
              "communitySyncCategories": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "sponsor",
                    "cross_promo",
                    "self_promo",
                    "interaction",
                    "intro",
                    "outro",
                    "recap"
                  ]
                }
              },
              "artworkWatermarkEnabled": {
                "type": "boolean"
              },
              "artworkBadgePosition": {
                "type": "string",
                "enum": [
                  "bottom-right",
                  "bottom-left",
                  "top-right",
                  "top-left"
                ]
              },
              "lowAdYieldAction": {
                "type": "string",
                "enum": [
                  "nothing",
                  "redetect",
                  "reprocess",
                  "full"
                ]
              },
              "episodeLogRetentionDays": {
                "type": "integer"
              },
              "episodeLogLevel": {
                "type": "string",
                "enum": [
                  "debug",
                  "info"
                ]
              },
              "downloadUserAgent": {
                "type": "string"
              },
              "feedUserAgent": {
                "type": "string"
              },
              "logDownloadQuery": {
                "type": "boolean"
              },
              "feedAuthEnabled": {
                "type": "boolean"
              },
              "downloadHistoryEnabled": {
                "type": "boolean"
              },
              "downloadGeoLookupEnabled": {
                "type": "boolean"
              },
              "downloadHistoryRetentionDays": {
                "type": "integer"
              },
              "minCutConfidence": {
                "type": "number",
                "format": "float"
              },
              "minContentBetweenAdsSeconds": {
                "type": "number",
                "format": "float",
                "minimum": 0,
                "maximum": 60
              },
              "adDetectionExcludeStartSeconds": {
                "type": "number",
                "format": "float",
                "minimum": 0,
                "maximum": 600,
                "description": "Seconds from the start of an episode during which ad markers are\nignored, so an intro cue is not mistaken for an ad break. 0 disables it.\nFeeds inherit it or set adDetectionExcludeStartOverride.\n"
              },
              "maxAdDurationSeconds": {
                "type": "number",
                "format": "float",
                "minimum": 30,
                "maximum": 3600
              },
              "maxAdDurationConfirmedSeconds": {
                "type": "number",
                "format": "float",
                "minimum": 30,
                "maximum": 3600
              },
              "maxLlmWindowAdSeconds": {
                "type": "number",
                "format": "float",
                "minimum": 60,
                "maximum": 900
              },
              "llmProvider": {
                "type": "string"
              },
              "systemoneBaseUrl": {
                "type": "string",
                "default": ""
              },
              "openaiBaseUrl": {
                "type": "string"
              },
              "pricingSourceMode": {
                "type": "string",
                "enum": [
                  "auto",
                  "litellm",
                  "free"
                ]
              },
              "notificationTimezone": {
                "type": "string",
                "description": "IANA zone for timestamp_local in webhook/email payloads (default UTC). Get/set via /settings/notifications/timezone."
              },
              "openrouterBaseUrl": {
                "type": "string"
              }
            }
          }
        }
      },
      "SystemStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "running"
            ]
          },
          "version": {
            "type": "string"
          },
          "feeds": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              }
            }
          },
          "episodes": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "byStatus": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "storage": {
            "type": "object",
            "properties": {
              "usedMb": {
                "type": "number"
              },
              "fileCount": {
                "type": "integer"
              }
            }
          },
          "settings": {
            "type": "object",
            "properties": {
              "retentionPeriodMinutes": {
                "type": "integer"
              },
              "whisperModel": {
                "type": "string",
                "description": "Effective transcription model from settings (local model, or the remote API model when the backend is openai-api)"
              },
              "whisperDevice": {
                "type": "string",
                "description": "Local compute device, or \"remote\" when transcription runs on a remote transcription API"
              },
              "whisperBackend": {
                "type": "string",
                "description": "Transcription backend in use (local, openai-api or parakeet; parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process)"
              },
              "whisperApiHost": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "scheme://host of the remote transcription endpoint, or null when transcription is local"
              },
              "baseUrl": {
                "type": "string"
              }
            }
          },
          "stats": {
            "type": "object",
            "properties": {
              "totalTimeSaved": {
                "type": "number",
                "description": "Total seconds of ad time removed across all episodes"
              },
              "totalInputTokens": {
                "type": "integer",
                "description": "Cumulative LLM input tokens consumed"
              },
              "totalOutputTokens": {
                "type": "integer",
                "description": "Cumulative LLM output tokens consumed"
              },
              "totalLlmCost": {
                "type": "number",
                "description": "Estimated cumulative LLM cost in USD"
              }
            }
          },
          "feedRefresh": {
            "type": "object",
            "description": "Scheduled-refresh health. Informational only: it never gates readiness, and /health ignores it. A shared network outage degrades feed freshness, not the process, so the aggregate is reported without naming any feed.",
            "properties": {
              "lastSuccessfulRefreshAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "outageDegraded": {
                "type": "boolean",
                "description": "True while a refresh pass is treated as a shared outage rather than per-feed breakage, which happens when a large enough share of a batch fails together. Per-feed failure counting is suspended for that pass so healthy feeds are not marked broken."
              },
              "outageAffectedCount": {
                "type": "integer",
                "description": "Feeds that failed together in the pass that opened the outage."
              },
              "nextRetryAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the jittered early retry is due. Null once the outage clears."
              }
            }
          },
          "podping": {
            "type": "object",
            "description": "Podping listener health. Informational only, the same as feedRefresh: scheduled RSS polling remains the fallback, so an outage here delays notifications rather than stopping discovery. While the listener is enabled, every configured node is checked at startup and every five minutes. Manual checks also work while it is disabled and do not change the active listener node. GET /podping/hosts reports the same node data for the Podping page.",
            "properties": {
              "listenerEnabled": {
                "type": "boolean"
              },
              "allNodesDown": {
                "type": "boolean",
                "description": "True only while every configured Hive RPC node is unavailable to the enabled live listener. Any live listener success clears it; a manual check does not change this field."
              },
              "degradedSince": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "check": {
                "$ref": "#/components/schemas/PodpingNodeCheck"
              },
              "nodes": {
                "type": "array",
                "description": "One entry per configured node, in the configured order.",
                "items": {
                  "type": "object",
                  "properties": {
                    "node": {
                      "type": "string"
                    },
                    "selected": {
                      "type": "boolean",
                      "description": "Historical last node to complete a successful RPC call."
                    },
                    "active": {
                      "type": "boolean",
                      "description": "True while this node has the live listener connection."
                    },
                    "consecutiveFailures": {
                      "type": "integer",
                      "description": "Failure streak, which survives a restart rather than resetting."
                    },
                    "lastSuccessAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "Last successful RPC response, shown as Last seen; null before success."
                    },
                    "nextRetryAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "lastFailureReason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "httpStatus": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Latest HTTP status, including 200 with invalid_response; null if none arrived."
                    },
                    "outcome": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "healthy",
                        "http_error",
                        "invalid_response",
                        "unreachable",
                        null
                      ],
                      "description": "Latest RPC outcome; null before the first check."
                    }
                  }
                }
              }
            }
          },
          "transcriber": {
            "type": "object",
            "description": "Health of the configured transcription backend. Which shape comes back depends on that backend, because the local reading says nothing when transcription runs on a remote API. Informational only, and never a readiness gate.",
            "properties": {
              "backend": {
                "type": "string",
                "enum": [
                  "local",
                  "openai-api",
                  "parakeet"
                ],
                "description": "The configured backend, which is what decides the rest of this object; parakeet runs NVIDIA Parakeet TDT 0.6B v3 in-process."
              },
              "available": {
                "type": "boolean",
                "description": "On the local backend, false after a GPU-memory exhaustion used up every batch-size retry, so a transcriber failing episode after episode is visible instead of silent. On the API backend, false when no base URL is configured, and otherwise the last cached probe result."
              },
              "reason": {
                "type": "string",
                "description": "Local backend only, and present only when the faster-whisper or ctranslate2 package is missing. Says what to install or which backend to switch to."
              },
              "device": {
                "type": "string",
                "enum": [
                  "cpu",
                  "cuda"
                ],
                "description": "Local backend only. The effective device, not the raw setting."
              },
              "lastOutcome": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Local backend only. The most recent local transcription outcome, shared across workers so any worker reports the same one. Null when nothing has been transcribed yet."
              },
              "probed": {
                "type": "boolean",
                "description": "API backend only. False when no probe result is cached; this endpoint never fires an outbound request of its own, so a never-probed endpoint reads as available."
              },
              "instanceCount": {
                "type": "integer",
                "description": "API backend only, and present only when probed is true."
              }
            }
          },
          "failover": {
            "type": "object",
            "description": "Same shapes as GET /failover, minus events (#806).",
            "properties": {
              "targets": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/FailoverTargetState"
                },
                "description": "Keyed by target name (llm-a, llm-b, transcriber)."
              },
              "probes": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/FailoverProbe"
                },
                "description": "Keyed by probe name (llm-a, llm-b, llm-failover, transcriber, transcriber-failover)."
              }
            }
          },
          "security": {
            "type": "object",
            "properties": {
              "cryptoReady": {
                "type": "boolean"
              },
              "plaintextSecretsCount": {
                "type": "integer"
              }
            }
          }
        }
      },
      "PodpingNodeCheck": {
        "type": "object",
        "required": [
          "checkId",
          "status",
          "requestedAt",
          "startedAt",
          "completedAt"
        ],
        "properties": {
          "checkId": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "idle",
              "pending",
              "running",
              "completed",
              "error"
            ]
          },
          "requestedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "healthyNodes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "totalNodes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ReleaseInfo": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string"
          },
          "releaseDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "url": {
            "type": "string"
          },
          "notes": {
            "type": "string",
            "description": "Release notes body, truncated to 2000 characters"
          }
        }
      },
      "ProcessingEpisode": {
        "type": "object",
        "required": [
          "episodeId",
          "slug",
          "title",
          "podcast",
          "startedAt",
          "stage",
          "queueTotal"
        ],
        "properties": {
          "episodeId": {
            "type": "string",
            "description": "Episode identifier"
          },
          "slug": {
            "type": "string",
            "description": "Podcast feed slug"
          },
          "title": {
            "type": "string",
            "description": "Episode title"
          },
          "podcast": {
            "type": "string",
            "description": "Podcast name"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When processing started (if available)"
          },
          "stage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Active pipeline label, \"queued\" for a waiting episode, or null when live stage detail is unavailable."
          },
          "queuedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the episode was added to the queue (queued entries only)"
          },
          "queuePosition": {
            "type": "integer",
            "description": "1-based offset-aware position in the pending queue (queued entries only)"
          },
          "queueTotal": {
            "type": "integer",
            "description": "Full waiting count across all pages, excluding active jobs."
          },
          "priority": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Dequeue priority of the queue row; null for a display-queue entry with no auto-process queue row (its reorder control is disabled)"
          },
          "admission": {
            "$ref": "#/components/schemas/QueueAdmission"
          }
        }
      },
      "QueueAdmission": {
        "type": "object",
        "description": "Why a waiting job is or is not starting (queued entries only). Scored from the same resolved phase set admission gates on, so a phase this run will not execute (a feed that skips its second pass, chapters turned off) never appears as the blocker.",
        "properties": {
          "blocked": {
            "type": "boolean",
            "description": "Whether this job is currently refused admission"
          },
          "phase": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "detection",
              "review",
              "verification",
              "chapters",
              null
            ],
            "description": "Pipeline phase whose account is holding the job, or null when the block is not phase-specific (processing paused, routes unresolved)"
          },
          "slot": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "primary",
              "secondary",
              null
            ],
            "description": "Credential slot of the blocking account"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "manual_rate_limit",
              "provider_rate_limit",
              "provider_account_changed",
              "processing_paused",
              null
            ],
            "description": "manual_rate_limit is a MinusPod-configured RPM/RPD/TPM cap; provider_rate_limit is a provider 429 hold; provider_account_changed means the run's frozen routes name an account the operator has since replaced; processing_paused is the global admission pause"
          },
          "resumesAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the hold releases, or null when there is no timed reset (an account change clears on the next attempt, not on a clock)"
          }
        }
      },
      "Pattern": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Pattern ID"
          },
          "scope": {
            "type": "string",
            "enum": [
              "global",
              "network",
              "podcast"
            ],
            "description": "Pattern scope level"
          },
          "network_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Network ID for network-scoped patterns"
          },
          "podcast_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Podcast slug for podcast-scoped patterns"
          },
          "text_template": {
            "type": "string",
            "description": "Text pattern template for matching"
          },
          "sponsor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sponsor name associated with this pattern"
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the pattern is currently active"
          },
          "can_split": {
            "type": "boolean",
            "description": "Whether the active pattern has at least two usable text pieces for splitting"
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reason for disabling the pattern"
          },
          "disabled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the pattern was deactivated; null while it is active."
          },
          "confirmation_count": {
            "type": "integer",
            "description": "Number of times pattern was confirmed correct"
          },
          "false_positive_count": {
            "type": "integer",
            "description": "Number of times pattern was marked as false positive"
          },
          "avg_duration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Running average of observed ad duration in seconds. Null for patterns that have not yet been matched with duration data."
          },
          "duration_samples": {
            "type": "integer",
            "description": "Number of duration observations used to compute avg_duration"
          },
          "last_matched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last time this pattern matched an ad"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "category": {
            "type": "string",
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap"
            ],
            "description": "What kind of content this pattern matches (issue #565). Absent when\nthe pattern is unclassified (a legacy row or one synced without a\ncategory); the UI shows such patterns as Uncategorized.\nCommunity-shared patterns carry their category through sync; a\nper-install community_sync_categories setting controls which\ncategories are accepted from community sync.\n"
          },
          "trust": {
            "type": "string",
            "enum": [
              "active",
              "unproven",
              "stale"
            ],
            "description": "Computed staleness-based trust tier (not stored). active: matched\nlocally within the last 90 days. stale: a community pattern with no\nlocal match in the last 90 days whose newest community-confirmation\nor creation timestamp is also older than 365 days. unproven: no\nrecent local match, but not stale; includes all local\n(non-community) patterns, which are never 'stale'.\n"
          }
        }
      },
      "Network": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Network identifier (e.g., twit, relay_fm)"
          },
          "name": {
            "type": "string",
            "description": "Human-readable network name"
          }
        }
      },
      "AdCorrection": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "confirm",
              "reject",
              "adjust",
              "create",
              "split",
              "recategorize"
            ],
            "description": "- confirm: Mark detection as correct (creates/updates pattern).\n  Optionally pass adjusted_start/adjusted_end to approve a trimmed\n  sub-span of a held marker (e.g. the reviewer's proposed trim);\n  the marker's boundaries move to the trimmed span before approval\n  and only the ad portion is cut on recut.\n- reject: Mark as false positive (stores text for cross-episode exclusion)\n- adjust: Correct boundaries (also creates pattern from adjusted segment)\n- create: Mark a brand-new ad the detector missed (top-level\n  start/end/sponsor/text_template, no original_ad)\n- split: Replace one marker with N single-sponsor markers (see the\n  endpoint description)\n- recategorize: Set this marker's segment category, which decides\n  whether the span is cut, beeped, or left in. Scoped to this\n  episode's marker; a linked pattern keeps its own category. The\n  only type accepted on a marker whose action resolved to keep,\n  since it is how that verdict is changed.\n"
          },
          "original_ad": {
            "type": "object",
            "description": "Existing marker to correct, matched by its start/end within tolerance. Required for every type except create.",
            "properties": {
              "start": {
                "type": "number"
              },
              "end": {
                "type": "number"
              },
              "pattern_id": {
                "type": "integer"
              }
            }
          },
          "sponsor": {
            "type": "string",
            "description": "Sponsor name to associate (for confirm/adjust/create)"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap",
              null
            ],
            "description": "create type only. Segment category stamped on both the manual marker and the pattern(s) created from it; each feed's segment actions decide what happens to future matches. Omit or send null to leave the marker and pattern uncategorized (resolves as sponsor)."
          },
          "adjusted_start": {
            "type": "number",
            "description": "Corrected start time in seconds (required for adjust; optional for confirm as a trimmed approval, must lie within the original span and both bounds must be supplied together)"
          },
          "adjusted_end": {
            "type": "number",
            "description": "Corrected end time in seconds (required for adjust; optional for confirm as a trimmed approval, must lie within the original span and both bounds must be supplied together)"
          },
          "split_points": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "split type only, divider times strictly inside the span"
          },
          "pieces": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "sponsor": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            },
            "description": "split type only, optional per-piece sponsor overrides"
          }
        }
      },
      "ProcessingStatus": {
        "type": "object",
        "properties": {
          "currentJob": {
            "type": [
              "object",
              "null"
            ],
            "description": "The episode being processed, or null when nothing is running",
            "properties": {
              "slug": {
                "type": "string"
              },
              "episodeId": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "podcastName": {
                "type": "string"
              },
              "stage": {
                "type": "string",
                "description": "Current processing stage (e.g. pass1:transcribing)"
              },
              "progress": {
                "type": "number",
                "description": "Progress percentage (0-100)"
              },
              "startedAt": {
                "type": "number",
                "description": "Unix timestamp the job started"
              },
              "elapsed": {
                "type": "number",
                "description": "Seconds since the job started"
              }
            }
          },
          "jobs": {
            "type": "array",
            "description": "Every episode currently processing, oldest first. currentJob is jobs[0].",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "episodeId": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "podcastName": {
                  "type": "string"
                },
                "stage": {
                  "type": "string",
                  "description": "Current processing stage (e.g. pass1:transcribing)"
                },
                "progress": {
                  "type": "number",
                  "description": "Progress percentage (0-100)"
                },
                "startedAt": {
                  "type": "number",
                  "description": "Unix timestamp the job started"
                },
                "elapsed": {
                  "type": "number",
                  "description": "Seconds since the job started"
                }
              }
            }
          },
          "whisper": {
            "$ref": "#/components/schemas/WhisperPoolSnapshot"
          },
          "queueLength": {
            "type": "integer",
            "description": "Number of episodes waiting to be processed"
          },
          "queuedEpisodes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "episodeId": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "podcastName": {
                  "type": "string"
                },
                "queuedAt": {
                  "type": "number"
                }
              }
            }
          },
          "feedRefreshes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "podcastName": {
                  "type": "string"
                },
                "newEpisodes": {
                  "type": "integer"
                },
                "startedAt": {
                  "type": "number"
                }
              }
            }
          },
          "hold": {
            "$ref": "#/components/schemas/QueueHold"
          },
          "lastUpdated": {
            "type": "number",
            "description": "Unix timestamp of the last status change"
          },
          "failover": {
            "type": "object",
            "description": "Failover state (#806), read through the same 5-second cache as GET /failover; never probes on this read path.",
            "properties": {
              "active": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "llm-a",
                    "llm-b",
                    "transcriber"
                  ]
                },
                "description": "Target names currently running on their failover configuration."
              },
              "targets": {
                "type": "object",
                "description": "Keyed by target name (llm-a, llm-b, transcriber).",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "active": {
                      "type": "boolean"
                    },
                    "source": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "auto",
                        "manual",
                        "probe",
                        null
                      ]
                    },
                    "since": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "QueueHold": {
        "type": "object",
        "description": "Why the queue is not moving. Reports what the maintenance tick last\nobserved; reading it never probes a service.\n",
        "properties": {
          "queuePaused": {
            "type": "boolean",
            "description": "True only while a rate-limit hold is stopping new claims. An\noffline wait parks specific episodes and leaves the rest of the\nqueue running, so it never sets this.\n"
          },
          "holdUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The provider's own rate-limit reset time while the pause is active, or null"
          },
          "holdSince": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the current pause began, or null when the queue is not paused"
          },
          "offlineHeld": {
            "type": "integer",
            "description": "Every episode deferred on a service other than the rate-limit\nhold, including any service not broken out in offlineServices.\n"
          },
          "offlineServices": {
            "type": "array",
            "description": "Per-service breakdown of offline-queue waits",
            "items": {
              "type": "object",
              "properties": {
                "service": {
                  "type": "string",
                  "enum": [
                    "llm",
                    "whisper",
                    "sponsorblock"
                  ],
                  "description": "sponsorblock counts YouTube episodes waiting on SponsorBlock, not offline-queue deferrals."
                },
                "held": {
                  "type": "integer"
                },
                "reachable": {
                  "type": [
                    "boolean",
                    "null"
                  ],
                  "description": "Verdict of the last probe. Null until the tick has probed\nonce, meaning \"not checked yet\" rather than \"up\".\n"
                },
                "checkedAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "WhisperPoolSnapshot": {
        "type": "object",
        "description": "Bounded admission state for remote Whisper requests, as read by this process's pool.",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "backend": {
            "type": "string"
          },
          "active": {
            "type": "boolean",
            "description": "enabled and backend is openai-api"
          },
          "inactiveReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "disabled",
              "local_backend",
              null
            ]
          },
          "capacity": {
            "type": "integer"
          },
          "inFlight": {
            "type": "integer"
          },
          "transcribingEpisodes": {
            "type": "integer"
          },
          "maxEpisodes": {
            "type": "object",
            "properties": {
              "configured": {
                "type": "integer"
              },
              "effective": {
                "type": "integer"
              }
            }
          },
          "leader": {
            "type": "boolean",
            "description": "Whether this process is the background leader that runs transcriptions. inFlight/transcribingEpisodes are only meaningful when true; other workers always report 0."
          }
        }
      },
      "ProcessingHistoryEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "History entry ID"
          },
          "podcast_id": {
            "type": "integer",
            "description": "Podcast ID"
          },
          "podcast_slug": {
            "type": "string",
            "description": "Podcast slug"
          },
          "podcast_title": {
            "type": "string",
            "description": "Podcast title"
          },
          "episode_id": {
            "type": "string",
            "description": "Episode ID"
          },
          "episode_title": {
            "type": "string",
            "description": "Episode title"
          },
          "processed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When processing completed"
          },
          "processing_duration_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Processing duration in seconds"
          },
          "status": {
            "type": "string",
            "enum": [
              "completed",
              "failed"
            ],
            "description": "Processing result status"
          },
          "ads_detected": {
            "type": "integer",
            "description": "Number of ads detected"
          },
          "error_message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Error message if processing failed"
          },
          "reprocess_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Reprocess number (null for first processing)"
          },
          "inputTokens": {
            "type": "integer",
            "description": "LLM input tokens used for this processing run"
          },
          "outputTokens": {
            "type": "integer",
            "description": "LLM output tokens used for this processing run"
          },
          "llmCost": {
            "type": "number",
            "description": "Estimated LLM cost in USD for this processing run"
          },
          "downloadedDuration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Duration of the downloaded copy this run processed, in seconds. Null for runs recorded before 2.53.0."
          },
          "appVersion": {
            "type": [
              "string",
              "null"
            ],
            "description": "MinusPod version that produced this run. Null for runs recorded before 2.78.4."
          },
          "mode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Processing mode. Chapters-only regeneration uses chapters; older runs may omit it."
          }
        }
      },
      "Sponsor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Sponsor ID"
          },
          "name": {
            "type": "string",
            "description": "Canonical sponsor name"
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Alternative names for this sponsor"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sponsor category"
          },
          "segment_category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "sponsor",
              "cross_promo",
              "self_promo",
              "interaction",
              "intro",
              "outro",
              "recap",
              null
            ],
            "description": "Segment category every read naming this sponsor is filed under, in detection, the pass-1 prompt hint, and learned patterns. Null leaves the detector's own category in place."
          },
          "common_ctas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Common calls to action associated with this sponsor"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Community tags applied to this sponsor"
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the sponsor is active"
          },
          "pattern_count": {
            "type": "integer",
            "description": "Number of active ad patterns linked to this sponsor"
          },
          "last_matched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Most recent time a linked pattern matched, or null if never"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Normalization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Normalization rule ID"
          },
          "terms": {
            "type": "string",
            "description": "Regex pattern to match (legacy alias - pattern - accepted on write)"
          },
          "canonical": {
            "type": "string",
            "description": "Canonical replacement string (legacy alias - replacement - accepted on write)"
          },
          "category": {
            "type": "string",
            "enum": [
              "sponsor",
              "url",
              "number",
              "phrase"
            ],
            "description": "Normalization category"
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the rule is active"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SearchShowResult": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "titleSnippet": {
            "type": [
              "string",
              "null"
            ],
            "description": "The title with its matched words highlighted; null when the title did not match"
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ],
            "description": "Highlighted match context from the description; null when nothing in it matched"
          }
        }
      },
      "SearchEpisodeResult": {
        "type": "object",
        "properties": {
          "feedSlug": {
            "type": "string"
          },
          "feedTitle": {
            "type": "string"
          },
          "episodeId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "publishDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "titleSnippet": {
            "type": [
              "string",
              "null"
            ],
            "description": "The title with its matched words highlighted; null when the title did not match"
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ],
            "description": "Highlighted match context from the description; null when nothing in it matched"
          }
        }
      },
      "SearchTranscriptResult": {
        "type": "object",
        "properties": {
          "feedSlug": {
            "type": "string"
          },
          "episodeId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "snippet": {
            "type": "string",
            "description": "Highlighted transcript match context"
          },
          "timestamp": {
            "type": [
              "number",
              "null"
            ],
            "description": "Always null; the search index has no offset back into the transcript timing"
          }
        }
      },
      "SearchPatternResult": {
        "type": "object",
        "description": "Advanced-page-only group; not shown in the Dashboard box or palette.",
        "properties": {
          "id": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "description": "Pattern scope (podcast slug, or \"global\")"
          },
          "sponsor": {
            "type": "string"
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SearchSponsorResult": {
        "type": "object",
        "description": "Advanced-page-only group; not shown in the Dashboard box or palette.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique webhook identifier"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL that receives webhook POST requests"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Event types this webhook is subscribed to"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the webhook is active"
          },
          "payloadTemplate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Custom payload template, or null for default format"
          },
          "contentType": {
            "type": "string",
            "description": "Content type for webhook delivery"
          }
        },
        "description": "Webhook configuration. The secret field is never returned in responses."
      },
      "NotificationTimezoneSettings": {
        "type": "object",
        "properties": {
          "timezone": {
            "type": "string",
            "description": "IANA zone name (e.g. 'America/New_York'), or 'UTC'",
            "example": "UTC"
          }
        }
      },
      "EmailNotificationSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "Episode Processed",
                "Episode Awaiting Approval",
                "Episode Pending Review",
                "Episode Failed",
                "Auth Failure",
                "Limit Exceeded",
                "Rate Limit Structural",
                "Feed Refresh Failed",
                "Update Available",
                "Cue Template Quiet",
                "Queue Held",
                "Queue Resumed",
                "Service Offline",
                "Service Reachable",
                "Failover Triggered",
                "Failover Cancelled"
              ]
            }
          },
          "smtpHost": {
            "type": "string"
          },
          "smtpPort": {
            "type": "integer"
          },
          "smtpSecurity": {
            "type": "string",
            "enum": [
              "none",
              "starttls",
              "ssl"
            ]
          },
          "smtpUsername": {
            "type": "string"
          },
          "smtpPasswordConfigured": {
            "type": "boolean",
            "description": "Whether an SMTP password is stored. The password itself is never returned."
          },
          "fromAddress": {
            "type": "string"
          },
          "recipients": {
            "type": "string",
            "description": "Comma-separated recipient addresses"
          }
        }
      },
      "EmailNotificationSettingsUpdate": {
        "type": "object",
        "description": "Partial update; omitted fields keep their stored values.",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "smtpHost": {
            "type": "string"
          },
          "smtpPort": {
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          },
          "smtpSecurity": {
            "type": "string",
            "enum": [
              "none",
              "starttls",
              "ssl"
            ]
          },
          "smtpUsername": {
            "type": "string"
          },
          "smtpPassword": {
            "type": "string",
            "writeOnly": true,
            "description": "Write-only. Empty string clears the stored password."
          },
          "fromAddress": {
            "type": "string"
          },
          "recipients": {
            "type": "string"
          }
        }
      },
      "RefreshOutcome": {
        "type": "object",
        "required": [
          "success",
          "status",
          "new_episodes",
          "queued_episodes",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "refreshed",
              "coalesced",
              "not_modified",
              "fetch_failed",
              "parse_failed",
              "internal_error"
            ]
          },
          "new_episodes": {
            "type": "integer",
            "minimum": 0
          },
          "queued_episodes": {
            "type": "integer",
            "minimum": 0
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RefreshAllResult": {
        "type": "object",
        "required": [
          "message",
          "feedCount",
          "success",
          "succeeded",
          "failed",
          "outcomes"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "feedCount": {
            "type": "integer",
            "minimum": 0
          },
          "success": {
            "type": "boolean"
          },
          "succeeded": {
            "type": "integer",
            "minimum": 0
          },
          "failed": {
            "type": "integer",
            "minimum": 0
          },
          "outcomes": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/RefreshOutcome"
            }
          }
        }
      },
      "FeedSubscriberKeyRecord": {
        "type": "object",
        "required": [
          "id",
          "label",
          "created_at",
          "last_used_at",
          "revoked_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}$"
          },
          "label": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DownloadHistoryEntry": {
        "type": "object",
        "required": [
          "id",
          "episodeId",
          "podcastSlug",
          "podcastTitle",
          "clientIp",
          "userAgent",
          "firstSeenAt",
          "lastSeenAt",
          "requestCount"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "episodeId": {
            "type": "string"
          },
          "episodeTitle": {
            "type": [
              "string",
              "null"
            ]
          },
          "podcastSlug": {
            "type": "string"
          },
          "podcastTitle": {
            "type": "string"
          },
          "clientIp": {
            "type": "string"
          },
          "userAgent": {
            "type": "string"
          },
          "subscriberLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Subscriber key label; null when the global key or no key was used."
          },
          "subscriberKeyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Subscriber key id; null when the global key or no key was used."
          },
          "subscriberRevoked": {
            "type": "boolean",
            "description": "True when the subscriber key has been revoked."
          },
          "userAgentBlocked": {
            "type": "boolean",
            "description": "True when the user agent matches the JIT blocked user agents list."
          },
          "lastRequestLine": {
            "type": [
              "string",
              "null"
            ],
            "description": "Method and path of the most recent merged request, with any key query value replaced by [redacted]; null for rows recorded before capture existed."
          },
          "lastRequestHeaders": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "string"
            },
            "description": "Allowlisted headers of the most recent merged request (Range, Accept, Accept-Encoding, Referer, If-Range, If-None-Match); null when none were sent."
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "geoStatus": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "ok",
              "private",
              "failed",
              "unresolvable",
              null
            ],
            "description": "null when no lookup has been stored for the IP. failed is retried after 24 hours; unresolvable (a permanent provider answer) is not retried."
          },
          "firstSeenAt": {
            "type": "string"
          },
          "lastSeenAt": {
            "type": "string"
          },
          "requestCount": {
            "type": "integer"
          }
        }
      },
      "FeedSubscriberKeyWithFeed": {
        "type": "object",
        "required": [
          "id",
          "label",
          "createdAt",
          "lastUsedAt",
          "revokedAt",
          "feedSlug",
          "feedTitle"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}$"
          },
          "label": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "feedSlug": {
            "type": "string"
          },
          "feedTitle": {
            "type": "string"
          }
        }
      },
      "FeedSubscriberKeyCreated": {
        "type": "object",
        "required": [
          "id",
          "token",
          "label",
          "created_at",
          "feedUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}$"
          },
          "token": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}\\.[0-9a-f]{64}$",
            "description": "Bearer credential returned only at creation."
          },
          "label": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "feedUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ProviderBudgetFxRate": {
        "type": "object",
        "required": [
          "localPerUsd",
          "source",
          "sourceDate",
          "fetchedAt"
        ],
        "properties": {
          "localPerUsd": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "sourceDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fetchedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ProviderBudgetStatus": {
        "type": "object",
        "required": [
          "provider",
          "spentMicrousd",
          "reservedMicrousd",
          "activeReservations"
        ],
        "properties": {
          "provider": {
            "type": "string"
          },
          "spentMicrousd": {
            "type": "integer",
            "minimum": 0
          },
          "reservedMicrousd": {
            "type": "integer",
            "minimum": 0
          },
          "activeReservations": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "ProviderBudget": {
        "type": "object",
        "required": [
          "enabled",
          "dailyLimitMicrousd",
          "maxReservations",
          "unknownCost",
          "unknownReserveMicrousd",
          "displayCurrency",
          "dailyLimit",
          "unknownReserve",
          "fxRate",
          "status"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "dailyLimitMicrousd": {
            "type": "integer",
            "minimum": 0
          },
          "maxReservations": {
            "type": "integer",
            "minimum": 1,
            "maximum": 64
          },
          "unknownCost": {
            "type": "string",
            "enum": [
              "deny",
              "allow",
              "reserve"
            ]
          },
          "unknownReserveMicrousd": {
            "type": "integer",
            "minimum": 0
          },
          "displayCurrency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "dailyLimit": {
            "type": "string"
          },
          "unknownReserve": {
            "type": "string"
          },
          "fxRate": {
            "$ref": "#/components/schemas/ProviderBudgetFxRate"
          },
          "status": {
            "$ref": "#/components/schemas/ProviderBudgetStatus"
          }
        }
      },
      "ProviderBudgetUpdate": {
        "type": "object",
        "required": [
          "enabled",
          "maxReservations",
          "unknownCost"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "maxReservations": {
            "type": "integer",
            "minimum": 1,
            "maximum": 64
          },
          "unknownCost": {
            "type": "string",
            "enum": [
              "deny",
              "allow",
              "reserve"
            ]
          },
          "displayCurrency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "description": "When present, dailyLimit and unknownReserve are required decimal strings."
          },
          "dailyLimit": {
            "type": "string"
          },
          "unknownReserve": {
            "type": "string"
          },
          "dailyLimitMicrousd": {
            "type": "integer",
            "minimum": 0,
            "description": "Required when displayCurrency is absent."
          },
          "unknownReserveMicrousd": {
            "type": "integer",
            "minimum": 0,
            "description": "Required when displayCurrency is absent."
          }
        }
      },
      "CurrencyOption": {
        "type": "object",
        "required": [
          "code",
          "name"
        ],
        "properties": {
          "code": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ProviderBudgetRate": {
        "type": "object",
        "required": [
          "currency",
          "localPerUsd",
          "source",
          "sourceDate",
          "dailyLimit",
          "unknownReserve"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "localPerUsd": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "sourceDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "dailyLimit": {
            "type": "string"
          },
          "unknownReserve": {
            "type": "string"
          }
        }
      },
      "UploadTooLarge": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "File is too large. This server accepts up to 1024 MB per upload."
          },
          "status": {
            "type": "integer",
            "example": 413
          },
          "maxUploadMb": {
            "type": "integer",
            "description": "The configured limit in MB.",
            "example": 1024
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "details": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request - invalid input",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LedgerDateBadRequest": {
        "description": "A from or to value could not be parsed as a date or timestamp. from is checked first, so only the first offending parameter is named.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "from must be a date (YYYY-MM-DD) or an ISO 8601 timestamp",
              "status": 400
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
