# Creații (/ro/docs/gateway/creatives)

Documente creative editabile în codai Studio — creezi un video, o imagine sau o prezentare ca JSON, le editezi prin operații, păstrezi fiecare versiune și le randezi în MP4.

O **creație** este un singur document editabil pentru fiecare produs Studio (video, imagine, prezentare, document sau foaie de calcul), păstrat ca JSON simplu — *Editable Creative Document* (ECD). Agenții și editoarele nu rescriu niciodată HTML: trimit **operații** mici, adresate prin id. Fiecare editare adaugă o versiune, deci poți oricând să te uiți înapoi, să revii sau să copiezi. Intrarea pentru randare este o funcție pură și deterministă a documentului.

Crearea, citirea și editarea creațiilor sunt **gratuite**. Costă doar [randările](./renders.mdx) și cadrele statice peste cota zilnică gratuită.

| Metodă | Cale | Rezultat |
| --- | --- | --- |
| `POST` | `/v1/studio/creatives` | Creație nouă la versiunea 1 (`201`) |
| `GET` | `/v1/studio/creatives?kind=&before=&limit=` | `{ object: "list", data, next_before }`, cele modificate recent primele |
| `GET` | `/v1/studio/creatives/{id}?seq=` | Documentul curent (sau de la `seq`) + asset-urile semnate |
| `GET` | `/v1/studio/creatives/{id}/outline` | Schiță text compactă, formate și variabile |
| `POST` | `/v1/studio/creatives/{id}/find` | Id-urile nodurilor care se potrivesc |
| `POST` | `/v1/studio/creatives/{id}/subtree` | JSON-ul complet al unor noduri sau al unei scene |
| `POST` | `/v1/studio/creatives/{id}/ops` | Aplică un lot de operații → versiune nouă |
| `GET` | `/v1/studio/creatives/{id}/versions` | Istoricul versiunilor |
| `POST` | `/v1/studio/creatives/{id}/revert` | Versiune curentă nouă cu documentul unei versiuni vechi |
| `POST` | `/v1/studio/creatives/{id}/fork` | Creație nouă dintr-o versiune (`201`) |
| `POST` | `/v1/studio/creatives/{id}/lint` | Contrast, zonă sigură, brand, accesibilitate, asset-uri lipsă… |
| `GET` | `/v1/studio/creatives/{id}/compiled?format=&scene=` | HTML de previzualizare cu URL-uri semnate |
| `POST` | `/v1/studio/creatives/{id}/render` | Randare MP4 (`202`, `codai-render`; `200` când randarea identică există deja) |
| `POST` | `/v1/studio/creatives/{id}/frame` | Cadre PNG statice la cel mult 6 momente (`200` sau `202` + un job de interogat) |
| `GET` | `/v1/studio/creatives/{id}/frames/{jobId}` | Interoghează un job de cadre (`200` cadre, `202` încă rulează) |
| `POST` | `/v1/studio/creatives/{id}/comments` | Transformă un comentariu pe un cadru într-o editare (facturat pe tokeni) |
| `POST` | `/v1/studio/creatives/{id}/variants` | Generează variante A/B ca derivate + randări draft (facturat pe tokeni) |
| `GET` | `/v1/studio/creatives/{id}/variants` | Ultimul grup de variante, cu stări și starea randării |
| `POST` | `/v1/studio/creatives/{id}/variants/{variantId}/pick` | Aplică varianta câștigătoare pe original |
| `DELETE` | `/v1/studio/creatives/{id}` | Ștergere |
| `POST` | `/v1/studio/brand-kits` | Creează un kit de brand din tokeni sau dintr-un fișier DTCG (`201`) |
| `POST` | `/v1/studio/brand-kits/extract` | Kit de brand (paletă, fonturi, logo) dintr-o pagină web sau dintr-o imagine de logo (`201`, facturat pe tokeni) |
| `GET` | `/v1/studio/brand-kits?limit=` | Kiturile tale de brand, cele mai noi primele |
| `GET` | `/v1/studio/brand-kits/{id}` | Un kit de brand cu URL semnat al logoului |
| `PATCH` | `/v1/studio/brand-kits/{id}` | Editează un kit de brand (nume, tokeni, DTCG, logo) |
| `GET` | `/v1/studio/brand-kits/{id}/dtcg` | Descarcă un kit de brand ca fișier DTCG `.tokens.json` |
| `DELETE` | `/v1/studio/brand-kits/{id}` | Șterge un kit de brand |
| `POST` | `/v1/studio/creatives/{id}/restyle` | Reaplică un kit de brand pe o creație → versiune nouă |
| `POST` | `/v1/studio/creatives/{id}/captions` | Subtitrări automate dintr-un clip de voce → versiune nouă (transcrierea se facturează pe minut) |
| `GET` | `/v1/studio/creatives/{id}/captions.srt?node_id=&seq=` | Descarcă subtitrările în format SubRip |
| `GET` | `/v1/studio/creatives/{id}/captions.vtt?node_id=&seq=` | Descarcă subtitrările în format WebVTT |
| `POST` | `/v1/studio/creatives/{id}/adapt` | Adaptează la preseturi de platformă (dimensiuni + zone sigure) → versiune nouă |
| `POST` | `/v1/studio/creatives/{id}/lint/fix` | Corectează automat problemele lint (gratuit; tură de agent plătită, opțională) |
| `GET` | `/v1/studio/creatives/{id}/controls` | Modelul panoului de ajustare: un control pentru fiecare variabilă |
| `GET` | `/v1/studio/creatives/{id}/locales?locale=&units=` | Limbile: progresul pe fiecare limbă și, la cerere, unitățile de traducere |
| `POST` | `/v1/studio/creatives/{id}/localize` | Traducere în cel mult 10 limbi → versiune nouă (facturată pe tokeni) |
| `POST` | `/v1/studio/creatives/{id}/locales/{locale}/fork` | Creație de sine stătătoare într-o singură limbă (`201`) |
| `POST` | `/v1/studio/creatives/{id}/template` | Salvează o creație ca șablon privat (`201`) |
| `GET` | `/v1/studio/templates?kind=&limit=` | Șabloanele tale, cele mai noi primele |
| `GET` | `/v1/studio/templates/{id}` | Un șablon cu documentul lui |
| `DELETE` | `/v1/studio/templates/{id}` | Șterge un șablon |
| `GET` | `/v1/studio/creatives/{id}/export.fcpxml?format=&seq=` | Pune în coadă o arhivă cu proiect FCPXML (`202`) |
| `GET` | `/v1/studio/creatives/{id}/export.lottie?format=&formats=&seq=&container=&download=` | Export Lottie JSON / dotLottie, gratuit și gata imediat (`200`) |
| `GET` | `/v1/studio/exports/{id}` | Starea exportului; URL semnat al arhivei după `succeeded` |
| `POST` | `/v1/studio/documents/{id}/creative` | Editează ca o creație o prezentare, un document sau o foaie de calcul (`201` sau `200` dacă e deja legată) |
| `POST` | `/v1/studio/creatives/{id}/build` | Generează dintr-o creație o versiune nouă PPTX / DOCX / XLSX |
| `POST` | `/v1/studio/creatives/{id}/shares` | Creează un link de vizualizare sau de revizuire (`201`, tokenul apare o singură dată) — vezi [Partajare, revizuire și remix](./sharing.mdx) |
| `GET` | `/v1/studio/creatives/{id}/shares` | Linkurile de partajare active |
| `DELETE` | `/v1/studio/creatives/{id}/shares/{shareId}` | Revocă un link de partajare (`204`) |
| `GET` | `/v1/studio/creatives/{id}/review` | Starea revizuirii și versiunea aprobată |
| `POST` | `/v1/studio/creatives/{id}/review` | Cere o revizuire sau revino la ciornă |
| `GET` | `/v1/studio/creatives/{id}/review/threads?status=` | Firele de comentarii ale revizuirii |
| `POST` | `/v1/studio/creatives/{id}/review/threads` | Fir nou de comentarii ancorat (`201`) |
| `POST` | `/v1/studio/creatives/{id}/review/threads/{threadId}/replies` | Răspunde într-un fir (`201`) |
| `POST` | `/v1/studio/creatives/{id}/review/threads/{threadId}/resolve` | Rezolvă un fir |
| `POST` | `/v1/studio/creatives/{id}/review/threads/{threadId}/reopen` | Redeschide un fir |
| `DELETE` | `/v1/studio/creatives/{id}/review/comments/{commentId}` | Șterge un comentariu de revizuire (`204`) |
| `POST` | `/v1/studio/shares/{token}/remix` | Copiază o creație partajată în contul tău (`201`) |

## Documentul [#documentul]

```json
{
  "schemaVersion": 1,
  "kind": "video",
  "title": "Teaser de lansare",
  "formats": [{ "id": "16x9", "width": 1920, "height": 1080 }],
  "defaultFormat": "16x9",
  "fps": 30,
  "brand": { "tokens": { "primary": "#ff5500" } },
  "vars": { "headline": { "type": "text", "value": "Salut, lume" } },
  "assets": { "3f2b8c1e-0000-4000-8000-000000000001": { "kind": "image", "mime": "image/png" } },
  "scenes": [
    {
      "id": "intro",
      "orderKey": "V",
      "durationMs": 4000,
      "nodes": [
        {
          "id": "title",
          "type": "text",
          "orderKey": "V",
          "layout": { "x": 200, "y": 400, "w": 1400, "h": 200 },
          "anim": [{ "preset": "fade-in" }],
          "props": { "spans": [{ "text": { "$var": "headline" } }], "size": 96, "color": { "$token": "primary" } }
        }
      ]
    }
  ],
  "audio": { "tracks": [] }
}
```

- **Scenele** rulează în ordinea `orderKey`; o scenă video durează între 0,5 și 30 s, iar un video cel mult 180 s.
- **Nodurile** sunt `text`, `shape`, `image`, `video`, `chart`, `captions`, `group` și `html_block`, fiecare cu un `id`, un `layout` și, opțional, `timing`, `anim` (preseturi precum `fade-in`, `slide-up`, `ken-burns`, `typewriter` sau cadre cheie) și suprascrieri per format (`overrides`).
- **Fișierele media** se referă la [asset-urile](./assets.mdx) tale prin id. `brand.tokens` și `vars` se referă ca `{ "$token": "…" }` și `{ "$var": "…" }`.
- Un document are cel mult **2 MB** de JSON canonic (`413 too_large`).

Fără `doc`, `POST /v1/studio/creatives` îți dă un document gol pentru tipul ales: un video 16:9 cu o scenă goală de 5 s, o imagine 1080×1080, o prezentare 16:9 cu un slide sau un document / o foaie de calcul goală.

```bash
curl https://ai.codai.ro/v1/studio/creatives \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "video", "title": "Teaser de lansare" }'
```

## Editarea prin operații [#editarea-prin-operații]

Trimite operațiile față de versiunea pe care ai citit-o (`base_seq`). Un lot se aplică în întregime sau deloc.

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/ops \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "base_seq": 3,
    "intent": "Titlu mai mare, fundal nou",
    "ops": [
      { "op": "setProps", "target": "title", "props": { "size": 120 } },
      { "op": "updateScene", "target": "intro", "patch": { "background": "#101010" } }
    ]
  }'
