# Embeddings, audio și realtime (/ro/docs/gateway/embeddings-audio)

Vectori, speech-to-text, text-to-speech, voce realtime peste WebSocket și tokenii efemeri care fac clienții din browser siguri.

Dincolo de chat, gateway-ul servește patru suprafețe media OpenAI-compatibile. Acceptă aceeași cheie, aceleași rate limits și scriu aceleași înregistrări de usage — dar niciuna nu pornește o sarcină facturabilă, așa că nu contează niciodată la plafonul de sarcini.

## Embeddings [#embeddings]

`POST /v1/embeddings`. Modelul implicit `codai-embed` se rezolvă la **voyage-code-3**, ales pentru retrieval de cod. Input-ul este un string sau un array nevid de string-uri; opțional `dimensions` și `user`. Se acceptă doar `encoding_format: "float"`.

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/embeddings \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "codai-embed", "input": ["salut lume", "vectorizează-mă"] }'
```

</Tab>
<Tab value="TypeScript">

```ts
const { embeddings } = await codai.embeddings({ input: ['salut lume', 'vectorizează-mă'] });
console.log(embeddings[0].length); // lățimea vectorului
```

</Tab>
<Tab value="Python">

```python
vectors = client.embeddings(["salut lume", "vectorizează-mă"])
print(len(vectors[0]))
```

</Tab>
</Tabs>

```json
{
  "object": "list",
  "model": "voyage-code-3",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, …] },
    { "object": "embedding", "index": 1, "embedding": [0.0789, 0.0012, …] }
  ],
  "usage": { "prompt_tokens": 7, "total_tokens": 7 }
}
```

Dacă modelul de embedding cerut nu e disponibil, gateway-ul poate servi o alternativă compatibilă și o dezvăluie în `x-codai-embed-fallback: <cerut>-><servit>`. Verifici header-ul când spațiile vectoriale trebuie să rămână identice între scrieri și citiri.

## Generare de imagini [#generare-de-imagini]

`POST /v1/images/generations`, compatibil OpenAI, servit de **modelele de imagine Google Gemini** pe Vertex AI. Body: `prompt` (obligatoriu, ≤ 8 000 de caractere), `model`, `n` (1–4, implicit 1), `size`, `aspect_ratio`, `image_size`, `reference_images`, `negative_prompt`. Implicit, imaginile vin inline, în base64, împreună cu `mime_type` (`response_format: "b64_json"`). Cu `response_format: "url"` fiecare imagine e stocată ca [asset](./assets.mdx), iar `data` conține obiecte asset cu un URL semnat valabil o oră (vezi [Un URL în loc de octeți](#un-url-în-loc-de-octeți)). Ai nevoie de o cheie API reală; token-urile efemere sunt respinse.

| `model` | Backend | Regiune | `image_size` | Max. imagini de intrare | ≈ Preț per imagine 1K |
| --- | --- | --- | --- | --- | --- |
| `codai-image` (implicit) | gemini-3.1-flash-image | `eu` | `512`, `1K`, `2K`, `4K` | 14 | 0,074 USD |
| `codai-image-fast` | gemini-3.1-flash-lite-image | `global` | `1K` | 4 | 0,034 USD |
| `codai-image-pro` | gemini-3-pro-image | `global` | `1K`, `2K`, `4K` | 14 | 0,134 USD |

Id-urile brute ale backend-ului sunt acceptate și ele ca `model`. `gemini-2.5-flash-image` (oprit de Google pe 2026-10-02), `imagen-4` și `imagen-4-fast` sunt retrase upstream și sunt servite ca `codai-image`, deci clienții vechi funcționează în continuare. Regiunea care a procesat cererea apare în header-ul `x-codai-data-region` — `codai-image` rulează pe multi-regiunea Vertex `eu`, deci prompturile și imaginile rămân în UE.

- **`aspect_ratio`** — `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, `9:21`, `1:4`, `4:1`, `1:8`, `8:1`. Are prioritate față de `size`.
- **`size`** (stil OpenAI) se traduce într-un raport de aspect: `1024x1024` → 1:1, `1536x1024` → 3:2, `1024x1536` → 2:3, `1792x1024` → 16:9, `1024x1792` → 9:16.
- **`image_size`** — rezoluția, implicit `1K`. O rezoluție pe care modelul nu o suportă dă 400.
- **`reference_images`** — `[{ "b64_json": "…", "mime_type": "image/png" }]` (PNG, JPEG, WebP, HEIC, HEIF; ≤ 7 MB fiecare). Sunt trimise modelului înaintea promptului; așa păstrezi consecvente un logo, o paletă, un produs sau un personaj de la o generare la alta.

