# Randări (/ro/docs/gateway/renders)

Randări codai Studio — transformi o compoziție HyperFrames HTML (cu asset-urile tale Studio) într-un MP4, apoi interoghezi până când e gata.

O **randare** transformă un video descris în HTML într-un MP4. Trimiți o compoziție HyperFrames, gateway-ul o pune în coada worker-ului de randare, iar tu interoghezi până când video-ul e gata. Id-ul public al modelului este `codai-render`.

| Metodă | Cale | Rezultat |
| --- | --- | --- |
| `POST` | `/v1/studio/renders` | `202` — randarea e în coadă; întoarce `id`-ul și prețul |
| `GET` | `/v1/studio/renders/{id}` | Starea, `progress` și asset-ul MP4 după `succeeded` |

## Compoziții HyperFrames [#compoziții-hyperframes]

O compoziție este un document HTML complet (`index.html`, până la 2.000.000 de caractere), așezat pe o linie de timp cu atribute `data-*`:

- elementul **rădăcină** are `data-composition-id`, `data-width` și `data-height` (pânza, în pixeli);
- fiecare **clip** (text, imagine, video, audio, orice element) are `data-start` și `data-duration` în secunde și un `data-track-index` (pistele mai mari se desenează deasupra);
- animațiile CSS și JavaScript rulează așa cum sunt scrise. **GSAP este permis**, la fel Lottie, Three.js și anime.js — URL-urile lor de CDN sunt servite din copii locale în randator, așa că un URL de CDN obișnuit funcționează.

Ieșirea durează până la finalul ultimului clip (`data-start` + `data-duration`), cel mult 180 de secunde.

```html
<div data-composition-id="launch" data-width="1920" data-height="1080" style="background:#0b0b10">
  <img class="clip" src="asset:3f2b8c1e-0000-4000-8000-000000000001"
       data-start="0" data-duration="8" data-track-index="0" style="width:100%;height:100%;object-fit:cover" />
  <h1 id="title" class="clip" data-start="1" data-duration="6" data-track-index="1">codai Studio</h1>
  <audio class="clip" src="asset:3f2b8c1e-0000-4000-8000-000000000002"
         data-start="0" data-duration="8" data-track-index="2"></audio>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
  <script>gsap.from('#title', { opacity: 0, y: 40, duration: 1, delay: 1 });</script>
</div>
```

### Folosirea asset-urilor Studio [#folosirea-asset-urilor-studio]

Referă orice [asset](./assets.mdx) al tău ca `asset:<asset_id>` într-un `src`, `href` sau `url(...)` din CSS — imagini generate, clipuri muzicale, video-uri, vorbire sau fișiere încărcate. Înainte de a pune randarea în coadă, gateway-ul verifică faptul că fiecare asset referit există, îți aparține și este `ready`; altfel cererea eșuează cu `400 asset_not_found` și nu se taxează nimic. Cel mult **50 de asset-uri distincte** per randare.

Pentru lovituri, whoosh-uri și alte efecte sonore, importă gratuit un sunet CC0 din [biblioteca de efecte sonore](./sfx.mdx) și referă asset-ul rezultat la fel.