```

```json
{ "seq": 4, "doc_hash": "9a3f…", "changed": ["title", "intro"], "warnings": [], "rebased": false }
```

Cele 25 de operații: `setProps`, `setLayout`, `setTiming`, `setAnim`, `insertNode`, `removeNode`, `moveNode`, `addScene`, `removeScene`, `updateScene`, `moveScene`, `setVar`, `removeVar`, `setAsset`, `replaceAsset`, `setAudioClip`, `removeAudioClip`, `setBrand`, `restyle`, `adaptFormat`, `setControl`, `setLocaleText`, `setLocale`, `setLocalization` și `setMeta`. Traducerile se editează cu `setLocaleText`, `setLocale` și `setLocalization` (vezi [Localizare](#localizare)). Adaptarea la o platformă este un singur `adaptFormat` (vezi [Platforme](#platforme)); descrierea felului în care se editează o variabilă în panoul de ajustare este un singur `setControl` (vezi [Panoul de ajustare](#panoul-de-ajustare)). Schimbarea tokenilor de brand este un singur `setBrand`; reaplicarea unui brand pe culorile și fonturile literale este un singur `restyle` (vezi [Restilizarea unei creații](#restilizarea-unei-creații)); înlocuirea unei imagini peste tot este un singur `replaceAsset`.

### Editări simultane [#editări-simultane]

Dacă altcineva a editat creația după ce ai citit versiunea `base_seq`, gateway-ul îți **rebazează** operațiile: dacă niciuna dintre țintele și proprietățile pe care le atingi nu s-a schimbat între timp, se aplică peste versiunea curentă (`rebased: true`). Altfel primești **409** cu ce s-a suprapus — reîncarcă și încearcă din nou:

```json
{
  "error": {
    "code": "stale",
    "type": "invalid_request_error",
    "message": "The creative changed since base_seq and 1 change(s) overlap; reload and retry.",
    "head_seq": 5,
    "conflicts": [{ "target": "title", "head_prop": "props.size", "prop": "props.size" }]
  }
}
```

### Operații respinse [#operații-respinse]

O operație invalidă, un id necunoscut sau un nod **blocat** dau `400` cu `code`-ul operației (`invalid`, `not_found`, `locked`, `duplicate_id`), `path` din operație sau document și `op_index`. Nu se salvează nimic. Trimite `force: true` ca să editezi intenționat noduri și variabile blocate.

## Citire pentru agenți [#citire-pentru-agenți]

Documentele mari nu trebuie trimise întregi. `outline` întoarce câte o linie pentru fiecare scenă și nod (id-uri, tipuri, etichete, text tăiat la 60 de caractere, momente); `find` întoarce id-uri de noduri după text, tip, scenă, asset sau blocare; `subtree` întoarce JSON-ul complet doar pentru id-urile cerute.

## Versiuni [#versiuni]

Fiecare editare este o versiune; istoricul doar crește. `revert` adaugă o versiune nouă cu un document mai vechi (`intent: "revert to N"`), deci și anularea, și refacerea sunt reveniri. `fork` pornește o creație nouă din orice versiune. `GET …?seq=N` citește o versiune veche.

## Previzualizare și randare [#previzualizare-și-randare]

`GET …/compiled` întoarce compoziția HyperFrames deterministă, cu asset-urile tale ca URL-uri semnate valabile o oră, gata pentru un iframe `srcdoc` (`Cache-Control: private, no-store`). `POST …/render` compilează versiunea curentă și pune în coadă un `codai-render` obișnuit:

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/render \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "format": "16x9", "quality": "draft" }'
```

