{
  "name": "fable7-transcripts API",
  "version": "1.0",
  "domain": "fable7-transcripts.feedmob.it.com",
  "base_url_local": "http://127.0.0.1:8001",
  "read_only": true,
  "time_zone": "GMT",
  "conventions": {
    "date_format": "YYYY-MM-DD",
    "modified_format": "epoch seconds (integer)",
    "filename_pattern_disk": "^(\\d{4}-\\d{2}-\\d{2})_(.+)_Transcript\\.vtt$",
    "filename_whitelist": "^[A-Za-z0-9_\\-\\u4e00-\\u9fff\\.]+\\.vtt$",
    "filename_traversal_returns": 404
  },
  "auth": {
    "auth_required_for": [
      "/meetings",
      "/meetings/{filename}",
      "/search",
      "/search/semantic"
    ],
    "public": [
      "/",
      "/health",
      "/docs",
      "/docs.md",
      "/docs.json",
      "/ui",
      "/ui/"
    ],
    "methods": [
      {
        "name": "bearer",
        "transport": "header",
        "format": "Authorization: Bearer <key>",
        "preferred_for": "programmatic access"
      },
      {
        "name": "api_key",
        "transport": "query",
        "format": "?api_key=<key>"
      },
      {
        "name": "k",
        "transport": "query",
        "format": "?k=<key>",
        "note": "short alias of api_key"
      }
    ],
    "missing_or_invalid_returns": 401
  },
  "endpoints": [
    {
      "path": "/health",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "response": {
        "content_type": "application/json",
        "schema": {
          "type": "object",
          "required": [
            "status",
            "transcripts"
          ],
          "properties": {
            "status": {
              "type": "string",
              "enum": [
                "ok"
              ]
            },
            "transcripts": {
              "type": "boolean",
              "description": "TRANSCRIPTS_DIR exists on disk"
            }
          }
        },
        "example": {
          "status": "ok",
          "transcripts": true
        }
      }
    },
    {
      "path": "/docs",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "response": {
        "content_type": "text/html; charset=utf-8",
        "description": "HTML rendering of /docs.md. Same source of truth."
      }
    },
    {
      "path": "/docs.md",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "response": {
        "content_type": "text/markdown; charset=utf-8",
        "description": "Raw markdown source. Canonical human-authored artifact."
      }
    },
    {
      "path": "/docs.json",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "response": {
        "content_type": "application/json; charset=utf-8",
        "description": "This document. Stable structured field names for AI tooling."
      }
    },
    {
      "path": "/ui/",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "aliases": [
        "/ui"
      ],
      "response": {
        "content_type": "text/html; charset=utf-8",
        "description": "Transcript browser SPA (static/index.html). Root / serves the API docs; /ui/ is where the browser UI lives."
      }
    },
    {
      "path": "/",
      "method": "GET",
      "auth_required": false,
      "params": [],
      "response": {
        "content_type": "text/html; charset=utf-8",
        "description": "API docs HTML (same source as /docs). Footer links to /ui/ for the browser SPA."
      }
    },
    {
      "path": "/meetings",
      "method": "GET",
      "auth_required": true,
      "params": [
        {
          "name": "meeting",
          "in": "query",
          "type": "string",
          "required": false,
          "description": "substring match on meeting name segment"
        },
        {
          "name": "date_from",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "date_to",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "limit",
          "in": "query",
          "type": "integer",
          "required": false,
          "default": 100,
          "minimum": 1,
          "maximum": 1000
        },
        {
          "name": "offset",
          "in": "query",
          "type": "integer",
          "required": false,
          "default": 0,
          "minimum": 0
        }
      ],
      "response": {
        "content_type": "application/json",
        "schema": {
          "type": "object",
          "required": [
            "total",
            "limit",
            "offset",
            "items"
          ],
          "properties": {
            "total": {
              "type": "integer"
            },
            "limit": {
              "type": "integer"
            },
            "offset": {
              "type": "integer"
            },
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "filename",
                  "date",
                  "meeting",
                  "size",
                  "modified"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "format": "YYYY-MM-DD"
                  },
                  "meeting": {
                    "type": "string"
                  },
                  "size": {
                    "type": "integer"
                  },
                  "modified": {
                    "type": "integer",
                    "description": "epoch seconds, GMT"
                  }
                }
              }
            }
          }
        },
        "example": {
          "total": 3,
          "limit": 10,
          "offset": 0,
          "items": [
            {
              "filename": "2026-07-08_KenTeam早会_Transcript.vtt",
              "date": "2026-07-08",
              "meeting": "KenTeam早会",
              "size": 14820,
              "modified": 1752614400
            }
          ]
        }
      }
    },
    {
      "path": "/meetings/{filename}",
      "method": "GET",
      "auth_required": true,
      "params": [
        {
          "name": "filename",
          "in": "path",
          "type": "string",
          "required": true,
          "pattern": "^[A-Za-z0-9_\\-\\u4e00-\\u9fff\\.]+\\.vtt$",
          "url_encoded": true,
          "traversal_returns": 404,
          "description": "literal basename; URL-encode non-ASCII bytes"
        }
      ],
      "response": {
        "content_type": "text/plain; charset=utf-8",
        "description": "Raw VTT body."
      }
    },
    {
      "path": "/search",
      "method": "GET",
      "auth_required": true,
      "params": [
        {
          "name": "q",
          "in": "query",
          "type": "string",
          "required": true,
          "min_length": 1,
          "max_length": 200,
          "description": "ripgrep regex pattern"
        },
        {
          "name": "meeting",
          "in": "query",
          "type": "string",
          "required": false,
          "description": "substring match on meeting name segment"
        },
        {
          "name": "date_from",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "date_to",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "context",
          "in": "query",
          "type": "integer",
          "required": false,
          "default": 2,
          "minimum": 0,
          "maximum": 10,
          "description": "lines of context per hit"
        },
        {
          "name": "limit",
          "in": "query",
          "type": "integer",
          "required": false,
          "default": 50,
          "minimum": 1,
          "maximum": 500,
          "description": "max files in results"
        }
      ],
      "response": {
        "content_type": "application/json",
        "schema": {
          "type": "object",
          "required": [
            "query",
            "files_matched",
            "results"
          ],
          "properties": {
            "query": {
              "type": "string"
            },
            "files_matched": {
              "type": "integer"
            },
            "results": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "filename",
                  "date",
                  "meeting",
                  "hits"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "format": "YYYY-MM-DD"
                  },
                  "meeting": {
                    "type": "string"
                  },
                  "hits": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "line",
                        "text"
                      ],
                      "properties": {
                        "line": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "example": {
          "query": "KenTeam",
          "files_matched": 1,
          "results": [
            {
              "filename": "2026-07-08_KenTeam早会_Transcript.vtt",
              "date": "2026-07-08",
              "meeting": "KenTeam早会",
              "hits": [
                {
                  "line": 12,
                  "text": "[00:01:23.456] KenTeam 周会开始"
                },
                {
                  "line": 47,
                  "text": "[00:12:01.000] 今天 KenTeam 主要讨论三个议题"
                }
              ]
            }
          ]
        }
      }
    },
    {
      "path": "/search/semantic",
      "method": "GET",
      "auth_required": true,
      "params": [
        {
          "name": "q",
          "in": "query",
          "type": "string",
          "required": true,
          "min_length": 1,
          "max_length": 2000,
          "description": "natural-language query (Chinese or English)"
        },
        {
          "name": "meeting",
          "in": "query",
          "type": "string",
          "required": false
        },
        {
          "name": "date_from",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "date_to",
          "in": "query",
          "type": "string",
          "format": "YYYY-MM-DD",
          "required": false,
          "inclusive": true
        },
        {
          "name": "limit",
          "in": "query",
          "type": "integer",
          "required": false,
          "default": 10,
          "minimum": 1,
          "maximum": 50,
          "description": "Max chunks in result; filters applied after KNN"
        }
      ],
      "response": {
        "content_type": "application/json",
        "schema": {
          "type": "object",
          "required": [
            "query",
            "files_matched",
            "results"
          ],
          "properties": {
            "query": {
              "type": "string"
            },
            "files_matched": {
              "type": "integer"
            },
            "results": {
              "type": "array",
              "items": {
                "type": "object",
                "required": [
                  "filename",
                  "date",
                  "meeting",
                  "hits"
                ],
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "date": {
                    "type": "string",
                    "format": "YYYY-MM-DD"
                  },
                  "meeting": {
                    "type": "string"
                  },
                  "hits": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "speaker",
                        "start_ts",
                        "end_ts",
                        "score",
                        "chunk_idx",
                        "text"
                      ],
                      "properties": {
                        "speaker": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "start_ts": {
                          "type": "string"
                        },
                        "end_ts": {
                          "type": "string"
                        },
                        "score": {
                          "type": "number",
                          "description": "cosine similarity 0-1"
                        },
                        "chunk_idx": {
                          "type": "integer"
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "example": {
          "query": "AI会议主题",
          "files_matched": 1,
          "results": [
            {
              "filename": "2026-07-10_KenTeam早会_Transcript.vtt",
              "date": "2026-07-10",
              "meeting": "KenTeam早会",
              "hits": [
                {
                  "speaker": "Windy",
                  "start_ts": "00:12:34.560",
                  "end_ts": "00:13:01.230",
                  "score": 0.521,
                  "chunk_idx": 47,
                  "text": "因为。 对。 对，因为昨天的AI会议。 发的这个。"
                }
              ]
            }
          ]
        }
      }
    }
  ],
  "errors": {
    "401": "Missing or invalid API key (any of the three auth methods)",
    "404": "File not found, filename outside whitelist, or path traversal attempt",
    "405": "Non-GET method on any path. Body: {\"detail\": \"read-only: only GET is allowed\"}",
    "500": "Search backend (ripgrep) failure; stderr surfaced in detail",
    "503": "Transcripts directory unavailable"
  },
  "limits": {
    "rate_limit": "none",
    "search_timeout_seconds": 20,
    "search_backend": "ripgrep",
    "meetings_default_limit": 100,
    "meetings_max_limit": 1000,
    "search_default_limit": 50,
    "search_max_limit": 500,
    "search_default_context": 2,
    "search_max_context": 10,
    "semantic_search_default_limit": 10,
    "semantic_search_max_limit": 50
  },
  "vector_kb": {
    "store": "PostgreSQL 17 + pgvector 0.8 (HNSW, cosine)",
    "embedding_model": "openai/text-embedding-3-small (1536 dims)",
    "embedding_provider": "OpenRouter",
    "chunking": "speaker turns grouped within 60s; max ~600 tokens per chunk",
    "indexing_cli": "/opt/transcripts-api/index.py",
    "indexing_subcommands": [
      "add",
      "add <filename>",
      "status",
      "rebuild"
    ],
    "indexing_no_http_endpoint": true,
    "indexing_failure_notify": "Slack DM to leo_yang (D0BBNEKHRDH)"
  },
  "methods_allowed": [
    "GET"
  ]
}