Pekka Voice

📘 Tài liệu API

Chuyển văn bản thành giọng nói qua API. Tạo key ở API Keys — key chỉ hiện 1 lần, lưu lại ngay.

Quản lý API Keys

Quick Start — 3 bước đơn giản

1 · Tạo API Key

Vào API Keys để tạo key (dạng sk_live_...).

2 · Lấy Voice ID

curl https://voice.getpekka.com/api/v1/voices \
  -H "Authorization: Bearer YOUR_API_KEY"

3 · Tạo TTS job rồi poll kết quả

curl -X POST https://voice.getpekka.com/api/v1/tts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Xin chào, đây là giọng nói AI",
    "voiceId": "YOUR_VOICE_ID",
    "speed": 1.0
  }'

🤖 Tích hợp bằng AI — copy prompt này

Dán prompt này vào ChatGPT/Claude/Cursor để trợ lý tự viết code tích hợp. Nhớ thay <API_KEY> bằng key thật.

Bạn hãy viết code tích hợp API chuyển văn bản thành giọng nói (TTS) của Pekka Voice cho dự án của tôi. Dưới đây là toàn bộ đặc tả, hãy đọc kỹ rồi hỏi tôi ngôn ngữ/framework nếu chưa rõ.

## Xác thực
Mọi request cần header:
  Authorization: Bearer <API_KEY>
API key dạng `sk_live_...`, lấy ở https://voice.getpekka.com/docs/api-keys
Base URL: https://voice.getpekka.com

## 1. Tạo audio — POST /api/v1/tts  (dùng cho văn bản dài, ĐÂY LÀ CÁCH NÊN DÙNG)
Body JSON:
  text        string  — văn bản cần đọc, tối đa 400.000 ký tự
  srt         string  — HOẶC nội dung file phụ đề .srt/.vtt (tối đa 2MB).
                        Gửi text HOẶC srt, gửi cả hai sẽ bị từ chối 400.
                        Chỉ phần chữ được đọc; timecode và thẻ định dạng bị bỏ.
  voiceId     string  — bắt buộc, lấy từ GET /api/v1/voices
  speed       number  — 0.8 đến 2.0, mặc định 1.0
  pauseMs     number  — nghỉ giữa câu (ms), 0 đến 15000, mặc định 100
  dictionary  boolean — mặc định true. Áp từ điển thay thế của tài khoản:
                        đọc "AI" thành "ây ai", "UBND" thành "ủy ban nhân
                        dân"... Quản lý danh sách từ trong giao diện web.
                        Gửi false nếu muốn đọc nguyên văn không thay thế.

Response 200 (state="completed"):
  { projectExportId, state, url, mp3Url, srtUrl, characterCount,
    blockCount, cueCount?, creditsCharged, error }
  - url    : audio gốc, luôn có khi completed. Đây là nguồn chính, dùng cái này
  - mp3Url : bản MP3 nhẹ hơn, best-effort — CÓ THỂ null dù đã completed
  - srtUrl : file phụ đề khớp audio vừa tạo. LƯU Ý timeline này sinh từ nhịp
             đọc thật của giọng, KHÔNG phải timeline trong file srt gửi lên.
  - cueCount: chỉ có khi gửi bằng `srt` — số cue đọc được. Hãy so với số cue
             trong file của tôi để phát hiện file hỏng một phần.

## 2. Xử lý status code — RẤT QUAN TRỌNG, đừng làm sai chỗ này
  200 — xong, dùng luôn `url`.
  202 — state là "pending"/"processing". ĐÂY KHÔNG PHẢI LỖI. Văn bản dài nên
        chưa xong trong một lần gọi. Hãy POLL `GET /api/v1/exports/{projectExportId}`
        mỗi 2–3 giây cho tới khi state = "completed". Mỗi lần poll cũng đẩy job
        tiến lên, nên ngừng poll là job đứng.
        TUYỆT ĐỐI KHÔNG gọi lại POST /api/v1/tts để "thử lại" — tạo job mới và tính tiền lần nữa.
  400 — sai tham số (đọc field `error`).
  401 — API key sai hoặc thiếu.
  402 — không đủ credit. Response có creditsNeeded, creditsRemaining.
  413 — body vượt 3MB.
  429 — vượt rate limit theo PHÚT (cửa sổ cố định 1 phút cho mỗi key). Đợi
        đến phút kế tiếp rồi thử lại, đừng retry liên tục. Giới hạn theo gói:
        Free 10, Starter 30, Pro 60, Business 300 request/phút.
  413 — body vượt 3MB.
  500 — state="failed", đây mới là lỗi thật sự cuối cùng.
  503 — hệ thống tạm lỗi kết nối cơ sở dữ liệu. Đợi theo header Retry-After
        (thường 60 giây) rồi thử lại. Không coi đây là lỗi vĩnh viễn.