Răspunde `202` cu `render_id`; interoghează-l cu [`GET /v1/studio/renders/{id}`](./renders.mdx), care raportează `creative_id`. Randarea se facturează ca orice randare, o singură dată, după ce a reușit. Formatul trebuie să fie 1920×1080, 1080×1920, 1080×1080 sau 1080×1350 (4:5). Randarea funcționează pentru creațiile **video** (celelalte tipuri răspund `400 not_supported_for_kind`); compilarea previzualizează și creațiile de tip prezentare, document și foaie de calcul (vezi [Documente](#documente-prezentare-document-foaie-de-calcul)).

Randările sunt compuse din segmente per scenă: scenele care nu s-au schimbat de la o randare anterioară se refolosesc, așa că o nouă randare după o editare mică randează doar scenele atinse (taxarea rămâne pe secundă de ieșire). Dacă randezi din nou exact aceeași versiune, același format, aceeași calitate și aceleași fps, răspunsul este imediat **200** cu randarea existentă (`cached: true`, `status: "succeeded"`) și nu se taxează nimic; corpul `202` al unei randări noi are `cached: false`.

`ai_label` (`corner`, `end_card` sau `none`) imprimă în video o etichetă IA vizibilă, ca la [randări](./renders.mdx#eticheta-ia-regulamentul-ue-privind-ia-art-50-alin-4). Dacă lipsește, este `corner` când creația folosește narațiune TTS sau imagini generate cu IA, altfel `none`; `none` cu narațiune TTS răspunde `400 ai_label_required`. Conform Regulamentului UE privind IA (art. 50 alin. 4), tu, ca implementator, rămâi responsabil de etichetarea deepfake-urilor pe care le publici.

## Cadre statice [#cadre-statice]

`POST …/frame` întoarce cadre PNG statice ale creației la cel mult **6** momente, pentru un format (implicit formatul implicit al documentului) și o versiune (implicit cea curentă):

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/frame \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "t_ms": [0, 1500, 4200], "format": "16x9", "exact": false }'
```

| Câmp | Semnificație |
| --- | --- |
| `t_ms` | 1–6 momente în milisecunde, fiecare sub durata totală a creației (obligatoriu) |
| `format` | Un `formats[].id` (implicit formatul implicit al documentului) |
| `exact` | `false` (implicit): o captură rapidă a HTML-ului compilat. `true`: cadre decupate dintr-o randare ciornă a scenelor care acoperă acele momente — identice la pixel cu MP4-ul; umple și cache-ul de scene pentru randările următoare |
| `version` | Versiunea documentului (`seq`); implicit cea curentă |

Apelul așteaptă cel mult aproximativ 90 de secunde. Când cadrele sunt gata, răspunde **200**:

```json
{
  "object": "studio.creative.frames",
  "frame_job_id": "5e6f7a8b-0000-4000-8000-000000000040",
  "status": "succeeded",
  "frames": [{ "t_ms": 1500, "url": "https://…semnat, 1 h…", "width": 1920, "height": 1080, "cached": false }],
  "head_seq": 3,
  "billed_micro_usd": "0"
}
```

Altfel răspunde **202** cu `{ "object": "studio.creative.frame_job", "frame_job_id": "…", "status": "running", "head_seq": 3 }`; interoghează `GET /v1/studio/creatives/{id}/frames/{jobId}` până întoarce același corp 200 (`202` cât timp rulează, `404` pentru un job necunoscut, `502 frame_failed` dacă a eșuat).

**Cache.** Cadrele deja randate pentru același document, format și moment vin din cache instantaneu și gratuit (`cached: true`); când toate cadrele vin din cache, `frame_job_id` este `null`.

**Cotă și preț.** 60 de joburi de cadre lansate pe utilizator pe zi UTC sunt gratuite; răspunsurile servite integral din cache sunt mereu gratuite și nu se numără. După aceea, cadrele rapide costă fix **0,025 $** (25 000 µUSD) pe job, iar cele exacte prețul randării ciornă (**0,005 $** pe secundă de scene randate din nou, minimum 0,025 $). `billed_micro_usd` arată suma.

**Erori.** `400 invalid_request` (corp invalid, mai mult de 6 momente, un moment în afara creației), `400 unsupported_format` (cadrele exacte cer 1920×1080, 1080×1920, 1080×1080 sau 1080×1350; cele rapide cel mult 3840 px pe latură), `400 not_supported_for_kind`, `429 too_many_frame_jobs` (cel mult 3 joburi de cadre în lucru), `502 frame_failed`, `503 renders_unavailable`.

## Comentarii pe cadru [#comentarii-pe-cadru]

`POST …/comments` transformă comentariul unui recenzent, fixat pe un cadru, într-o editare. O tură scurtă de agent pe server citește schița, nodul comentat (sau scena, ori scena care rulează la `t_ms`) și comentariul, apoi propune operații aplicate exact ca la `POST …/ops`, pe `base_seq`-ul tău:

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/comments \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "node_id": "title", "t_ms": 1200, "comment": "Mărește titlul și fă-l alb", "base_seq": 3 }'
```

| Câmp | Semnificație |
| --- | --- |
| `t_ms` | Momentul cursorului la comentariu (obligatoriu) |
| `comment` | Ce trebuie schimbat, 1–2000 de caractere (obligatoriu) |
| `base_seq` | `head_seq`-ul citit ultima dată (obligatoriu; o bază veche e rebazată ca la `/ops`) |
| `node_id` / `scene_id` | Nodul sau scena selectată (implicit: scena care rulează la `t_ms`) |
| `apply` | `true` (implicit) scrie o versiune; `false` întoarce doar `ops` propuse |

Răspunde **200** `{ "head_seq": 4, "applied_ops": 1, "summary": "Am mărit titlul.", "version": { "seq": 4, "doc_hash": "…" } }`. O modificare suprapusă de la `base_seq` răspunde `409 stale` (reîncarcă, reîncearcă); când comentariul nu cere o editare răspunsul este `422 no_edit`, cu replica modelului în `error.text`; după 60 s, `504 timeout`. Operațiile respinse primesc automat o rundă de reparare.

**Preț.** Facturat pe tokeni ca orice apel text `codai` (rânduri de utilizare `codai::creative-comment`, cache de prompt activ) — un comentariu obișnuit costă mult sub 0,01 $.

## Variante A/B [#variante-ab]

`POST …/variants` cere unei ture scurte de agent pe server variante A/B ale creației la `base_seq`. Fiecare variantă schimbă doar aspectele din `focus` și este salvată ca o **creație nouă** derivată din `base_seq` (titlul `"<titlu> — <etichetă>"`, operațiile aplicate ca versiunea 2 de `agent`), deci o poți deschide, edita, randa sau șterge ca pe orice creație. Originalul nu se schimbă până nu alegi o câștigătoare.

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/variants \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "count": 3, "focus": ["hook", "cta"], "brief": "Black Friday, public tânăr", "base_seq": 4 }'
```

| Câmp | Semnificație |
| --- | --- |
| `base_seq` | Versiunea din care se derivă (obligatoriu) |
| `count` | 1–5 variante (implicit 3) |
| `focus` | Oricare dintre `hook` (deschiderea), `title`, `cta`, `visual` (culori, așezare, imagini, mișcare), `copy` (implicit `hook`, `title`, `cta`) |
| `brief` | Indicații opționale, cel mult 1000 de caractere |
| `render` | `true` (implicit, doar video) pune în coadă o randare draft pentru fiecare variantă |
| `format` | Id-ul formatului pentru randările draft (implicit: formatul implicit al documentului) |

Răspunde **200** `{ "group_id": "…", "base_seq": 4, "variants": [{ "creative_id": "…", "label": "Hook cu întrebare", "rationale": "…", "applied_ops": 2, "head_seq": 2, "render_id": "…" }] }` fără să aștepte randările — interoghează fiecare `render_id` cu `GET /v1/studio/renders/{id}`. Propunerile invalide sunt ignorate (o rundă automată de reparare); când niciuna nu e validă răspunsul este `422 no_edit`; după 90 s, `504 timeout`. O randare care nu a putut fi pusă în coadă lasă varianta cu `render_error`.

`GET …/variants` întoarce ultimul grup: fiecare variantă cu `status` (`candidate`, `winner`, `rejected`) și starea salvată a randării draft (`poster_url` și `url` după ce a reușit).

`POST …/variants/{variantId}/pick` cu `{ "base_seq": <versiunea curentă a originalului> }` aplică operațiile câștigătoarei pe original exact ca `POST …/ops` (autor `agent`, intenție `variant: <etichetă>`; o bază veche e rebazată, o modificare suprapusă dă `409 stale`) și marchează câștigătoarea `winner`, iar celelalte `rejected`. Răspunde `{ "head_seq", "version", "winner", "rebased" }`.

**Preț.** Generarea este facturată pe tokeni ca orice apel text `codai` (rânduri de utilizare `codai::creative-variants`, cache de prompt activ — mult sub 0,05 $ pentru trei variante); fiecare randare draft se facturează la interogare ca orice randare draft (0,005 $ pe secundă de ieșire, minimum 0,025 $). Listarea și alegerea sunt gratuite.

## Kituri de brand [#kituri-de-brand]

Un kit de brand este un set reutilizabil de tokeni de brand — cinci roluri de culoare `primary`, `secondary`, `accent`,
`background`, `text`, până la 12 culori suplimentare cu nume, familiile de fonturi `heading` și `body` și o rază de
colț opțională — plus un logo opțional. Kiturile sunt salvate ca tokeni de design [DTCG](https://www.designtokens.org/)
(formatul 2025.10), așa că le poți importa din instrumentele de design și le poți exporta către ele.

### Formatul DTCG [#formatul-dtcg]

```json
{
  "$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
  "color": {
    "$type": "color",
    "primary": { "$value": { "colorSpace": "srgb", "components": [0.004, 0.439, 0.502], "hex": "#017080" } },
    "secondary": { "$value": { "colorSpace": "srgb", "components": [0.137, 0.671, 0.737], "hex": "#23abbc" } },
    "accent": { "$value": { "colorSpace": "srgb", "components": [0.902, 0.718, 0.192], "hex": "#e6b731" } },
    "background": { "$value": { "colorSpace": "srgb", "components": [0.969, 0.973, 0.98], "hex": "#f7f8fa" } },
    "text": { "$value": { "colorSpace": "srgb", "components": [0.059, 0.067, 0.082], "hex": "#0f1115" } },
    "sand": { "$value": { "colorSpace": "srgb", "components": [0.949, 0.902, 0.82], "hex": "#f2e6d1" } }
  },
  "font": {
    "$type": "fontFamily",
    "heading": { "$value": ["Manrope"] },
    "body": { "$value": ["Inter", "Arial"] }
  },
  "radius": { "$type": "dimension", "base": { "$value": { "value": 8, "unit": "px" } } }
}
```

- Cele cinci roluri de culoare sunt obligatorii. Culorile suplimentare au un nume format dintr-o literă mică urmată de
  cel mult 31 de litere mici, cifre sau `-` (cel mult 12 culori).
- Importul este tolerant: `$value` al unei culori poate fi și un șir `"#abc"` / `"#aabbcc"`, `{ "hex" }` sau
  `{ "colorSpace": "srgb", "components" }`; `$type` poate sta pe token sau pe grupul lui; `$value` al unui font poate
  fi un singur șir. Kiturile salvate folosesc mereu forma canonică de mai sus.
- `$value` al unui font enumeră 1–4 nume de familii (o listă goală înseamnă necunoscut); `radius` are 0–200 px și este
  opțional.
- JSON-ul canonic are cel mult 16 KB. Un fișier de tokeni invalid primește `400 invalid_tokens`, cu problemele în
  `error.details.issues`.

Fiecare kit răspunde cu `dtcg`, cu tokenii plați scriși în creații (mai jos), cu `contrast` și cu `updated_at`; câmpurile
sunt `{ "id", "name", "source_url", "dtcg": { … }, "tokens": { … }, "contrast": [ … ], "logo_asset_id", "logo_url",
"logo_mime_type", "created_at", "updated_at" }`, adică identificatorul, numele, sursa, tokenii, contrastul și logoul.

**Contrast.** `contrast` enumeră raportul de contrast WCAG pentru cinci perechi fixe — text pe fundal, primar pe fundal,
fundal pe primar, accent pe fundal și secundar pe fundal — sub forma `{ "foreground", "background", "ratio", "aa" }`
(`ratio` rotunjit la 2 zecimale, `aa` este `true` de la 4,5:1 în sus).

**Tokeni plați.** Creațiile păstrează o hartă plată în `brand.tokens`: cele cinci roluri de culoare, `headingFont` și
`bodyFont` (prima familie, sau `""`), fiecare culoare suplimentară sub numele ei, `radius` (px) când există, plus
aliasurile de document `color.bg` (= fundal), `color.fg` (= text), `color.accent` (= primar), `font.heading` și
`font.body`, ca și creațiile convertite din [prezentări, documente și foi de calcul](#documente-prezentare-document-foaie-de-calcul) să
urmeze kitul.

### Creare, import, editare, export [#creare-import-editare-export]

`POST /v1/studio/brand-kits` creează manual un kit. Trimiți `name` (1–100 caractere) și exact unul dintre `tokens`
(harta plată: toate cele cinci culori ca `#rrggbb`, plus `headingFont` / `bodyFont` de cel mult 100 de caractere) sau
`dtcg` (un fișier DTCG de importat); opțional `logo_asset_id` (unul dintre asset-urile tale `image` gata procesate,
altfel `404 Asset not found.`) și `source_url` (https). Răspunde **201** cu kitul.

```bash
curl https://ai.codai.ro/v1/studio/brand-kits \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme", "tokens": { "primary": "#e4002b", "secondary": "#1d3557", "accent": "#ffb703",
        "background": "#ffffff", "text": "#111111", "headingFont": "Manrope", "bodyFont": "Inter" } }'
```

`PATCH /v1/studio/brand-kits/{id}` editează un kit: oricare dintre `name`, `tokens` (parțial — combinat peste kitul
curent), `dtcg` (înlocuiește tokenii) și `logo_asset_id` (`null` elimină logoul); cel puțin un câmp și niciodată
`tokens` împreună cu `dtcg`. Răspunde **200** cu kitul; un kit care nu e al tău sau a fost șters dă `404`. Creațiile
care folosesc deja kitul își păstrează tokenii până când le restilizezi.

`GET /v1/studio/brand-kits/{id}/dtcg` descarcă kitul ca fișier DTCG (`application/json`,
`content-disposition: attachment; filename="<slug>.tokens.json"`), cu
`"$extensions": { "ro.codai": { "kit_id", "name", "logo_asset_id" } }` adăugat, gata pentru orice instrument de design
care înțelege DTCG.

### Extragere dintr-o pagină web sau dintr-un logo [#extragere-dintr-o-pagină-web-sau-dintr-un-logo]

`POST /v1/studio/brand-kits/extract` construiește un kit din exact unul dintre `url` (o pagină publică `https`) sau
`logo_asset_id` (o imagine de logo aflată deja în biblioteca ta); `name` este opțional în ambele cazuri.

```bash
curl https://ai.codai.ro/v1/studio/brand-kits/extract \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://acme.example/" }'
```

**Dintr-un URL**, gateway-ul descarcă pagina și până la 3 dintre foile ei de stil (fiecare redirecționare este verificată
din nou contra adreselor private și interne; 10 s, 3 MB, doar HTML), adună candidații de culori, fonturi și logo
(`theme-color`, proprietăți CSS personalizate, `font-family`, linkuri Google Fonts, imagini de logo, iconițe touch,
`og:image`), iar o singură tură limitată a agentului de pe server alege o paletă coerentă (text lizibil pe fundal) și
fonturile. Logoul ales (PNG, JPEG sau WebP de cel mult 5 MB — SVG nu se importă) este salvat în biblioteca ta Studio ca
asset `image`.

**Dintr-un logo** (`{ "logo_asset_id": "<id asset imagine>" }`), imaginea trebuie să fie unul dintre asset-urile tale
PNG, JPEG sau WebP gata procesate, de cel mult 5 MB (altfel `400 unsupported_media`). O singură tură de viziune citește
paleta și propune fonturi potrivite logoului; logoul devine logoul kitului, iar `source_url` este `null`.

Ambele răspund **201** cu kitul (salvat ca DTCG). Erori: `400 url_blocked` (gazdă privată sau blocată),
`400 unsupported_media`, `422 fetch_failed` (pagina nu a putut fi descărcată sau nu e HTML), `422 extract_failed`
(niciun kit valid), `504 timeout` după 60 s.

### Aplicarea unui kit [#aplicarea-unui-kit]

- **La creare** — `POST /v1/studio/creatives` cu `"brand_kit_id": "<id kit>"`: versiunea 1 are deja
  `brand = { kitId, tokens }` și logoul înregistrat în `assets` (fără o versiune în plus). Un kit necunoscut dă `404`.
- **Pe o creație existentă** — `POST /v1/studio/creatives/{id}/restyle` (mai jos).
- **Manual** — trimite `[{ "op": "setBrand", "kitId": "<id kit>", "tokens": { … } }]` (plus un `setAsset` pentru logo)
  la `POST …/ops`; asta înlocuiește doar tokenii, culorile literale rămân cum sunt.

Nodurile folosesc tokenii ca `{ "$token": "primary" }`. `GET /v1/studio/brand-kits` listează kiturile tale (cele mai noi
primele), `GET …/{id}` citește unul, `DELETE …/{id}` îl șterge (creațiile își păstrează tokenii pe care îi au deja;
logoul rămâne în bibliotecă).

### Restilizarea unei creații [#restilizarea-unei-creații]

`POST /v1/studio/creatives/{id}/restyle` reaplică un brand pe o creație făcută înaintea kitului sau cu culori scrise
direct în noduri:

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/restyle \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "brand_kit_id": "<id kit>", "snap": true }'
```

| Câmp | Semnificație |
| --- | --- |
| `brand_kit_id` / `tokens` | Exact unul: unul dintre kiturile tale sau o hartă plată de tokeni |
| `base_seq` | Versiunea citită (implicit: capul); o bază veche este rebazată ca la `/ops` |
| `snap` | `true` mută orice altă culoare literală pe cea mai apropiată culoare din kit (implicit `false`) |
| `fonts` | `false` lasă fonturile neatinse (implicit `true`) |
| `force` | `true` restilizează și nodurile blocate |

Gateway-ul trimite o singură operație `restyle` (plus un `setAsset` pentru logoul kitului, dacă are unul) prin aceeași
cale de editare ca `POST …/ops` (intenția `restyle with brand kit <nume>`) și răspunde **200** cu același corp ca
`/ops`, inclusiv `warnings`. Regulile:

1. **Transformare în tokeni.** Brandul nou este vechiul `brand.tokens` cu tokenii kitului deasupra. Fiecare `#rrggbb`
   literal din fundalurile scenelor și din proprietățile nodurilor (inclusiv opririle de gradient) egal cu valoarea unui
   token vechi devine o referință de token, de exemplu `{ "$token": "primary" }`, deci preia culoarea nouă.
