Zani

API v1

Audio shqip me një thirrje

Dërgo tekst, merr audio. Këtu ke gjithçka për ta futur Zanin në produktin tënd, ose e le agjentin tënd ta bëjë për ty.

Fillimi i shpejtë

  1. 1Krijo një çelës te Paneli → API. Fillon me zn_live_. Ruaje si variabël mjedisi, p.sh. ZANI_API_KEY.
  2. 2Dërgo një POST te /v1/text-to-speech me tekstin dhe zërin.
  3. 3Përgjigja është JSON me një audioUrl. Shkarko skedarin nga ajo lidhje.
# 1. Krijo audion
curl -s -X POST https://www.tryzani.com/v1/text-to-speech \
  -H "Authorization: Bearer $ZANI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Mirë se vini në Zani.","voice":"arben","dialect":"kosovo"}' \
  | tee response.json

# 2. Shkarko skedarin nga audioUrl
curl -s "$(jq -r .audioUrl response.json)" -o zani.mp3

Kopjo prompt-in për agjentin tënd

Ngjite në Claude Code, Cursor, Codex ose çdo agjent tjetër. Ai lexon specifikimin e plotë dhe integron Zanin në projektin tënd, me çelësin në variabël mjedisi.

You are integrating the Zani text-to-speech API (natural Albanian voices) into this project.

## What Zani is
Zani turns text into natural Albanian speech (dialects: standard, kosovo, gheg, tosk). It is a REST API: JSON in, JSON out, Bearer auth.
Base URL: https://www.tryzani.com
Full docs: https://www.tryzani.com/docs

## Before you write code
1. Inspect the project: language, framework, where server-side code lives, how secrets/env vars are handled, and any existing audio/TTS code. Follow the project's conventions.
2. If it is not obvious, ask me where audio should be generated and what to do with it (play in the UI, save a file, serve to users).
3. Ask me to create an API key at https://www.tryzani.com/app/developers (it starts with "zn_live_") and store it in the environment variable ZANI_API_KEY. Never hard-code, log or commit the key. Never call the API from browser code: go through a server route or function.

## API
Auth header on every request: Authorization: Bearer $ZANI_API_KEY
Rate limit: 60 requests per minute per key, all endpoints combined. On HTTP 429 wait for the Retry-After header (60 seconds) and retry.

### POST https://www.tryzani.com/v1/text-to-speech
JSON body:
- text (string, required): 1 to 20000 characters. 1 character costs 1 credit, charged only when generation succeeds.
- voice (string, required): voice slug or id, e.g. "vjosa". List them with GET /v1/voices.
- dialect: standard | kosovo | gheg | tosk (default standard)
- emotion: neutral | warm | confident | energetic | calm | empathetic | serious | joyful | solemn (default warm)
- pace: slow | medium | medium-fast | fast (default medium)
- energy: low | medium | high (default medium)
- purpose: advertisement | audiobook | accessibility | social | documentary | meditation | education | news | podcast | narration | character (default narration)
- personality: free text, max 240 characters (optional)
- format: mp3 | wav (default mp3)

Response 200 (JSON): { id, status, queued, audioUrl, durationMs, characterCount, creditsCharged, format, voice: { id, slug, name }, error, createdAt, ... }
- status is one of "completed", "queued", "processing", "failed".
- Texts up to 900 characters finish inside the request: status "completed" and audioUrl set.
- Texts over 900 characters are queued: status "queued", audioUrl null. Poll GET /v1/generations/{id} every 2-3 seconds until status is "completed" (audioUrl set) or "failed" (error set). Stop after about 5 minutes.
- audioUrl is a temporary signed link (about 1 hour). Download the bytes and store them yourself if they are needed later.

### GET https://www.tryzani.com/v1/generations/{id}
Same JSON shape as above. Use it to poll queued generations. 404 if the id is not yours.

### GET https://www.tryzani.com/v1/voices
Returns { voices: [{ id, slug, name, gender, age, dialect, description, tags, badge, custom, sampleUrl }] }. sampleUrl is a ready-made audio sample of the voice.

### GET https://www.tryzani.com/v1/account
Returns { credits, plan, status }. Use it to check the remaining balance.