```bash
curl https://ai.codai.ro/v1/images/generations \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "codai-image", "prompt": "flat minimalist logo of a paper plane", "n": 1, "aspect_ratio": "1:1", "image_size": "1K" }' \
  | jq -r '.data[0].b64_json' | base64 -d > plane.png
```

```json
{ "created": 1790000000, "data": [{ "b64_json": "iVBORw0KGgo…", "mime_type": "image/png" }] }
```

### Editare de imagini [#editare-de-imagini]

`POST /v1/images/edits` modifică una sau mai multe imagini existente pe baza unei instrucțiuni — aceleași modele, limite și taxare. Trimiți JSON cu `image` (un `{b64_json, mime_type}` sau o listă de până la 14) ori un formular `multipart/form-data` în stil OpenAI, cu fișierul în `image` (sau `image[]` repetat). `mask` nu e suportat (400): descrie în prompt zona de modificat.

```bash
curl https://ai.codai.ro/v1/images/edits \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -F model=codai-image \
  -F prompt="replace the background with a soft sunset gradient, keep the logo unchanged" \
  -F image=@plane.png \
  | jq -r '.data[0].b64_json' | base64 -d > plane-sunset.png
```

SDK-uri: `client.images.generate(...)` și `client.images.edit(...)` (TypeScript și Python, body JSON).

### Proveniență [#proveniență]

Imaginile de pe endpoint-ul UE poartă **credențiale de conținut C2PA** (un chunk `caBX` în PNG) și un watermark **SynthID**. Stochează și servește octeții exact cum îi primești — re-encodarea, redimensionarea sau ștergerea metadatelor elimină manifestul C2PA.

### Taxare [#taxare]

Costul se calculează din tokenii raportați de model, la tarifele Vertex ale modelului rezolvat: tokenii imaginii generate cântăresc cel mai mult (o imagine 1K pe `codai-image` ≈ 0,074 USD; 2K ≈ 0,11 USD; 4K ≈ 0,17 USD), iar fiecare imagine de intrare sau de referință adaugă o fracțiune de cent. Costul apare în `x-codai-cost-micro-usd`, intră în plafoanele de cheltuieli și e debitat din portofelul preplătit în EUR (vezi [Limite și prețuri](./limits-pricing.mdx)). Plătești doar imaginile returnate efectiv: când filtrul de siguranță blochează toate imaginile, răspunsul e `400` cu codul `content_filtered` și nu se taxează nimic. Lasă până la 120 s per cerere.

## Muzică și video (codai Studio) [#muzică-și-video-codai-studio]

Două lane-uri de media generativă pe Vertex AI. Ambele cer o cheie API reală, merg cel mai bine cu **prompturi în engleză** (Veo și Lyria 2 resping alte limbi, iar Lyria refuză prompturile care numesc artiști) și sunt procesate în **SUA** — conturile cu rezidență de date în UE primesc `400`. Prețurile de mai jos sunt prețurile de listă upstream, în USD; costul upstream apare în `x-codai-cost-micro-usd`, iar portofelul preplătit în EUR e debitat la tariful codai pentru media (convertit în EUR cu adaosul pentru media), exact ca la imagini.

| Endpoint | Backend | Rezultat | Preț |
| --- | --- | --- | --- |
| `POST /v1/audio/music`, `codai-music` (implicit) | Lyria 3 Clip | MP3 de 30 s cu voce și versuri (sau instrumental) | **0,04 USD per apel** |
| `POST /v1/audio/music`, `codai-music-pro` | Lyria 3 Pro | piesă întreagă de până la 184 s, MP3, voce, versuri, `bpm`, `duration_seconds` | **0,08 USD per apel** |
| `POST /v1/audio/music`, `codai-music-legacy` | Lyria 2 | WAV instrumental de 30 s, 48 kHz | **0,06 USD per clip** |
| `POST /v1/videos` + `GET /v1/videos/{id}` | Veo 3.1 Fast | MP4, 4/6/8 s, 720p sau 1080p, audio opțional | **0,10 USD pe secundă** |