2. **Aliniere (snap).** Cu `snap: true`, orice altă culoare literală devine o referință la cel mai apropiat, perceptual,
   dintre cele cinci roluri de culoare (distanță OKLab). Culorile cu transparență (`#rrggbbaa`) rămân neschimbate și
   sunt numărate într-un avertisment.
3. **Fonturi după rol.** Un font literal egal cu un token de font vechi devine acel token. Orice alt font literal pe un
   nod text, listă sau tabel devine `headingFont` pentru titluri (rolurile `title`, `subtitle`, `heading1`–`heading4`,
   `column-heading`, `stat-value` sau text de mărime 48 și peste în afara documentelor) și `bodyFont` în rest.
4. **Nodurile blocate** (sau cele aflate într-un grup blocat) sunt sărite cu avertismentul
   `restyle skipped locked node <id>`, în afară de cazul `force: true`.

Aceeași operație funcționează în orice lot trimis la `/ops`:

```json
{ "op": "restyle", "kitId": "<kit id>", "tokens": { "primary": "#e4002b", "headingFont": "Manrope" }, "snap": false, "fonts": true }
```

Verifică apoi rezultatul cu cadre statice și cu `lint` (contrast).

### SDK-uri și unelte de agent [#sdk-uri-și-unelte-de-agent]

```ts
const kit = await codai.brandKits.create({ name: 'Acme', dtcg });
await codai.brandKits.update(kit.id, { tokens: { accent: '#ffb703' } });
const tokensFile = await codai.brandKits.exportDtcg(kit.id);
const fromLogo = await codai.brandKits.extract({ logo_asset_id: logoAssetId });
const restyled = await codai.creatives.restyle(c.id, { brand_kit_id: kit.id, snap: true });
```

```python
tokens_file = client.brand_kits.export_dtcg(kit["id"])
from_logo = client.brand_kits.extract(logo_asset_id=logo_asset_id)
restyled = client.creatives.restyle(c["id"], brand_kit_id=kit["id"], snap=True)
```

Agenții de pe desktop, telefon și VS Code folosesc `creative_brand_extract` (`url` sau `logo_asset_id`),
`creative_brand_kit_save` (fără `brand_kit_id` creează un kit din `name` + `tokens` sau `dtcg`; cu el, editează acel
kit) și `creative_restyle` (`creative_id`, `brand_kit_id` sau `tokens`, `snap`, `fonts`). `creative_edit` acceptă și
operația `restyle`.

**Preț.** Extragerea este facturată pe tokeni ca orice apel text `codai` (rând de utilizare `codai::brand-kit-extract`,
cache de prompt activ — de obicei mult sub 0,01 $, dintr-un URL sau dintr-un logo). Crearea, editarea, exportul,
listarea, citirea, ștergerea, aplicarea kiturilor și restilizarea sunt gratuite.

## Subtitrări [#subtitrări]