## Errors
Failures return JSON { "error": string } with these statuses:
- 400 invalid request (an "issues" array explains which field)
- 401 missing or invalid API key
- 402 not enough credits
- 404 voice or generation not found
- 429 rate limit exceeded (honour Retry-After)
- 500 / 502 temporary generation failure (no credits charged; retry once after a few seconds)
Error messages are written in Albanian: map them to your own user-facing messages instead of showing them raw.

## What to build
- A small wrapper module (for example lib/zani.ts or the equivalent for this stack) exposing speak({ text, voice, dialect?, emotion?, pace?, energy?, purpose?, format? }) that returns the audio bytes or a stored file path. It must handle polling, a 60 second request timeout, retry on 429 (Retry-After) and on 5xx once, and throw typed errors.
- Validate text length (max 20000) before calling. Split longer content on sentence boundaries into several requests.
- Cache audio for identical (text, voice, options) so credits are not spent twice.
- Add tests for the wrapper using a mocked fetch, plus a tiny command or script that generates one sentence so I can try it.
- Do not add new dependencies unless they are genuinely needed.

## Done when
An example call in this project turns Albanian text into an audio file, the key is read from ZANI_API_KEY, errors are handled, and you have told me exactly how to run it.

Autentifikimi

Çdo kërkesë duhet të ketë çelësin tënd në kokën Authorization:

Authorization: Bearer zn_live_xxxxxxxxxxxxxxxx

Çelësi është sekret. Thirre API-n vetëm nga serveri yt, kurrë nga shfletuesi ose aplikacioni mobil. Nëse del në publik, revoko atë te Paneli → API dhe krijo një të ri.

Gjenero audio

POST/v1/text-to-speech

Trupi është JSON. Vetëm dy fushat e para janë të detyrueshme, të tjerat kanë vlera të mira paraprake.

  • texti detyrueshëm

    string

    Teksti për t'u shndërruar në zë. 1 deri 20 000 karaktere. 1 karakter = 1 kredi.

  • voicei detyrueshëm

    string

    Slug-u ose ID e zërit, p.sh. vjosa. Lista e plotë në GET /v1/voices.

  • dialectopsional
    standardkosovoghegtosk

    Dialekti. Parazgjedhja: standard.

  • emotionopsional
    neutralwarmconfidentenergeticcalmempatheticseriousjoyfulsolemn

    Emocioni i të folurit. Parazgjedhja: warm.

  • paceopsional
    slowmediummedium-fastfast

    Shpejtësia. Parazgjedhja: medium.

  • energyopsional
    lowmediumhigh

    Energjia. Parazgjedhja: medium.

  • purposeopsional
    advertisementaudiobookaccessibilitysocialdocumentarymeditationeducationnewspodcastnarrationcharacter

    Qëllimi i audios; rregullon stilin e tregimit. Parazgjedhja: narration.

  • personalityopsional

    string

    Tekst i lirë deri në 240 karaktere, p.sh. «i qetë dhe i ngrohtë».

  • formatopsional
    mp3wav

    Formati i skedarit. Parazgjedhja: mp3.

{
  "text": "Mirë se vini në Zani.",
  "voice": "arben",
  "dialect": "kosovo",
  "emotion": "confident",
  "purpose": "advertisement",
  "format": "mp3"
}

Përgjigja

Kthehet JSON, jo skedari i audios. Kur status është completed, audioUrl është lidhja e skedarit.

{
  "id": "cmugn72xj0000yq8hn9twxq9u",
  "status": "completed",
  "queued": false,
  "text": "Mirë se vini në Zani.",
  "characterCount": 21,
  "durationMs": 2140,
  "creditsCharged": 21,
  "format": "mp3",
  "dialect": "kosovo",
  "voice": { "id": "…", "slug": "arben", "name": "Arben" },
  "audioUrl": "https://…/audio/cmugn72xj0000yq8hn9twxq9u.mp3?X-Amz-…",
  "error": null,
  "createdAt": "2026-09-25T10:14:03.000Z"
}
  • status: completed, queued, processing ose failed.
  • audioUrl është e përkohshme (rreth 1 orë). Shkarko skedarin dhe ruaje vetë.
  • creditsCharged është numri i kredive që u zbritën.

