/** * SDK TypeScript mínimo do Camile Creative Intelligence (CCI). * Funciona em Node 18+ e browsers modernos (fetch nativo). */ export const DEFAULT_BASE = "https://api.camileai.com/creative/v1"; export class CCIError extends Error { status: number; code: string; details?: unknown; constructor(status: number, code: string, message: string, details?: unknown) { super(`${status} ${code}: ${message}`); this.status = status; this.code = code; this.details = details; } } export type Json = Record; export class CciClient { constructor( private readonly apiKey: string, private readonly baseUrl: string = DEFAULT_BASE, ) { if (!apiKey) throw new Error("apiKey obrigatória"); } private headers(idempotencyKey?: string): Record { const h: Record = { "x-api-key": this.apiKey, "content-type": "application/json", accept: "application/json", }; if (idempotencyKey) h["idempotency-key"] = idempotencyKey; return h; } private async request(method: string, path: string, body?: unknown, idempotencyKey?: string): Promise { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: this.headers(idempotencyKey), body: body === undefined ? undefined : JSON.stringify(body), }); const data = await res.json().catch(() => ({ ok: false, code: "invalid_response", error: res.statusText })); if (!res.ok || data.ok === false) { throw new CCIError(res.status, data.code ?? "error", data.error ?? "erro desconhecido", data.details); } return data as T; } capabilities() { return this.request("GET", "/capabilities"); } templates() { return this.request("GET", "/templates"); } fonts() { return this.request("GET", "/fonts"); } analyze(brief: Json) { return this.request("POST", "/briefs/analyze", { brief }); } createDesign(input: { brief?: Json; ir?: Json; template?: string; render?: { format?: string }; title?: string; idempotencyKey?: string; }) { const body: Json = { render: input.render ?? { format: "png" } }; if (input.brief) body.brief = input.brief; if (input.ir) body.ir = input.ir; if (input.template) body.template = input.template; if (input.title) body.title = input.title; return this.request("POST", "/designs", body, input.idempotencyKey); } getDesign(designId: string) { return this.request("GET", `/designs/${designId}`); } listDesigns(limit = 25) { return this.request("GET", `/designs?limit=${limit}`); } updateDesign(designId: string, input: { brief?: Json; ir?: Json; template?: string; render?: { format?: string }; title?: string; idempotencyKey?: string }) { const body: Json = { render: input.render ?? { format: "png" } }; if (input.brief) body.brief = input.brief; if (input.ir) body.ir = input.ir; if (input.template) body.template = input.template; if (input.title) body.title = input.title; return this.request("PATCH", `/designs/${designId}`, body, input.idempotencyKey); } revisions(designId: string) { return this.request("GET", `/designs/${designId}/revisions`); } getRevision(designId: string, revision: number) { return this.request("GET", `/designs/${designId}/revisions/${revision}`); } render(designId: string, format = "png", width?: number, height?: number) { return this.request("POST", `/designs/${designId}/render`, { format, width, height }); } campaign(brief: Json, formats: Json[], idempotencyKey?: string) { return this.request("POST", "/campaigns", { brief, formats }, idempotencyKey); } campaignFull(brief: Json, formats: Json[], opts: { video?: Json; idempotencyKey?: string } = {}) { const body: Json = { brief, formats }; if (opts.video) body.video = opts.video; return this.request("POST", "/campaigns/full", body, opts.idempotencyKey); } animate(designId: string, opts: { duration?: number; motion?: string; transition?: string; width?: number; height?: number; fps?: number; audioAssetId?: string; audioVolume?: number; audioLoop?: boolean; audioFadeIn?: number; audioFadeOut?: number; audioNormalize?: boolean; callbackUrl?: string; idempotencyKey?: string; } = {}) { const body: Json = { duration: opts.duration ?? 4, motion: opts.motion ?? "zoom_in", transition: opts.transition ?? "none", }; if (opts.width) body.width = opts.width; if (opts.height) body.height = opts.height; if (opts.fps) body.fps = opts.fps; if (opts.audioAssetId) { body.audio_asset_id = opts.audioAssetId; body.audio_volume = opts.audioVolume ?? 1; body.audio_loop = opts.audioLoop ?? true; body.audio_fade_in = opts.audioFadeIn ?? 0; body.audio_fade_out = opts.audioFadeOut ?? 1; body.audio_normalize = opts.audioNormalize ?? false; } if (opts.callbackUrl) body.callback_url = opts.callbackUrl; return this.request("POST", `/designs/${designId}/animate`, body, opts.idempotencyKey); } video(scenes: Json[], opts: { width?: number; height?: number; fps?: number; audio?: Json; beatSync?: boolean; formats?: string[]; callbackUrl?: string; idempotencyKey?: string } = {}) { const body: Json = { scenes, width: opts.width ?? 1080, height: opts.height ?? 1080, fps: opts.fps ?? 24, }; if (opts.audio) body.audio = opts.audio; if (opts.beatSync) body.beat_sync = true; if (opts.formats) body.formats = opts.formats; if (opts.callbackUrl) body.callback_url = opts.callbackUrl; return this.request("POST", "/videos", body, opts.idempotencyKey); } transformAsset(assetId: string, operations: Json[], opts: { outputFormat?: string; quality?: number; background?: string; filename?: string; idempotencyKey?: string; } = {}) { const body: Json = { operations, output_format: opts.outputFormat ?? "png", quality: opts.quality ?? 92, background: opts.background ?? "#FFFFFF", }; if (opts.filename) body.filename = opts.filename; return this.request("POST", `/assets/${assetId}/transform`, body, opts.idempotencyKey); } async uploadAsset(file: Blob, filename = "asset", audio = false): Promise { const form = new FormData(); form.append("file", file, filename); const res = await fetch(`${this.baseUrl}${audio ? "/assets/audio" : "/assets"}`, { method: "POST", headers: { "x-api-key": this.apiKey, accept: "application/json" }, body: form, }); const data = await res.json().catch(() => ({ ok: false, code: "invalid_response", error: res.statusText })); if (!res.ok || data.ok === false) { throw new CCIError(res.status, data.code ?? "error", data.error ?? "erro desconhecido", data.details); } return data as Json; } uploadAudio(file: Blob, filename = "audio") { return this.uploadAsset(file, filename, true); } listAssets(limit = 25) { return this.request("GET", `/assets?limit=${limit}`); } getAsset(assetId: string) { return this.request("GET", `/assets/${assetId}`); } addNode(designId: string, node: Json, opts: { parentId?: string; index?: number; render?: { format?: string }; idempotencyKey?: string } = {}) { const body: Json = { node, render: opts.render ?? { format: "png" } }; if (opts.parentId) body.parent_id = opts.parentId; if (opts.index !== undefined) body.index = opts.index; return this.request("POST", `/designs/${designId}/nodes`, body, opts.idempotencyKey); } updateNode(designId: string, nodeId: string, patch: Json, opts: { render?: { format?: string }; idempotencyKey?: string } = {}) { return this.request("PATCH", `/designs/${designId}/nodes/${nodeId}`, { patch, render: opts.render ?? { format: "png" } }, opts.idempotencyKey); } removeNode(designId: string, nodeId: string, opts: { format?: string; idempotencyKey?: string } = {}) { return this.request("DELETE", `/designs/${designId}/nodes/${nodeId}?format=${opts.format ?? "png"}`, undefined, opts.idempotencyKey); } duplicateDesign(designId: string, opts: { title?: string; render?: { format?: string }; idempotencyKey?: string } = {}) { const body: Json = { render: opts.render ?? { format: "png" } }; if (opts.title) body.title = opts.title; return this.request("POST", `/designs/${designId}/duplicate`, body, opts.idempotencyKey); } async bundle(designId: string): Promise { const res = await fetch(`${this.baseUrl}/designs/${designId}/bundle`, { headers: { "x-api-key": this.apiKey } }); if (!res.ok) throw new CCIError(res.status, "bundle_failed", res.statusText); return res.blob(); } async preview(designId: string, width = 480, format = "png"): Promise { const res = await fetch(`${this.baseUrl}/designs/${designId}/preview?width=${width}&format=${format}`, { headers: { "x-api-key": this.apiKey } }); if (!res.ok) throw new CCIError(res.status, "preview_failed", res.statusText); return res.blob(); } diffRevisions(designId: string, fromRevision: number, toRevision: number) { return this.request("GET", `/designs/${designId}/revisions/${fromRevision}/diff/${toRevision}`); } restoreRevision(designId: string, revision: number, opts: { format?: string; idempotencyKey?: string } = {}) { return this.request("POST", `/designs/${designId}/revisions/${revision}/restore?format=${opts.format ?? "png"}`, undefined, opts.idempotencyKey); } materializeLayout(ir: Json) { return this.request("POST", "/layouts/materialize", { ir }); } estimate(operation: string, params: Json = {}) { return this.request("POST", "/billing/estimate", { operation, params }); } priceBook() { return this.request("GET", "/billing/price-book"); } billingPlan() { return this.request("GET", "/billing/plan"); } ledger(limit = 50) { return this.request("GET", `/billing/ledger?limit=${limit}`); } reservations(limit = 50) { return this.request("GET", `/billing/reservations?limit=${limit}`); } providers() { return this.request("GET", "/providers"); } generateImage(prompt: string, opts: { width?: number; height?: number; seed?: number; provider?: string; idempotencyKey?: string } = {}) { const body: Json = { prompt, width: opts.width ?? 1024, height: opts.height ?? 1024 }; if (opts.seed !== undefined) body.seed = opts.seed; if (opts.provider) body.provider = opts.provider; return this.request("POST", "/generations", body, opts.idempotencyKey); } planBrief(goal: string, opts: { tone?: string; language?: string; format?: Json; provider?: string; idempotencyKey?: string } = {}) { const body: Json = { goal, tone: opts.tone ?? "", language: opts.language ?? "pt-BR" }; if (opts.format) body.format = opts.format; if (opts.provider) body.provider = opts.provider; return this.request("POST", "/planning/brief", body, opts.idempotencyKey); } metrics() { return this.request("GET", "/metrics"); } async waitJob(jobId: string, opts: { timeoutMs?: number; intervalMs?: number } = {}) { const deadline = Date.now() + (opts.timeoutMs ?? 300_000); const interval = opts.intervalMs ?? 1000; while (Date.now() < deadline) { const job = await this.job(jobId) as Json; if (["succeeded", "failed", "cancelled"].includes(String(job.status))) return job; await new Promise((resolve) => setTimeout(resolve, interval)); } throw new Error(`job ${jobId} não terminou no tempo limite`); } job(jobId: string) { return this.request("GET", `/jobs/${jobId}`); } retryJob(jobId: string) { return this.request("POST", `/jobs/${jobId}/retry`); } cancelJob(jobId: string) { return this.request("POST", `/jobs/${jobId}/cancel`); } usage(days = 30) { return this.request("GET", `/usage/summary?days=${days}`); } signedUrl(artifactId: string, ttl = 3600) { return this.request("GET", `/artifacts/${artifactId}/signed-url?ttl=${ttl}`); } async download(artifactId: string): Promise { const res = await fetch(`${this.baseUrl}/artifacts/${artifactId}/content`, { headers: this.headers() }); if (!res.ok) throw new CCIError(res.status, "download_failed", await res.text()); return res.blob(); } // ----- procedural, brand, workflow e gateway ----- proceduralStyles() { return this.request("GET", "/procedural/styles"); } proceduralRender(opts: { style?: string; seed?: number; width?: number; height?: number; density?: number; phase?: number; title?: string; eyebrow?: string; tone?: string; palette?: string; format?: string; idempotencyKey?: string; } = {}) { const body: Json = { style: opts.style ?? "mesh", seed: opts.seed ?? 7, width: opts.width ?? 1080, height: opts.height ?? 1080, density: opts.density ?? 1, phase: opts.phase ?? 0, format: opts.format ?? "png", }; for (const key of ["title", "eyebrow", "tone", "palette"] as const) { if (opts[key]) body[key] = opts[key]; } return this.request("POST", "/procedural/render", body, opts.idempotencyKey); } proceduralVideo(opts: { styles: string[]; duration?: number; fps?: number; width?: number; height?: number; seed?: number; density?: number; title?: string; eyebrow?: string; tone?: string; palette?: string; transition?: string; formats?: string[]; idempotencyKey?: string; }) { const body: Json = { styles: opts.styles, duration: opts.duration ?? 3, fps: opts.fps ?? 24, width: opts.width ?? 1080, height: opts.height ?? 1080, seed: opts.seed ?? 7, density: opts.density ?? 1, transition: opts.transition ?? "fade", }; for (const key of ["title", "eyebrow", "tone", "palette", "formats"] as const) { if (opts[key] !== undefined) body[key] = opts[key]; } return this.request("POST", "/videos/procedural", body, opts.idempotencyKey); } brandKit() { return this.request("GET", "/brand-kit"); } setBrandKit(opts: { name?: string; colors?: string[]; fonts?: Json; logoAssetId?: string; logoPosition?: string; logoSize?: number; logoMargin?: number; tone?: string; palette?: string; metadata?: Json; } = {}) { const body: Json = { name: opts.name ?? "", colors: opts.colors ?? [], logo_position: opts.logoPosition ?? "bottom_right", tone: opts.tone ?? "", palette: opts.palette ?? "", }; if (opts.fonts) body.fonts = opts.fonts; if (opts.logoAssetId) body.logo_asset_id = opts.logoAssetId; if (opts.logoSize !== undefined) body.logo_size = opts.logoSize; if (opts.logoMargin !== undefined) body.logo_margin = opts.logoMargin; if (opts.metadata) body.metadata = opts.metadata; return this.request("PUT", "/brand-kit", body); } deleteBrandKit() { return this.request("DELETE", "/brand-kit"); } async exportHtml(designId: string): Promise { const res = await fetch(`${this.baseUrl}/designs/${designId}/export/html`, { headers: this.headers() }); if (!res.ok) throw new CCIError(res.status, "export_failed", await res.text()); return res.text(); } briefFromImage(assetId: string, opts: { goal?: string; language?: string; tone?: string; provider?: string; idempotencyKey?: string; } = {}) { const body: Json = { asset_id: assetId, goal: opts.goal ?? "", language: opts.language ?? "pt-BR", tone: opts.tone ?? "", }; if (opts.provider) body.provider = opts.provider; return this.request("POST", "/briefs/from-image", body, opts.idempotencyKey); } workflow(steps: Json[], opts: { outputs?: string[]; idempotencyKey?: string } = {}) { const body: Json = { steps }; if (opts.outputs) body.outputs = opts.outputs; return this.request("POST", "/workflows/run", body, opts.idempotencyKey); } gatewayStatus() { return this.request("GET", "/billing/gateway/status"); } checkout(planId: string, opts: { successUrl?: string; cancelUrl?: string } = {}) { const body: Json = { plan_id: planId }; if (opts.successUrl) body.success_url = opts.successUrl; if (opts.cancelUrl) body.cancel_url = opts.cancelUrl; return this.request("POST", "/billing/gateway/checkout", body); } portal(opts: { returnUrl?: string } = {}) { const body: Json = {}; if (opts.returnUrl) body.return_url = opts.returnUrl; return this.request("POST", "/billing/gateway/portal", body); } }