### Muzică [#muzică]

`POST /v1/audio/music` e sincron: un clip `codai-music` durează circa 12 s, o piesă `codai-music-pro` 30–60 s (până la 4 minute pentru piese lungi) — setează un timeout de client de cel puțin 300 s. Body:

- `prompt` (obligatoriu) — gen, stare, instrumente, structură; fără nume de artiști.
- `model` — `codai-music` (implicit), `codai-music-pro` sau `codai-music-legacy`.
- `lyrics` (≤ 3 000 caractere) — versurile de cântat exact; `instrumental: true` — fără voce; `bpm` (40–220); `duration_seconds` (10–184, doar `codai-music-pro`; `codai-music` are mereu circa 30 s); `language` — indiciu pentru limba versurilor, de exemplu `ro`.
- `negative_prompt`, `seed`, `n` (1–4) — **doar `codai-music-legacy`**; trimise unui model Lyria 3, întorc `400`.
- `response_format` — `b64_json` (implicit) sau `url`.

Fiecare element din `data` conține `mime_type` (`audio/mpeg` pentru Lyria 3, `audio/wav` pentru legacy), `duration_seconds` și, pentru Lyria 3, `lyrics` (așa cum au fost cântate, câte un rând `[start:end]`), `caption` (descrierea piesei făcută de model), `lyrics_match` și `warnings`. Când filtrul de siguranță blochează promptul, răspunsul e `400` `content_filtered` și nu se taxează nimic.

<Callout type="warn">
**Versurile în română (și în alte limbi decât engleza) sunt best effort.** Lyria 3 cântă cel mai sigur în engleză. Când trimiți `lyrics`, gateway-ul transcrie rezultatul și întoarce `lyrics_match` (0–1, 1 = cuvintele cântate se potrivesc cu ale tale); sub 0,6 adaugă avertismentul `lyrics_mismatch` — generează din nou sau simplifică versurile. Dacă verificarea nu poate rula, primești `lyrics_match: null` și `lyrics_check_unavailable`. Apelul se taxează în ambele cazuri.
</Callout>

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl --max-time 300 https://ai.codai.ro/v1/audio/music \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "codai-music-pro",
    "prompt": "upbeat Romanian pop, female vocals, acoustic guitar and light synths",
    "lyrics": "Vara asta e a noastră\nSoarele ne cheamă afară",
    "language": "ro",
    "bpm": 118,
    "duration_seconds": 60
  }' | tee song.json | jq -r '.data[0].b64_json' | base64 -d > song.mp3
jq '.data[0] | {duration_seconds, lyrics_match, warnings}' song.json
```

</Tab>
<Tab value="TypeScript">

```ts
const r = await codai.music.generate({
  model: 'codai-music-pro',
  prompt: 'upbeat Romanian pop, female vocals, acoustic guitar and light synths',
  lyrics: 'Vara asta e a noastră\nSoarele ne cheamă afară',
  language: 'ro',
  bpm: 118,
  duration_seconds: 60,
});
await writeFile('song.mp3', r.clips[0]!);
console.log(r.tracks[0]?.lyrics_match, r.warnings); // de ex. 0.82, []
```

</Tab>
<Tab value="Python">

```python
r = client.music.generate({
    "model": "codai-music-pro",
    "prompt": "upbeat Romanian pop, female vocals, acoustic guitar and light synths",
    "lyrics": "Vara asta e a noastră\nSoarele ne cheamă afară",
    "language": "ro",
    "bpm": 118,
    "duration_seconds": 60,
})
open("song.mp3", "wb").write(r.clips[0])
print(r.tracks[0]["lyrics_match"], r.warnings)
```

</Tab>
</Tabs>

```json
{
  "created": 1790000000,
  "model": "codai-music-pro",
  "data": [{
    "b64_json": "SUQzB…",
    "mime_type": "audio/mpeg",
    "duration_seconds": 61.4,
    "lyrics": "[0:6] Vara asta e a noastră\n[6:12] Soarele ne cheamă afară",
    "caption": "Upbeat pop with acoustic guitar… bpm: 118, duration_secs: 61",
    "lyrics_match": 0.82,
    "warnings": []
  }]
}
```

Modelul instrumental anterior rămâne disponibil ca `codai-music-legacy`:

```bash
curl --max-time 180 https://ai.codai.ro/v1/audio/music \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "codai-music-legacy", "prompt": "calm lo-fi piano with soft vinyl crackle, 80 bpm", "negative_prompt": "vocals", "n": 1 }' \
  | jq -r '.data[0].b64_json' | base64 -d > track.wav