Pentru video-uri narate, creează vocea cu `POST /v1/audio/speech` și `{ "model": "codai-speech", "store": true, "timestamps": true }`: `asset.asset_id` din răspuns intră în `<audio src="asset:<id>">`, iar `words` (`[{ word, start, end }]`) îți dă momentele pentru subtitrări (vezi [text-to-speech](./embeddings-audio.mdx#marcaje-de-timp-per-cuvânt-și-vorbire-stocată)).

`variables` (opțional) este un obiect cu variabilele compoziției HyperFrames, pentru șabloane care citesc valori la randare.

## Cererea [#cererea]

```json
{
  "model": "codai-render",
  "html": "<div data-composition-id=\"launch\" data-width=\"1920\" data-height=\"1080\">…</div>",
  "aspect_ratio": "16:9",
  "quality": "final",
  "fps": 30,
  "title": "Launch teaser"
}
```

| Câmp | Implicit | Semnificație |
| --- | --- | --- |
| `html` | — | Compoziția (obligatoriu). |
| `model` | `codai-render` | Singurul model de randare. |
| `aspect_ratio` | `16:9` | `16:9` (1920×1080), `9:16` (1080×1920) sau `1:1` (1080×1080). Trebuie să se potrivească cu `data-width`/`data-height` ale rădăcinii, altfel `400`. |
| `quality` | `final` | `final` sau `draft` (vezi mai jos). |
| `fps` | `30` | `24`, `25` sau `30`; doar la `final`. |
| `variables` | — | Variabilele compoziției HyperFrames. |
| `title` | — | Până la 200 de caractere, stocat cu asset-ul. |
| `ai_label` | automat | `corner`, `end_card` sau `none` — eticheta IA vizibilă (vezi mai jos). |

### Limite [#limite]

- cel mult **180 de secunde** de ieșire;
- cel mult **1080p** (cele trei rapoarte de aspect de mai sus);
- **24, 25 sau 30 fps**;
- **3 randări** în coadă sau în lucru per utilizator în același timp — a patra primește `429 too_many_renders`.

### Draft sau final [#draft-sau-final]

| | `draft` | `final` |
| --- | --- | --- |
| Cadre pe secundă | 15 fps | 24 / 25 / 30 fps |
| Rezoluție | 540p pe latura scurtă | 1080p complet |
| Normalizarea volumului | nu | la −14 LUFS integrat, −1 dBTP |
| Preț | jumătate | întreg |

Iterezi cu `draft`, apoi randezi ca `final` versiunea pe care o păstrezi.

### Eticheta IA (Regulamentul UE privind IA, art. 50 alin. 4) [#eticheta-ia-regulamentul-ue-privind-ia-art-50-alin-4]

`ai_label` imprimă în MP4 o etichetă vizibilă:

| Valoare | Efect |
| --- | --- |
| `corner` | O etichetă mică „Generat cu IA” (pentru conținut în engleză: „AI-generated”) în colțul din dreapta jos, pe toată durata videoclipului. |
| `end_card` | Un cadru final de circa 2 s cu eticheta. |
| `none` | Fără etichetă. |

Dacă lipsește, eticheta este `corner` când randarea folosește narațiune TTS (o voce sintetică) sau imagini generate cu IA și `none` în rest. Un `none` explicit la o randare cu narațiune TTS este refuzat cu `400 ai_label_required`. Fiecare răspuns de randare (`202` și `GET`) raportează valoarea rezolvată în `ai_label`; aceeași opțiune există la [randarea creațiilor](./creatives.mdx#previzualizare-și-randare) și la loturi (`ai_label` se aplică randării fiecărui rând).

> **Regulamentul UE privind IA, art. 50 alin. 4.** Dacă publici un videoclip generat sau manipulat cu IA care seamănă cu persoane, locuri sau evenimente reale (un deepfake), tu — ca implementator — ești obligat să dezvălui că acesta a fost generat artificial. Eticheta te ajută să faci asta, dar nu îți înlocuiește obligația. Vezi [Politica de utilizare acceptabilă](https://codai.ro/ro/acceptable-use).

## Starea și QA [#starea-și-qa]

`GET /v1/studio/renders/{id}` întoarce `queued`, `rendering` (cu `progress` între 0 și 1), `succeeded` sau `failed`.

```json
{
  "id": "9c1d2e3f-0000-4000-8000-000000000010",
  "object": "studio.render",
  "status": "succeeded",
  "progress": 1,
  "quality": "final",
  "aspect_ratio": "16:9",
  "fps": 30,
  "error": null,
  "qa": { "duration_seconds": 31.2, "width": 1920, "height": 1080, "lufs": -14.1, "warnings": [] },
  "cost_micro_usd": "320000",
  "data": [{ "asset_id": "9c1d2e3f-0000-4000-8000-000000000010", "kind": "render", "url": "https://storage.googleapis.com/…", "mime_type": "video/mp4", "…": "…" }],
  "poster_url": "https://storage.googleapis.com/…poster.jpg…"
}
```

După succes, `data` conține MP4-ul ca un singur obiect asset (`kind: "render"`, URL semnat valabil o oră), iar `poster_url` un cadru-poster JPEG. `id`-ul randării este și `asset_id`-ul ei, așa că apare în `GET /v1/assets?kind=render` și poți obține oricând un URL proaspăt cu `GET /v1/assets/{id}`.

Fiecare randare trece printr-o verificare tehnică QA. O compoziție care nu trece de lint, iese mai lungă decât limita, are dimensiuni greșite sau e aproape complet neagră ori înghețată **eșuează** — `error` are forma `<cod>: <mesaj>`, cu coduri precum `lint_failed`, `render_failed`, `too_long`, `dimension_mismatch`, `black_video`, `frozen_video` sau `timeout`. Problemele mai mici nu opresc randarea; apar în `qa.warnings`:

| Avertisment | Semnificație |
| --- | --- |
| `no_audio` | Video-ul nu are pistă audio. |
| `silent_audio` | Există o pistă audio, dar e tăcută. |
| `lufs_off_target` | Volumul perceput e la mai mult de 1 LU de −14 LUFS după normalizare. |

## Cronologii de scene [#cronologii-de-scene]

`POST /v1/studio/timelines` construiește un video dintr-o listă de scene, fără să scrii HTML. Fiecare scenă este una dintre imaginile sau video-urile tale `ready`, afișată pe tot cadrul; imaginile primesc un zoom lent, iar video-urile își păstrează sunetul propriu la `video_volume`.

```json
{
  "title": "Teaser de produs",
  "aspect_ratio": "16:9",
  "quality": "draft",
  "transition": "fade",
  "music_asset_id": "3f2b8c1e-0000-4000-8000-000000000003",
  "narration_defaults": { "model": "codai-speech", "language": "ro-RO" },
  "scenes": [
    { "id": "intro", "kind": "image", "asset_id": "3f2b8c1e-0000-4000-8000-000000000001", "seconds": 4,
      "prompt": "A sunrise over a mountain lake", "narration": { "text": "Fiecare dimineață începe undeva." } },
    { "kind": "video", "asset_id": "3f2b8c1e-0000-4000-8000-000000000002", "seconds": 6 }
  ]
}
```

| Câmp | Observații |
| --- | --- |
| `scenes` | 1–30 de scene, cel mult 180 s în total (`timeline_too_long`). `kind` trebuie să corespundă tipului MIME al asset-ului. |
| `scenes[].id` | Opțional, `^[a-z0-9-]{1,32}$`, implicit `s1`…`sN`. Rămâne același de la o regenerare la alta. |
| `scenes[].seconds` | 2–30 s (implicit 5). O scenă narată durează cel puțin cât narațiunea plus 0,6 s. |
| `scenes[].fit` | `cover` (implicit, cu decupare) sau `contain` (cu benzi negre). |
| `scenes[].narration` | Sintetizată **acum** cu `narration_defaults.model` și taxată per apel (`narration_cost_micro_usd`). Nu este disponibilă pentru conturile cu rezidența datelor în UE (403 `residency_unavailable`). |
| `transition` | `fade` (implicit, estompare de 0,5 s) sau `none`. |
| `music_asset_id`, `music_volume` | Unul dintre fișierele tale audio, sub întreaga cronologie (volum 0–1, implicit 0,126). |

Răspunsul este `202`, cu `id`, `root_render_id`, `scene_count`, `duration_seconds` și prețul. Interoghează `GET /v1/studio/renders/{id}` ca la orice randare; randarea se taxează la fel, o singură dată, după succes. Starea unei randări cu cronologie conține și `timeline` (scenele, fiecare cu `asset` și `narration_asset`), `parent_render_id` și `root_render_id`. Adaugă `?expand=assets` ca să primești URL-uri semnate pentru asset-urile scenelor; fără el, `url` este `null`.

Un asset necunoscut, al altcuiva, neterminat sau de tip greșit întoarce 400 `asset_not_found`, iar id-urile problematice sunt listate în `error.details.missing`.

### Regenerarea unei scene [#regenerarea-unei-scene]

`POST /v1/studio/renders/{id}/scenes/{sceneId}/regenerate` creează o randare **nouă** în care o singură scenă este înlocuită. Randarea sursă rămâne neschimbată; cea nouă are `parent_render_id` egal cu sursa și păstrează `root_render_id`, așa că `GET /v1/studio/renders?root_render_id=…` listează toate versiunile. Sursa trebuie să fie o randare cu cronologie reușită (altfel 409 `not_a_timeline`).

| Corp | Efect | Cost |
| --- | --- | --- |
| `{"visual": {"asset_id": "…"}}` | Folosește în schimb una dintre imaginile sau video-urile tale. | niciunul |
| `{"visual": {"prompt": "…"}}` pe o scenă cu imagine | Imagine nouă cu `codai-image`, în raportul de aspect al cronologiei. | taxat acum |
| `{"visual": {"prompt": "…", "seconds": 6}}` pe o scenă video | Clip nou `codai-video` (4, 6 sau 8 s, implicit 8). Randarea rămâne `queued` cu `stage: "generating_scene"` până când un poll vede clipul. | taxat o singură dată, când clipul e gata |
| `{"narration": {"text": "…"}}` | Narează din nou scena. | taxat acum |
| `{"narration": {"asset_id": "…"}}` | Folosește unul dintre fișierele tale audio ca narațiune. | niciunul |
| `{"narration": null}` | Elimină narațiunea. | niciunul |

Dacă omiți `prompt` sau `text`, se refolosește valoarea stocată în scenă. Răspunsul `202` întoarce `scene_cost_micro_usd` (generarea taxată până acum) alături de prețul randării; noua randare se taxează după succes. Video-ul și narațiunea nu sunt disponibile pentru conturile cu rezidența datelor în UE.

`GET /v1/studio/renders` listează randările tale, cele mai noi primele (`before`, `limit` 1–100, `root_render_id`), fără să le interogheze și fără URL-uri semnate.

## Prețuri [#prețuri]

Se taxează o singură dată, la primul poll care vede randarea terminată, pe **secundă de ieșire** (rotunjit în sus la secunde întregi; până la 50 ms de padding al encoderului nu se taxează):

| Calitate | Pe secundă de ieșire | Minimum |
| --- | --- | --- |
| `final` | 0,01 $ | 0,05 $ |
| `draft` | 0,005 $ | 0,025 $ |

O randare `final` de 31,2 s costă 32 × 0,01 $ = **0,32 $**. Randările eșuate nu costă nimic. Răspunsul `202` repetă prețul în `price_micro_usd_per_second` și `minimum_micro_usd`; costul apare în `cost_micro_usd` după succes. Conturile cu portofel plătesc aceeași sumă, convertită ca la celelalte servicii media (vezi [Limite și prețuri](./limits-pricing.mdx)).

## Erori [#erori]

| HTTP | `code` | Când |
| --- | --- | --- |
| 400 | `bad_request` | Corp invalid sau `aspect_ratio` nu se potrivește cu `data-width`/`data-height` ale rădăcinii. |
| 400 | `asset_not_found` | Un asset referit e necunoscut, nu e al tău sau nu e `ready`. |
| 400 | `timeline_too_long` | O cronologie (sau o versiune regenerată) ar dura mai mult de 180 s. |
| 400 | `ai_label_required` | `ai_label: "none"` la o randare cu narațiune TTS (vocea sintetică trebuie etichetată). |
| 403 | `forbidden` | Cheia nu are voie să folosească `codai-render`. |
| 403 | `residency_unavailable` | S-a cerut narațiune sau o scenă video generată pe un cont cu rezidența datelor în UE. |
| 402 / 429 | `subscription_inactive` / `rate_limit_exceeded` | Abonament inactiv sau plafon de cheltuieli atins. |
| 429 | `too_many_renders` | Ai deja 3 randări în coadă sau în lucru. |
| 404 | `not_found` | Id de randare necunoscut sau randarea altui utilizator. |
| 409 | `not_a_timeline` | Regenerare de scenă pe o randare care nu s-a terminat cu succes sau nu are cronologie. |
| 503 | `renders_unavailable` | Randările nu sunt configurate pe acest gateway. |

## Exemple [#exemple]

### curl [#curl]

```bash
ID=$(curl -s https://ai.codai.ro/v1/studio/renders \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d @render.json | jq -r .id)

while :; do
  R=$(curl -s "https://ai.codai.ro/v1/studio/renders/$ID" -H "Authorization: Bearer $CODAI_API_KEY")
  S=$(echo "$R" | jq -r .status)
  echo "$S $(echo "$R" | jq -r .progress)"
  [ "$S" = succeeded ] || [ "$S" = failed ] && break
  sleep 5
done
curl -L -o render.mp4 "$(echo "$R" | jq -r '.data[0].url')"
```

### SDK TypeScript [#sdk-typescript]

```ts
import { readFile } from 'node:fs/promises';
import { Codai } from 'codai-sdk';
const client = new Codai({ apiKey: process.env.CODAI_API_KEY! });

const html = await readFile('index.html', 'utf8');
const r = await client.renders.render(
  { html, aspect_ratio: '16:9', quality: 'draft' },
  { pollIntervalMs: 5000, timeoutMs: 15 * 60_000 },
);
console.log(r.asset.url, r.qa?.warnings, r.costMicroUsd);
```

`client.renders.create(body)` și `client.renders.retrieve(id)` sunt cele două apeluri brute; `render()` interoghează până la `succeeded` și aruncă o eroare la `failed` sau la expirarea timpului.

Cronologii și regenerarea scenelor:

```ts
const job = await client.renders.createTimeline({
  quality: 'draft',
  scenes: [
    { id: 'intro', kind: 'image', asset_id: imageId, seconds: 4, narration: { text: 'Bună ziua.' } },
    { kind: 'video', asset_id: videoId },
  ],
});
// …interoghează client.renders.retrieve(job.id) până la succeeded, apoi:
const v2 = await client.renders.regenerateScene(job.id, 'intro', { visual: { prompt: 'The same lake at dusk' } });
const versions = await client.renders.list({ rootRenderId: job.root_render_id });
const detail = await client.renders.retrieve(v2.id, { expandAssets: true });
```

### SDK Python [#sdk-python]

```python
import os
from pathlib import Path
from codai import Codai

client = Codai(api_key=os.environ["CODAI_API_KEY"])
r = client.renders.render(
    {"html": Path("index.html").read_text(encoding="utf-8"), "aspect_ratio": "9:16", "quality": "final"},
    poll_interval=5,
    timeout=900,
)
print(r.asset["url"], r.qa["warnings"], r.cost_micro_usd)
```

`render()` ridică `RuntimeError` când randarea a eșuat și `TimeoutError` când nu se termină la timp; `create()` și `retrieve()` întorc JSON-ul brut.

Și cronologiile, și regenerarea scenelor întorc JSON-ul brut:

```python
job = client.renders.create_timeline({
  "quality": "draft",
  "scenes": [
    {"id": "intro", "kind": "image", "asset_id": image_id, "seconds": 4, "narration": {"text": "Bună ziua."}},
    {"kind": "video", "asset_id": video_id},
  ],
})
# …interoghează client.renders.retrieve(job["id"]) până la succeeded, apoi:
v2 = client.renders.regenerate_scene(job["id"], "intro", {"narration": None})
versions = client.renders.list(root_render_id=job["root_render_id"])
detail = client.renders.retrieve(v2["id"], expand_assets=True)
```
