Repository navigation
Expand file tree
/
Copy pathopenapi.yaml
More file actions
514 lines (514 loc) · 21 KB
/
Copy pathopenapi.yaml
File metadata and controls
514 lines (514 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
openapi: 3.0.3
info:
title: Pravaha API
version: 0.3.0
license: { name: Apache-2.0, url: 'https://www.apache.org/licenses/LICENSE-2.0' }
description: >
Pravaha's internal API (v3). Prose explanation of auth, validation and
rate-limiting decisions: docs/API.md. Every endpoint maps to an FR in docs/PRD.md.
Every JSON request body must be sent with Content-Type: application/json,
otherwise the route answers 415 (CSRF guard, docs/SECURITY.md).
servers:
- url: https://pravaha-cyan.vercel.app/api
description: Production
- url: http://localhost:3000/api
description: Local development
components:
securitySchemes:
organizerCookie:
type: apiKey
in: cookie
name: pravaha_org
cloudinarySignature:
type: apiKey
in: header
name: X-Cld-Signature
schemas:
Error:
type: object
properties:
error:
type: object
properties:
code: { type: string }
message: { type: string }
Lecture:
type: object
properties:
id: { type: string, format: uuid }
title: { type: string }
speaker: { type: string, nullable: true }
status: { type: string, enum: [processing, ready, transcript_failed] }
visibility: { type: string, enum: [unlisted, public] }
durationS: { type: number, nullable: true }
publicId: { type: string }
createdAt: { type: string, format: date-time }
trialExpiresAt: { type: string, format: date-time, nullable: true, description: "Set only on trial sessions" }
moments: { type: integer, description: "GET /lectures only: indexed moments" }
chapters: { type: integer, description: "GET /lectures only: distinct chapters" }
hasStudyPack: { type: boolean, description: "GET /lectures only" }
SearchResult:
type: object
properties:
segmentId: { type: integer }
lectureId: { type: string, format: uuid }
publicId: { type: string }
title: { type: string }
speaker: { type: string, nullable: true }
startS: { type: number }
endS: { type: number }
text: { type: string }
snippet:
type: array
description: "Plain-text parts; hit = matched term. Never HTML."
items:
type: object
properties:
text: { type: string }
hit: { type: boolean }
chapterTitle: { type: string, nullable: true }
Citation:
type: object
properties:
n: { type: integer }
segmentId: { type: integer }
lectureId: { type: string, format: uuid }
title: { type: string }
speaker: { type: string, nullable: true }
startS: { type: number }
endS: { type: number }
text: { type: string }
momentUrl: { type: string, format: uri }
Reel:
type: object
nullable: true
description: "Answer Reel: the cited moments stitched into one Cloudinary video (≤5 clips, ≤90 s)"
properties:
url: { type: string, format: uri }
durationS: { type: number }
clips: { type: integer }
Moment:
type: object
properties:
segmentId: { type: integer }
startS: { type: number }
endS: { type: number }
StudyPack:
type: object
properties:
summary: { type: array, items: { type: string }, maxItems: 3 }
concepts:
type: array
items:
allOf:
- $ref: '#/components/schemas/Moment'
- { type: object, properties: { name: { type: string } } }
quiz:
type: array
items:
allOf:
- $ref: '#/components/schemas/Moment'
- type: object
properties:
question: { type: string }
options: { type: array, items: { type: string }, minItems: 4, maxItems: 4 }
correctIndex: { type: integer, minimum: 0, maximum: 3 }
explanation: { type: string }
highlights:
type: array
maxItems: 5
items:
allOf:
- $ref: '#/components/schemas/Moment'
- { type: object, properties: { label: { type: string, nullable: true } } }
Insights:
type: object
properties:
daily:
type: array
description: "Questions per day, last 14 days (India time), oldest first"
items:
type: object
properties:
day: { type: string, format: date }
answered: { type: integer }
unanswered: { type: integer }
topSessions:
type: array
description: "Sessions cited by answers in the last 30 days, most first (published only in the demo Studio)"
items:
type: object
properties:
lectureId: { type: string, format: uuid }
title: { type: string }
answers: { type: integer }
totals:
type: object
properties:
questions: { type: integer }
answeredRate: { type: number, minimum: 0, maximum: 1 }
shares: { type: integer }
gaps:
type: array
items: { type: object, properties: { question: { type: string }, times: { type: integer }, lastAsked: { type: string, format: date-time } } }
topQuestions:
type: array
items: { type: object, properties: { question: { type: string }, times: { type: integer }, answered: { type: integer } } }
topMoments:
type: array
items:
type: object
properties:
segmentId: { type: integer }
lectureId: { type: string, format: uuid }
title: { type: string }
startS: { type: number }
text: { type: string }
opens: { type: integer }
shares: { type: integer }
paths:
/organizer/session:
post:
summary: Organizer sign-in with passcode
operationId: signIn
security: [] # public
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [passcode]
properties:
passcode: { type: string }
responses:
'204': { description: Signed in; sets pravaha_org cookie }
'400': { description: Invalid body, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'401': { description: Wrong passcode, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
delete:
summary: Organizer sign-out
operationId: signOut
security: [] # public
responses:
'204': { description: Cookie cleared }
/upload-signature:
post:
summary: Sign a CldUploadWidget upload for an existing lecture (allow-listed params only)
description: "Organizer preset: organizer only. Trial preset: public, only for a fresh trial row (POST /trials)."
operationId: signUpload
security: [{ organizerCookie: [] }, {}]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [paramsToSign]
properties:
paramsToSign:
type: object
description: "Only public_id, upload_preset, timestamp, source"
additionalProperties: true
responses:
'200':
description: Signature
content:
application/json:
schema:
type: object
properties:
signature: { type: string }
'400': { description: Unsignable params or unknown lecture }
'401': { description: Not an organizer }
/trials:
post:
summary: Start a public trial session (first 60 s kept, unlisted, deleted after 24 h)
operationId: startTrial
security: [] # public, rate-limited
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [title, rightsConfirmed]
properties:
title: { type: string, minLength: 1, maxLength: 140 }
rightsConfirmed: { type: boolean, enum: [true] }
responses:
'201':
description: Trial created; upload with the returned preset
content:
application/json:
schema:
type: object
properties:
lecture: { $ref: '#/components/schemas/Lecture' }
uploadPreset: { type: string }
'400': { description: Invalid body }
'429': { description: Per-network or daily trial limit reached, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'503': { description: Trials are switched off (TRIALS_PER_DAY=0) }
/webhooks/cloudinary:
post:
summary: Cloudinary notification (transcription complete/failed)
operationId: cloudinaryWebhook
security: [{ cloudinarySignature: [] }]
requestBody:
required: true
content:
application/json:
schema: { type: object, additionalProperties: true }
responses:
'200': { description: Received (including unknown public_id / ignored kinds) }
'401': { description: Invalid or stale signature — no side effects }
'500': { description: DB write failed — Cloudinary retries }
/lectures:
post:
summary: Create a session before upload (rights confirmation required)
operationId: createLecture
security: [{ organizerCookie: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [title, rightsConfirmed]
properties:
title: { type: string, minLength: 1, maxLength: 140 }
speaker: { type: string, maxLength: 80 }
rightsConfirmed: { type: boolean, enum: [true] }
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
lecture: { $ref: '#/components/schemas/Lecture' }
uploadPreset: { type: string }
'400': { description: Invalid body }
'401': { description: Not an organizer }
get:
summary: List sessions (public+ready for learners; all for organizers)
operationId: listLectures
security: [] # public
responses:
'200':
description: Sessions
content:
application/json:
schema: { type: array, items: { $ref: '#/components/schemas/Lecture' } }
/lectures/{id}:
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
get:
summary: One session (unlisted reachable by direct link)
operationId: getLecture
security: [] # public
responses:
'200': { description: Session, content: { application/json: { schema: { $ref: '#/components/schemas/Lecture' } } } }
'404': { description: Not found }
patch:
summary: Update visibility / title / speaker
operationId: updateLecture
security: [{ organizerCookie: [] }]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
visibility: { type: string, enum: [public, unlisted] }
title: { type: string, minLength: 1, maxLength: 140 }
speaker: { type: string, maxLength: 80 }
responses:
'200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/Lecture' } } } }
'400': { description: Invalid body }
'401': { description: Not an organizer }
'404': { description: Not found }
/search:
get:
summary: Find — full-text search over spoken content
operationId: search
security: [] # public
parameters:
- { name: q, in: query, required: true, schema: { type: string, minLength: 2, maxLength: 200 } }
- { name: lectureId, in: query, required: false, schema: { type: string, format: uuid } }
responses:
'200':
description: Results
content:
application/json:
schema:
type: object
properties:
results: { type: array, items: { $ref: '#/components/schemas/SearchResult' } }
exact: { type: boolean, description: "false: no moment had every word, so results are the closest matches (any of the words)" }
'400': { description: Invalid query }
/ask:
post:
summary: Ask — grounded answer with validated clip citations
operationId: ask
security: [] # public
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [question]
properties:
question: { type: string, minLength: 3, maxLength: 300 }
lectureId: { type: string, format: uuid }
responses:
'200':
description: Answer, refusal, or fallback
content:
application/json:
schema:
type: object
properties:
status: { type: string, enum: [answered, not_found, fallback] }
answer: { type: string, nullable: true }
citations: { type: array, items: { $ref: '#/components/schemas/Citation' } }
reel: { $ref: '#/components/schemas/Reel' }
followUps: { type: array, maxItems: 3, items: { type: string, maxLength: 120 }, description: 'Suggested next questions (answered only)' }
answerId: { type: string, nullable: true, pattern: '^[A-Za-z0-9_-]{10}$', description: 'Stored answer for the share page /a/{answerId} (answered only; null if it could not be stored)' }
application/x-ndjson:
schema:
type: string
description: >
Sent when the request has Accept: application/x-ndjson. One JSON event per line:
{"type":"retrieved","moments":n,"sessions":[...]}, then {"type":"writing"} if the model is called,
then {"type":"result", ...the application/json body}, or {"type":"error"}.
'400': { description: Invalid body }
'429': { description: Rate limited (Retry-After header) }
/lectures/{id}/study-pack:
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
post:
summary: (Re)generate a session's Study Pack (also generated automatically after ingest)
operationId: generateStudyPack
security: [{ organizerCookie: [] }]
responses:
'200': { description: Study Pack, content: { application/json: { schema: { $ref: '#/components/schemas/StudyPack' } } } }
'400': { description: 'Session not ready, or too short' }
'401': { description: Not an organizer }
'404': { description: Not found }
'502': { description: Every model in the AI chain failed }
'503': { description: GEMINI_API_KEY not configured }
/events:
post:
summary: Anonymous Moment analytics (open / share) for organizer Insights
operationId: recordMomentEvent
security: [] # public
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [segmentId, kind]
properties:
segmentId: { type: integer, minimum: 1 }
kind: { type: string, enum: [open, share] }
responses:
'204': { description: Recorded }
'400': { description: Invalid body }
'404': { description: Unknown segment }
/answers/{id}/feedback:
post:
summary: Anonymous helpful / not helpful vote on a stored answer, for organizer Insights
operationId: recordAnswerFeedback
security: [] # public
parameters:
- { name: id, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z0-9_-]{10}$' } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [helpful]
properties:
helpful: { type: boolean }
responses:
'204': { description: Recorded }
'400': { description: Invalid body }
'404': { description: Unknown answer }
/insights:
get:
summary: Organizer Insights over the last 30 days (knowledge gaps, most asked, most shared)
description: "Public while the demo Studio is on (STUDIO_DEMO): link-like questions and Moments from unpublished sessions are left out."
operationId: getInsights
security: [{ organizerCookie: [] }, {}]
responses:
'200': { description: Insights, content: { application/json: { schema: { $ref: '#/components/schemas/Insights' } } } }
'401': { description: Not an organizer and the demo Studio is off }
/insights/export:
get:
summary: One Insights report as a CSV download, each list in full (up to 1000 rows)
description: "Same access as /insights. Cells that start with = + - @ get a leading ' (CSV injection)."
operationId: exportInsights
security: [{ organizerCookie: [] }, {}]
parameters:
- { name: report, in: query, required: true, schema: { type: string, enum: [gaps, questions, moments, daily, sessions] } }
responses:
'200': { description: 'CSV with a UTF-8 byte-order mark, as an attachment', content: { text/csv: { schema: { type: string } } } }
'400': { description: Unknown report, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'401': { description: Not an organizer and the demo Studio is off }
/concepts:
get:
summary: The concept map, every concept the published library teaches
operationId: listConcepts
security: [{}]
responses:
'200': { description: Concepts, most-taught first, content: { application/json: { schema: { type: array, items: { type: object, properties: { key: { type: string }, name: { type: string }, sessions: { type: integer } } } } } } }
/concepts/{key}:
get:
summary: One concept and the moments, across sessions, that explain it
operationId: getConcept
security: [{}]
parameters:
- { name: key, in: path, required: true, schema: { type: string, pattern: '^[a-z0-9-]{1,80}$' } }
responses:
'200': { description: The concept and up to 5 moments, content: { application/json: { schema: { type: object } } } }
'404': { description: No such concept, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/organizer/index:
post:
summary: Tag every session's Cloudinary asset and read the tags back with the Search API
operationId: syncCloudinaryIndex
security: [{ organizerCookie: [] }]
responses:
'200': { description: Sync result, content: { application/json: { schema: { type: object } } } }
'401': { description: Not an organizer }
get:
summary: The tagged assets, from Cloudinary's Search API
operationId: listCloudinaryIndex
security: [{ organizerCookie: [] }]
responses:
'200': { description: Assets with tags and contextual metadata, content: { application/json: { schema: { type: object } } } }
'401': { description: Not an organizer }
/lectures/{id}/notes:
get:
summary: A session's study notes, every point linked to its timestamp
operationId: getStudyNotes
security: [{}]
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
- { name: format, in: query, required: false, schema: { type: string, enum: [md] }, description: 'md returns Markdown instead of JSON' }
- { name: download, in: query, required: false, schema: { type: string }, description: 'With format=md, send as an attachment' }
responses:
'200': { description: Notes as JSON, or Markdown with format=md, content: { application/json: { schema: { type: object } }, text/markdown: { schema: { type: string } } } }
'404': { description: No notes for this session, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
/status:
get:
summary: Live status, the database, every AI model, Cloudinary credits and library size
operationId: getStatus
security: [{}]
responses:
'200': { description: Operational or degraded, content: { application/json: { schema: { type: object } } } }
'503': { description: Outage, the database is unreachable, content: { application/json: { schema: { type: object } } } }