Tài liệu API SaydiVoice
Trang này cần JavaScript để hiển thị dạng tương tác. Toàn bộ nội dung tham chiếu nằm ngay dưới đây, và cũng có sẵn ở dạng văn bản thuần tại /llms-full.txt.
Base URL https://voice.saydi.ai/api. Xác thực bằng Authorization: Bearer sv_live_….
Giới thiệu
Cách đưa SaydiVoice vào hệ thống của bạn — kể cả khi bạn không biết code.
SaydiVoice đọc văn bản thành giọng nói tự nhiên. Bạn dùng ngay trong Studio, hoặc để hệ thống/AI của bạn tự gọi.
Tóm lại: tích hợp gồm 3 bước — 1. Lấy khoá truy cập. 2. Lấy mã giọng. 3. Dán một lời nhắc vào AI, AI sẽ viết phần kết nối giúp bạn. Không cần biết code — bắt đầu ở Không cần biết code.
Bạn thuộc nhóm nào?
Bạn chỉ cần đúng hai thứ — Một khoá truy cập (dãy bí mật bắt đầu bằng sv_live_) và một mã giọng (đọc bằng giọng nào, ví dụ vi-adam). Cả hai nằm trong Lấy mã giọng.
Không cần biết code
Ba bước để AI tích hợp SaydiVoice vào hệ thống của bạn.
Bạn không cần đọc code hay học thuật ngữ kỹ thuật. Bạn chỉ cần một công cụ AI biết viết code — Claude Code, Codex, Antigravity, DeepSeek, Cursor — và làm theo 3 bước sau. Gặp từ lạ thì mở Từ điển nhanh ở tab khác.
- Lấy khoá truy cập — Bấm nút Console nhà phát triển ở góc trên bên phải trang này — nó mở trang tạo khoá. Rồi bấm Tạo key. Một dãy bắt đầu bằng
sv_live_ hiện ra — copy lấy. Dãy này chỉ hiện một lần, hãy dán vào nơi an toàn (Notes, trình quản lý mật khẩu). - Lấy mã giọng — Mở Studio, trỏ vào giọng bạn thích rồi bấm biểu tượng copy (chỗ hiện chữ Copy voice ID). Mã trông như
vi-adam. Chưa chọn được giọng thì cứ dùng vi-adam. - Dán lời nhắc vào AI — Mở công cụ AI của bạn, dán nguyên khung bên dưới rồi gửi. AI sẽ đọc tài liệu chính thức và viết phần kết nối giúp bạn.
Muốn giọng của chính bạn? — Trong Studio có mục nhân bản giọng và không cần code: nhân bản giọng của bạn ở đó, rồi copy mã giọng y như bước 2.
Lời nhắc để dán
Tiếng Việt
Bạn hãy đọc tài liệu chính thức của SaydiVoice ở địa chỉ:
https://voice.saydi.ai/llms-full.txt
Sau đó giúp tôi tích hợp SaydiVoice (đọc văn bản thành giọng nói) vào dự án này.
Yêu cầu:
- Khoá truy cập: đọc từ biến môi trường SAYDI_API_KEY. Tôi sẽ tự điền khoá — đừng hỏi tôi khoá và đừng viết khoá thẳng vào code.
- Mã giọng mặc định: vi-adam. Tôi có thể đổi sang mã khác lấy từ GET /samples, hoặc mã vc_... nếu là giọng nhân bản.
- Viết một hàm speak(text, voice_id) trả về nội dung file audio, gọi POST /v1/audio/speech.
- Kết quả trả về là audio, không phải JSON. Nếu nhận được JSON thì đó là lỗi.
- Gặp lỗi 429 thì chờ theo header Retry-After rồi thử lại.
- Chỉ dùng endpoint, tham số và mã lỗi có trong tài liệu trên. Thiếu thông tin thì hỏi lại tôi, đừng tự đoán.
- Cuối cùng chỉ cho tôi chính xác cần dán khoá vào file nào.
English
Read the official SaydiVoice documentation at:
https://voice.saydi.ai/llms-full.txt
Then help me integrate SaydiVoice (text to speech) into this project.
Requirements:
- Access key: read it from the SAYDI_API_KEY environment variable. I will fill it in myself — do not ask me for the key and never hardcode it.
- Default voice code: vi-adam. I may switch to another one from GET /samples, or a vc_... code for a cloned voice.
- Write one function speak(text, voice_id) that returns the audio file content, calling POST /v1/audio/speech.
- The response is audio, not JSON. If you get JSON, it is an error.
- On 429, wait for the Retry-After header and retry.
- Use only the endpoints, parameters and error codes in that documentation. If something is missing, ask me instead of guessing.
- Finally, tell me exactly which file to put the key in.
Đừng dán khoá truy cập vào chat AI — Lời nhắc trên yêu cầu AI đọc khoá từ file cấu hình, nên bạn không phải đưa khoá cho AI. Hãy giữ khoá riêng — nó giống như mật khẩu.
Làm sao biết đã xong
- Bạn nhập được một câu trong sản phẩm và nghe nó đọc lên.
- Âm thanh phát ra — đó là file audio thật, không phải thông báo lỗi.
- Đúng giọng bạn đã chọn. Sai giọng thường là do mã giọng chưa được nhận — kiểm tra ở Lấy mã giọng.
Không có công cụ AI? Gửi đoạn này cho developer của bạn
Copy đoạn dưới rồi gửi cho người làm kỹ thuật cho bạn (email, Zalo, chat). Gửi khoá truy cập ở tin nhắn riêng, đừng gửi chung một tin.
Handoff message
Hello, we would like to use SaydiVoice for text to speech.
Integration docs: https://voice.saydi.ai/docs/
Full machine-readable copy: https://voice.saydi.ai/llms-full.txt
What needs to be done:
- Call POST https://voice.saydi.ai/api/v1/audio/speech
header: Authorization: Bearer <key>
body: {"model":"tts-1","voice":"<voice id>","input":"<text>"}
- The response is audio bytes; errors return JSON.
- For voice cloning: POST /v1/voices (multipart, with consent=true) to get a vc_... id, then use that id above.
- On 429, wait for the Retry-After header and retry.
I will send the access key in a separate message.
Bắt đầu nhanh
Dành cho developer: gọi request đầu tiên trong 3 bước.
Trang này dành cho developer — Base URL https://voice.saydi.ai/api, mọi request gửi kèm Authorization: Bearer sv_live_.... Nếu bạn không viết code, hãy dùng Không cần biết code — để AI làm giúp.
- Tạo API key — Mở /developers/#/keys, tạo key và copy secret — chỉ hiện một lần.
- Lưu vào biến môi trường —
export SAYDI_API_KEY="sv_live_..." trên server. Không commit và không để trong code phía client. - Chọn voice ID —
vi-adam dùng được ngay. Muốn chọn giọng khác, xem catalogue trong Lấy voice ID. - Gọi request đầu tiên — POST tới /v1/audio/speech và ghi response body ra file.
cURL
export SAYDI_API_KEY="sv_live_REPLACE_WITH_YOUR_KEY"
curl -sS -X POST "https://voice.saydi.ai/api/v1/audio/speech" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","voice":"vi-adam","input":"Xin chào, đây là API của SaydiVoice."}' \
--output hello.mp3
# Inspect what you were charged (characters) and how long it took:
# -D - prints the response headers, e.g. X-OV-Chars: 41
Python
import os, requests
API_KEY = os.environ["SAYDI_API_KEY"]
BASE = "https://voice.saydi.ai/api"
r = requests.post(
f"{BASE}/v1/audio/speech",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "tts-1",
"voice": "vi-adam",
"input": "Xin chào, đây là API của SaydiVoice.",
},
timeout=120,
)
r.raise_for_status()
open("hello.mp3", "wb").write(r.content)
print("chars charged:", r.headers.get("X-OV-Chars"))
Node.js
import { writeFile } from "node:fs/promises";
const API_KEY = process.env.SAYDI_API_KEY;
const BASE = "https://voice.saydi.ai/api";
const res = await fetch(`${BASE}/v1/audio/speech`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "tts-1",
voice: "vi-adam",
input: "Xin chào, đây là API của SaydiVoice.",
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("hello.mp3", Buffer.from(await res.arrayBuffer()));
console.log("chars charged:", res.headers.get("X-OV-Chars"));
Kết quả là audio bytes — Thành công trả audio bytes (audio/mpeg hoặc audio/wav). Response JSON là lỗi.
Lấy mã giọng
Mã giọng là gì và lấy ở đâu — dễ nhất là copy từ Studio.
Mã giọng là một mã ngắn cho biết đọc văn bản bằng giọng nào, ví dụ vi-adam. Có hai loại:
| Loại | Dạng | Lấy ở đâu |
|---|
| A ready-made voice | vi-adam | Copy it from the Studio voice list, or list them with the API |
| Your own cloned voice | vc_9f2c1a4b... | Clone it in the Studio and copy the code, or upload it through the API (see Clone a voice, then call it) |
Dễ nhất: copy từ Studio
- Mở Studio — Bấm nút Studio ở góc trên bên phải trang này.
- Tìm giọng bạn thích — Dùng danh sách giọng, bấm play để nghe thử từng giọng.
- Bấm biểu tượng copy — Trỏ vào một giọng rồi bấm copy — chỗ đó hiện chữ Copy voice ID. Bạn đã có mã như
vi-adam. - Dán vào chỗ cần mã giọng — Ví dụ vào lời nhắc cho AI ở trang Không cần biết code.
Chưa chọn được? Cứ dùng vi-adam — Mã này dùng được ngay. Sau này đổi sang mã giọng khác cũng không phải sửa gì thêm.
Cho developer: liệt kê bằng API
cURL
curl -sS "https://voice.saydi.ai/api/samples" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
| jq -r '.[] | [.voice_id, .display_name, .language] | @tsv'
# vi-adam Adam — Giọng hot tiktok vi
# vi-cuppy Cuppy — Nữ miền Nam vi
# Narrow the list, e.g. Vietnamese voices only, 20 max:
curl -sS "https://voice.saydi.ai/api/samples?lang=vi&limit=20" \
-H "Authorization: Bearer $SAYDI_API_KEY"
Python
import os, requests
API_KEY = os.environ["SAYDI_API_KEY"]
r = requests.get(
"https://voice.saydi.ai/api/samples",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
r.raise_for_status()
for v in r.json():
print(v["voice_id"], v["display_name"], v["language"], sep="\t")
Node.js
const API_KEY = process.env.SAYDI_API_KEY;
const res = await fetch("https://voice.saydi.ai/api/samples", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
for (const v of await res.json()) {
console.log([v.voice_id, v.display_name, v.language].join("\t"));
}
200 — Mảng giọng nói. Dùng voice_id trong request; display_name chỉ để hiển thị.
[
{
"name": "adam-11labs-vi",
"voice_id": "vi-adam",
"display_name": "Adam — Giọng hot tiktok",
"gender": "male",
"language": "vi",
"locale": "vi-VN",
"use_case": "conversational",
"featured": true
}
]
Dùng mã, đừng dùng tên hiển thị — voice_id mới là thứ API nhận. Tên hiển thị (ví dụ "Adam — Giọng hot tiktok") chỉ là nhãn, không đảm bảo giữ nguyên.
Nghe thử trước khi dùng
cURL
# Catalogue voice: streams its reference clip.
curl -sS "https://voice.saydi.ai/api/samples/vi-adam/audio" \
-H "Authorization: Bearer $SAYDI_API_KEY" --output preview.wav
# Cloned voice: a short sentence, rendered once and cached. Not charged.
curl -sS "https://voice.saydi.ai/api/v1/voices/$VOICE_ID/preview" \
-H "Authorization: Bearer $SAYDI_API_KEY" --output preview.mp3
Python
import os, requests
API_KEY = os.environ["SAYDI_API_KEY"]
VOICE_ID = os.environ["VOICE_ID"]
H = {"Authorization": f"Bearer {API_KEY}"}
# Catalogue voice: streams its reference clip.
r = requests.get("https://voice.saydi.ai/api/samples/vi-adam/audio", headers=H, timeout=30)
r.raise_for_status()
open("preview.wav", "wb").write(r.content)
# Cloned voice: a short sentence, rendered once and cached. Not charged.
r = requests.get(f"https://voice.saydi.ai/api/v1/voices/{VOICE_ID}/preview", headers=H, timeout=60)
r.raise_for_status()
open("preview.mp3", "wb").write(r.content)
Node.js
import { writeFile } from "node:fs/promises";
const API_KEY = process.env.SAYDI_API_KEY;
const VOICE_ID = process.env.VOICE_ID;
const H = { Authorization: `Bearer ${API_KEY}` };
// Catalogue voice: streams its reference clip.
let res = await fetch("https://voice.saydi.ai/api/samples/vi-adam/audio", { headers: H });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("preview.wav", Buffer.from(await res.arrayBuffer()));
// Cloned voice: a short sentence, rendered once and cached. Not charged.
res = await fetch(`https://voice.saydi.ai/api/v1/voices/${VOICE_ID}/preview`, { headers: H });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("preview.mp3", Buffer.from(await res.arrayBuffer()));
Gọi một giọng
Dành cho developer: gửi văn bản kèm mã giọng, nhận file audio.
Toàn bộ phần tích hợp chỉ có vậy. Gửi văn bản kèm voice ID bạn đã chọn; ghi response body ra file. Phần còn lại của tài liệu là tuỳ chọn.
cURL
curl -sS -X POST "https://voice.saydi.ai/api/v1/audio/speech" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","voice":"vi-adam","input":"Xin chào, đây là giọng Adam."}' \
-D headers.txt --output hello.mp3
grep -i 'x-ov-voice\|x-ov-chars' headers.txt
Python
import os, requests
r = requests.post(
"https://voice.saydi.ai/api/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
json={
"model": "tts-1",
"voice": "vi-adam", # any voice_id from GET /samples
"input": "Xin chào, đây là giọng Adam.",
},
timeout=120,
)
r.raise_for_status() # a JSON body here means an error
open("hello.mp3", "wb").write(r.content)
print("voice:", r.headers["X-OV-Voice"], "chars:", r.headers["X-OV-Chars"])
Node.js
import { writeFile } from "node:fs/promises";
const res = await fetch("https://voice.saydi.ai/api/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAYDI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "tts-1",
voice: "vi-adam", // any voice_id from GET /samples
input: "Xin chào, đây là giọng Adam.",
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("hello.mp3", Buffer.from(await res.arrayBuffer()));
console.log("voice:", res.headers.get("X-OV-Voice"), "chars:", res.headers.get("X-OV-Chars"));
OpenAI SDK
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SAYDI_API_KEY"],
base_url="https://voice.saydi.ai/api/v1", # only this line changes
)
with client.audio.speech.with_streaming_response.create(
model="tts-1",
voice="vi-adam", # a SaydiVoice voice_id
input="Xin chào, đây là giọng Adam.",
) as response:
response.stream_to_file("hello.mp3")
Xác nhận đúng giọng đã đọc
Mọi response audio đều có X-OV-Voice (giọng thực sự đã đọc, sau mọi fallback) và X-OV-Chars (số ký tự bị trừ). Hãy log lại: nếu X-OV-Voice khác ID bạn gửi thì giá trị voice đã không resolve — xem callout bên dưới.
Các thay đổi thường gặp
| Tôi muốn | Đổi gì |
|---|
| Use a different voice | voice: any voice_id from GET /samples or a cloned vc_... id |
| Return WAV instead of MP3 | response_format: wav (also opus, flac, pcm, aac) |
| Speak slower or faster | speed: 0.25–4.0, default 1.0 |
| Render faster / higher quality | model: tts-1 (16 steps, faster) or tts-1-hd (32 steps, better) |
| Speak more than 4096 characters | Use POST /tts — same sample field, no 4096-character limit |
| Design a voice from a description | instructions on this endpoint, or instruct on POST /tts |
Tên giọng OpenAI không chọn được giọng ở đây — alloy, nova, shimmer và các tên OpenAI khác được nhận để tương thích, nhưng catalogue không có giọng nào trùng tên — tất cả đều về một giọng mặc định (vi-adam nếu chưa cấu hình khác) và response ghi X-OV-Voice-Source: openai-alias-default. Muốn chọn đúng giọng, hãy truyền voice_id của SaydiVoice.
Xác thực
Xác thực bằng một API key.
Gửi key qua header Authorization: Bearer sv_live_.... Không để key trong URL, log hay code phía client.
Request headers
Authorization: Bearer sv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Dùng công cụ AI của bạn
Mở công cụ nào, dán gì, kiểm tra ra sao — theo từng công cụ.
Trang này chi tiết theo từng công cụ, dùng kèm trang Không cần biết code: copy lời nhắc ở đó trước, rồi tìm công cụ của bạn bên dưới.
Tìm công cụ của bạn
| Công cụ của bạn | Mở thế nào | Làm gì |
|---|
| Claude Code | The Anthropic coding app, opened on your project folder | Paste the instruction. It can open the documentation link itself. |
| Codex (OpenAI) | The Codex coding app on your project | Paste the instruction. It can open the link itself. |
| Antigravity (Google) | Open your project and switch on the agent panel | Paste the instruction. If it cannot open the link, attach the file (below). |
| Cursor, Windsurf and other AI editors | Open the project and turn on Agent / Chat mode | Paste the instruction. |
| DeepSeek, ChatGPT and other web chats | Open the chat page | A web chat cannot open links — attach the file (below) with the instruction. |
Nếu AI không mở được link
Mở link dưới bằng trình duyệt rồi lưu trang lại, sau đó gửi file đó chung với lời nhắc.
Documentation file
https://voice.saydi.ai/llms-full.txt
# Trong trình duyệt: mở link trên rồi bấm Ctrl+S (Cmd+S trên Mac) để lưu thành file.
# Gửi file vừa lưu kèm lời nhắc ở trang "Không cần biết code".
Việc nên giao cho AI
Tiếng Việt
Thêm chức năng đọc văn bản thành giọng nói vào dự án này.
- Tạo file src/tts/saydivoice.py (đổi cho phù hợp nếu dự án không dùng Python).
- Hàm: synthesize(text: str, voice_id: str = "vi-adam", fmt: str = "mp3") -> bytes
- Gọi POST https://voice.saydi.ai/api/v1/audio/speech, header Authorization: Bearer <SAYDI_API_KEY>.
- Nếu chưa có SAYDI_API_KEY thì báo lỗi thật rõ, đừng chạy tiếp.
- Lỗi 4xx/5xx thì ném lỗi kèm nội dung error.message trong JSON.
- Gặp 429 thì thử lại một lần theo header Retry-After.
- Trả về nguyên bytes audio, không giải mã thành văn bản.
- Thêm một test giả lập lời gọi HTTP.
English
Add a Vietnamese voice-over helper to this project.
- File: src/tts/saydivoice.py (adapt if this project is not Python).
- Function: synthesize(text: str, voice_id: str = "vi-adam", fmt: str = "mp3") -> bytes
- POST https://voice.saydi.ai/api/v1/audio/speech with Authorization: Bearer <SAYDI_API_KEY>.
- Fail fast with a clear message if SAYDI_API_KEY is not set.
- On 4xx/5xx raise an error that includes the JSON error.message.
- Retry once on 429 using Retry-After.
- Return raw audio bytes; never decode them as text.
- Add one test that mocks the HTTP call.
Kiểm tra kết quả
- Nghe ra âm thanh thật, không phải thông báo lỗi.
- Đúng giọng bạn chọn. Nếu sai giọng, mã giọng chưa được nhận — xem Lấy mã giọng.
- Khoá truy cập không bị viết thẳng vào code — AI phải chỉ cho bạn file cần dán khoá.
- Khi gửi quá nhanh, nó tự chờ rồi thử lại.
Mỗi trang đều có nút Sao chép cho AI — Sao chép cho AI ở đầu mỗi trang copy sẵn một lời nhắc kèm tài liệu của trang đó — tiện khi bạn chỉ cần một phần, ví dụ nhân bản giọng.
Quản lý key
Tạo, liệt kê, rotate và thu hồi API key.
Các route này quản lý key và cần access token của người dùng, không phải API key.
GET /api-keys — Liệt kê key
Trả về toàn bộ key của tài khoản. Không bao giờ trả secret hay hash.
cURL
curl -sS "https://voice.saydi.ai/api/api-keys" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
200 — Danh sách key, mới nhất trước.
{
"ok": true,
"max_keys": 20,
"keys": [
{
"id": "key_1f0c9a2b7d3e4f56",
"name": "Production",
"prefix": "sv_live_Ab12Cd",
"scopes": ["tts"],
"status": "active",
"created_at": 1758844800.0,
"last_used_at": 1758848400.0,
"expires_at": null,
"revoked_at": null
}
]
}
POST /api-keys — Tạo key
Tạo key và trả secret đúng một lần, ở trường key.
| Name | Type | Required | Description |
|---|
name | string | no | Nhãn để nhận biết key. Mặc định "Default key". |
scopes | string[] | no | Dành cho bản sau. Mặc định ["tts"]. |
cURL
curl -sS -X POST "https://voice.saydi.ai/api/api-keys" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Production"}'
201 — Đã tạo. Lưu key ngay — không thể đọc lại.
{
"ok": true,
"id": "key_1f0c9a2b7d3e4f56",
"name": "Production",
"prefix": "sv_live_Ab12Cd",
"scopes": ["tts"],
"status": "active",
"created_at": 1758844800.0,
"key": "sv_live_Ab12CdEf34Gh56Ij78Kl90Mn12Op34Qr"
}
409 — Đã đạt giới hạn số key của tài khoản. Thu hồi key không dùng trước.
{"detail": "API key limit reached (20). Revoke an unused key first."}
DELETE /api-keys/{key_id} — Thu hồi key
Thu hồi ngay. Key ngừng xác thực ở request kế tiếp.
cURL
curl -sS -X DELETE "https://voice.saydi.ai/api/api-keys/key_1f0c9a2b7d3e4f56" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
200 — Đã thu hồi.
{"ok": true, "id": "key_1f0c9a2b7d3e4f56", "status": "revoked"}
404 — Không có key này trên tài khoản.
{"detail": "API key not found."}
POST /api-keys/{key_id}/rotate — Rotate key
Cấp secret mới cho cùng key id. Secret cũ ngừng hoạt động ngay.
Giữ nguyên id và tên key, chỉ thay secret.
cURL
curl -sS -X POST "https://voice.saydi.ai/api/api-keys/KEY_ID/rotate" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json"
GET /api-keys/status — Trạng thái quản lý key
Probe công khai: quản lý key có sẵn sàng không, giới hạn key mỗi tài khoản, và các scope.
cURL
curl -sS "https://voice.saydi.ai/api/api-keys/status"
200 — Ảnh chụp trạng thái.
{"available": true, "max_keys": 20, "scopes": ["tts", "voices"]}
POST /v1/audio/speech
Text to speech tương thích OpenAI.
Tương thích OpenAI: cùng path, cùng body, cùng response bytes audio, cùng error envelope — SDK OpenAI chỉ cần đổi base_url. Hai field hành xử khác một cách có chủ ý; bảng dưới nêu rõ.
| Phần | OpenAI | SaydiVoice |
|---|
| Path, auth, JSON body, audio bytes | POST /v1/audio/speech | Identical |
| model | tts-1, tts-1-hd, gpt-4o-mini-tts | tts-1 → 16 steps, tts-1-hd → 32 steps; any other id is accepted and uses the server default |
| voice | 13 built-in voices, or {id} for a custom voice | A SaydiVoice voice. The 13 OpenAI names are accepted but ALL resolve to one default (see the callout below); the {id} object form is rejected |
| instructions | Free-form style sentence on a fixed voice (gpt-4o-mini-tts) | Voice-design mode with a PRESET vocabulary (gender, age, pitch, accent, whisper). A free-form sentence is 400 with error.param "instructions"; so is combining it with a SaydiVoice voice |
| response_format | mp3, opus, aac, flac, wav, pcm | Same six; aac is served as mp3 |
| stream_format | audio (chunked), sse (audio events) | audio is accepted but the file arrives in one response; sse is rejected with 400 |
| Errors | {"error": {...}}, 400 for a bad field | Same envelope here; a bad field keeps status 422. Native endpoints keep {"detail": ...} |
| Name | Type | Required | Description |
|---|
input | string | yes | Văn bản cần đọc. Schema tối đa 4096 ký tự; trần ký tự mỗi lượt của gói cũng áp dụng (Free: 3000), vượt là 429 với error.code "insufficient_quota". Văn bản dài hơn dùng POST /tts. |
voice | string | yes | Voice ID của SaydiVoice: vi-adam (hoặc id khác từ GET /samples), hoặc id vc_... đã nhân bản từ POST /v1/voices. Tên OpenAI vẫn được nhận nhưng KHÔNG chọn được giọng — tất cả về giọng mặc định. X-OV-Voice báo giọng thực sự đã đọc. |
model | string | no | "tts-1" = 16 bước (nhanh hơn), "tts-1-hd" = 32 bước (chất lượng cao hơn). Bỏ trống = mặc định server. |
response_format | string | no | mp3 (mặc định), opus, aac, flac, wav, pcm. Hiện aac được trả về dạng mp3. |
speed | number | no | Tốc độ đọc, 0.25–4.0. Mặc định 1.0. |
instructions | string | no | Chế độ voice design, cùng engine với instruct ở POST /tts. Từ vựng là PRESET — gender, age, pitch, accent, whisper — ví dụ "female, british accent, whisper". Câu style tự do (kiểu gpt-4o-mini-tts) bị từ chối 400, error.param "instructions". Không kết hợp được với giọng SaydiVoice: design mode dựng người nói từ câu lệnh. |
stream_format | string | no | Field tương thích. 'audio' được nhận và cả file về trong một response; 'sse' bị từ chối với 400 vì audio events chưa có. |
Tên giọng OpenAI được nhận, nhưng không chọn được giọng — Catalogue không có giọng nào tên alloy/ash/ballad/coral/echo/fable/nova/onyx/sage/shimmer/verse/marin/cedar, nên tất cả đều về một giọng mặc định (vi-adam, đổi bằng OMNIVOICE_OPENAI_VOICE_DEFAULT). Response ghi rõ: X-OV-Voice-Source: openai-alias-default kèm X-OV-Voice-Hint. Muốn chọn giọng, truyền voice_id của SaydiVoice.
cURL
curl -sS -X POST "https://voice.saydi.ai/api/v1/audio/speech" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "tts-1-hd",
"voice": "vi-adam",
"input": "Higher quality, 32 diffusion steps.",
"response_format": "wav",
"speed": 1.0
}' \
--output speech.wav
Python
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SAYDI_API_KEY"],
base_url="https://voice.saydi.ai/api/v1", # only this line changes
)
# The body is unchanged. `voice` is a SaydiVoice voice_id from GET /samples:
# OpenAI's built-in names are accepted but all resolve to one default voice.
with client.audio.speech.with_streaming_response.create(
model="tts-1",
voice="vi-adam",
input="Drop-in replacement.",
) as response:
response.stream_to_file("speech.mp3")
Node.js
import { writeFile } from "node:fs/promises";
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.SAYDI_API_KEY,
baseURL: "https://voice.saydi.ai/api/v1", // only this line changes
});
// The body is unchanged. `voice` is a SaydiVoice voice_id from GET /samples:
// OpenAI's built-in names are accepted but all resolve to one default voice.
const response = await client.audio.speech.create({
model: "tts-1",
voice: "vi-adam",
input: "Drop-in replacement.",
});
await writeFile("speech.mp3", Buffer.from(await response.arrayBuffer()));
Header tính phí — Mỗi response audio báo chi phí và thời gian: X-OV-Chars, X-Audio-Duration, X-Generation-Time, X-RTF.
| Header | Ý nghĩa |
|---|
| X-OV-Chars | Characters charged for this request |
| X-OV-Voice | Voice actually used (after fallback) |
| X-Audio-Duration | Generated audio length in seconds |
| X-Generation-Time | Compute time in seconds |
| X-RTF | Real-time factor: generation time / audio duration |
POST /tts
Text to speech đầy đủ tham số.
Dùng endpoint này cho văn bản dài, ép thời lượng, tự động ngắt nghỉ, voice design hoặc sampling.
Chọn giọng — ba chế độ
| Chế độ | Truyền gì | Kết quả |
|---|
| Voice cloning | sample (+ optional ref_text) | Speaks in a catalogue voice copied from its reference clip |
| Voice design | instruct | The model creates a voice matching a text description |
| Auto voice | neither | The model picks a voice for the text |
| Name | Type | Required | Description |
|---|
text | string | yes | Văn bản cần đọc. |
sample | string | no | voice_id từ GET /samples (name cũ vẫn được chấp nhận). Bật chế độ nhân bản. |
voice_id | string | no | Giọng đã đăng ký từ POST /v1/voices (ví dụ vc_9f2c...). Không cần gửi audio mẫu; server dùng clip và transcript đã lưu. |
instruct | string | no | Mô tả giọng, ví dụ "female, low pitch, british accent". Bỏ qua nếu có sample. |
ref_text | string | no | Ghi đè transcript của clip mẫu. Giúp nhân bản chính xác hơn. |
lang | string | no | Gợi ý ngôn ngữ (ISO 639-1). Dùng để chuẩn hoá văn bản và phát âm. |
breaks | object | no | Tự chèn khoảng nghỉ (ms) theo loại dấu câu: {"sentence":450,"comma":250,"semicolon":300,"paragraph":600}. |
duration | number | no | Ép độ dài output, giây (tối đa 600). Ghi đè speed. Hữu ích để khớp khung thời gian. |
speed | number | no | Hệ số tốc độ, 0–10. Bỏ qua nếu có duration. |
num_step | integer | no | Số bước diffusion 1–256. Mặc định 32; 16 nhanh hơn. |
guidance_scale | number | no | Classifier-free guidance, 0–50. Mặc định 2.0. |
t_shift | number | no | Dịch noise schedule, 0–1. Mặc định 0.1. |
denoise | boolean | no | Thêm token denoise ở đầu. Mặc định true. |
preprocess_prompt | boolean | no | Làm sạch clip mẫu trước khi nhân bản. Mặc định true. |
postprocess_output | boolean | no | Cắt khoảng lặng dài trong output. Mặc định true. |
audio_chunk_duration | number | no | Độ dài mỗi chunk cho văn bản dài, giây (tối đa 120). Mặc định 15. |
audio_chunk_threshold | number | no | Độ dài ước tính để bắt đầu chia chunk (tối đa 600). Mặc định 30. |
output_format | string | no | wav, mp3, flac hoặc ogg. Mặc định theo cấu hình server. |
Long text with pauses and a fixed duration
curl -sS -X POST "https://voice.saydi.ai/api/tts" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Câu thứ nhất. Câu thứ hai, có dấu phẩy.",
"sample": "vi-adam",
"lang": "vi",
"breaks": {"sentence": 450, "comma": 250},
"num_step": 32,
"output_format": "mp3"
}' \
--output long.mp3
Voice design (no reference clip)
curl -sS -X POST "https://voice.saydi.ai/api/tts" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "A calm narrator for a documentary.",
"instruct": "male, middle-aged, low pitch, american accent",
"lang": "en"
}' \
--output designed.mp3
POST /tts/file
Tạo giọng nói từ file .txt.
Gửi kịch bản dưới dạng file .txt UTF-8 thay vì nhúng chuỗi dài vào JSON.
cURL
curl -sS -X POST "https://voice.saydi.ai/api/tts/file" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-F "text_file=@script.txt" \
-F "sample=vi-adam" \
-F "lang=vi" \
-F "output_format=mp3" \
--output script.mp3
Nhận mọi field của POST /tts; dùng text_file thay cho text.
GET /samples
Liệt kê giọng đọc và metadata.
Truyền voice_id vào voice (OpenAI-compatible) hoặc sample (native). name cũ vẫn dùng được; dùng display_name để hiển thị.
| Name | Type | Required | Description |
|---|
lang | string | no | Chỉ trả giọng có ngôn ngữ chính khớp, ví dụ "vi". |
limit | integer | no | Trả tối đa bao nhiêu giọng, sau khi sắp theo mức dùng. |
cURL
curl -sS "https://voice.saydi.ai/api/samples?lang=vi&limit=5" \
-H "Authorization: Bearer $SAYDI_API_KEY"
Python
import os, requests
r = requests.get(
"https://voice.saydi.ai/api/samples?lang=vi&limit=5",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
timeout=30,
)
r.raise_for_status()
for v in r.json():
print(v["voice_id"], v["display_name"], sep="\t")
Node.js
const res = await fetch(
"https://voice.saydi.ai/api/samples?lang=vi&limit=5",
{ headers: { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` } },
);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
for (const v of await res.json()) {
console.log([v.voice_id, v.display_name].join("\t"));
}
200 — Mảng các object giọng nói.
[
{
"name": "adam-11labs-vi",
"voice_id": "vi-adam",
"audio_file": "adam-11labs-vi.wav",
"has_transcript": true,
"transcript_preview": "Xin chào, tôi là Adam...",
"display_name": "Adam — Giọng hot tiktok",
"gender": "male",
"age": "young",
"accent": "vietnamese",
"locale": "vi-VN",
"language": "vi",
"languages": ["vi"],
"use_case": "conversational",
"category": "high_quality",
"featured": true,
"usage_count": 12840
}
]
GET /samples/{name}/audio — Nghe thử giọng
Stream clip mẫu của một giọng để nghe trước khi tạo.
cURL
curl -sS "https://voice.saydi.ai/api/samples/vi-adam/audio" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
--output preview.wav
Python
import os, requests
r = requests.get(
"https://voice.saydi.ai/api/samples/vi-adam/audio",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
timeout=30,
)
r.raise_for_status()
open("preview.wav", "wb").write(r.content)
Node.js
import { writeFile } from "node:fs/promises";
const res = await fetch("https://voice.saydi.ai/api/samples/vi-adam/audio", {
headers: { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("preview.wav", Buffer.from(await res.arrayBuffer()));
GET /v1/models — Liệt kê model
Danh sách model tương thích OpenAI: tts-1 (16 bước) và tts-1-hd (32 bước).
cURL
curl -sS "https://voice.saydi.ai/api/v1/models" \
-H "Authorization: Bearer $SAYDI_API_KEY"
Python
import os, requests
r = requests.get(
"https://voice.saydi.ai/api/v1/models",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
timeout=30,
)
r.raise_for_status()
for m in r.json()["data"]:
print(m["id"])
Node.js
const res = await fetch("https://voice.saydi.ai/api/v1/models", {
headers: { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
for (const m of (await res.json()).data) {
console.log(m.id);
}
Nhân bản giọng rồi gọi qua API
Dành cho developer: tải file ghi âm lên, nhận mã giọng, rồi đọc bằng mã đó.
Nhân bản một lần, nhận voice ID, rồi dùng ID đó như mọi giọng khác. Các request sau không phải upload lại clip.
- Chuẩn bị clip mẫu — 10–15 giây một người nói. Audio sạch, không nhạc, không người thứ hai.
- Đăng ký — POST /v1/voices — Upload multipart (
file, name, language, consent). Response chứa id và status. - Chờ
ready — ready là dùng được ngay — trường hợp thường gặp. processing là hệ thống đang tách nhạc nền: gọi GET /v1/voices/{voice_id} (thường vài giây). - Đọc bằng ID đó — POST /v1/audio/speech với
voice là id vc_.... Đây chính là call ở Gọi một giọng.
cURL
API="https://voice.saydi.ai/api"
AUTH="Authorization: Bearer $SAYDI_API_KEY"
# 1. Register the clip once. Returns a stable voice id.
resp=$(curl -sS -X POST "$API/v1/voices" \
-H "$AUTH" \
-H "Idempotency-Key: my-voice-v1" \
-F "file=@my-voice.m4a;type=audio/mp4" \
-F "name=My voice" \
-F "language=vi" \
-F "consent=true")
voice_id=$(echo "$resp" | jq -r '.id')
status=$(echo "$resp" | jq -r '.status')
echo "voice_id: $voice_id status: $status"
# 2. If separation is running, wait for ready (usually seconds).
while [ "$status" = "processing" ]; do
sleep 2
status=$(curl -sS "$API/v1/voices/$voice_id" -H "$AUTH" | jq -r '.status')
done
# 3. Speak with it. No clip to upload.
curl -sS -X POST "$API/v1/audio/speech" \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d "{\"model\":\"tts-1\",\"voice\":\"$voice_id\",\"input\":\"Xin chào từ giọng của tôi.\"}" \
--output hello.mp3
Python
import os, time, requests
key = os.environ["SAYDI_API_KEY"]
base = "https://voice.saydi.ai/api"
h = {"Authorization": f"Bearer {key}"}
# 1. Register the clip once. Returns a stable voice id.
with open("my-voice.m4a", "rb") as fh:
r = requests.post(
f"{base}/v1/voices",
files={"file": ("my-voice.m4a", fh, "audio/mp4")},
data={"name": "My voice", "language": "vi", "consent": "true"},
headers={**h, "Idempotency-Key": "my-voice-v1"}, # retry-safe
timeout=300,
)
r.raise_for_status()
voice = r.json()
voice_id = voice["id"] # e.g. vc_9f2c1a4b7e0d4a1c8b3f5e6d7a9c0b12
print("voice_id:", voice_id, "status:", voice["status"])
# 2. If separation is running, wait for ready (usually seconds).
for _ in range(30):
if voice["status"] != "processing":
break
time.sleep(2)
voice = requests.get(f"{base}/v1/voices/{voice_id}", headers=h, timeout=60).json()
# 3. Speak with it. No clip to upload.
audio = requests.post(
f"{base}/v1/audio/speech", headers=h,
json={"model": "tts-1", "voice": voice_id, "input": "Xin chào từ giọng của tôi."},
timeout=180,
)
audio.raise_for_status()
open("hello.mp3", "wb").write(audio.content)
Node.js
import { readFile, writeFile } from "node:fs/promises";
const API = "https://voice.saydi.ai/api";
const H = { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` };
// 1. Register the clip once. Returns a stable voice id.
const form = new FormData();
form.append("file", new Blob([await readFile("my-voice.m4a")], { type: "audio/mp4" }), "my-voice.m4a");
form.append("name", "My voice");
form.append("language", "vi");
form.append("consent", "true");
let res = await fetch(`${API}/v1/voices`, {
method: "POST",
headers: { ...H, "Idempotency-Key": "my-voice-v1" }, // retry-safe
body: form,
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
let voice = await res.json();
const voiceId = voice.id; // e.g. vc_9f2c1a4b7e0d4a1c8b3f5e6d7a9c0b12
console.log("voice_id:", voiceId, "status:", voice.status);
// 2. If separation is running, wait for ready (usually seconds).
while (voice.status === "processing") {
await new Promise((r) => setTimeout(r, 2000));
res = await fetch(`${API}/v1/voices/${voiceId}`, { headers: H });
voice = await res.json();
}
// 3. Speak with it. No clip to upload.
res = await fetch(`${API}/v1/audio/speech`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ model: "tts-1", voice: voiceId, input: "Xin chào từ giọng của tôi." }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("hello.mp3", Buffer.from(await res.arrayBuffer()));
Clip mẫu dùng được cần gì
- 10–15 giây nói liên tục của một người — độ dài cho độ tương đồng tốt nhất.
- Không nhạc nền, không người nói thứ hai, không vang phòng nhiều.
- WAV, MP3, M4A, OGG, FLAC hoặc WebM; tối đa 5 phút — server giữ đoạn tốt nhất.
- Gửi
ref_text (transcript chính xác) nếu có; bỏ trống thì hệ thống tự phiên âm. POST /voice/transcribe trả về transcript.
Quyền sử dụng và slot nhân bản — consent=true là bắt buộc và chỉ được đăng ký giọng bạn có quyền sử dụng. Mỗi gói có số slot nhân bản; vượt hạn mức trả 402 với code: voice_limit_reached. Đăng ký thất bại không tiêu tốn slot.
Cách một lần — POST /tts/clone nhân bản và đọc trong một request multipart. Hợp cho nhu cầu một lần; upload lại clip mỗi lần gọi sẽ chậm hơn, nên dùng giọng đã đăng ký cho việc lặp lại.
POST /v1/voices
Đăng ký giọng một lần, dùng lại bằng voice_id.
Gửi clip kèm tên; nhận lại voice_id ổn định. SaydiVoice kiểm tra audio, giữ đoạn nói liền mạch tốt nhất, phiên âm và tách nhạc nền nếu có. Clip mẫu được cache ở server nên các lần sinh sau nhanh hơn và không phải upload lại. Giọng này cũng hiện trong Studio → Giọng của tôi, và giọng tạo trong Studio cũng hiện ở đây. Hướng dẫn đầy đủ ở Nhân bản rồi gọi qua API.
POST /v1/voices — Đăng ký giọng
Upload multipart. Trả về object giọng nói; status là ready khi dùng được ngay.
| Name | Type | Required | Description |
|---|
file | file | yes | Clip mẫu: WAV, MP3, M4A, OGG, FLAC hoặc WebM. Dài tối đa 5 phút; hệ thống tự chọn đoạn tốt nhất. |
name | string | yes | Tên hiển thị. Không cần duy nhất — hãy định danh giọng bằng id, đừng dùng tên. |
consent | string | yes | Xác nhận bạn có quyền nhân bản giọng này. Gửi true, hoặc JSON {"accepted": true}. Được lưu kèm IP và phiên bản điều khoản. |
language | string | no | Gợi ý ISO-639-1 (ví dụ vi). Cũng quyết định ngôn ngữ của câu nghe thử. |
ref_text | string | no | Transcript của clip. Bỏ trống thì hệ thống tự phiên âm; gửi kèm (khi clip đã được chuẩn bị sẵn) để bỏ qua bước đó. |
separate | string | no | auto (mặc định) chỉ tách nhạc nền khi phát hiện; true luôn tách; false không tách. |
Idempotency-Key | string | no | Header. Gọi lại với cùng giá trị sẽ trả về giọng đã tạo thay vì tạo thêm — nên timeout không làm mất slot. |
cURL
curl -sS -X POST "https://voice.saydi.ai/api/v1/voices" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-F "file=@my-recording.m4a" \
-F "name=Giọng của tôi" \
-F "language=vi" \
-F "consent=true"
# -> {"id":"vc_9f2c...","status":"ready","ref_text":"...","quality":{...}}
# Then speak with it — no clip to upload:
curl -sS -X POST "https://voice.saydi.ai/api/v1/audio/speech" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"tts-1","voice":"vc_9f2c...","input":"Xin chào từ giọng của tôi."}' \
--output hello.mp3
Python
import os, requests
key = os.environ["SAYDI_API_KEY"]
base = "https://voice.saydi.ai/api"
with open("my-recording.m4a", "rb") as fh:
voice = requests.post(
f"{base}/v1/voices",
headers={"Authorization": f"Bearer {key}"},
files={"file": ("my-recording.m4a", fh, "audio/mp4")},
data={"name": "Giọng của tôi", "language": "vi", "consent": "true"},
timeout=300,
)
voice.raise_for_status()
voice = voice.json()
print(voice["id"], voice["status"], voice.get("quality", {}).get("warnings"))
audio = requests.post(
f"{base}/v1/audio/speech",
headers={"Authorization": f"Bearer {key}"},
json={"model": "tts-1", "voice": voice["id"], "input": "Xin chào từ giọng của tôi."},
timeout=180,
)
audio.raise_for_status()
open("hello.mp3", "wb").write(audio.content)
Node.js
import { readFile, writeFile } from "node:fs/promises";
const key = process.env.SAYDI_API_KEY;
const base = "https://voice.saydi.ai/api";
const form = new FormData();
form.append("file", new Blob([await readFile("my-recording.m4a")], { type: "audio/mp4" }), "my-recording.m4a");
form.append("name", "Giọng của tôi");
form.append("language", "vi");
form.append("consent", "true");
let voice = await fetch(`${base}/v1/voices`, {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
});
if (!voice.ok) throw new Error(`HTTP ${voice.status}: ${await voice.text()}`);
voice = await voice.json();
console.log(voice.id, voice.status, voice.quality?.warnings);
const audio = await fetch(`${base}/v1/audio/speech`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "tts-1", voice: voice.id, input: "Xin chào từ giọng của tôi." }),
});
if (!audio.ok) throw new Error(`HTTP ${audio.status}: ${await audio.text()}`);
await writeFile("hello.mp3", Buffer.from(await audio.arrayBuffer()));
201 — Đã tạo giọng.
{
"id": "vc_9f2c1a4b7e0d4a1c8b3f5e6d7a9c0b12",
"object": "voice",
"name": "Giọng của tôi",
"status": "ready",
"language": "vi",
"ref_seconds": 7.4,
"ref_text": "Xin chào, đây là đoạn nói trong clip mẫu.",
"quality": {
"speech_seconds": 6.9,
"snr_db": 26.4,
"trimmed_seconds": 42.1,
"music_detected": false,
"separated": false,
"warnings": []
},
"consent": {"terms_version": "voice-terms-2026-01"},
"preview_url": "/v1/voices/vc_9f2c1a4b7e0d4a1c8b3f5e6d7a9c0b12/preview"
}
200 — Gọi lại idempotent: giọng đã tồn tại và được trả về nguyên trạng.
{
"id": "vc_9f2c1a4b7e0d4a1c8b3f5e6d7a9c0b12",
"status": "ready",
"idempotent_replay": true
}
processing và ready — ready là dùng được ngay — trường hợp thường gặp. processing là hệ thống còn đang tách nhạc nền: gọi GET /v1/voices/{voice_id} (thường vài giây) trước khi sinh audio. Giọng failed trả lại slot và kèm error.code. Hai lỗi nên xử lý riêng: 503 storage_unavailable là tạm thời — retry có backoff; 410 ref_missing là audio đã mất hẳn — đăng ký lại giọng (không tốn thêm slot).
GET /v1/voices — Liệt kê và quản lý giọng
Liệt kê giọng của bạn, lấy một giọng, đổi tên, xoá, hoặc nghe thử.
| Endpoint | Công dụng |
|---|
GET /v1/voices | This account's voices, newest first, with usage (slots used/limit). Filter with ?status=ready. |
GET /v1/voices/{voice_id} | One voice: status, ref_text and the quality report. |
PATCH /v1/voices/{voice_id} | Rename: JSON {"name": "..."}. The id never changes. |
DELETE /v1/voices/{voice_id} | Delete the voice and its stored audio, freeing its clone slot. Immediate. |
GET /v1/voices/{voice_id}/preview | MP3 of a short sentence in that voice. Rendered once and cached. Not charged against your characters. |
cURL
curl -sS "https://voice.saydi.ai/api/v1/voices" \
-H "Authorization: Bearer $SAYDI_API_KEY"
curl -sS -X PATCH "https://voice.saydi.ai/api/v1/voices/$VOICE_ID" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Giọng nam trầm"}'
curl -sS "https://voice.saydi.ai/api/v1/voices/$VOICE_ID/preview" \
-H "Authorization: Bearer $SAYDI_API_KEY" --output preview.mp3
curl -sS -X DELETE "https://voice.saydi.ai/api/v1/voices/$VOICE_ID" \
-H "Authorization: Bearer $SAYDI_API_KEY"
Python
import os, requests
key = os.environ["SAYDI_API_KEY"]
base = "https://voice.saydi.ai/api"
h = {"Authorization": f"Bearer {key}"}
voice_id = "vc_9f2c..." # the id to manage
# List every registered voice.
print(requests.get(f"{base}/v1/voices", headers=h, timeout=30).json())
# Rename it.
r = requests.patch(
f"{base}/v1/voices/{voice_id}",
headers=h, json={"name": "Giọng nam trầm"}, timeout=30,
)
r.raise_for_status()
# Save a preview clip.
audio = requests.get(f"{base}/v1/voices/{voice_id}/preview", headers=h, timeout=60)
audio.raise_for_status()
open("preview.mp3", "wb").write(audio.content)
# Delete it.
r = requests.delete(f"{base}/v1/voices/{voice_id}", headers=h, timeout=30)
r.raise_for_status()
Node.js
import { writeFile } from "node:fs/promises";
const key = process.env.SAYDI_API_KEY;
const base = "https://voice.saydi.ai/api";
const h = { Authorization: `Bearer ${key}` };
const voiceId = "vc_9f2c..."; // the id to manage
// List every registered voice.
console.log(await (await fetch(`${base}/v1/voices`, { headers: h })).json());
// Rename it.
let res = await fetch(`${base}/v1/voices/${voiceId}`, {
method: "PATCH",
headers: { ...h, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Giọng nam trầm" }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
// Save a preview clip.
res = await fetch(`${base}/v1/voices/${voiceId}/preview`, { headers: h });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("preview.mp3", Buffer.from(await res.arrayBuffer()));
// Delete it.
res = await fetch(`${base}/v1/voices/${voiceId}`, { method: "DELETE", headers: h });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
Quyền sử dụng và hạn mức slot — Chỉ đăng ký giọng bạn có quyền sử dụng; API từ chối request thiếu consent. Mỗi gói có số slot nhân bản (clone_slots) — vượt hạn mức trả về 402 với code: voice_limit_reached. Đăng ký thất bại không tiêu tốn slot.
POST /tts/clone
Nhân bản và đọc trong một request.
Nhân bản và đọc trong một request multipart. Audio sạch, một người nói, dài 10–15 giây cho kết quả tốt nhất.
| Name | Type | Required | Description |
|---|
text | string | yes | Văn bản cần đọc bằng giọng nhân bản. |
ref_audio | file | yes | Clip mẫu. Nên là WAV mono; 10–15 giây audio sạch một người nói là tốt nhất. Tối đa 10 MB. |
ref_text | string | no | Transcript của clip. Rất nên có; tự phiên âm nếu bỏ trống. |
lang | string | no | Gợi ý ngôn ngữ cho chuẩn hoá và phiên âm. |
duration | number | no | Ép độ dài output, giây. Ghi đè speed. |
speed | number | no | Hệ số tốc độ. |
guidance_scale | number | no | Thang classifier-free guidance. |
num_step | integer | no | Số bước diffusion. |
breaks | string | no | Chuỗi JSON cấu hình auto-pause, ví dụ {"sentence":450,"comma":250}. |
output_format | string | no | wav, mp3, flac hoặc ogg. |
cURL
curl -sS -X POST "https://voice.saydi.ai/api/tts/clone" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-F "ref_audio=@reference.wav;type=audio/wav" \
-F "ref_text=Đây là câu nói trong clip mẫu." \
-F "text=Xin chào, đây là giọng đã nhân bản." \
-F "output_format=mp3" \
--output cloned.mp3
Python
import os, requests
with open("reference.wav", "rb") as fh:
r = requests.post(
"https://voice.saydi.ai/api/tts/clone",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
data={
"text": "Xin chào, đây là giọng đã nhân bản.",
"ref_text": "Đây là câu nói trong clip mẫu.",
"output_format": "mp3",
},
files={"ref_audio": ("reference.wav", fh, "audio/wav")},
timeout=180,
)
r.raise_for_status()
open("cloned.mp3", "wb").write(r.content)
Node.js
import { readFile, writeFile } from "node:fs/promises";
const form = new FormData();
form.append("ref_audio", new Blob([await readFile("reference.wav")], { type: "audio/wav" }), "reference.wav");
form.append("ref_text", "Đây là câu nói trong clip mẫu.");
form.append("text", "Xin chào, đây là giọng đã nhân bản.");
form.append("output_format", "mp3");
const res = await fetch("https://voice.saydi.ai/api/tts/clone", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` },
body: form,
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
await writeFile("cloned.mp3", Buffer.from(await res.arrayBuffer()));
Lưu ý — Audio mẫu tối đa 10 MB. Chỉ nhân bản giọng bạn có quyền sử dụng; upload lại clip mỗi lần gọi sẽ chậm hơn dùng giọng đã lưu.
POST /voice/transcribe
Phiên âm clip mẫu để nhân bản chính xác hơn.
Phiên âm clip mẫu ngắn để lấy ref_text, giúp nhân bản chính xác hơn.
| Name | Type | Required | Description |
|---|
audio | file | yes | Clip mẫu. Nên WAV mono, tối đa 10 MB. |
lang | string | no | Gợi ý ngôn ngữ tuỳ chọn (ISO 639-1). |
cURL
curl -sS -X POST "https://voice.saydi.ai/api/voice/transcribe" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-F "audio=@reference.wav" \
-F "lang=vi"
Python
import os, requests
with open("reference.wav", "rb") as fh:
r = requests.post(
"https://voice.saydi.ai/api/voice/transcribe",
headers={"Authorization": f"Bearer {os.environ['SAYDI_API_KEY']}"},
files={"audio": ("reference.wav", fh, "audio/wav")},
data={"lang": "vi"},
timeout=300,
)
r.raise_for_status()
print(r.json())
Node.js
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("audio", new Blob([await readFile("reference.wav")], { type: "audio/wav" }), "reference.wav");
form.append("lang", "vi");
const res = await fetch("https://voice.saydi.ai/api/voice/transcribe", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SAYDI_API_KEY}` },
body: form,
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
200 — Transcript kèm đánh giá chất lượng. Kiểm tra ok, không chỉ transcript.
{
"ok": true,
"reason": "ok",
"transcript": "Đây là câu nói trong clip mẫu.",
"word_count": 8,
"char_count": 25
}
Credits & quota
Cách tính phí text và nhân bản.
Text to speech và nhân bản tính theo ký tự, dùng chung một quota cho studio và API.
| Sự kiện | Meter | Có trừ không |
|---|
| POST /v1/audio/speech | characters | input length |
| POST /tts, /tts/file | characters | text length |
| POST /tts/clone | characters | text length |
| POST /voice/transcribe | feature gate | No |
| GET /samples, /v1/models | none | No |
cURL
# Current subscription (plan, cycle, period end)
curl -sS "https://voice.saydi.ai/api/billing/me" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
# Would this text fit? Cheap check before spending GPU time.
curl -sS "https://voice.saydi.ai/api/billing/quota/preflight?chars=1200" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
# Everything the developer console shows, in one call (see Usage API below):
curl -sS "https://voice.saydi.ai/api/usage/summary?days=30" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
Python
import os, requests
h = {"Authorization": f"Bearer {os.environ['SAYDI_ACCESS_TOKEN']}"}
# Current subscription (plan, cycle, period end)
plan = requests.get("https://voice.saydi.ai/api/billing/me", headers=h, timeout=30)
plan.raise_for_status()
print(plan.json())
# Would this text fit? Cheap check before spending GPU time.
q = requests.get("https://voice.saydi.ai/api/billing/quota/preflight?chars=1200", headers=h, timeout=30)
q.raise_for_status()
print(q.json())
# Everything the developer console shows, in one call (see Usage API below):
s = requests.get("https://voice.saydi.ai/api/usage/summary?days=30", headers=h, timeout=30)
s.raise_for_status()
print(s.json())
Node.js
const H = { Authorization: `Bearer ${process.env.SAYDI_ACCESS_TOKEN}` };
// Current subscription (plan, cycle, period end)
let res = await fetch("https://voice.saydi.ai/api/billing/me", { headers: H });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
// Would this text fit? Cheap check before spending GPU time.
res = await fetch("https://voice.saydi.ai/api/billing/quota/preflight?chars=1200", { headers: H });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
// Everything the developer console shows, in one call (see Usage API below):
res = await fetch("https://voice.saydi.ai/api/usage/summary?days=30", { headers: H });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
API sử dụng
Truy vấn số dư, mức sử dụng và lịch sử gọi.
Cần access token của người dùng. Dùng các endpoint này cho dashboard và cảnh báo của bạn.
GET /usage/summary — Credits và số request
Số dư credits (dùng chung với studio) kèm số request API và chuỗi theo ngày.
| Name | Type | Required | Description |
|---|
days | integer | no | Khoảng thời gian của chuỗi, 1–365. Mặc định 30. |
cURL
curl -sS "https://voice.saydi.ai/api/usage/summary?days=30" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
Python
import os, requests
r = requests.get(
"https://voice.saydi.ai/api/usage/summary?days=30",
headers={"Authorization": f"Bearer {os.environ['SAYDI_ACCESS_TOKEN']}"},
timeout=30,
)
r.raise_for_status()
print(r.json())
Node.js
const res = await fetch("https://voice.saydi.ai/api/usage/summary?days=30", {
headers: { Authorization: `Bearer ${process.env.SAYDI_ACCESS_TOKEN}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
200 — Credits lấy từ quỹ chung; requests/characters chỉ đếm traffic qua API.
{
"ok": true,
"plan_code": "pro",
"period_start": 1758844800.0,
"period_end": 1761436800.0,
"credits": {
"status": "ready",
"unlimited": false,
"limit": 600000,
"used": 41250,
"remaining": 558750,
"extra": 0,
"day_limit": null,
"day_used": 3200,
"day_remaining": null
},
"api": {
"limits": {"api_concurrency": 5, "clone_slots": 50, "max_chars_request": 100000},
"requests": {"total": 412, "success": 401, "errors": 11},
"characters": 41250,
"last_call_at": 1760001234.5,
"status": "ready",
"days": [
{"day": "2026-09-25", "requests": 18, "characters": 2100, "errors": 0},
{"day": "2026-09-26", "requests": 24, "characters": 3200, "errors": 2}
]
}
}
GET /usage/calls — Lịch sử gọi API
Các lần gọi mới nhất trước, kể cả lần bị từ chối, kèm cursor để phân trang.
| Name | Type | Required | Description |
|---|
limit | integer | no | Số bản ghi mỗi trang, 1–200. Mặc định 50. |
before | number | no | Cursor: giá trị next_before của trang trước. |
cURL
curl -sS "https://voice.saydi.ai/api/usage/calls?limit=25" \
-H "Authorization: Bearer $SAYDI_ACCESS_TOKEN"
Python
import os, requests
r = requests.get(
"https://voice.saydi.ai/api/usage/calls?limit=25",
headers={"Authorization": f"Bearer {os.environ['SAYDI_ACCESS_TOKEN']}"},
timeout=30,
)
r.raise_for_status()
print(r.json())
Node.js
const res = await fetch("https://voice.saydi.ai/api/usage/calls?limit=25", {
headers: { Authorization: `Bearer ${process.env.SAYDI_ACCESS_TOKEN}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
console.log(await res.json());
200 — Dữ liệu lưu 400 ngày. next_before là null ở trang cuối.
{
"ok": true,
"status": "ready",
"next_before": 1760001000.0,
"calls": [
{
"ts": 1760001234.5,
"method": "POST",
"endpoint": "/v1/audio/speech",
"status": 200,
"chars": 41,
"voice": "vi-adam",
"latency_ms": 3820,
"key_id": "key_1f0c9a2b7d3e4f56",
"limit_reason": ""
},
{
"ts": 1760001000.0,
"method": "POST",
"endpoint": "/tts",
"status": 429,
"chars": 0,
"voice": "",
"latency_ms": 6,
"key_id": "key_1f0c9a2b7d3e4f56",
"limit_reason": "api-key-window"
}
]
}
Rate limit & đồng thời
Giới hạn request theo key và concurrency.
Giới hạn áp theo từng API key; concurrency theo gói.
| Giới hạn | Mặc định | Phản hồi khi vượt |
|---|
| Requests per key | 60 / minute | 429, Retry-After, X-OV-Limit: api-key-window |
| Concurrent generations | Starter 2 · Creator 3 · Pro 5 · Custom 10 | 429, Retry-After, X-OV-Limit: api-concurrency |
cURL
TEXT="Xin chào, đây là API của SaydiVoice."
attempt=0
while :; do
code=$(curl -sS -o speech.mp3 -w "%{http_code}" -X POST "https://voice.saydi.ai/api/v1/audio/speech" \
-H "Authorization: Bearer $SAYDI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"tts-1\",\"voice\":\"vi-adam\",\"input\":\"$TEXT\"}")
[ "$code" != "429" ] && break
attempt=$((attempt + 1))
[ "$attempt" -ge 4 ] && { echo "rate limited after retries" >&2; exit 1; }
sleep $((5 * 2 ** attempt)) # exponential backoff
done
Python
import time, requests
def speak(text, voice, tries=4):
for attempt in range(tries):
r = requests.post(
"https://voice.saydi.ai/api/v1/audio/speech",
headers={"Authorization": f"Bearer {KEY}"},
json={"input": text, "voice": voice},
timeout=180,
)
if r.status_code != 429:
r.raise_for_status()
return r.content
wait = int(r.headers.get("Retry-After", "5"))
time.sleep(max(1, wait) * (2 ** attempt)) # exponential backoff
raise RuntimeError("rate limited after retries")
Node.js
const speak = async (text, voice, tries = 4) => {
for (let attempt = 0; attempt < tries; attempt++) {
const res = await fetch("https://voice.saydi.ai/api/v1/audio/speech", {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ input: text, voice }),
});
if (res.status !== 429) {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
return Buffer.from(await res.arrayBuffer());
}
const wait = Number(res.headers.get("Retry-After") || "5");
await new Promise((r) => setTimeout(r, Math.max(1, wait) * 2 ** attempt * 1000)); // exponential backoff
}
throw new Error("rate limited after retries");
};
Khi gặp 429 — Chờ theo Retry-After, rồi retry với backoff và jitter.
Mã lỗi
Dạng lỗi, mã thường gặp và cách retry.
Endpoint native trả {"detail": ...}. POST /v1/audio/speech và GET /v1/models trả dạng OpenAI {"error": {...}} cho MỌI lỗi — sai credential (401), field sai (422), hết hạn mức hoặc quá nhanh (429), lỗi server (500) — và giữ nguyên các header của response (Retry-After, X-OV-Limit). Một khác biệt có chủ ý: field sai ở đây giữ status 422, còn OpenAI trả 400 cho cùng lỗi đó.
Native
{
"detail": "Field 'text' must not be empty."
}
OpenAI-compatible
{
"error": {
"message": "Field 'input' must not be empty.",
"type": "invalid_request_error",
"param": "input",
"code": "invalid_value"
}
}
| Status | Ý nghĩa | Thử lại? |
|---|
| 400 | Malformed request, empty text, or undecodable reference audio | No — fix the request |
| 401 | Missing, unknown, revoked or expired credential | No — fix the credential |
| 403 | Account or token blocked for suspected abuse | No — contact support |
| 404 | Endpoint or voice not found | No |
| 413 | Text too long, or upload above 10 MB | No — split or compress |
| 422 | Request failed schema validation (wrong type or range) | No — check parameter types |
| 429 | Quota exhausted, per-key rate limit, or concurrency cap (on /v1/*, error.code separates them: insufficient_quota vs rate_limit_exceeded) | Yes — after Retry-After |
| 500 | Unexpected server error | Yes — with backoff |
| 503 | Backend temporarily unavailable or not configured | Yes — with backoff |
X-OV-Limit trên 403/429 cho biết limit nào đã chặn request.
| X-OV-Limit | Status | Nguyên nhân |
|---|
| api-key-window | 429 | Per-key request rate exceeded |
| api-concurrency | 429 | Too many in-flight generations for the plan |
| usage-limit | 429 / 403 | Credit quota exhausted, or account soft-limited |
| blocked | 403 | Account or token blocked |
| ip-window | 429 | Edge per-IP limit (shared infrastructure, not your key) |
| concurrency | 503 | Server-wide in-flight ceiling reached |
Từ điển nhanh
Các từ kỹ thuật trên trang này, giải thích bằng tiếng Việt dễ hiểu.
Gặp một từ lạ? Tra ở đây. Bạn không cần nhớ gì cả — trang này chỉ để đọc phần còn lại dễ hơn.
| Từ | Nghĩa dễ hiểu |
|---|
| API | The way two programs talk to each other. Here: how your system asks SaydiVoice to read text out loud. |
| Access key | A secret code starting with sv_live_. It tells SaydiVoice it is you. Treat it like a password and never share it. |
| Voice ID / voice code | A short code for one voice, for example vi-adam. |
| Endpoint | One specific address that does one job. For example /v1/audio/speech is the address that reads text out loud. |
| Request / response | The request is what you send; the response is what comes back. |
| Header | Extra information sent along with every request, such as your access key. |
| Bearer | How the key is written in the header: Authorization: Bearer sv_live_.... |
| JSON | A text format for structured data. SaydiVoice returns JSON when something went wrong. |
| Audio bytes | The content of a sound file. Saved as an .mp3 it plays normally. |
| Multipart | A way of sending a file (for example a voice recording) together with a request. |
| 429 / rate limit | You are sending requests too fast. Wait a moment and try again. |
| Consent | Your confirmation that you have the right to clone that voice. |
| Clone | Making a voice that sounds like the recording you uploaded. |
Vẫn chưa rõ? — Gửi email tới contact@saydi.ai và mô tả bằng lời của bạn việc bạn đang muốn làm — không cần giải thích kỹ thuật.
Changelog
Thay đổi gần đây và chính sách phiên bản.
| Ngày | Phiên bản | Thay đổi | Breaking? |
|---|
| 2026-09-26 | 1.0 | Giai đoạn 1: per-account API keys, per-key rate limit, per-plan concurrency, shared character credits. | — |
Chính sách phiên bản — Thay đổi cộng thêm không cần báo trước. Thay đổi breaking sẽ có prefix /vN mới và ghi tại đây.
Hỗ trợ
Kênh hỗ trợ.
| Chủ đề | Kênh |
|---|
| Integration questions | contact@saydi.ai |
| Higher limits / Enterprise | contact@saydi.ai |
| Suspected abuse or a leaked key | Revoke the key in the dashboard, then contact support |
| Service status | GET /health (no auth required) |