## 3. Văn bản ngắn — POST /api/v1/tts/sync
Trả audio ngay trong response, không cần poll. CHỈ dùng cho văn bản tối đa
1500 ký tự. Body: { text, voiceId, speed }. Trường `url` trả về là đường dẫn
tương đối (/api/audio/...) — ghép với https://voice.getpekka.com trước khi dùng.
Dùng khi cần độ trễ thấp (chatbot, thông báo).

QUAN TRỌNG: văn bản dài hơn 1500 ký tự thì dùng POST /api/v1/tts (mục 1), KHÔNG
tự cắt nhỏ rồi gọi /tts/sync song song. Cách đó tốn nhiều request hơn, dễ chạm
rate limit theo phút, và không có file ghép + phụ đề SRT.

## 4. Danh sách giọng — GET /api/v1/voices
## 5. Kiểm tra credit — GET /api/v1/usage
Trả { plan, creditBalance, accountBalance, keyRemaining, limitedBy, creditLimit,
creditsUsed, creditsPerMonth, ... }. creditBalance = min(accountBalance,
keyRemaining) — số thực sự tiêu được; limitedBy nói trần nào đang chặn
("account" = pool chung hết, "key" = hạn mức riêng của key hết).

## Cách tính tiền
1 credit = 1 ký tự (tính trên văn bản SAU khi bóc phụ đề, nên timecode không
mất tiền). Nhóm giọng tiết kiệm chỉ tính 1/3. Câu đã tạo rồi mà tạo lại y hệt
thì dùng cache, không trừ thêm.

## Yêu cầu với code bạn viết
- Đọc API key từ biến môi trường, KHÔNG hardcode.
- Có hàm poll cho trường hợp 202: poll mỗi 2–3 giây, có tổng thời gian chờ tối đa để không treo vô hạn.
- Link audio (url, mp3Url, srtUrl) là link công khai, KHÔNG cần header Authorization để tải.
- Xử lý riêng từng status code ở trên, thông báo lỗi rõ ràng bằng tiếng Việt.
- Có timeout hợp lý cho HTTP request (job dài có thể mất vài phút).
- Viết kèm một ví dụ chạy được ngay.

POST /api/v1/ttsRecommended

Văn bản dài → 1 file audio + phụ đề SRT. Trả thẳng url + srtUrl khi xong. Văn bản rất dài trả về HTTP 202 (state: "pending", không phải lỗi) — dùng projectExportId poll GET /api/v1/exports/{id} tới khi completed, đừng gọi lại POST /api/v1/tts để "thử lại" (tạo job mới, không tiếp tục job cũ). Chỉ HTTP 500 mới là lỗi thật.

Tham số:

  • text (string) — tối đa 400.000 ký tự. Bắt buộc trừ khi dùng srt.
  • srt (string) — nội dung file .srt/.vtt, tối đa 2MB, thay cho text (gửi cả hai sẽ bị từ chối). Chỉ tính tiền phần chữ, bỏ timecode.
  • voiceId (string, bắt buộc) — lấy từ danh sách giọng.
  • speed (number) — 0.8–2.0, mặc định 1.0.
  • pauseMs (number) — nghỉ giữa câu (ms), 0–15000, mặc định 100.
  • dictionary (boolean, mặc định true) — áp từ điển thay thế của tài khoản. Gửi false để đọc đúng nguyên văn.

Gửi bằng file phụ đề:

curl -X POST https://voice.getpekka.com/api/v1/tts \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile srt phude.srt --arg v YOUR_VOICE_ID '{srt: $srt, voiceId: $v}')"

Response có thêm cueCount (số cue đọc được — đối chiếu để phát hiện file hỏng một phần). srtUrl trả về là timeline mới sinh từ nhịp đọc thật, không phải timeline gốc bạn gửi lên.

url là audio gốc, luôn có khi completed — dùng làm nguồn chính. mp3Url là bản MP3 nhẹ hơn, sinh sau — best-effort, có thể vẫn null dù đã completed.

Response:

{
  "projectExportId": "3f6c1c9a-...",
  "state": "completed",
  "url": "https://.../api/audio/<hash>",
  "mp3Url": "https://.../api/audio/<hash>",  // null nếu chưa/không có bản MP3
  "srtUrl": "https://.../api/exports/<id>/srt",
  "characterCount": 203,
  "blockCount": 3,
  "creditsCharged": 203,
  "error": null
}

GET /api/v1/exports/{id}

Kiểm tra tiến trình, lấy URL audio + phụ đề khi xong. Poll mỗi 2–3 giây.