```

### Video [#video]

`POST /v1/videos` pornește un job și răspunde `202` cu `{ id, object: "video.job", status: "running", model, seconds, estimated_cost_micro_usd }`. Body: `prompt` (obligatoriu), `negative_prompt`, `seconds` (`4`, `6` sau `8`, implicit 8), `aspect_ratio` (`16:9` implicit, sau `9:16`), `resolution` (`720p` implicit, sau `1080p`), `audio` (implicit `true`), `image` (`{ b64_json, mime_type }` — primul cadru, pentru image-to-video; forțează 8 s), `seed`.

Interogează `GET /v1/videos/{id}` la câteva secunde (un job durează de obicei 1–3 minute). `status` e `running`, `succeeded` (cu `data[0].b64_json`, `video/mp4`), `failed` sau `filtered` (filtrul de siguranță — nu se taxează nimic). Video-ul se **taxează exact o dată, când jobul reușește** — chiar dacă interogările ajung pe instanțe diferite ale gateway-ului; interogările ulterioare ale aceluiași job returnează din nou video-ul fără taxare. MP4-ul terminat e mereu stocat ca [asset](./assets.mdx), deci apare și în `GET /v1/assets?kind=video`. Adaugă `?response_format=url` la interogare ca să primești asset-ul (URL semnat) în locul base64 — recomandat pentru video-urile 1080p, care sunt mari. Id-ul jobului e semnat și legat de contul tău: alți utilizatori primesc `404`.

```bash
ID=$(curl -s https://ai.codai.ro/v1/videos \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "a paper plane gliding over a sunny city, cinematic", "seconds": 4 }' | jq -r .id)

until [ "$(curl -s https://ai.codai.ro/v1/videos/$ID -H "Authorization: Bearer $CODAI_API_KEY" | tee job.json | jq -r .status)" != "running" ]; do sleep 5; done
jq -r '.data[0].b64_json' job.json | base64 -d > plane.mp4
```

SDK-uri: `client.music.generate({ prompt })`, `client.videos.create(...)` / `.retrieve(id)` și `client.videos.generate({ prompt })`, care creează jobul și interoghează până reușește (aruncă eroare la `failed`, `filtered` sau timeout). Python: `client.music.generate({...})`, `client.videos.generate({...}, poll_interval=5.0, timeout=300.0)`.

### Un URL în loc de octeți [#un-url-în-loc-de-octeți]

`response_format: "url"` funcționează pe `POST /v1/images/generations`, `POST /v1/images/edits`, `POST /v1/audio/music` (câmp în body) și `GET /v1/videos/{id}` (parametru de query). Rezultatul e stocat ca [asset](./assets.mdx) timp de 90 de zile, iar fiecare element din `data` e un obiect asset în loc de `{ b64_json, … }`:

```json
{
  "created": 1790000000,
  "model": "codai-music-legacy",
  "data": [{
    "asset_id": "7d0f2c1e-…",
    "kind": "music",
    "status": "ready",
    "model": "codai-music-legacy",
    "url": "https://storage.googleapis.com/…&X-Goog-Signature=…",
    "mime_type": "audio/wav",
    "bytes": 5760044,
    "duration_seconds": 30,
    "width": null,
    "height": null,
    "created_at": "2026-10-10T08:00:00.000Z",
    "expires_at": "2026-10-10T09:00:00.000Z"
  }]
}
```

`url` e un URL semnat valabil **o oră**, care nu cere cheia API, așa că îl poți pune direct într-un element `<img>`, `<audio>` sau `<video>`. Când expiră, apelează `GET /v1/assets/{asset_id}` pentru unul nou. Prețul și taxarea sunt identice cu `b64_json`. Un gateway fără stocare de asset-uri răspunde `503` cu codul `assets_unavailable`; acolo, `b64_json` implicit funcționează în continuare.

SDK-uri: trimite `response_format: 'url'` în body-ul pentru `images.generate` / `images.edit` / `music.generate` și citește `result.assets`; pentru video, trimite `{ responseFormat: 'url' }` (în Python `response_format="url"`) la `videos.retrieve` / `videos.generate` și citește `result.asset`.

## Speech-to-text [#speech-to-text]

`POST /v1/audio/transcriptions`, formular multipart, maximum **25 MB**. Câmpuri: `file` (obligatoriu), `model` (implicit `codai-transcribe`) și opționalele `language`, `prompt`, `response_format`, `temperature` — trimise mai departe ca atare.

| `model` | Backend |
| --- | --- |
| `codai-transcribe`, `codai-stt`, `whisper-1`, `whisper` | Whisper |
| `gpt-4o-mini-transcribe` | gpt-4o-mini-transcribe |

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/audio/transcriptions \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -F file=@sedinta.webm \
  -F model=codai-transcribe \
  -F language=ro
```

</Tab>
<Tab value="TypeScript">

```ts
import { readFile } from 'node:fs/promises';

const bytes = await readFile('sedinta.webm');
const text = await codai.audio.transcribe({ file: bytes, filename: 'sedinta.webm' });
```

</Tab>
<Tab value="Python">

```python
with open("sedinta.webm", "rb") as f:
    text = client.transcribe(f.read(), filename="sedinta.webm")
```

</Tab>
</Tabs>

```json
{ "text": "Să începem cu checklist-ul de deploy." }
```

Facturarea este fixă per minut de audio, estimată din dimensiunea încărcării (aproximativ 1 MB per minut de voce comprimată, minimum un minut) și înregistrată ca `cost_micro_usd` pe rândul de usage.

## Text-to-speech [#text-to-speech]

`POST /v1/audio/speech`. Body JSON: `input` (obligatoriu, ≤ 4 096 caractere), `model` (implicit `codai-tts`), `voice`, `response_format` (implicit `mp3`; merg și `wav`, `opus`, `pcm`), `speed`, `instructions` (≤ 1 000 caractere, doar modelele expresive), `language` (BCP-47, implicit `ro-RO`, folosit de modelele codai-speech), `timestamps` și `store`. Implicit, body-ul răspunsului sunt byte-ii audio.

| `model` | Backend | Voce implicită | Preț (listă upstream) | Note |
| --- | --- | --- | --- | --- |
| `codai-speech` | Google Chirp 3 HD | `Aoede` | 30 USD / 1M caractere | **Română nativă**, 30 de voci, rapid (circa 1 s pentru o replică de 16 s). |
| `codai-speech-expressive` | Gemini 2.5 Flash TTS | `Kore` | 0,50 USD / 1M tokeni de intrare + 10 USD / 1M tokeni audio | Română nativă, aceleași voci, ghidate prin `instructions` (*„vorbește cald și rar, ca un prezentator radio”*). Circa 6 s pentru o replică de 16 s. |
| `codai-tts`, `tts-1`, `tts` | TTS standard | `alloy` | fix per 1 000 caractere | Ieftin, neutru. |
| `codai-tts-expressive`, `gpt-4o-mini-tts` | gpt-4o-mini-tts | `marin` | fix per 1 000 caractere | Acceptă `instructions` precum *„calm, cald, română scurtă, ușor amuzat”*. Voci: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse, marin, cedar. |

**Voci în română** pentru `codai-speech` / `codai-speech-expressive` — trimite numele scurt în `voice` (devine `<language>-Chirp3-HD-<Nume>`) sau un nume complet de voce Google: Achernar, Achird, Algenib, Algieba, Alnilam, Aoede, Autonoe, Callirrhoe, Charon, Despina, Enceladus, Erinome, Fenrir, Gacrux, Iapetus, Kore, Laomedeia, Leda, Orus, Puck, Pulcherrima, Rasalgethi, Sadachbia, Sadaltager, Schedar, Sulafat, Umbriel, Vindemiatrix, Zephyr, Zubenelgenubi. Aceleași voci vorbesc și alte limbi când setezi `language` (de exemplu `en-US`).

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/audio/speech \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "codai-speech", "input": "Bună ziua! Bine ai venit la codai.", "voice": "Charon" }' \
  --output salut.mp3
