SPB Git

Repository Public

route.ts 6.7 KB 35 lines typescript

1const str = { type: "string" }, num = { type: "number" }, bool = { type: "boolean" };2const film = { type: "object", properties: { id: str, slug: str, title: str, synopsis: str, takeaway: str, language: str, kind: str, duration: num, video: str, videoLow: str, poster: str, thumb: str, publishedAt: str, likes: num, views: num } };3const ok = (description: string, schema: object = { type: "object" }) => ({ description, content: { "application/json": { schema } } });4const err = { "401": { description: "Not signed in" }, "403": { description: "Role too low, or cross-origin request" }, "409": { description: "Not allowed in the current state (budget, kill switch, status)" }, "429": { description: "Rate limited" } };5const localeParam = { name: "locale", in: "query", schema: { type: "string", enum: ["en", "fr"], default: "en" } };6const idParam = { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } };7const body = (properties: object, required: string[] = []) => ({ content: { "application/json": { schema: { type: "object", properties, required } } } });89const doc = {10  openapi: "3.1.0",11  info: { title: "CLIP API", version: "1.0.0", description: "Public catalogue API (no authentication) and studio API (session cookie, same-origin, role-based). Public responses never contain prompts or costs." },12  servers: [{ url: "https://clip.spboucher.ai" }],13  components: { securitySchemes: { session: { type: "apiKey", in: "cookie", name: "clip_session" } } },14  paths: {15    "/api/films": { get: { summary: "Published films, newest first", parameters: [localeParam, { name: "cursor", in: "query", schema: str }, { name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 48, default: 12 } }, { name: "series", in: "query", schema: str }, { name: "tag", in: "query", schema: str }, { name: "language", in: "query", schema: str }, { name: "kind", in: "query", schema: str }, { name: "maxDuration", in: "query", schema: num }, { name: "sort", in: "query", schema: { type: "string", enum: ["latest", "top"] } }], responses: { "200": ok("A page of films", { type: "object", properties: { films: { type: "array", items: film }, nextCursor: { type: ["string", "null"] } } }) } } },16    "/api/films/{slug}": { get: { summary: "One film with provenance, sources and explained recommendations", parameters: [{ name: "slug", in: "path", required: true, schema: str }, localeParam], responses: { "200": ok("Film"), "404": { description: "Unknown or withdrawn film" } } } },17    "/api/films/{slug}/react": { post: { summary: "Anonymous like / unlike", requestBody: body({ like: bool }, ["like"]), responses: { "200": ok("New like count", { type: "object", properties: { likes: num } }) } } },18    "/api/series": { get: { summary: "Editorial series", parameters: [localeParam], responses: { "200": ok("Series") } } },19    "/api/search": { get: { summary: "Explainable hybrid search", parameters: [{ name: "q", in: "query", required: true, schema: str }, localeParam], responses: { "200": ok("Results with the signals that matched") } } },20    "/api/events": { post: { summary: "Anonymous view / complete / share counter", requestBody: body({ filmId: str, kind: { type: "string", enum: ["view", "complete", "share"] }, locale: str }, ["filmId", "kind"]), responses: { "204": { description: "Recorded" } } } },21    "/api/reports": { post: { summary: "Report a problem with a film", requestBody: body({ film: str, reason: str, detail: str, contact: str }, ["detail"]), responses: { "201": ok("Filed") } } },22    "/api/health": { get: { summary: "Dependency health", responses: { "200": ok("ok or degraded"), "503": { description: "down" } } } },23    "/api/studio/concepts": { post: { summary: "Create a project from a brief and queue four scored pitches", security: [{ session: [] }], parameters: [{ name: "Idempotency-Key", in: "header", schema: str }], requestBody: body({ theme: str, objective: str, audience: str, mood: str, genre: str, constraints: str, language: { type: "string", enum: ["en", "fr", "bilingual"] }, seriesSlug: str, shots: { type: "integer", minimum: 2, maximum: 6 }, mode: { type: "string", enum: ["manual", "assisted", "autonomous"] }, quality: { type: "string", enum: ["draft", "master"] } }, ["theme"]), responses: { "202": ok("Queued", { type: "object", properties: { projectId: str, duplicate: bool } }), ...err } } },24    "/api/studio/projects": { get: { summary: "Recent projects", security: [{ session: [] }], responses: { "200": ok("Projects"), ...err } }, post: { summary: "Create a project from a single concept", security: [{ session: [] }], parameters: [{ name: "Idempotency-Key", in: "header", schema: str }], requestBody: body({ theme: str }, ["theme"]), responses: { "201": ok("Created"), ...err } } },25    "/api/studio/projects/{id}/generate": { post: { summary: "Run a pipeline stage (or the next one)", security: [{ session: [] }], parameters: [idParam], requestBody: body({ stage: { type: "string", enum: ["concept", "script", "storyboard", "keyframes", "voice", "video", "assemble", "qa"] }, fresh: bool }), responses: { "202": ok("Queued; repeating the call returns the same job", { type: "object", properties: { jobId: str, stage: str, duplicate: bool } }), ...err } } },26    "/api/studio/shots/{id}/regenerate": { post: { summary: "Regenerate the keyframe, clip or voice of one shot (capped)", security: [{ session: [] }], parameters: [idParam], requestBody: body({ what: { type: "string", enum: ["image", "video", "voice"] }, approve: bool }, ["what"]), responses: { "202": ok("Queued"), ...err } } },27    "/api/studio/jobs/{id}": { get: { summary: "State of a stage run and its provider calls", security: [{ session: [] }], parameters: [idParam], responses: { "200": ok("Job"), ...err } } },28    "/api/studio/films/{id}/approve": { post: { summary: "Approve the current cut (id = project id)", security: [{ session: [] }], parameters: [idParam], requestBody: body({ override: bool, note: str }), responses: { "200": ok("Approved"), ...err } } },29    "/api/studio/films/{id}/publish": { post: { summary: "Publish or schedule an approved cut (id = project id)", security: [{ session: [] }], parameters: [idParam], requestBody: body({ scheduleFor: { type: "string", format: "date-time" }, featured: bool }), responses: { "200": ok("Published or scheduled"), ...err } } },30    "/api/studio/costs": { get: { summary: "Spend, ceilings, estimated vs actual", security: [{ session: [] }], responses: { "200": ok("Costs"), ...err } } },31    "/api/studio/kill-switch": { post: { summary: "Stop or allow new paid jobs", security: [{ session: [] }], requestBody: body({ on: bool }, ["on"]), responses: { "200": ok("State", { type: "object", properties: { killSwitch: bool, inFlight: num } }), ...err } } },32  },33};34export function GET() { return Response.json(doc, { headers: { "Cache-Control": "public, max-age=3600" } }); }35
Files main
Menu

Theme