fable7-transcripts API

Read-only HTTP API for meeting transcripts. Domain: fable7-transcripts.feedmob.it.com · JSON unless noted · timestamps are GMT.

Conventions

Authentication

Three ways to pass the API key, all equivalent. GET /health and GET /docs* are public.

Authorization header (preferred for programmatic access)

curl -H "Authorization: Bearer $KEY" \
  'http://127.0.0.1:8001/meetings?limit=5'

Query parameter api_key

curl 'http://127.0.0.1:8001/meetings?limit=5&api_key=YOUR_KEY'

Short alias k

curl 'http://127.0.0.1:8001/meetings?limit=5&k=YOUR_KEY'

Missing or invalid key → 401.

Endpoints

GET /health — public

Liveness probe. No auth.

curl http://127.0.0.1:8001/health
{ "status": "ok", "transcripts": true }

GET /docs — public

HTML rendering of this page (markdown rendered to HTML, basic CSS).

GET /docs.md — public

Raw markdown, Content-Type: text/markdown; charset=utf-8. Best for curl and AI ingestion.

GET /docs.json — public

Structured metadata (endpoints, params, auth, sample responses), Content-Type: application/json; charset=utf-8. Stable field names for tools.

GET /ui/ — public

Transcript browser SPA (the original interactive UI). HTML page that loads index.html. Root / also serves the API docs, so this is where the browser lives.

GET /meetings — auth required

List transcripts with pagination and filtering.

Param Type Default Notes
meeting string Substring match on meeting name segment
date_from YYYY-MM-DD Inclusive lower bound
date_to YYYY-MM-DD Inclusive upper bound
limit int 1-1000 100 Page size
offset int ≥0 0 Page offset
curl -H "Authorization: Bearer $KEY" \
  'http://127.0.0.1:8001/meetings?date_from=2026-07-01&date_to=2026-07-12&limit=10'
{
  "total": 3,
  "limit": 10,
  "offset": 0,
  "items": [
    {
      "filename": "2026-07-08_KenTeam早会_Transcript.vtt",
      "date": "2026-07-08",
      "meeting": "KenTeam早会",
      "size": 14820,
      "modified": 1752614400
    }
  ]
}

GET /meetings/{filename} — auth required

Raw VTT body, Content-Type: text/plain; charset=utf-8. URL-encode the filename.

curl -H "Authorization: Bearer $KEY" \
  'http://127.0.0.1:8001/meetings/2026-07-08_KenTeam%E6%97%A9%E4%BC%9A_Transcript.vtt'

Returns 404 if the filename is outside the whitelist or contains .., /, or \.

GET /search — auth required

Full-text search via ripgrep.

Param Type Default Notes
q string 1-200 required ripgrep regex pattern
meeting string Substring match on meeting name
date_from YYYY-MM-DD Inclusive
date_to YYYY-MM-DD Inclusive
context int 0-10 2 Lines of context per hit
limit int 1-500 50 Max files in result
curl -H "Authorization: Bearer $KEY" \
  'http://127.0.0.1:8001/search?q=KenTeam'
{
  "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 主要讨论三个议题" }
      ]
    }
  ]
}

Errors

Code When
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

Vector search

Backed by PostgreSQL 17 + pgvector; chunks embedded via OpenRouter openai/text-embedding-3-small (1536 dims). The HTTP surface only exposes the read side; indexing is local.

GET /search/semantic — auth required

Vector (cosine) similarity search over transcript chunks. Embeds the query via OpenRouter and returns the top-K most similar chunks. Cost is one embedding call per request.

Param Type Default Notes
q string 1-2000 required natural-language query (Chinese or English)
meeting string Substring match on meeting name
date_from YYYY-MM-DD Inclusive
date_to YYYY-MM-DD Inclusive
limit int 1-50 10 Max chunks in result (filters applied after KNN)
curl -H "Authorization: Bearer ***   'http://127.0.0.1:28765/search/semantic?q=AI会议主题&limit=5'
{
  "query": "AI会议主题",
  "files_matched": 3,
  "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会议。 发的这个。"
        }
      ]
    }
  ]
}

Indexing CLI — local only

There is no HTTP endpoint for indexing. Run the CLI directly on the host:

# Add any new files not yet indexed (the common case, e.g. after sync.py)
python /opt/transcripts-api/index.py add

# Add one specific file
python /opt/transcripts-api/index.py add 2026-07-14_Ai会议-重复_Transcript.vtt

# Show current index stats
python /opt/transcripts-api/index.py status

# Full rebuild (rare — wipes everything and re-embeds from scratch)
python /opt/transcripts-api/index.py rebuild

add runs cheaply: it queries SELECT DISTINCT filename FROM transcript_chunks and only embeds files that aren't there yet. rebuild runs full TRUNCATE + re-embed of every .vtt on disk and is the only operation that meaningfully spends on the OpenRouter quota.

On any failure (one or more files), the CLI sends a single Slack DM to leo_yang summarising which files failed and why. Success runs log to logs/access.log only — no DM unless requested.


Also available: /docs.md (raw markdown) · /docs.json (structured metadata) · /ui/ (transcript browser SPA)