```

</Tab>
<Tab value="TypeScript">

```ts
import { writeFile } from 'node:fs/promises';

const audio = await codai.audio.speech({
  model: 'codai-speech',
  input: 'Bună ziua! Bine ai venit la codai.',
  voice: 'Charon',
});
await writeFile('salut.mp3', Buffer.from(audio));
```

</Tab>
<Tab value="Python">

```python
audio = client.audio.speech({"model": "codai-speech", "input": "Bună ziua! Bine ai venit la codai.", "voice": "Charon"})
with open("salut.mp3", "wb") as f:
    f.write(audio)
```

</Tab>
</Tabs>

### Marcaje de timp per cuvânt și vorbire stocată [#marcaje-de-timp-per-cuvânt-și-vorbire-stocată]

Setează `timestamps: true` și/sau `store: true` (orice model de vorbire) și răspunsul devine JSON în loc de byte-i audio:

- `words` — `[{ word, start, end }]` în secunde, calculate printr-o transcriere făcută după sinteză (doar cu `timestamps: true`). Dacă transcrierea eșuează, cererea reușește totuși cu `words: null` și antetul `x-codai-timestamps: unavailable`.
- `b64_json` — audio-ul, când `store` nu e setat; sau `asset` — cu `store: true` audio-ul e salvat ca [asset](./assets.mdx) studio de tip `speech` (cu `words` în metadate), pe care îl poți referi ca `asset:<asset_id>` într-o [randare](./renders.mdx), de exemplu pentru subtitrări.
- `object: "audio.speech"`, `model`, `voice`, `language`, `mime_type`, `duration_seconds`, `cost_micro_usd`.

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/audio/speech \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "codai-speech", "input": "Bună ziua!", "language": "ro-RO", "timestamps": true, "store": true }'
```

