API đọc văn bản tiếng Việt
API chuyển văn bản tiếng Việt thành giọng nói, trả audio ngay trong lúc sinh. Dùng được theo ba cách:
- HTTP, tương thích API
audio/speechcủa OpenAI; - WebSocket, cho độ trễ thấp nhất và mốc theo câu;
- SDK Python và JavaScript, cùng plugin LiveKit và Pipecat.
Tài liệu này dành cho người tích hợp. Phần vận hành server xem README.md và DEPLOY.md.
Trong các ví dụ, $BASE là địa chỉ server (ví dụ https://tts.example.com) và $KEY là API key.
Bắt đầu nhanh
curl $BASE/v1/audio/speech -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"input": "Xin chào, tôi là trợ lý ảo."}' -o chao.mp3
from mon_ai import Client # pip install ./clients/python
client = Client(api_key=KEY, base_url=BASE)
open("chao.mp3", "wb").write(client.speech("Xin chào!"))
import { Client } from "@mon-ai/sdk"; // clients/js
const client = new Client({ apiKey: KEY, baseUrl: BASE });
const mp3 = await client.speech("Xin chào!");
Thư viện openai cũng gọi được, chỉ cần đổi base_url:
from openai import OpenAI
client = OpenAI(api_key=KEY, base_url=f"{BASE}/v1")
client.audio.speech.create(model="tts-1", voice="alloy", input="Xin chào!").write_to_file("chao.mp3")
Xác thực và giới hạn
- Key: gửi
Authorization: Bearer <key>trong mọi request. Riêng WebSocket còn nhận?api_key=<key>, vì trình duyệt không đặt được header. Key nằm trong trang web thì người dùng trang đó đều xem được: hãy gọi API từ server của bạn, hoặc dùng một key riêng có hạn mức thấp. - Hạn mức ký tự mỗi tháng:
- Mỗi văn bản bị trừ số ký tự ngay khi bắt đầu đọc.
- Văn bản bị huỷ trước khi có audio, hoặc hỏng do lỗi phía server, được hoàn lại.
- Hết hạn mức thì nhận
429, mãinsufficient_quota. - Tháng tính theo giờ Việt Nam. Xem số đã dùng ở
GET /v1/usage.
- Số request chạy cùng lúc của mỗi tài khoản, tính cả request đang chờ: vượt thì nhận
429, mãconcurrency_limit. - Server đầy (mọi engine bận và hàng đợi đã đủ): nhận
429; thử lại sau ít giây. - Văn bản: tối đa 5000 ký tự mỗi request.
Đọc văn bản qua HTTP: POST /v1/audio/speech
Body JSON giống OpenAI, thêm sample_rate:
| Trường | Kiểu | Mặc định | |
|---|---|---|---|
input |
chuỗi | (bắt buộc) | văn bản, tối đa 5000 ký tự |
voice |
chuỗi | giọng mặc định | tên trong GET /v1/voices, hoặc id giọng riêng (v_…). Tên giọng của OpenAI (alloy, nova…) được hiểu là giọng mặc định |
response_format |
chuỗi | mp3 |
mp3, opus (Ogg), aac (ADTS), flac, wav, pcm (16-bit little-endian, mono), mulaw (G.711 µ-law) |
sample_rate |
số | 24000 (mulaw: 8000) |
24000, 16000 hoặc 8000 |
speed |
số | 1.0 | 0,5–2,0; đổi tốc độ, giữ nguyên cao độ |
model |
chuỗi | không bắt buộc: tts-1, tts-1-hd, gpt-4o-mini-tts, vi-pocket đều là cùng một model |
Audio được gửi dần ngay khi sinh (chunked):
- byte đầu tiên đến sau khoảng 40 ms với
pcm,wav,mulaw, và khoảng 65 ms vớimp3(đo trên server, chưa tính đường mạng); - model sinh nhanh hơn thời gian thực khoảng 8 lần.
Header trả về:
Content-Type: vớipcmvàmulawcó kèm tần số mẫu, ví dụaudio/pcm;rate=24000;X-Sample-Rate.
File wav khi stream có header ghi độ dài "không xác định". Đa số trình phát vẫn đọc được; SDK tự sửa header khi tải
trọn file.
Văn bản được chuẩn hoá trước khi đọc. Không cần tự viết số, ngày, giờ, tiền, số điện thoại, đơn vị, từ tiếng Anh hay
công thức ra chữ: 25-30°C, 2.500.000đ, 14h30, H2SO4, Vinhomes Grand Park đều được đọc đúng.
Độ trễ thấp nhất và mốc theo câu: WebSocket /v1/audio/stream
Mở wss://…/v1/audio/stream với header Authorization: Bearer <key>, hoặc với ?api_key=<key>. Sau đó gửi một văn
bản dưới dạng JSON (text frame):
{"text": "Chào mừng quý vị. Chúc một ngày tốt lành!", "format": "pcm", "sample_rate": 24000, "speed": 1.0}
format là pcm (mặc định) hoặc mulaw; các trường khác giống HTTP. Server gửi lại các message sau:
| Message | Ý nghĩa |
|---|---|
| frame nhị phân | audio (pcm: 16-bit little-endian mono; mulaw: 1 byte mỗi mẫu) |
{"type": "segment", "index": 0, "text": "Chào mừng quý vị.", "start": 0.0} |
đến trước audio của một đoạn: một hoặc vài câu nguyên vẹn của văn bản, đúng như bạn viết. start là số giây tính từ đầu audio của văn bản này. |
{"type": "done", "audio_sec": 4.2, "ttfa_ms": 41, "chars": 41, "cancelled": false} |
hết văn bản: số giây audio, thời gian tới byte đầu (phía server), số ký tự đã tính, có bị huỷ không |
{"type": "error", "message": "...", "code": "insufficient_quota"} |
văn bản không đọc được; kết nối vẫn mở |
- Một văn bản một lúc: chờ
donerồi mới gửi văn bản tiếp, nếu không sẽ nhận lỗibusy. Nên giữ kết nối để đọc nhiều văn bản liên tiếp, đỡ mất thời gian mở lại. - Huỷ:
{"type": "cancel"}dừng văn bản đang đọc. Server dừng sinh ngay, rồi gửidonevới"cancelled": true. - Mốc theo câu: các câu được ghép thành đoạn tối đa khoảng 50 âm tiết, mỗi đoạn sinh một lần.
starttính từ số mẫu âm thanh đã sinh nên khớp với audio nhận được, kể cả khi đổispeed. Muốn phụ đề từng câu thì gửi từng câu một; voice agent vốn làm như vậy. - Ứng dụng:
- phụ đề, hoặc tô sáng câu đang đọc (đoạn đang phát là đoạn cuối có
start≤ vị trí đang phát); - biết đã đọc tới đâu khi người dùng ngắt lời.
- phụ đề, hoặc tô sáng câu đang đọc (đoạn đang phát là đoạn cuối có
- Message có
typemà client chưa biết thì bỏ qua: sau này có thể có thêm loại message mới.
Giọng đọc
-
GET /v1/voicestrả các giọng tài khoản dùng được: giọng có sẵn của dịch vụ, rồi giọng riêng của tài khoản.{"voices": ["ngoc-lan", "minh-khang", "v_k3j9…"], "default": "ngoc-lan", "details": [{"id": "ngoc-lan", "name": "Ngọc Lan", "gender": "female", "region": "north", "description": "Giọng nữ trẻ, rõ ràng"}], "custom": [{"id": "v_k3j9…", "name": "Giọng Lan", "seconds": 5.9, "denoised": false, "created": "…"}]}detailsmô tả các giọng có sẵn (gender:female,malehoặc rỗng;region:north,central,southhoặc rỗng). Danh sách và tên giọng có thể thay đổi; hãy đọc danh sách này thay vì ghi cứng id. Bỏvoicethì dùng giọngdefault. -
POST /v1/voices(multipart) tạo giọng riêng từ một bản ghi âm. Giọng dùng được ngay với"voice": "<id>".curl $BASE/v1/voices -H "Authorization: Bearer $KEY" -F file=@ghi_am.m4a -F name="Giọng Lan" -F consent=true # {"id": "v_k3j9…", "name": "Giọng Lan", "seconds": 5.9, "denoised": false, "created": "…"}- File: wav, mp3, m4a, ogg, webm…, tối đa 10 MB; server chỉ đọc 30 giây đầu.
- Nội dung: 6–10 giây một người nói, ở phòng yên tĩnh. Cần ít nhất 2,5 giây giọng nói. Bản ghi ồn được khử nhiễu tự động.
consent=truelà bắt buộc: bạn xác nhận người có giọng đã đồng ý cho tạo giọng.- Mỗi tài khoản có tối đa 20 giọng riêng (mặc định của server). Giọng riêng chỉ tài khoản tạo ra nó dùng được.
-
DELETE /v1/voices/{id}xoá giọng riêng.GET /v1/voices/{id}/sampletải bản ghi đã làm sạch mà giọng được tạo từ đó (wav).
Lồng tiếng từ phụ đề: /v1/dubs
Đọc từng câu của file phụ đề tiếng Việt đúng thời điểm của nó, rồi trộn vào video hay âm thanh gốc. Việc chạy ở nền: gửi lên, rồi xem tiến độ và tải kết quả.
curl $BASE/v1/dubs -H "Authorization: Bearer $KEY" \
-F subtitles=@tap1.srt -F media=@tap1.mp4 -F mode=voiceover -F name="Tập 1"
# {"id": "d_8f2k…", "status": "queued", "chars": 12840, ...}
curl $BASE/v1/dubs/d_8f2k… -H "Authorization: Bearer $KEY" # status, done / total
curl "$BASE/v1/dubs/d_8f2k…/files/video?download=1" -H "Authorization: Bearer $KEY" -o tap1-long-tieng.mp4
Trường của POST /v1/dubs (multipart):
| Trường | Mặc định | Ý nghĩa |
|---|---|---|
subtitles |
file SRT hoặc WebVTT (UTF-8, UTF-16 hoặc Windows-1258), tối đa 2 MB | |
lines |
thay cho subtitles: JSON [{"start": 1.5, "end": 3.2, "text": "...", "voice": null}] (giây; voice của riêng câu đó) |
|
media |
không có | video hoặc âm thanh gốc; không có thì chỉ xuất giọng đọc theo thời gian phụ đề |
mode |
voiceover |
voiceover: thuyết minh, giữ âm thanh gốc và hạ nhỏ khi giọng đọc nói; replace: bỏ âm thanh gốc |
background |
không có | với replace: track nhạc nền và tiếng động (không có lời) để trộn vào |
duck_db |
−15 (voiceover), −6 (replace) |
âm thanh gốc hay track nền nhỏ đi bao nhiêu dB khi giọng đọc nói, −40 đến 0 |
voice |
giọng mặc định | giọng chung của các câu |
speed |
1.0 | tốc độ đọc mọi câu, 0.5–2 |
max_speed |
1.3 | câu dài hơn khoảng trống tới câu sau được đọc nhanh dần, tối đa tới mức này; quá nữa thì câu sau chờ |
name |
tên file | tên dự án |
- Giới hạn mặc định của server: file tối đa 500 MB và 120 phút; mỗi tài khoản giữ tối đa 20 dự án. Dự án và file của nó tự xoá sau 7 ngày kể từ lần đổi cuối.
- Tính phí: mỗi câu tính số ký tự của nó, trừ vào hạn mức ngay khi gửi (hết hạn mức thì 429). Dự án đang chạy chiếm một request cùng lúc của tài khoản. Dự án lỗi được hoàn ký tự. Dự án bị huỷ chỉ tính các câu đã đọc.
- Thứ tự chạy: server làm lần lượt từng dự án. Mỗi câu chỉ dùng engine khi không có yêu cầu đọc trực tiếp nào đang chờ, nên lồng tiếng không làm chậm API realtime.
Các endpoint khác:
GET /v1/dubs: danh sách dự án.GET /v1/dubs/{id}: chi tiết một dự án:status:queued,running,done,failedhoặccancelled;stage,done,total: tiến độ theo câu;lines: các câu;placed: mỗi câu được đặt thế nào, gồmat(đọc từ giây thứ mấy),speed,duration,late(bắt đầu muộn bao nhiêu vì câu trước chưa xong),over(còn tràn bao nhiêu);files: các file kết quả.
GET /v1/dubs/{id}/files/{video|audio|voice|subtitles}, thêm?download=1để tải về:video: video MP4 với âm thanh mới; hình được giữ nguyên, không nén lại; codec MP4 không chứa được thì xuất MKV;audio: âm thanh đã trộn, M4A 48 kHz stereo;voice: giọng đọc riêng, WAV 24 kHz;subtitles: phụ đề SRT.
POST /v1/dubs/{id}/rendervới JSON{"lines", "voice", "mode", "duck_db", "speed", "max_speed", "name"}, trường nào cũng không bắt buộc: xuất lại với lời đã sửa. Chỉ câu mới hoặc đã sửa được đọc lại và tính phí.POST /v1/dubs/{id}/cancel,DELETE /v1/dubs/{id}.GET /v1/dubs/{id}/lines/{i}: nghe một câu đã đọc (WAV).GET /v1/dubs/{id}/source: file gốc đã tải lên.POST /v1/dubs/parse: phụ đề thành{"lines"}, không lưu và không tính phí. Gửi file trong trường multipartsubtitles, hoặc JSON{"text"}.
Nội dung lồng tiếng là do AI tạo. Khi đăng tải, khách hàng chịu trách nhiệm ghi nhãn theo Luật Trí tuệ nhân tạo, và phải có quyền với video gốc.
Lượng dùng: GET /v1/usage
?start=YYYY-MM-DD&end=YYYY-MM-DD; mặc định là tháng này.
{"customer": "acme",
"quota": {"month": "2026-10", "monthly_chars": 1000000, "used_chars": 37408, "remaining_chars": 962592,
"resets_at": "2026-11-01T00:00:00+07:00", "max_streams": 2, "running": 0},
"period": {"start": "2026-10-01", "end": "2026-10-10"},
"total": {"requests": 329, "chars": 37408, "audio_sec": 1807.3, "cancelled": 8, "errors": 0},
"daily": [{"date": "2026-10-09", "requests": 281, "chars": 34645, "audio_sec": 1637.6, "cancelled": 4, "errors": 0}],
"by_key": [...]}
GET /health không cần key: {"status": "ok", ...}, hoặc 503 khi server chưa sẵn sàng.
Lỗi
Mọi lỗi HTTP có dạng như của OpenAI: {"error": {"message": "...", "type": "...", "code": "..."}}.
| HTTP | type / code |
Khi nào |
|---|---|---|
| 400 | invalid_request_error |
văn bản trống hoặc quá dài, sai giọng (kèm danh sách voices), sai định dạng hay tần số mẫu; no_voice khi server chưa mở giọng nào; với giọng riêng còn có consent_required, bad_recording, file_too_large, voice_limit; với lồng tiếng: bad_subtitles, bad_dub, dub_limit |
| 401 | authentication_error |
thiếu key, sai key, key đã thu hồi |
| 403 | permission_error / account_disabled |
tài khoản bị khoá |
| 404 | giọng riêng hay dự án lồng tiếng không có hoặc không thuộc tài khoản | |
| 409 | dub_running |
xuất lại một dự án đang chạy: huỷ trước |
| 413 | file_too_large |
file lồng tiếng lớn hơn giới hạn của server |
| 429 | insufficient_quota |
hết hạn mức tháng |
| 429 | rate_limit_error / concurrency_limit |
quá số request cùng lúc của tài khoản |
| 429 | rate_limit_error |
server đầy, thử lại sau |
| 500 | server_error |
lỗi phía server |
Riêng WebSocket: key sai hoặc tài khoản bị khoá thì kết nối bị từ chối ngay khi mở (HTTP 403). Các lỗi khác đến dưới
dạng message error.
SDK và tích hợp
| Cài | Dùng | |
|---|---|---|
| Python | pip install ./clients/python |
Client / AsyncClient: speech, speech_stream, stream (WebSocket: bytes, Segment, Done), connect() để giữ kết nối, voices, create_voice, usage. clients/python |
| JavaScript (trình duyệt, Node 18+) | clients/js |
Client: speech, speechStream, stream, connect(), voices, createVoice, usage. clients/js |
| LiveKit Agents | pip install "./clients/python[livekit]" |
AgentSession(tts=mon_ai.livekit.TTS()) |
| Pipecat | pip install "./clients/python[pipecat]" |
MonTTSService(settings=MonTTSService.Settings(speed=1.0)) trong pipeline |
| Thư viện OpenAI có sẵn | openai (Python), plugin OpenAI của LiveKit (base_url=f"{BASE}/v1"); OpenAITTSService của Pipecat chỉ nhận tên giọng OpenAI, khi đó được giọng mặc định |
Với voice agent, LLM trả lời theo kiểu stream. LiveKit và Pipecat tự gom chữ thành từng câu, rồi gửi mỗi câu là một
request: câu đầu có tiếng sau khoảng 50 ms. Với tổng đài, dùng sample_rate 8000, hoặc mulaw qua WebSocket, để
khỏi phải đổi tần số mẫu.