`POST /v1/studio/creatives/{id}/captions` adaugă **subtitrări automate** unei creații video: transcrie un clip de voce
cu momentul fiecărui cuvânt, corectează ortografia după scenariu când narațiunea este a ta și inserează un nod
`captions` stilizat în zona sigură a formatului implicit (deasupra interfeței platformei când formatul este legat de o
platformă, vezi [Platforme](#platforme)).

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/captions \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "preset": "karaoke-pop" }'
```

| Câmp | Semnificație |
| --- | --- |
| `clip_id` | Clipul audio de subtitrat (implicit: primul clip de voce) |
| `preset` | `bold-pill` (implicit), `clean-outline`, `karaoke-pop` sau `minimal` |
| `node_id` | Id-ul nodului de subtitrare (implicit `captions`, apoi `captions-2`…); un nod de subtitrare existent cu acest id este înlocuit |
| `script` | Textul exact al narațiunii, cel mult 20 000 de caractere (implicit: textul din care a fost generat asset-ul de voce) |
| `language` | Limba transcrierii (implicit: `locale` al documentului) |
| `retranscribe` | `true` ignoră cuvintele din cache și transcrie din nou |
| `base_seq` / `force` | Ca la `/ops` (implicit: capul) |

**Momentele cuvintelor.** Sunetul clipului (un asset `speech`, `music` sau `video` al tău, gata procesat, de cel mult
25 MB) este transcris de Whisper, cu câte un marcaj de timp pentru fiecare cuvânt. Cuvintele sunt relative la clip: se
scade `inMs`-ul lui, iar cuvintele din afara intervalului tăiat al clipului sunt eliminate.

**Corectarea după scenariu.** Când clipul este narațiunea ta — vocea generată în Studio își păstrează textul sursă —
sau când trimiți `script`, transcrierea este aliniată cuvânt cu cuvânt cu scenariul: momentele vin din transcriere,
ortografia (nume, diacritice, punctuație) din scenariu. Când scenariul nu se potrivește cu ce s-a spus, transcrierea
rămâne neschimbată (`corrected: false`).

**Cuvinte în cache.** Cuvintele sunt salvate pe asset-ul de voce (`meta.caption_words`), așa că o nouă subtitrare a
aceluiași clip — alt preset, altă creație care folosește același asset, un `node_id` nou — le refolosește gratuit
(`cached: true`). Trimite `retranscribe: true` ca să forțezi o transcriere nouă.

Editarea trece pe aceeași cale ca `POST …/ops` (un `insertNode` în scena care conține începutul clipului sau un
`setProps` pe nodul existent) și răspunde **200** cu corpul de la `/ops`, plus câmpurile
`{ "node_id", "words", "cached", "corrected", "cost_micro_usd" }`.

**Preseturi.** Mărimile sunt o fracțiune din latura scurtă a formatului:

| Preset | Stil | Mărimea textului | Cuvinte pe rând | Culori |
| --- | --- | --- | --- | --- |
| `bold-pill` | Fundal rotunjit sub text | 5,5 % | 4 | Alb |
| `clean-outline` | Text cu contur | 5 % | 6 | Alb |
| `karaoke-pop` | Evidențiere cuvânt cu cuvânt | 6 % | 3 | Alb, evidențiere cu tokenul `accent` (sau `#ffd60a`) |
| `minimal` | Text cu contur | 4 % | 8 | Alb |

Schimbarea ulterioară a presetului este un singur `setProps` pe nodul de subtitrare (stil, cuvinte pe rând, mărime,
culori).

**Export SRT și VTT.** `GET /v1/studio/creatives/{id}/captions.srt` și `GET …/captions.vtt` descarcă subtitrările pe
cronologia documentului (SubRip `text/plain` sau WebVTT `text/vtt`, `content-disposition: attachment`), rând cu rând,
exact cum le arată videoclipul. `node_id` alege un singur nod de subtitrare (implicit: toate, combinate în timp), iar
`seq` o versiune (implicit: capul). Fără subtitrări răspunsul este `404 No captions.`. Exportul este gratuit.

**Erori.** `400 unsupported_kind` (nu este o creație video), `400 unsupported_media` (sunet de peste 25 MB),
`404 Clip not found.`, `404 Asset not found.`, `403 residency_unavailable` (cu rezidența datelor în UE transcrierea nu
este disponibilă), `503 transcription_unavailable`.

**Preț.** O transcriere nouă costă aproximativ **0,006 $ pe minut** de sunet al clipului (rotunjit în sus la minut,
rând de utilizare `codai::captions`, același tarif ca transcrierea prin `/v1/audio`). Cuvintele din cache, corectarea
după scenariu, preseturile și exportul SRT/VTT sunt gratuite.

## Platforme [#platforme]

Un preset de platformă înseamnă o dimensiune de ieșire plus partea din cadru acoperită de interfața platformei (bara
de descriere, butoanele de acțiune, bara de progres). `POST /v1/studio/creatives/{id}/adapt` adaptează o creație video
sau imagine la una sau mai multe platforme, într-o singură versiune:

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/adapt \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platforms": ["tiktok", "feed-portrait", "youtube"] }'
```

Zona sigură este partea din fiecare latură lăsată liberă de text; mărimea minimă a textului este în pixeli de ieșire:

| Platformă | Etichetă | Dimensiune | Zonă sigură (sus / dreapta / jos / stânga) | Text minim |
| --- | --- | --- | --- | --- |
| `tiktok` | TikTok | 1080×1920 | 8 % / 13 % / 20 % / 5 % | 36 px |
| `reels` | Instagram Reels | 1080×1920 | 7 % / 10 % / 18 % / 5 % | 36 px |
| `shorts` | YouTube Shorts | 1080×1920 | 7 % / 12 % / 17 % / 5 % | 36 px |
| `feed-square` | Feed 1:1 | 1080×1080 | 5 % pe fiecare latură | 32 px |
| `feed-portrait` | Feed 4:5 | 1080×1350 | 5 % pe fiecare latură | 32 px |
| `youtube` | YouTube 16:9 | 1920×1080 | 5 % pe fiecare latură | 28 px |

Zonele sunt aproximări intenționat prudente ale ghidurilor publicate de platforme, care se schimbă des.

| Câmp | Semnificație |
| --- | --- |
| `platforms` | 1–6 id-uri de platformă distincte (obligatoriu) |
| `focal` | `none` (implicit) sau `vision` — vezi mai jos |
| `base_seq` / `force` | Ca la `/ops` (implicit: capul); `force` adaptează și nodurile blocate |

Gateway-ul trimite câte o operație `adaptFormat` pentru fiecare platformă, pe aceeași cale de editare ca `/ops`. Pentru
fiecare platformă:

1. Un format de aceeași dimensiune este refolosit; altfel se adaugă unul cu id-ul de aspect al presetului (`9x16`,
   `1x1`, `4x5`, `16x9`, cu sufixul `-2`, `-3`… când id-ul aparține unui format de altă dimensiune), iar legătura se
   notează în `meta.platforms[<id format>]`.
2. Imaginile și videoclipurile pe tot cadrul rămân pe tot cadrul; textele, subtitrările și grupurile sunt mutate și
   încadrate în zona sigură a platformei (micșorate la lățimea zonei când sunt mai late). Formatul implicit se
   modifică direct pe nod, orice alt format prin suprascrieri per format (`overrides`).
3. Textul sub minimul platformei primește o mărime mai mare doar în acel format.
4. Nodurile blocate sunt sărite cu un avertisment (în afară de cazul `force: true`).

Aceeași operație funcționează în orice lot trimis la `/ops`:

```json
{ "op": "adaptFormat", "platform": "reels", "focal": { "hero": { "x": 0.62, "y": 0.4 } } }
```

`focal` (opțional) fixează punctul de interes (0–1 pe fiecare axă) al nodurilor imagine și video în acel format, ca
decuparea să îl păstreze în cadru. Fără el, reîncadrarea este deterministă și centrată.

**Punct focal inteligent (vision).** `"focal": "vision"` este o opțiune **la cerere, facturată**: o tură de vision
analizează imaginile de nivel superior care acoperă tot cadrul (asset-uri PNG, JPEG sau WebP ale tale, gata procesate,
de cel mult 5 MB, maximum 6) și întoarce un punct focal pentru fiecare, folosit apoi de toate formatele noi. Rulează o
singură dată pe apel, pentru toate platformele, cu o limită de 60 s; dacă eșuează, adaptarea are loc oricum, cu
decupări centrate și avertismentul `vision focal unavailable`.

Răspunde **200** cu corpul de la `/ops`, plus câmpurile
`{ "formats": [{ "platform", "format_id", "width", "height" }], "focal": { "<id nod>": { "x", "y" } } | null }`.
Erori: `400 unsupported_kind` (doar creații video și imagine), `400 invalid_request`.

**Randări în 4:5.** Randările și cadrele exacte acceptă formatul `feed-portrait` (1080×1350) alături de 1920×1080,
1080×1920 și 1080×1080, așa că o creație adaptată pentru un feed 4:5 se randează direct.

**Preț.** Adaptarea este gratuită. Doar `focal: "vision"` este facturat pe tokeni ca orice apel vision `codai` (rând de
utilizare `codai::adapt-focal`, cache de prompt activ — de obicei mult sub 0,01 $ pe apel).

## Lint și corecturi [#lint-și-corecturi]

`POST /v1/studio/creatives/{id}/lint` rulează verificări statice, fără randare. Erorile blochează randarea;
avertismentele îți sunt arătate ție și agenților. Fiecare problemă are forma
`{ "code", "severity", "message", "target", "value"?, "format"?, "fixable"? }` — `format` numește formatul la care se
referă o problemă specifică unui format, iar `fixable: true` marchează problemele pe care corectura automată gratuită
le poate rezolva — iar răspunsul adaugă `fixable`, numărul acestor probleme.

| Cod | Severitate | Corectabil | Ce înseamnă |
| --- | --- | --- | --- |
| `low_contrast` | eroare | nu | Contrastul textului sub pragul WCAG (4,5:1 sau 3:1 pentru text mare) |
| `outside_safe_area` | avertisment | nu | Text sau subtitrări în afara zonei sigure (zona platformei când formatul implicit este legat de o platformă) |
| `empty_text` | avertisment | nu | Un nod text fără text |
| `captions_no_words` | eroare | nu | Un nod de subtitrare fără cuvinte |
| `captions_no_clip` | avertisment | nu | Subtitrări care trimit la un clip audio lipsă |
| `missing_asset` | eroare | nu | Un id de asset neînregistrat în document |
| `scene_zero_duration` | eroare / avertisment | nu | O scenă de 0 ms (eroare) sau un nod care nu apare niciodată (avertisment) |
| `too_long` | eroare | nu | Durata totală depășește 180 s |
| `chart_no_svg` | avertisment | nu | Un grafic fără SVG randat în prealabil |
| `unresolved_ref` | avertisment | nu | O referință la o variabilă sau la un token necunoscut |
| `empty_list` | avertisment | nu | O listă fără text în elemente |
| `off_brand_color` | avertisment | da | O culoare literală din afara paletei de brand (nuanțele neutre de gri nu sunt semnalate niciodată) |
| `outside_platform_safe_zone` | avertisment | da | Text sub interfața unei platforme, într-un format legat de acea platformă |
| `text_too_small` | avertisment | da | Text sub pragul de lizibilitate al unui format (`value` = px) |
| `missing_alt` | avertisment | nu | O imagine dintr-o prezentare sau dintr-un document fără text alternativ |
| `missing_captions` | avertisment | nu | Un clip de voce fără nod de subtitrare — folosește [Subtitrări](#subtitrări) |
| `reading_speed` | avertisment | nu | Un rând de subtitrare mai rapid de 20 de caractere pe secundă (`value` = caractere pe secundă) |
| `flashing` | eroare / avertisment | da / nu | O animație care pâlpâie de peste 3 ori pe secundă (eroare, corectabilă) sau cel puțin 4 scene consecutive mai scurte de 334 ms (avertisment) |

Pragul de lizibilitate este minimul platformei (vezi [Platforme](#platforme)), altfel 2,5 % din latura scurtă a
formatului.

**Corecturi automate.** `POST /v1/studio/creatives/{id}/lint/fix` transformă problemele corectabile într-un singur lot
de operații, aplicat ca la `/ops`:

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/lint/fix \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "codes": ["off_brand_color", "text_too_small"] }'
```

| Câmp | Semnificație |
| --- | --- |
| `codes` | Corectează doar aceste coduri (implicit: toate codurile corectabile) |
| `agent` | `true` adaugă o tură plătită de agent pentru problemele rămase (implicit `false`) |
| `base_seq` / `force` | Ca la `/ops` (implicit: capul); `force` corectează și nodurile blocate |

Corecturile deterministe sunt gratuite: culorile din afara brandului devin un `{ "$token" }` al celei mai apropiate
culori de brand, textul prea mic primește o mărime mai mare (sau un `adaptFormat` în formatele de platformă), textul aflat
sub interfața unei platforme este readaptat cu `adaptFormat`, iar animațiile cu cadre cheie care pâlpâie sunt eliminate.
Când nu există nimic de corectat, răspunde **200** fără să scrie o versiune (`changed: []`).

Cu `agent: true`, după lotul determinist o tură scurtă de agent pe server primește problemele rămase și schița și
propune operații (text alternativ, contrast, viteză de citire…), aplicate pe aceeași cale de editare, cu o rundă de
reparare. Dacă agentul eșuează primești totuși rezultatul determinist și avertismentul `agent fix unavailable`.

Răspunde **200** cu corpul de la `/ops`, plus câmpurile `{ "fixed": [{ "code", "target" }], "remaining": [ … ],
"agent": { "ops", "cost_micro_usd" } | null }` — `remaining` este rezultatul lint de după corectură.

**Preț.** Lint-ul și corecturile deterministe sunt gratuite. `agent: true` este facturat pe tokeni ca orice apel text
`codai` (rând de utilizare `codai::lint-fix`, cache de prompt activ — de obicei mult sub 0,01 $).

## Panoul de ajustare [#panoul-de-ajustare]

Editoarele din hub, desktop și telefon arată un **panou de ajustare** pentru variabilele documentului (`vars`): câte un
selector de culoare, un cursor, un comutator, o listă de opțiuni, un câmp text sau un selector de imagine pentru
fiecare variabilă. Variabilele își păstrează tipul și valoarea în `vars`; `meta.controls[<cheie variabilă>]` descrie
doar cum se editează fiecare.

```json
{ "op": "setControl", "key": "accent", "control": { "kind": "select", "label": "Accent", "group": "Colours", "options": [{ "value": "#e4002b", "label": "Red" }, { "value": "#1d3557", "label": "Navy" }] } }
```

| Tip de control | Se potrivește variabilelor de tip | Câmpuri suplimentare |
| --- | --- | --- |
| `text` | `text`, `number` | `multiline` |
| `color` | `color` | — |
| `slider` | `number` | `min` și `max` (obligatorii, `min` < `max`), `step` |
| `toggle` | `boolean` | — |
| `select` | `text`, `number`, `color` | `options` (obligatoriu, 1–24 elemente `{ "value", "label"? }`) |
| `image` | `image` | — |

Orice control acceptă și `label` (cel mult 100 de caractere) și `group` (cel mult 60 de caractere, grupează panoul).
`setControl` cu `"control": null` șterge controlul. Ca orice [operație respinsă](#operații-respinse), un control care
nu se potrivește tipului variabilei răspunde `400` cu codul `invalid`, iar o variabilă necunoscută `400` cu codul
`not_found`.

`GET /v1/studio/creatives/{id}/controls` întoarce modelul panoului, `{ "head_seq", "controls": [{ "key", "label",
"control", "value", "type", "locked" }] }`: câte o intrare pentru fiecare variabilă, cu controlul salvat când este
valid, altfel unul dedus din tip (culoare → `color`, boolean → `toggle`, imagine → `image`, număr → `slider` în jurul
valorii curente, text → `text`). Schimbarea unei valori este o operație `setVar` obișnuită pe `POST …/ops`; editarea
unui control este `setControl`. Citirea și editarea controalelor sunt gratuite.

## SDK-uri și unelte de agent (subtitrări, platforme, lint, controale) [#sdk-uri-și-unelte-de-agent-subtitrări-platforme-lint-controale]

```ts
const cap = await codai.creatives.captions(c.id, { preset: 'bold-pill' });
const srt = await codai.creatives.captionsSrt(c.id, { node_id: cap.node_id });
const adapted = await codai.creatives.adapt(c.id, { platforms: ['tiktok', 'feed-portrait'], focal: 'vision' });
const fixed = await codai.creatives.lintFix(c.id, { agent: true });
const { controls } = await codai.creatives.controls(c.id);
```

```python
cap = client.creatives.captions(c["id"], preset="bold-pill")
vtt = client.creatives.captions_vtt(c["id"])
adapted = client.creatives.adapt(c["id"], ["tiktok", "feed-portrait"], focal="vision")
fixed = client.creatives.lint_fix(c["id"], agent=True)
controls = client.creatives.controls(c["id"])["controls"]
```

Agenții de pe desktop, telefon și VS Code folosesc `creative_captions` (`creative_id`, `clip_id`, `preset`, `script`,
`language`), `creative_adapt` (`creative_id`, `platforms`, `focal`) și `creative_lint_fix` (`creative_id`, `codes`,
`agent`); `creative_lint` enumeră codurile noi, iar `creative_edit` acceptă operațiile `adaptFormat` și `setControl`.

## Localizare [#localizare]

O creație poate avea **mai multe limbi**. Documentul sursă rămâne așa cum este; fiecare limbă în plus este un set de
**suprascrieri** păstrate în document sub `meta.localization`, iar fiecare citire care primește un `locale`
(previzualizare, randare, cadre statice, subtitrări, lint) le aplică pe loc. Când o limbă este gata să trăiască
separat, o copiezi într-o creație de sine stătătoare.

### Modelul de suprascrieri [#modelul-de-suprascrieri]

```json
{
  "meta": {
    "localization": {
      "source": "ro",
      "glossary": ["codai", "Black Friday"],
      "locales": {
        "en": {
          "tone": "informal",
          "texts": { "title:spans.0": { "t": "Autumn sale", "h": "3fa2c9d1e0b4a7f6", "by": "agent" } },
          "audio": { "voice-1": "<id asset de voce>" },
          "captions": { "captions": [{ "text": "Autumn", "startMs": 0, "endMs": 420 }] }
        }
      }
    }
  }
}
```

- `source` este limba documentului însuși (implicit: `locale` al documentului, altfel `en`). Limbile sunt etichete
  BCP-47 (`ro`, `en`, `de-DE`, `pt-BR`), normalizate la intrare (`ro_ro` → `ro-RO`).
- `texts` asociază cheii unei **unități de traducere** traducerea ei `t`, amprenta `h` a textului sursă din care a
  fost făcută și `by` (`agent` sau `user`). Când textul sursă se schimbă ulterior, amprenta nu se mai potrivește și
  traducerea devine **învechită** — este folosită în continuare până o reîmprospătezi.
- `audio` înlocuiește asset-ul unui clip de voce pentru acea limbă, iar `captions` înlocuiește cuvintele unui nod de
  subtitrare (vezi [Voce și subtitrări](#voce-și-subtitrări)).

**Unități de traducere.** Fiecare text literal din document este o unitate, adresată printr-o cheie stabilă:

| Tip de unitate | Cheie | Unde |
| --- | --- | --- |
| `text` | `<id nod>:spans.<i>` | Un segment de text |
| `list` | `<id nod>:items.<i>.spans.<j>` | Un segment dintr-un element de listă |
| `table`, `cell` | `<id nod>:columns.<c>`, `<id nod>:rows.<r>.<c>`, `<id nod>:caption` | Antetele, celulele text și legenda tabelului (grilele din foi: antete și celule text; formulele, datele și numerele rămân neschimbate) |
| `chart` | `<id nod>:spec.title`, `spec.alt`, `spec.categories.<i>`, `spec.series.<i>.name` | Etichetele graficului |
| `alt`, `caption` | `<id nod>:alt`, `<id nod>:caption` | Textul alternativ și legenda imaginii |
| `html`, `param` | `<id nod>:html.<i>`, `<id nod>:params.<cheie>` | Fragmentele de text ale unui bloc HTML (etichetele rămân neatinse) și parametrii lui text |
| `captions` | `<id nod>:words` | Cuvintele subtitrării, unite prin spații |
| `notes` | `scene:<id scenă>:notes` | Notele vorbitorului |
| `title` | `doc:title` | Titlul creației |
| `var` | `var:<cheie>` | O variabilă text |
| `narration` | `clip:<id clip>` | Textul narațiunii unui clip de voce (folosit doar cu `voice: true`) |

Textele goale, numerele simple și segmentele `{ "$var" }` sunt sărite (variabila este propria ei unitate); un document
are cel mult 2000 de unități. Unitățile text au un **buget de lungime** (`max_chars`), calculat din dimensiunea casetei
și mărimea fontului în formatul implicit, astfel încât traducerile care ar depăși caseta sunt semnalate.

### Citirea limbilor [#citirea-limbilor]

`GET /v1/studio/creatives/{id}/locales` întoarce gratuit starea fiecărei limbi:

```json
{
  "head_seq": 7,
  "source": "ro",
  "glossary": ["codai"],
  "total": 18,
  "locales": [{ "locale": "en", "tone": "informal", "translated": 17, "missing": 1, "stale": 2, "voice": 0 }]
}
```

Cu `?locale=en&units=1` adaugă `units` (`offset`, `limit` cel mult 500, implicit 200) și `units_total`: câte o
intrare pentru fiecare unitate, cu `key`, `node_id`, `kind`, `text` (sursa), `max_chars`, traducerea curentă
`translation` (sau `null`), `by`, `stale` și `locked`. O limbă necunoscută răspunde `404 Locale not found.`.

### Traducerea cu modelul [#traducerea-cu-modelul]

`POST /v1/studio/creatives/{id}/localize` traduce creația în cel mult **10** limbi într-un singur apel și salvează
totul ca **o singură** versiune nouă (autor `agent`, intenție `localize: en, de`):

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/localize \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "locales": ["en", "de"], "tone": "informal", "glossary": ["codai", "Black Friday"] }'
```

| Câmp | Semnificație |
| --- | --- |
| `locales` | 1–10 limbi țintă distincte, niciodată limba sursă (obligatoriu) |
| `tone` | `formal` (adresare politicoasă, de exemplu „dumneavoastră”, „Sie”) sau `informal` („tu”, „du”); salvat pentru fiecare limbă |
| `glossary` | Cel mult 100 de termeni (fiecare de cel mult 100 de caractere) păstrați exact cum sunt scriși; salvat în document |
| `only_missing` | `true` (implicit) traduce doar unitățile fără o traducere actuală; `false` traduce totul din nou |
| `voice` | `true` (doar video) reînregistrează și narațiunea și resincronizează subtitrările pentru fiecare limbă — vezi mai jos |
| `base_seq` / `force` | Ca la `/ops` (implicit: capul); `force` traduce și nodurile blocate |

Regulile pe care le urmează modelul: substituenții, numerele, adresele URL și emoji-urile se păstrează; termenii din
glosar și **numele și sloganul brandului** din kitul de brand al creației sunt copiate întocmai; tonul este respectat;
fiecare traducere rămâne în limita `max_chars` a unității (mai scurt este în regulă). Unitățile sunt trimise în tranșe
(cel mult 300 de unități / 30 000 de caractere pe tură de model, trei limbi în paralel); cheile necunoscute și
răspunsurile goale sunt ignorate, iar un răspuns lipsă primește o rundă de reparare.

Răspunde **200** cu corpul de la `/ops`, plus câmpurile
`{ "locales": [{ "locale", "translated", "remaining", "voice_clips" }], "cost_micro_usd" }`. O limbă care nu a putut fi
tradusă adaugă avertismentul `translation unavailable for <locale>`; când toate limbile au eșuat, răspunsul este
`503 localize_unavailable`. Erori: `400 invalid` (o limbă greșită sau limba sursă), `400 nothing_to_translate` (toate
unitățile sunt deja traduse și actuale).

**Preț.** Traducerea este facturată pe tokeni ca orice apel text `codai` (rânduri de utilizare `codai::localize`, cache
de prompt activ — o creație obișnuită costă mult sub 0,01 $ pe limbă). Citirea limbilor, editările manuale și copiile
sunt gratuite.

### Voce și subtitrări [#voce-și-subtitrări]

Cu `voice: true`, pentru fiecare clip de voce care este propria ta narațiune Studio (un asset de voce gata, care și-a
păstrat textul sursă), narațiunea este tradusă odată cu textele, reînregistrată cu aceleași setări de voce în limba
țintă și înlocuită doar pentru acea limbă (`audio`). Când un nod de subtitrare afișează acel clip, vocea nouă este
transcrisă, iar subtitrările traduse sunt resincronizate (`captions`). Narațiunea se facturează ca orice narațiune, iar
transcrierea ca la [Subtitrări](#subtitrări) (rând de utilizare `codai::captions`). Un clip care nu poate fi
reînregistrat adaugă avertismentul `voice unavailable for <clip> (<locale>)`; textele sunt traduse oricum.

### Editarea manuală a traducerilor [#editarea-manuală-a-traducerilor]

Editările manuale sunt operații obișnuite pe `POST …/ops`:

```json
{ "op": "setLocaleText", "locale": "en", "texts": { "title:spans.0": "Autumn sale", "doc:title": null } }
```

- `setLocaleText` setează cel mult 500 de unități ale unei limbi (`null` șterge o traducere, care apoi contează ca
  lipsă); amprenta salvată este cea a textului sursă curent, deci unitatea devine din nou actuală. `by` are implicit
  valoarea `user`. O cheie necunoscută dă `not_found`, o unitate a unui nod blocat `locked` (fără `force`), iar limba
  sursă `invalid`.
- `setLocale` setează `tone` al unei limbi, `audio` (id clip → id asset; asset-ul trebuie înregistrat cu `setAsset`) și
  `captions` (id nod de subtitrare → cuvinte); `"remove": true` șterge limba.
- `setLocalization` setează limba sursă `source` a documentului, glosarul `glossary` și legătura `forkOf`;
  `"reset": true` șterge toate limbile.

### Verificări [#verificări]

`POST …/lint` adaugă trei avertismente pentru creațiile traduse, fiecare cu `locale`:

- `missing_translation` — unul pentru fiecare limbă, `value` = numărul de unități fără traducere.
- `stale_translation` — o unitate al cărei text sursă s-a schimbat după traducere (cel mult 50 pe limbă; mesajul
  numește cheia).
- `text_overflow_locale` — **corectabil**: o traducere mai lungă decât permite caseta de text în formatul implicit.
  `POST …/lint/fix` setează pe acel nod `autoFit: "shrink"`, astfel încât textul se micșorează ca să încapă în orice
  limbă.

Trimite `{ "locale": "en" }` la `POST …/lint` ca să verifici chiar documentul tradus (contrast, zonă sigură… în
engleză).

### Previzualizarea, randarea și exportul unei limbi [#previzualizarea-randarea-și-exportul-unei-limbi]

Parametrul `locale` funcționează pe `GET …/compiled` (în interogare), `POST …/render` (în corp; randarea reține limba),
`POST …/frame` (în corp), `GET …/captions.srt` și `GET …/captions.vtt` (în interogare) și `POST …/lint` (în corp).
Limba sursă (sau lipsa lui `locale`) dă documentul așa cum este; o limbă necunoscută răspunde
`404 Locale not found.`.

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/render \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "format": "9x16", "quality": "final", "locale": "en" }'
```

Randările unei limbi se facturează ca orice randare.

### Copierea unei limbi [#copierea-unei-limbi]

`POST /v1/studio/creatives/{id}/locales/{locale}/fork` cu `{ "title"?, "seq"? }` creează o **creație de sine
stătătoare** în acea limbă, din versiunea `seq` (implicit: capul): traducerile sunt scrise chiar în document,
suprascrierile sunt eliminate, `source` devine acea limbă, iar `forkOf` reține `{ creativeId, seq, locale }`. Titlul
implicit este `"<titlu> (<locale>)"`. Răspunde **201** cu creația nouă, plus câmpurile
`{ "locale", "fork_of": { "creative_id", "seq" } }`. Gratuit.

### SDK-uri și unelte de agent (localizare) [#sdk-uri-și-unelte-de-agent-localizare]

```ts
const status = await codai.creatives.locales(c.id);
const units = await codai.creatives.locales(c.id, { locale: 'en', units: true });
const done = await codai.creatives.localize(c.id, { locales: ['en', 'de'], tone: 'informal', glossary: ['codai'] });
const html = await codai.creatives.compiled(c.id, { locale: 'en' });
const job = await codai.creatives.render(c.id, { quality: 'draft', locale: 'en' });
const fork = await codai.creatives.forkLocale(c.id, 'en', { title: 'Teaser de lansare (EN)' });
```

```python
status = client.creatives.locales(c["id"])
done = client.creatives.localize(c["id"], ["en", "de"], tone="informal", glossary=["codai"])
issues = client.creatives.lint(c["id"], locale="en")
fork = client.creatives.fork_locale(c["id"], "en", title="Teaser de lansare (EN)")
```

Agenții de pe desktop, telefon și VS Code folosesc `creative_localize` (`creative_id`, `locales`, `tone`, `glossary`,
`voice`, `only_missing`) și `creative_locale_fork` (`creative_id`, `locale`, `title`); `creative_lint` primește
`locale` și enumeră codurile de traducere, iar `creative_edit` acceptă operațiile `setLocaleText`, `setLocale` și
`setLocalization`. Ca să umpli câte o creație pentru fiecare rând dintr-un tabel — opțional într-o altă limbă pe
fiecare rând — vezi [Loturi](./batches.mdx).

## Șabloane [#șabloane]

Salvează oricare dintre creațiile tale ca **șablon privat, reutilizabil**: alegi nodurile text și imagine care se schimbă
de la o folosire la alta, iar fiecare devine o variabilă a șablonului. `POST /v1/studio/creatives/{id}/template` ia
creația de la `seq` (implicit: capul):

```bash
curl https://ai.codai.ro/v1/studio/creatives/$CREATIVE_ID/template \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Promo 9:16", "vars": [
        { "node_id": "n_title", "prop": "text", "key": "headline", "label": "Titlu" },
        { "node_id": "n_photo", "prop": "image", "key": "photo" } ] }'
```

- `prop: "text"` pe un nod `text`: fragmentele lui devin unul singur, cu stilul primului fragment; textul curent devine
  valoarea implicită.
- `prop: "image"` pe un nod `image` cu un asset literal: id-ul asset-ului curent devine valoarea implicită (asset-ul
  rămâne în document).
- `key` este o literă urmată de cel mult 63 de litere, cifre sau `_`, unică în șablon; `label` are implicit numele
  nodului. Între 1 și 50 de variabile.

Răspunde **201** cu șablonul: `{ "id", "object": "studio.template", "title", "kind", "source_creative_id",
"source_seq", "thumbnail_url", "created_at", "doc" }`, unde câmpul `vars_schema` descrie fiecare variabilă, de exemplu
`{ "headline": { "type": "text", "label", "default", "node_id", "prop" } }`. Șablonul își păstrează propria copie a documentului; editările ulterioare ale creației nu îl
schimbă. Erori: `400 node_not_found`, `400 invalid_pick` (tip de nod greșit, deja variabilă sau o imagine fără asset
literal), `400 duplicate_key`, `404` (creație sau `seq` necunoscute).

`GET /v1/studio/templates?kind=video` listează șabloanele tale (cele mai noi primele, fără `doc`), `GET …/{id}` citește
unul cu documentul lui, `DELETE …/{id}` îl șterge (creațiile făcute din el nu sunt afectate).

**Creare dintr-un șablon** — `POST /v1/studio/creatives` cu `template` și valorile în `vars` (după cheie; cheile
omise își păstrează valoarea implicită):

```bash
curl https://ai.codai.ro/v1/studio/creatives \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "video", "template": "<id șablon>", "vars": { "headline": "Reduceri de toamnă", "photo": "<id asset imagine>" } }'
```

Versiunea 1 este documentul șablonului cu valorile aplicate; `title` are implicit titlul șablonului, iar `brand_kit_id`
funcționează în continuare (se aplică după variabile). O valoare `text` are cel mult 5000 de caractere, o valoare
`image` trebuie să fie id-ul unui asset imagine al tău. Erori: `400 invalid_request` (`template` împreună cu `doc` sau
`vars` fără `template`), `400 kind_mismatch`, `400 unknown_var`, `400 invalid_var_value`, `404` (șablon necunoscut;
un asset imagine care nu este al tău dă `404 asset_not_found`).

```ts
const tpl = await codai.creatives.saveTemplate(c.id, {
  title: 'Promo 9:16',
  vars: [{ node_id: 'n_title', prop: 'text', key: 'headline', label: 'Titlu' }],
});
const { data } = await codai.templates.list({ kind: 'video' });
const fromTpl = await codai.creatives.create({ kind: 'video', template: tpl.id, vars: { headline: 'Reduceri de toamnă' } });
await codai.templates.delete(tpl.id);
```

```python
tpl = client.creatives.save_template(
    c["id"], vars=[{"node_id": "n_title", "prop": "text", "key": "headline", "label": "Titlu"}], title="Promo 9:16"
)
templates = client.templates.list(kind="video")["data"]
from_tpl = client.creatives.create({"kind": "video", "template": tpl["id"], "vars": {"headline": "Reduceri de toamnă"}})
client.templates.delete(tpl["id"])
```

**Preț.** Salvarea, listarea, citirea, ștergerea și folosirea șabloanelor sunt gratuite.

## Export FCPXML [#export-fcpxml]

Predă o creație video unui editor desktop: `GET /v1/studio/creatives/{id}/export.fcpxml` pune în coadă o **arhivă zip**
cu un proiect FCPXML 1.10 și toate fișierele la care trimite:

- `project.fcpxml` — o secvență cu fps-ul (24, 25 sau 30) și dimensiunea documentului; câte un clip pentru fiecare
  scenă, în ordinea redării, numit după scenă, cu un marcaj care poartă id-ul scenei; vocea, muzica și efectele sonore
  sunt clipuri audio conectate, pe benzi proprii (voce `-1`, muzică `-2`, efecte `-3`), cu amplificarea lor.
- `media/seg-NNNN.mp4` — segmentele video pe scene, randate la calitate finală, fără sunet.
- `media/audio-*` — fișierele de voce, muzică și efecte sonore folosite de cronologie.

Dezarhivează-l și deschide `project.fcpxml` în **Final Cut Pro** (File → Import → XML) sau în **DaVinci Resolve** (File
→ Import → Timeline); căile către media sunt relative, așa că păstrează dosarul întreg. **Premiere Pro** îl deschide
prin importul FCPXML sau după o conversie (de exemplu cu un convertor din FCPXML în XML); CapCut pentru desktop poate
importa fișierele media.

```bash
curl "https://ai.codai.ro/v1/studio/creatives/$ID/export.fcpxml?format=16x9" \
  -H "Authorization: Bearer $CODAI_API_KEY"
# 202 { "id": "<id export>", "object": "studio.export", "status": "queued", ... }

curl https://ai.codai.ro/v1/studio/exports/$EXPORT_ID \
  -H "Authorization: Bearer $CODAI_API_KEY"
```

Implicit, `format` este primul format al documentului de 1920×1080, 1080×1920 sau 1080×1080, iar `seq` este versiunea
curentă. Corpul răspunsului `202` este un `StudioExport` cu câmpurile `id`, `object` (`studio.export`), `status`,
`creative_id`, `seq`, `format`, `fps`, `filename`, `url`, `expires_at`, `bytes`, `rendered_seconds`, `cost_micro_usd`,
`warnings`, `error` și `created_at`. Interoghează `GET /v1/studio/exports/{id}` (răspunde mereu `200` cât timp exportul
e al tău) până când `status` devine `succeeded` — atunci `url` este un link de descărcare semnat, valabil o oră (fiecare
interogare semnează unul nou) — sau `failed`, cu `error.code` și `error.message`. Asset-urile audio lipsă sau care nu
sunt gata sunt sărite și apar în `warnings`. Erori: `400 unsupported_kind` (creația nu e video),
`400 unsupported_format`, `400 too_long` (peste 180 s), `404` (creație sau `seq` necunoscut), `429 too_many_renders`
(exporturile se numără împreună cu randările la limita de 3 lucrări în curs).

```ts
const queued = await codai.creatives.exportFcpxml(c.id, { format: '16x9' });
const done = await codai.exports.wait(queued.id); // interoghează la 3 s, cel mult 15 min
if (done.status === 'succeeded') console.log(done.filename, done.url, done.expires_at, done.cost_micro_usd);
else console.error(done.error?.code, done.error?.message);
```

```python
queued = client.creatives.export_fcpxml(c["id"], format="16x9")
done = client.exports.wait(queued["id"])  # interoghează la 3 s, cel mult 15 min
if done["status"] == "succeeded":
    print(done["filename"], done["url"], done["expires_at"], done["cost_micro_usd"])
```

**Preț.** Segmentele sunt aceleași pe care le folosesc randările finale: scenele deja randate (de un export anterior
sau de o randare finală) se refolosesc gratuit. Se facturează doar **secundele randate acum**, la prețul randării
finale, o singură dată, după ce exportul a reușit (`rendered_seconds`, `cost_micro_usd`); un export făcut numai din
segmente din cache nu costă nimic. Interogarea este gratuită.

## Export Lottie [#export-lottie]

Folosește o creație video pe web sau într-o aplicație ca animație vectorială ușoară: `GET /v1/studio/creatives/{id}/export.lottie` o exportă în format **Lottie**, sincron și gratuit — fără randare, fără coadă.

- `container=json` (implicit) — un singur JSON Bodymovin cu imaginile încorporate ca data URL. Rulează în
  [lottie-web](https://github.com/airbnb/lottie-web), în playerele Lottie pentru Android și iOS și în importatoarele
  pentru After Effects.
- `container=dotlottie` — o arhivă dotLottie v2 (`.lottie`: `manifest.json`, `a/<format>.json`, `i/<asset>.<ext>`),
  mai mică, pentru playerele dotLottie.
- `formats=all` — toate formatele documentului (dotLottie: câte o animație pe format; JSON: o arhivă zip cu fișierele
  JSON). Fără el, `format` alege unul (implicit formatul implicit al documentului); dacă le trimiți pe amândouă primești
  `400 invalid_request`.
- `seq` — versiunea (implicit cea curentă).

```bash
# fișierul însuși, direct pe disc
curl -OJ "https://ai.codai.ro/v1/studio/creatives/$ID/export.lottie?format=16x9&download=1" \
  -H "Authorization: Bearer $CODAI_API_KEY"

# toate formatele într-o singură arhivă dotLottie, ca obiect export cu URL semnat
curl "https://ai.codai.ro/v1/studio/creatives/$ID/export.lottie?container=dotlottie&formats=all" \
  -H "Authorization: Bearer $CODAI_API_KEY"
# 200 { "id": "<id export>", "object": "studio.export", "kind": "lottie", "container": "dotlottie",
#       "status": "succeeded", "format": "all", "url": "https://…", "cost_micro_usd": "0", "warnings": [...] }
```

Fără `download`, răspunsul este un `StudioExport` (același obiect ca la exportul FCPXML), deja `succeeded`: `url` este
un link de descărcare semnat, valabil o oră, `rendered_seconds` este `0`, iar `cost_micro_usd` `0`; îl poți citi din nou
cu `GET /v1/studio/exports/{id}`. Cu `download=1`, răspunsul este fișierul însuși (`application/json` sau
`application/zip`, `content-disposition: attachment`) și nu se salvează nimic. Numele fișierelor:
`<slug>-<format>.lottie.json` (JSON), `<slug>-<format|all>.lottie` (dotLottie), `<slug>-lottie.zip` (JSON, toate
formatele).

**Fidelitate.** Lottie este un format vectorial, așa că nu tot ce poate arăta un video încape în el. `warnings`
enumeră ce a fost aproximat sau lăsat deoparte:

| | |
| --- | --- |
| **Se exportă** | forme (dreptunghi, elipsă, linie), imagini (cover / contain, punct focal, decupare, colțuri rotunjite), text (fonturi de sistem), liste (ca text), subtitrări, grupuri, fundaluri (culoare, gradient liniar, imagine), toate presetările de animație și animațiile cu cadre-cheie, tranzițiile dintre scene, câte un marcaj pe scenă |
| **Se aproximează** | apariția cuvânt cu cuvânt, evidențierea progresivă, stilul subtitrărilor, stilurile de text mixte în același nod, animația opacității unui grup |
| **Nu se exportă** | nodurile video, grafic, bloc HTML, tabel, grilă de celule și audio; imaginile peste bugetul de încorporare de 8 MB |

**Proveniență.** Fiecare fișier spune că a fost făcut cu AI: `meta` din JSON conține `g: "codai Studio"`,
`tc: "AI-generated"`, `ai_generated: true` și `custom: { ai_generated: true, generator: "codai-studio", doc_hash }`, iar
fiecare răspuns `2xx` are antetele `x-codai-ai-generated: true` și `x-codai-generator: codai/codai-studio`. Erori:
`400 unsupported_kind` (creația nu e video), `400 invalid_request`, `404` (creație sau `seq` necunoscut),
`413 too_large` (fișierul ar depăși 20 MB).

```ts
const e = await codai.creatives.exportLottie(c.id, { container: 'dotlottie', formats: 'all' });
console.log(e.filename, e.url, e.warnings);
const file = await codai.creatives.downloadLottie(c.id, { format: '16x9' }); // { bytes, filename, contentType }
```

```python
e = client.creatives.export_lottie(c["id"], container="dotlottie", formats="all")
print(e["filename"], e["url"], e["warnings"])
f = client.creatives.download_lottie(c["id"], format="16x9")  # {"bytes", "filename", "content_type"}
```

**Preț.** Gratuit: fără randare, fără rând de consum, fără plafon de cheltuieli.

## Documente (prezentare, document, foaie de calcul) [#documente-prezentare-document-foaie-de-calcul]

[Documentele](./documents.mdx) Studio — prezentările (PPTX), documentele (DOCX) și foile de calcul (XLSX) — pot fi
editate și ele ca o creație. Creația păstrează același conținut ca `spec`-ul documentului, sub formă de noduri cu tip
pe care le editezi prin operații, le previzualizezi live și le generezi înapoi într-un fișier.

**Reprezentarea.**

- **Prezentare** — câte o scenă pe slide (`s1`, `s2`, …), cu aranjarea slide-ului în `layout` (`title`, `section`,
  `bullets`, `two_column`, `image`, `chart`, `table`, `quote`, `stats`, `timeline`, `closing`) și notele pentru
  vorbitor în `notes`, pe scenă. Nodurile sunt așezate pe formatul 16:9 (1920×1080) sau 4:3 (1440×1080), cu id-uri
  precum `s3-title`, `s3-bullets`, `s3-left-heading` sau `s3-stat2-value`. Listele cu marcatori devin un nod `list`,
  tabelele un nod `table`, graficele un nod `chart`, cele două coloane câte un `group`, iar fiecare indicator și
  fiecare punct din cronologie câte un `group`.
- **Document** — câte o scenă pe pagină (`p1`, `p2`, …; o pagină nouă oriunde spec-ul avea un `page_break`). Fiecare
  pagină conține un singur `group` cu rolul `flow`, ai cărui copii sunt blocurile în ordine (`b0`, `b1`, …): titluri,
  paragrafe, liste, tabele, imagini, grafice și citate.
- **Foaie de calcul** — câte o scenă pe foaie (`sh1`, …, cu numele foii), cu un nod `cell_grid` (`columns`, `rows`,
  `freezeHeader`, `autofilter`, `totals`, `conditional`) plus câte un nod `chart` pentru fiecare grafic, cu celula lui
  de ancorare în `anchor`.

Fiecare nod are un **rol semantic** în `role`: `title`, `subtitle`, `heading1`–`heading4`, `paragraph`, `quote`,
`attribution`, `caption`, `contact`, `footer`, `flow`, `column-left`, `column-right`, `column-heading`, `stat`,
`stat-value`, `stat-label`, `stat-delta`, `timeline-item`, `timeline-date`, `timeline-label` și `timeline-detail`.
Generarea citește doar rolurile, tipurile de nod și aranjările scenelor — niciodată id-urile —, așa că poți redenumi,
muta și restiliza nodurile liber. Fragmentele de text pot fi `code`, pot avea un link în `link` (https) sau o notă de
subsol în `footnote` (DOCX); imaginile primesc `alt` și `caption`; paragrafele pot fi aliniate `justify`. Culorile și
fonturile trimit la token-urile temei (`color.bg`, `color.fg`, `color.accent`, `font.heading`, `font.body`), iar
`meta.document` păstrează setările documentului (tema, limba, raportul de aspect, subsolul, mărimea paginii,
orientarea, antetul, numerotarea paginilor, cuprinsul).

**Dintr-un document.** `POST /v1/studio/documents/{id}/creative` (fără corp) convertește unul dintre documentele tale.
Este idempotent: primul apel creează creația (`201`, `existing: false`) și o leagă de document (documentul raportează
apoi `creative_id`); apelurile următoare întorc aceeași creație (`200`, `existing: true`). Corpul răspunsului este
creația obișnuită plus `document_id` și `existing`. Erori: `404` (document necunoscut), `409 document_spec_missing`
(nu există spec stocat), `400 invalid_spec`, `413 too_large`. Gratuit.

```bash
curl -X POST https://ai.codai.ro/v1/studio/documents/$DOC_ID/creative \
  -H "Authorization: Bearer $CODAI_API_KEY"
# 201 { "id": "<id creație>", "object": "studio.creative", "kind": "deck", "head_seq": 1, ..., "document_id": "…", "existing": false }
```

**Editare și previzualizare.** Folosești `outline`, `find`, `subtree` și operațiile ca la orice creație.
`GET …/compiled` previzualizează acum live și creațiile de tip prezentare, document și foaie de calcul: slide-urile,
paginile sau foile apar una sub alta într-o singură pagină HTML (`duration_ms: 0`, plus `kind` în răspuns), iar
fiecare scenă și fiecare nod pot fi adresate prin id, deci un clic în previzualizare selectează nodul.

**Generare.** `POST /v1/studio/creatives/{id}/build` cu `{ "seq"?, "preview"?, "filename"? }` convertește creația
(implicit versiunea curentă) înapoi într-un spec de document și generează fișierul PPTX, DOCX sau XLSX pe aceeași cale
ca `POST /v1/studio/documents`. Fiecare generare este o versiune nouă a aceluiași document (`based_on` = cea mai nouă
generare, deci `version` crește) și reține `creative_id` și `creative_seq`. Nodurile pe care un document nu le poate
păstra (forme libere, blocuri HTML, videoclipuri, texte suplimentare) sunt sărite și apar în `warnings`; o scenă
adăugată fără `layout` primește unul dedus din nodurile ei. Erori: `400 not_supported_for_kind` (creații video sau
imagine), `400 invalid_spec` (spec-ul convertit nu trece validarea) și orice eroare de generare a
`POST /v1/studio/documents`.

```bash
curl https://ai.codai.ro/v1/studio/creatives/$ID/build \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "preview": true }'
# 200 { "object": "studio.creative_build", "creative_id": "…", "seq": 4, "warnings": [], "document": { "version": 2, "file": { "url": "…" }, ... } }
```

```ts
const c = await codai.creatives.fromDocument(docId); // c.existing spune dacă era deja legată
const { ids } = await codai.creatives.find(c.id, { type: 'list' }); // listele cu marcatori
await codai.creatives.edit(c.id, {
  baseSeq: c.head_seq,
  ops: [{ op: 'updateScene', target: 's2', patch: { notes: 'Începe cu marja din T3.' } }],
});
const built = await codai.creatives.build(c.id, { preview: true });
console.log(built.document.version, built.document.file.url, built.warnings);
```

```python
c = client.creatives.from_document(doc_id)
client.creatives.edit(
    c["id"],
    base_seq=c["head_seq"],
    ops=[{"op": "updateScene", "target": "s2", "patch": {"notes": "Începe cu marja din T3."}}],
)
built = client.creatives.build(c["id"], preview=True)
print(built["document"]["version"], built["document"]["file"]["url"], built["warnings"])
```

**Preț.** Conversia, editarea, previzualizarea (`compiled`) și generarea sunt gratuite. Costă doar `preview: true`:
previzualizarea documentului se facturează ca `POST /v1/studio/documents/{id}/previews` (0,002 $ pe pagină, minimum
0,01 $).

## Cronologiile devin creații [#cronologiile-devin-creații]

`POST /v1/studio/timelines` funcționează ca înainte și acum salvează aceeași cronologie și ca o creație editabilă: corpul `202` are `creative_id` (sau `null` dacă copia nu a putut fi făcută — randarea nu este niciodată afectată).

## Partajare, revizuire și remix [#partajare-revizuire-și-remix]

O creație poate fi partajată printr-un **link de vizualizare** public, doar pentru citire (oricine are o cheie API o poate remixa în propriul cont, cu resursele copiate), sau printr-un **link de revizuire**, unde persoane fără cont lasă comentarii ancorate pe un nod și pe un moment, iar aprobatorii aprobă sau cer modificări; o aprobare fixează versiunea. Totul este gratuit — vezi [Partajare, revizuire și remix](./sharing.mdx).

## Hub (intern) [#hub-intern]

Hub-ul apelează aceleași rute sub `/internal/studio/creatives…`, `/internal/studio/brand-kits…`, `/internal/studio/templates…` și `/internal/studio/exports…`, cu un token de serviciu și un `user_id`; corpurile și erorile sunt identice. Kiturile de brand sunt oglindite complet: `POST /internal/studio/brand-kits`, `PATCH /internal/studio/brand-kits/:id`, `GET /internal/studio/brand-kits/:id/dtcg`, `POST /internal/studio/brand-kits/extract` (cu `url` sau `logo_asset_id`) și `POST /internal/studio/creatives/:id/restyle`. Sunt oglindite și subtitrările, platformele, corecturile și panoul de ajustare: `POST /internal/studio/creatives/:id/captions`, `GET /internal/studio/creatives/:id/captions.srt` și `.vtt`, `POST /internal/studio/creatives/:id/adapt`, `POST /internal/studio/creatives/:id/lint/fix` și `GET /internal/studio/creatives/:id/controls`. Este oglindită și localizarea: `GET /internal/studio/creatives/:id/locales`, `POST /internal/studio/creatives/:id/localize` și `POST /internal/studio/creatives/:id/locales/:locale/fork`; loturile se află sub `/internal/studio/batches…` (vezi [Loturi](./batches.mdx)); linkurile de partajare, revizuirile și remixul se află sub `/internal/studio/creatives/:id/shares…`, `/internal/studio/creatives/:id/review…` și `/internal/studio/shares/:token/remix` (vezi [Partajare, revizuire și remix](./sharing.mdx)).

## SDK-uri [#sdk-uri]

```ts
const c = await codai.creatives.create({ kind: 'video', title: 'Teaser de lansare' });
const r = await codai.creatives.edit(c.id, {
  baseSeq: c.head_seq,
  ops: [{ op: 'setMeta', title: 'Teaser de lansare v2' }],
});
const job = await codai.creatives.render(c.id, { quality: 'draft' });

let frames = await codai.creatives.frame(c.id, { t_ms: [0, 1500] });
while (frames.status === 'running') {
  await new Promise((r) => setTimeout(r, 2000));
  frames = await codai.creatives.frameJob(c.id, frames.frame_job_id);
}
const edit = await codai.creatives.comment(c.id, { t_ms: 1500, comment: 'Fă titlul roșu', base_seq: 2 });
const ab = await codai.creatives.variants(c.id, { base_seq: edit.head_seq, count: 3, focus: ['hook', 'cta'] });
await codai.creatives.pickVariant(c.id, ab.variants[0].creative_id, { base_seq: edit.head_seq });

const kit = await codai.brandKits.extract({ url: 'https://acme.example/' });
const branded = await codai.creatives.create({ kind: 'image', brand_kit_id: kit.id });
```

```python
c = client.creatives.create({"kind": "video", "title": "Teaser de lansare"})
client.creatives.edit(c["id"], base_seq=c["head_seq"], ops=[{"op": "setMeta", "title": "v2"}])
job = client.creatives.render(c["id"], {"quality": "draft"})
frames = client.creatives.frame(c["id"], t_ms=[0, 1500], exact=True)
if frames["status"] == "running":
    frames = client.creatives.frame_job(c["id"], frames["frame_job_id"])  # interoghează până la "succeeded"
edit = client.creatives.comment(c["id"], t_ms=1500, comment="Fă titlul roșu", base_seq=2)
ab = client.creatives.variants(c["id"], base_seq=edit["head_seq"], count=3, focus=["hook", "cta"])
client.creatives.pick_variant(c["id"], ab["variants"][0]["creative_id"], base_seq=edit["head_seq"])
```