</Tab>
<Tab value="TypeScript">

```ts
const r = await codai.audio.speechJson({
  model: 'codai-speech',
  input: 'Bună ziua!',
  language: 'ro-RO',
  timestamps: true,
  store: true,
});
console.log(r.words, r.asset?.asset_id); // [{ word: 'Bună', start: 0.12, end: 0.48 }, …]
```

</Tab>
<Tab value="Python">

```python
r = client.audio.speech_json({
    "model": "codai-speech",
    "input": "Bună ziua!",
    "language": "ro-RO",
    "timestamps": True,
    "store": True,
})
print(r.words, r.asset["asset_id"])
```

</Tab>
</Tabs>

```json
{
  "object": "audio.speech",
  "model": "codai-speech",
  "voice": "Aoede",
  "language": "ro-RO",
  "mime_type": "audio/mpeg",
  "duration_seconds": 1.1,
  "asset": { "asset_id": "3f2b8c1e-…", "kind": "speech", "status": "ready", "url": "https://storage.googleapis.com/…", "…": "…" },
  "words": [
    { "word": "Bună", "start": 0.12, "end": 0.48 },
    { "word": "ziua!", "start": 0.52, "end": 1.02 }
  ],
  "cost_micro_usd": "300"
}
```

Facturare: `codai-tts*` e fix per 1 000 caractere de input; `codai-speech*` urmează prețurile de listă upstream de mai sus. Prețurile sunt convertite în EUR cu adaosul codai pentru media și debitate din portofel; `timestamps: true` adaugă transcrierea la tariful speech-to-text. Costul upstream apare în `cost_micro_usd` / `x-codai-cost-micro-usd`. Ca și celelalte lane-uri din afara UE, modelele codai-speech nu sunt disponibile conturilor cu rezidență a datelor în UE.

### Vorbire în streaming cu viseme (română) [#vorbire-în-streaming-cu-viseme-română]

