Read-only HTTP API for meeting transcripts. Domain: fable7-transcripts.feedmob.it.com · JSON unless noted · timestamps are GMT.
date strings are ISO YYYY-MM-DD, used in filter params and item metadata.modified is an integer Unix epoch (seconds, GMT).filename in responses is the literal basename, e.g. 2026-07-08_KenTeam早会_Transcript.vtt. Pass it to /meetings/{filename} after URL-encoding the Chinese chars.^[A-Za-z0-9_\-\u4e00-\u9fff\.]+\.vtt$. Anything else, including path traversal .., returns 404.YYYY-MM-DD_<meeting name>_Transcript.vtt; files outside this pattern are silently skipped in /meetings and /search.Three ways to pass the API key, all equivalent. GET /health and GET /docs* are public.
curl -H "Authorization: Bearer $KEY" \
'http://127.0.0.1:8001/meetings?limit=5'
api_keycurl 'http://127.0.0.1:8001/meetings?limit=5&api_key=YOUR_KEY'
kcurl 'http://127.0.0.1:8001/meetings?limit=5&k=YOUR_KEY'
Missing or invalid key → 401.
GET /health — publicLiveness probe. No auth.
curl http://127.0.0.1:8001/health
{ "status": "ok", "transcripts": true }
GET /docs — publicHTML rendering of this page (markdown rendered to HTML, basic CSS).
GET /docs.md — publicRaw markdown, Content-Type: text/markdown; charset=utf-8. Best for curl and AI ingestion.
GET /docs.json — publicStructured metadata (endpoints, params, auth, sample responses), Content-Type: application/json; charset=utf-8. Stable field names for tools.
GET /ui/ — publicTranscript 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 requiredList 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 requiredRaw 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 requiredFull-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 主要讨论三个议题" }
]
}
]
}
| 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 |
/search runs ripgrep with a 20s timeout. Very large corpora or pathological regexes may hit it./meetings returns at most limit items; paginate with offset.POST, PUT, PATCH, DELETE, HEAD, OPTIONS) return 405 on every path. The service does not write anything under the transcripts directory. Indexing (write side of the vector KB) is a local CLI operation at /opt/transcripts-api/index.py — there is intentionally no HTTP endpoint for it, to keep API spend under operator control.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 requiredVector (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会议。 发的这个。"
}
]
}
]
}
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.