Tekste të gjata

Tekstet deri në 900 karaktere përfundojnë brenda të njëjtës kërkesë. Ato më të gjata përpunohen në sfond: përgjigja ka status: "queued" dhe audioUrl: null.

GET/v1/generations/{id}

Pyete çdo 2 deri 3 sekonda derisa status të bëhet completed (ka audioUrl) ose failed (ka error). Ka të njëjtën formë si përgjigja e mësipërme.

Lista e zërave

GET/v1/voices

Kthen zërat publikë dhe zërat e tu të personalizuar. Përdor slug në fushën voice. sampleUrl është një prezantim i gatshëm i zërit.

{
  "voices": [
    {
      "id": "…",
      "slug": "vjosa",
      "name": "Vjosa",
      "gender": "female",
      "age": 32,
      "dialect": "standard",
      "description": "Zëri ynë kryesor…",
      "tags": ["signature", "standard"],
      "badge": "PRO",
      "custom": false,
      "sampleUrl": "https://…/api/voices/…/sample"
    }
  ]
}

Kredite

GET/v1/account

Shiko kreditet e mbetura dhe planin, p.sh. para se të nisësh një punë të madhe.

{ "credits": 4820, "plan": "studio", "status": "active" }

Kufij dhe çmim

  • 60 / minutë

    Kërkesa për çelës, të gjitha pikat së bashku. Kur e kalon merr 429 me Retry-After.

  • 1 karakter = 1 kredi

    Zbritet vetëm kur audioja krijohet me sukses. Gjenerimet e dështuara nuk kushtojnë.

  • 20 000 karaktere

    Maksimumi për një kërkesë. Për tekste më të gjata, ndaji te fundi i fjalisë.

  • 900 karaktere

    Mbi këtë prag gjenerimi bëhet në sfond dhe e merr me GET /v1/generations/{id}.

Gabimet

Çdo gabim kthen JSON me fushën error. Mesazhet janë në shqip, prandaj shfaq mesazhin tënd te përdoruesi.

  • 400

    Kërkesa nuk është e vlefshme

    Trupi mungon ose një fushë është gabim. Përgjigja ka edhe listën issues.

  • 401

    Çelës i munguar ose i pavlefshëm

    Kontrollo kokën Authorization: Bearer zn_live_…, dhe që çelësi nuk është revokuar.

  • 402

    Kredi të pamjaftueshme

    Teksti kërkon më shumë karaktere se kreditet e mbetura. Shto kredi te Planet.

  • 404

    Nuk u gjet

    Zëri ose gjenerimi nuk ekziston, ose nuk është i yti.

  • 429

    Shumë kërkesa

    60 kërkesa në minutë për çelës. Prit sipas kokës Retry-After dhe provo sërish.

  • 500

    Gjenerimi dështoi

    Gabim i përkohshëm. Kredite nuk zbriten. Provo sërish pas pak sekondash.

{ "error": "Nuk ke kredi të mjaftueshme." }

Shembuj të plotë

Këto funksione bëjnë gjithçka: dërgojnë kërkesën, presin nëse teksti është i gjatë dhe kthejnë bajtet e audios.

const BASE = "https://www.tryzani.com";
const headers = {
  Authorization: `Bearer ${process.env.ZANI_API_KEY}`,
  "Content-Type": "application/json",
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function speak(text, voice = "vjosa", options = {}) {
  const res = await fetch(`${BASE}/v1/text-to-speech`, {
    method: "POST",
    headers,
    body: JSON.stringify({ text, voice, ...options }),
  });
  if (!res.ok) throw new Error((await res.json()).error ?? res.statusText);
  let job = await res.json();

  // Tekstet mbi 900 karaktere përpunohen në sfond.
  while (job.status === "queued" || job.status === "processing") {
    await sleep(2500);
    const poll = await fetch(`${BASE}/v1/generations/${job.id}`, { headers });
    job = await poll.json();
  }
  if (job.status !== "completed") throw new Error(job.error ?? "Gjenerimi dështoi");

  const audio = await fetch(job.audioUrl); // lidhje e përkohshme, ruaje skedarin
  return Buffer.from(await audio.arrayBuffer());
}