`POST /v1/audio/speech/stream` transmite voci neurale **Azure AI Speech** ca Server-Sent Events: bucăți audio intercalate cu momentele **visemelor** și ale **cuvintelor**, pe același ceas audio, ca un avatar să sincronizeze buzele cât rulează sunetul. Model `codai-tts-ro`. Body: `input` (≤ 4 096 caractere, escapat XML în SSML, deci marcajele se citesc literal), `voice` (`alina` → ro-RO-AlinaNeural, implicit; `emil` → ro-RO-EmilNeural; sau orice nume de voce neurală Azure, de exemplu `en-US-AvaNeural`), `response_format` (`pcm` = PCM16 mono 24 kHz, implicit; sau `mp3`), `speed` (0.5–2.0).

| Eveniment SSE | `data` |
| --- | --- |
| `start` | `{ "type": "start", "model", "voice", "format", "content_type", "sample_rate" }` |
| `audio` | `{ "type": "audio", "delta": "<bucată base64>" }` |
| `viseme` | `{ "type": "viseme", "id": 0-21, "offsetMs": 125 }` — id de visemă Azure |
| `word` | `{ "type": "word", "text": "Salut", "offsetMs": 50, "durationMs": 300 }` |
| `done` | `{ "type": "done", "audio_ms", "audioBytes", "visemes", "words" }` |
| `error` | `{ "type": "error", "error": { "code", "message" } }` |

`offsetMs` se măsoară de la primul eșantion audio, deci programezi fiecare visemă la `începutRedare + offsetMs`.

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl -N https://ai.codai.ro/v1/audio/speech/stream \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": "Bună seara! Mulțumesc pentru trandafir!", "voice": "alina" }'
```

</Tab>
<Tab value="TypeScript">

```ts
for await (const ev of codai.audio.speechStream({ input: 'Bună seara!', voice: 'alina' })) {
  if (ev.type === 'audio') player.enqueue(ev.audio); // PCM16 mono 24 kHz
  if (ev.type === 'viseme') avatar.scheduleViseme(ev.id, ev.offsetMs);
}
```

</Tab>
<Tab value="Python">

```python
for ev in client.audio.speech_stream({"input": "Bună seara!", "voice": "emil"}):
    if ev["type"] == "audio":
        pcm.write(ev["audio_bytes"])