{
  "projectExportId": "3f6c1c9a-...",
  "state": "completed",          // pending | processing | completed | failed
  "url": "https://.../api/audio/<hash>",
  "mp3Url": "https://.../api/audio/<hash>",  // null nếu chưa/không có bản MP3
  "srtUrl": "https://.../api/exports/<id>/srt",
  "characterCount": 203,
  "blocksReady": 480,   // chỉ có khi state pending/processing — số câu ĐÃ tạo xong audio
  "blocksTotal": 621,   // (kể cả khi characterCount/blockCount còn 0 vì bước ghép cuối chưa chạy)
  "creditsCharged": 203,
  "error": null
}

Văn bản dài: characterCount/blockCount chỉ có ở bước ghép cuối — trong lúc còn pending, xem tiến độ thật qua blocksReady/blocksTotal.

Ví dụ polling bằng Node.js:

async function waitForAudio(exportId) {
  while (true) {
    const res = await fetch(
      `https://voice.getpekka.com/api/v1/exports/${exportId}`,
      { headers: { Authorization: "Bearer YOUR_API_KEY" } }
    )
    const job = await res.json()
    if (job.state === "completed") return job       // job.url + job.srtUrl
    if (job.state === "failed") throw new Error(job.error)
    await new Promise((r) => setTimeout(r, 2500))    // đợi 2.5s rồi thử lại
  }
}

POST /api/v1/tts/sync

Văn bản ngắn (≤ 1.500 ký tự) — trả URL audio ngay, không cần poll. Body: { text, voiceId, speed? }.

{
  "url": "/api/audio/<hash>",
  "creditsUsed": 42,
  "creditsRemaining": 2958
}

GET /api/v1/voices

Danh sách giọng — lấy id để dùng làm voiceId ở 2 endpoint TTS phía trên.

Query params (không bắt buộc):

  • page (number) — mặc định 1.
  • limit (number) — số giọng mỗi trang, mặc định 20.
curl "https://voice.getpekka.com/api/v1/voices?page=1&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

{
  "items": [
    {
      "id": "vi-VN-HoaiMyNeural",
      "name": "Hoài My",
      "description": "Giọng nữ miền Bắc, ấm áp",
      "tags": ["nữ", "miền Bắc"],
      "economy": true,
      "free": false,
      "charsPerCredit": 3
    }
  ],
  "total": 87,
  "hasNext": true,
  "page": 1
}

economy: true = giọng rẻ hơn, free: true = 0 credit. Lặp page tới khi hasNext là false để lấy hết danh sách.

GET /api/v1/usage

Số dư của CHÍNH API key đang gọi. creditBalance là số thực sự tiêu được, tức phần nhỏ hơn giữa HAI trần: số dư chung của tài khoản (accountBalance) và phần còn lại của riêng key này (keyRemaining, null khi key không đặt hạn mức riêng). limitedBy cho biết trần nào đang chặn — "account" nghĩa là chủ tài khoản phải nạp thêm, tăng hạn mức key cũng không giúp gì.

{
  "plan": "pro",
  "creditBalance": 412350,
  "accountBalance": 412350,
  "keyRemaining": 500000,
  "limitedBy": "account",
  "creditLimit": 500000,
  "creditsUsed": 87650,
  "creditsExpireAt": "2026-08-16T00:00:00.000Z",
  "creditsPerMonth": 1000000
}

Giới hạn & lỗi

  • Rate limit theo gói: Free 10 · Starter 30 · Pro 60 · Business 300 request/phút.
  • Credits dùng chung web app + API, tỉ lệ ký tự/credit khác nhau theo giọng — xem charsPerCredit trong GET /api/v1/voices.
  • Mỗi lần gọi đều trừ credit theo độ dài văn bản, kể cả gọi lại nội dung y hệt.
  • Mỗi API key có thể đặt hạn mức riêng (quản lý ở /docs/api-keys) — hết hạn mức riêng báo 402 dù tài khoản còn credit.
  • 401 key sai/thiếu · 402 hết credits · 413 body quá 3MB · 429 vượt rate limit (tính theo phút) · 500 lỗi hệ thống · 503 cơ sở dữ liệu tạm lỗi — đợi theo Retry-After rồi thử lại.

Best Practices

  • Polling: mỗi 2–3 giây; văn bản ngắn thường xử lý trong vài giây.
  • Bảo mật: không bao giờ expose API key trong code frontend — luôn gọi từ backend.
  • Error handling: luôn check state trước khi dùng url; nếu failed, xem creditsCharged: bằng 0 nghĩa là credit đã được hoàn lại.
Cần hỗ trợ? Tham gia cộng đồng: Cộng đồng Facebook