```

</Tab>
</Tabs>

Azure Speech rulează în **westeurope**, deci conturile cu rezidență de date în UE pot folosi acest lane. Facturare: **16 $ per 1M caractere de input**, taxat doar după ce s-a produs audio; nu pornește niciodată o sarcină. Când gateway-ul nu are o cheie Azure Speech, ruta răspunde `503` cu codul `tts_unavailable`.

## Tokeni efemeri [#tokeni-efemeri]

Browserele și webview-urile nu trebuie să dețină niciodată cheia ta de lungă durată. În schimb, generezi server-side un **token de scurtă durată cu scop limitat** cu `POST /v1/tokens` și îl dai clientului. Scopuri: `realtime`, `audio`, `embeddings` și `live` — un singur token care ajunge atât la `/v1/realtime`, cât și la `/v1/audio/*`, pentru un co-host live care ascultă și vorbește. Chat-ul nu poate fi generat intenționat. TTL 60–3 600 s, implicit 600.

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://ai.codai.ro/v1/tokens \
  -H "Authorization: Bearer $CODAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "realtime", "ttl_seconds": 600 }'
```

</Tab>
<Tab value="TypeScript">

```ts
const { token, expiresAt } = await codai.mintToken('realtime', 600);
// trimiți `token` către browser
```

</Tab>
<Tab value="Python">

```python
minted = client.mint_token("realtime", ttl_seconds=600)
token = minted["token"]
```

</Tab>
</Tabs>

```json
{ "token": "codai_eph_v1.…", "expires_at": "2026-09-23T12:10:00.000Z", "scope": "realtime" }
```

Token-ul se folosește exact ca o cheie API — `Authorization: Bearer codai_eph_v1.…` — dar doar pe suprafața scopului său. Nu poate genera alți tokeni (`403 forbidden`). Revocarea cheii generatoare sau retrogradarea tier-ului ei invalidează tokenii în circulație în fereastra de cache a principalului.

## Voce realtime [#voce-realtime]

`WSS /v1/realtime?model=<alias>` — audio bidirecțional pe un singur WebSocket, retransmis ca atare către protocolul upstream.

| `model` | Protocol | Backend |
| --- | --- | --- |
| `codai-realtime` (implicit pentru clienți în stil OpenAI) | OpenAI Realtime | gpt-realtime-2.1 |
| `codai-transcribe-live` | OpenAI Realtime, `intent=transcription` | Doar STT în streaming — parțiale și evenimente de tură, fără răspuns de model |
| `codai-voice` | Gemini Live `BidiGenerateContent` | gemini-live-2.5-flash-native-audio |

Dacă `model` lipsește, gateway-ul presupune `codai-speech`.

### Parametrii transcrierii live [#parametrii-transcrierii-live]

`codai-transcribe-live` acceptă doi parametri opționali în query:

- **`language`** — `ro`, `en`, `ro-RO`, … Indiciul e scris în sesiunea de transcriere (`session.audio.input.transcription.language`), dacă propriul tău `session.update` nu setează deja unul.
- **`session_id`** — id-ul conversației tale, înregistrat pe rândul de usage (același câmp pe care îl completează `x-codai-session-id` pe rutele HTTP).

Cu **`language=ro`** și Deepgram configurat pe gateway, sesiunea rulează pe **Deepgram Nova-3** (rezultate intermediare, punctuație, formatare inteligentă). Protocolul nu se schimbă: trimiți în continuare `input_audio_buffer.append` (PCM16 mono 24 kHz în base64), `input_audio_buffer.commit` și `input_audio_buffer.clear` și primești `conversation.item.input_audio_transcription.delta` și `.completed`. Delta-urile doar adaugă text; când Deepgram revizuiește cuvinte anterioare, delta-urile se opresc, iar `.completed.transcript` poartă textul final — consideră-l sursa de adevăr. Fiecare `commit` primește exact un `.completed`. Dacă Deepgram nu e configurat sau eșuează înainte să se deschidă socket-ul, sesiunea trece pe lane-ul Azure cu audio-ul deja trimis. `upstream_model` de pe rândul de usage îți spune ce lane a servit (`deepgram:nova-3` sau `gpt-live-transcribe`), iar `ttfb_ms` e timpul până la primul delta de transcriere.

```ts title="subtitrari-live.ts"
import { Codai } from 'codai-sdk';

// token generat server-side cu scope 'live'
const codai = new Codai({ apiKey: token });
const live = codai.realtime.connect({ model: 'codai-transcribe-live', language: 'ro', sessionId: 'stream-42' });
mic.onFrame((pcm24k: Int16Array) => live.appendAudio(pcm24k));
for await (const ev of live) {
  if (ev.type === 'conversation.item.input_audio_transcription.delta') subtitrari.adauga(ev.delta);
  if (ev.type === 'conversation.item.input_audio_transcription.completed') subtitrari.fixeaza(ev.transcript);
}
```

### Autentificarea upgrade-ului [#autentificarea-upgrade-ului]

<Callout type="warn">
Cheile brute `codai_…` sunt **respinse** în query string (cod de închidere `4401`). Ar ajunge în log-uri de cereri, proxy-uri și istoricul browserului. Browserele trebuie să folosească un token efemer `realtime` sau `live`.
</Callout>

- **Browser:** `wss://ai.codai.ro/v1/realtime?model=codai-realtime&key=codai_eph_v1.…`
- **Server:** trimiți `Authorization: Bearer <cheie sau token>` pe cererea de upgrade — `?key=` nu mai e necesar.

```ts title="browser.ts"
// token obținut de la backend-ul tău, care a apelat POST /v1/tokens
const ws = new WebSocket(
  `wss://ai.codai.ro/v1/realtime?model=codai-realtime&key=${encodeURIComponent(token)}`,
);
ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'session.update', session: { instructions: 'Răspunde scurt.' } }));
};
ws.onmessage = (ev) => {
  const event = JSON.parse(ev.data);
  if (event.type === 'response.audio.delta') {
    /* adaugi PCM-ul base64 la player */
  }
};
```

Coduri de închidere pe care le poți vedea: `4401` credențial invalid sau nepermis, `4503` lane-ul cerut nu e configurat pe acest gateway, `1011` eroare internă. Usage-ul se înregistrează la închiderea sesiunii, cu durata ei.
