Node n8n
Community node của VieNeu đã hoàn thiện và đã kiểm thử, nhưng gói này chưa được
phát hành công khai. Nó được đánh dấu private: true trong package.json và
chưa bao giờ được đẩy lên npm, nên hôm nay không có gì để bạn cài từ một public
registry.
Nếu bạn muốn chạy nó ngay, hãy liên hệ VieNeu và hỏi xin gói — chúng tôi có thể đưa bạn một bản build. Theo dõi changelog Cloud API để biết thông báo khi nó lên npm; phần Cài đặt bên dưới có cả hai đường.
Mọi thứ còn lại trên trang này — các operation, tham số, field đầu ra và hành vi khi lỗi — đều đúng với gói đã build, nên bạn có thể quyết định node này có phải thứ mình cần hay không trước khi hỏi xin.
VieNeu có một community node cho n8n — một node, bốn resource, audio ra dưới dạng một binary field. Đây là một programmatic node, không phải trigger: nó nằm giữa workflow và biến text tiếng Việt thành một file mà node kế tiếp có thể gửi, lưu hay tải lên.
Có hai cách gọi VieNeu từ n8n:
- Node — ô chọn giọng có tìm kiếm, tự động định tuyến giữa endpoint text ngắn
và text dài, và lỗi API được ánh xạ vào
failure.causemáy đọc được của n8n. Cần gói (xem trên) và một n8n tự host. - HTTP Request node thuần — không phải cài gì, chạy được trên n8n Cloud, và ai cũng dùng được ngay hôm nay. Xem Không muốn cài node? ở cuối trang. Hai template workflow nhập sẵn đi kèm gói.
npm install n8n-nodes-vieneuCái tên đó chưa ai giữ chỗ trên npm, nên thứ lệnh này phân giải ra hôm nay không phải gói của chúng tôi. Đừng cài nó và đừng dán nó vào hộp thoại community node của n8n.
Cài đặt
Chỉ n8n tự host. n8n Cloud chỉ cài các gói đã được kiểm định trên npm, nên đường community node ở đó không dùng được dù có chuyện gì xảy ra — hãy dùng đường HTTP Request thay thế.
Khi gói đã được phát hành
Đây là hình dạng của việc cài đặt sau này. Chưa cái nào chạy được cả — gói chưa có trên npm:
Settings → Community nodes → Install → n8n-nodes-vieneu
Khi đó sẽ không cần đến bất cứ thứ gì bên dưới. Hãy xem changelog trước khi đi theo đường thủ công.
Hôm nay
Hỏi xin VieNeu gói này. Thứ bạn nhận là thư mục mã nguồn n8n-nodes-vieneu/,
bạn tự build lấy; repository chứa nó là private, nên không có git clone nào để
đưa bạn.
Yêu cầu trước:
| Yêu cầu | Từ đâu ra |
|---|---|
| Node 20.19 trở lên | engines.node: ">=20.19" trong package.json của gói |
pnpm — corepack enable là đủ | gói ghim packageManager: "pnpm@10.24.0"; npm và yarn không thay thế được ở đây (xem bên dưới) |
n8n đóng gói kèm n8n-workflow 2.37.1 trở lên | phiên bản mà gói được biên dịch theo, và mã nguồn import NodeConnectionTypes, thứ các bản cũ hơn không export |
n8n không công bố một con số "phiên bản tối thiểu" rõ ràng cho chuyện này; hãy
kiểm tra instance của bạn thực sự đóng gói kèm cái gì bằng npm ls n8n-workflow
ở nơi cài n8n. Gói khai báo n8nNodesApiVersion: 1. Trên một bản n8n quá cũ để
export NodeConnectionTypes, node không hề xuất hiện trong panel; trên bản quá
cũ so với type Failure, việc rẽ nhánh theo failure.cause mô tả
bên dưới chẳng làm gì cả.
Build nó:
cd n8n-nodes-vieneu
pnpm install
pnpm build
pnpm verify
pnpm build là bắt buộc. dist/ bị git ignore, và dist/ đúng là thứ mà khối
n8n trong package.json trỏ n8n vào — một bản chép mới tinh thì không có nó.
pnpm verify là phép kiểm đáng chạy. Nó require() bản dist/ đã build theo
đúng cách loader của n8n làm và khởi tạo class được export, bắt được ba lỗi mà
nếu không thì chỉ lộ ra dưới dạng một node lặng lẽ không bao giờ xuất hiện trong
panel: một đường dẫn trong package.json không còn khớp bố cục được emit, một
icon mà tsc đã không chép sang, và một class n8n không dựng lên được.
Dùng pnpm, đừng dùng npm install. Gói nằm ngoài workspace của chính
repository và mang theo một pnpm-workspace.yaml rỗng để biến thư mục của nó
thành workspace root; nếu pnpm không đọc file đó, một lần install chạy từ đây sẽ
đi ngược lên tận gốc repository và không cài gì cả, thoát ra mã 0 mà chẳng có gì
để build.
Rồi thì hoặc chép bản build vào, hoặc trỏ n8n tới thư mục gói:
# Option A — copy into n8n's private-node folder, then restart n8n.
mkdir -p ~/.n8n/custom
cp -r dist/nodes dist/credentials ~/.n8n/custom/
# Option B — leave it in place and point n8n at it.
export N8N_CUSTOM_EXTENSIONS="/abs/path/to/n8n-nodes-vieneu"
n8n start
Hai lỗi tốn cả một buổi chiều:
~/.n8n/customkhông phải~/.n8n/nodes. Cái sau chứa các community node cài từ npm; một bản build riêng đặt vào đó sẽ bị bỏ qua.N8N_CUSTOM_EXTENSIONSnhận một danh sách đường dẫn tuyệt đối ngăn cách bằng dấu chấm phẩy, trên mọi nền tảng. Danh sách ngăn bằng dấu hai chấm không báo lỗi — nó chỉ nạp về con số không.
Với Docker, mount gói đã build vào container rồi đặt chính biến đó trỏ tới đường dẫn bên trong container.
Node khi đó sẽ xuất hiện với tên VieNeu trong panel node. Type đầy đủ của nó
là n8n-nodes-vieneu.vieneu.
Gói biên dịch được, unit test của nó pass, và pnpm verify nạp được bản build
theo đúng hình dạng loader của n8n mong đợi — nhưng chưa ai nạp nó vào một
instance n8n đang chạy cả, và hai template workflow cũng vậy, chúng được kiểm
theo cấu trúc chứ không phải bằng cách import thật. Hãy coi lần cài đầu tiên của
bạn là một smoke test, và báo cho chúng tôi cái gì hỏng thay vì cho rằng đó
là lỗi của bạn.
Node cố tình không khai usableAsTool, nên một AI Agent không gọi được nó. Sinh
giọng nói tiêu token của tài khoản ở mọi lời gọi, và một agent gọi nó theo kiểu
dò dẫm là một trải nghiệm đầu tiên sai. Agent có bề mặt riêng được hỗ trợ:
MCP server.
Credential
Một loại credential duy nhất, VieNeu API, hai field và không gì khác.
| Field | Tên trong workflow JSON | Bắt buộc | Mặc định |
|---|---|---|---|
| API Key | apiKey | Có | — (che như mật khẩu, placeholder vn_sk_…) |
| Base URL | baseUrl | Có | https://api.vieneu.io/api/v1 |
Tiền tố /api/v1 trong Base URL là thật. Controller được gắn ở v1 nhưng
ứng dụng đặt một prefix api toàn cục, nên các path /v1/... ghi trong tài liệu
thực ra được phục vụ ở /api/v1/.... Một base URL https://api.vieneu.io/v1 sẽ
404 mọi thứ. Chỉ đổi field này khi cần trỏ sang staging hay một bản triển khai tự
host; dấu gạch chéo cuối bị cắt, và một giá trị rỗng thì lùi về mặc định.
Bấm Test sau khi dán key. Phép test gọi GET /voices vào base URL đã cấu
hình — nó không tốn token và được miễn khỏi rate limiter, nhưng guard của nó vẫn
từ chối một key sai định dạng hoặc đã thu hồi bằng 401, nên một key sai sẽ hỏng
ngay tại đây thay vì lặng lẽ trả về danh mục công khai.
Key nào hợp lệ, và một key vn_test_ làm được gì, ở đâu cũng như nhau: xem
Xác thực. Điều duy nhất đáng bận tâm
trong n8n là trần 100 từ mỗi request của key vn_test_ hiện ra dưới dạng một lỗi
400 trên text dài, chứ không phải lỗi quota — node không kiểm prefix cũng
không đếm từ tại chỗ.
Key không bao giờ lọt vào một biến của workflow
Đây là lý do key là một credential chứ không phải một tham số của node:
- Tham số của node được ghi vào lịch sử thực thi, vào workflow JSON mà người ta dán lên issue, và vào các dòng log. Một credential thì được mã hoá khi lưu và tham chiếu theo id — nó không nằm trong workflow đã export.
- Không đoạn code nào trong gói đọc
apiKeycả. Chính n8n tự dựngAuthorization: Bearer {{$credentials.apiKey}}, bên tronghttpRequestWithAuthentication, từ khốiauthenticatecủa credential. Không có biến nào giữ key để một expression hay một dòng log chạm tới được. - Mọi thông báo lỗi, mô tả và response body đính kèm đều đi qua một bộ redact
trước. Bearer token và bất cứ thứ gì có dạng
vn_sk_…,vn_test_…haysk_…đều thành***REDACTED***trước khi n8n lưu chúng vào dữ liệu thực thi.
Resource và operation
| Resource | Operation | Gọi | Tham số |
|---|---|---|---|
| Speech | Generate | POST /audio/speech hoặc POST /tts + poll | Text, Engine, Voice, Put Output File in Field, Options |
| Job | Get | GET /tts/{jobId} | Job ID, Download Audio, Put Output File in Field |
| Voice | Get Many | GET /voices | Engine, Search, Return All, Limit |
| Engine | Get Many | GET /engines | không có |
Engine: Get Many không có tham số riêng nào. Nó trả về một item cho mỗi engine,
gồm cả billingMultiplier đang chạy — hãy đọc cái đó thay vì đọc bất kỳ bảng
nào, kể cả ảnh chụp trong Engine.
Những gì node không làm
Không phải mọi thứ API cung cấp đều được phơi ra, và các phần bỏ đi là có chủ ý: một operation tiêu tiền thật hoặc cần upload file là thứ tệ hại nếu một workflow chạm phải do vô ý, mà một Loop Over Items thì chạm nhiều lần.
| Thiếu | Vì sao |
|---|---|
Nhân bản giọng (POST /voices, /clone, /prepare, /upload) | Một khoản phí cố định 5000 token trước mọi hệ số (15000 trên v4) cho một clip, một lần upload multipart, và một suất trừ vào hạn mức clone của gói. Những giọng nhân bản bạn đã có thì vẫn chọn được trong danh sách Voice; bạn chỉ không tạo mới được từ một workflow. |
DELETE /voices/:voiceId | Phá huỷ dữ liệu, và không hoàn tác được từ bên trong một workflow. |
Lồng tiếng và SRT (/dub, /srt) | Cả hai đều có hình dạng upload, cả hai đều tính phí theo lời gọi. |
POST /dialogue | Chỉ cần key và không phải upload, nhưng 50 lượt × 5000 ký tự là không có trần nào trong request schema — mức rủi ro lớn nhất trong một lời gọi đơn lẻ trên cả bề mặt này, hơn hẳn phần còn lại. |
Streaming (POST /tts/stream) | Phản hồi của nó là một định dạng khung có tiền tố độ dài riêng, lẫn cả khung heartbeat vào giữa; n8n không có bộ parse khung nào và sẽ trả lại cho bạn một buffer mù. Nó cũng chặn ở 4 stream đồng thời cho mỗi key, và mọi stream đều tính phí theo hệ số v4 bất kể engine gửi lên là gì. Xem Streaming nếu bạn cần bề mặt đó. |
Chọn giọng
Field Voice là một resource locator với hai chế độ:
- From List — tìm kiếm được, dựa trên
GET /voices. Việc lọc diễn ra ngay trong node, vì API không có tham số search: nó bỏ dấu (kể cảđ/Đ) và đòi mọi từ đều phải khớp với id, name, description, gender, region, engine và kind. Các dòng đọc làName — gender · region · engine (id), kèm· your clonebên trong nhóm facet cho bất kỳ giọng nào API trả về vớikind: "cloned"— clone của chính bạn lẫn clone do admin công bố, nên một clone công khai do người khác tạo cũng bị gắn nhãn như vậy. Danh sách phân trang 250 một lần. - By ID — dành cho expression. Chép id thật chính xác, kể cả dấu tiếng Việt.
Đặt Engine trước sẽ giới hạn danh sách lại, và ở Speech: Generate danh sách
nạp lại khi Engine đổi. Hãy làm vậy: một giọng chỉ dựng được trên đúng engine của
nó và không bao giờ được thay bằng giọng khác — lệch là một lỗi 400 cứng. Ô chọn
này nạp trực tiếp từ GET /engines, nên từ khi v3 ngừng ngày 2026-09-24 nó chỉ
còn mỗi v4; trường hợp lệch duy nhất còn lại là một slug v3 cũ dán vào By ID.
clone_… không chạy trên text ngắnRoute text ngắn mà node chọn mặc định, POST /audio/speech, từ chối thẳng mọi
giọng nhân bản — phép kiểm giọng của nó chạy mà không có user id, nên nó không
phân biệt được clone của bạn với clone của bất kỳ ai và từ chối tất cả bằng một
lỗi 400 ghi "Cloned voice … can only be used on POST /v1/tts or POST
/v1/tts/stream." Điều này áp dụng cho cả clone do admin công bố.
Route text dài là POST /tts, và route đó có nhận clone — cả của bạn lẫn các
clone đã công bố. Vậy nên để dùng một giọng clone_… từ node, hãy giữ item
vượt ngưỡng Synchronous Route Threshold, hoặc đặt Options → Synchronous
Route Threshold về 1 để ép mọi item đi đường async.
Đừng nghe lời khuyên kèm lỗi 400 của node ở đây. Nó nói nguyên nhân thường gặp là một giọng thuộc engine kia, điều đó đúng với giọng trong danh mục nhưng chẳng liên quan gì đến chuyện này.
Id không mang tính mô tả. Nhiều id là slug khó đọc như vieneu-2-000494 mà giọng
lại tên là Minh Đức, và vài id thẳng thừng là tên một người chẳng liên quan.
Đừng bao giờ suy ra giới tính hay danh tính từ một id.
Speech: Generate
| Tham số | Kiểu | Mặc định | Ghi chú |
|---|---|---|---|
| Text | string (4 dòng) | — | Bắt buộc. Tính phí theo ký tự với sàn 50 ký tự — xem Tính phí. |
| Engine Name or ID | options | mặc định của tài khoản | Để trống = mặc định của tài khoản quyết định, và nó cũng quyết định luôn đơn giá. |
| Voice | resource locator | mặc định của engine | Giới hạn theo Engine. |
| Put Output File in Field | string | data | Bắt buộc. Binary property mà audio rơi vào. |
| Options | collection | {} | Chín option, bên dưới. |
Options
| Option | Tên | Mặc định | Ghi chú |
|---|---|---|---|
| AI Refine | aiRefine | false | Chạy chuẩn hoá và kiểm duyệt bằng AI trước. Tốn một khoản phụ phí trên số ký tự và thêm độ trễ. Không route /v1 nào báo lại khoản phụ phí đó — help text của node dẫn con số 1.3× tại thời điểm viết. Đây cũng là thứ duy nhất có thể sinh ra một lỗi 422. |
| Emotion Name or ID | emotion | mặc định của engine | Nạp từ GET /emotion-tags cho engine đang chọn. Tuỳ engine: v4 không có kiểu đọc nào, nên danh sách ở đó luôn rỗng. |
| File Name | fileName | suy ra | Để trống thì tên được suy ra từ định dạng mà API thực sự tạo ra. |
| Format | format | wav | wav, mp3, opus, pcm, ulaw. |
| Download Audio | downloadAudio | true | Chỉ áp cho route text dài — xem bên dưới. |
| Job Timeout (Seconds) | jobTimeout | 600 | Tối thiểu 5. Poll một job text dài trong bao lâu. |
| Poll Interval (Seconds) | pollInterval | 2 | Tối thiểu 0.5, và bị kẹp về 500 ms trong code bất kể workflow JSON ghi gì. |
| Speed | speed | 1 | 0.5–2.0. |
| Synchronous Route Threshold | syncMaxChars | 500 | Tối thiểu 1. Chỗ hai route tách đôi. |
Node không gửi sample rate nào và cũng không phơi ra option nào cho nó.
Job Timeout tồn tại vì node phải poll. Nếu job của bạn đủ dài để cái timeout đó
thành rủi ro thật, hãy đăng ký một webhook thay vào đó: VieNeu POST một sự kiện
có chữ ký khi job chạm trạng thái kết thúc, và một node trigger Webhook của
n8n nhận nó. POST /tts — route mà option này chi phối — là route duy nhất sinh
ra những sự kiện đó. Xem Webhooks.
Hai route, chọn theo độ dài text
| Text ngắn | Text dài | |
|---|---|---|
| Điều kiện | độ dài ≤ syncMaxChars (mặc định 500) | dài hơn |
| Endpoint | POST /audio/speech (chặn) | POST /tts, rồi poll GET /tts/{jobId} |
| Field trong body | input, response_format, speed, và voice / engine / emotion / aiRefine khi có đặt | text, speed, và voiceId / engine / emotion / aiRefine khi có đặt |
| Định dạng | cả năm | chỉ WAV — route này không có tham số format |
| Giọng nhân bản | bị từ chối bằng 400 | được nhận (của bạn và clone do admin công bố) |
Download Audio | bị bỏ qua; byte về thẳng trong phản hồi | có tác dụng |
| JSON ra thêm | sampleRate, requestId | jobId, duration, audioUrl, audioUrlExpiresIn |
format và mimeType được đặt trên cả hai route và được liệt kê cùng các
field dùng chung bên dưới — đừng coi sự có mặt của chúng là dấu hiệu
cho biết một item đã đi route nào. Hãy đọc route cho việc đó.
Để ý là tên field khác nhau giữa hai route (input/voice so với
text/voiceId). Node lo chuyện đó; nó chỉ quan trọng nếu bạn chép một body qua
lại giữa node và một HTTP Request node.
Nâng syncMaxChars lên nghĩa là bắt node giữ một kết nối HTTP — và một suất
worker của n8n — mở suốt cả quá trình tổng hợp.
Audio hoàn chỉnh trên route text dài được tải về từ một URL S3 presigned
không kèm xác thực. S3 từ chối một request presigned nếu nó còn mang thêm
header Authorization, nên riêng lời gọi đó cố tình bỏ qua credential.
Những phép kiểm chạy trước khi bị tính bất cứ đồng nào
Các trường hợp này hỏng ngay tại chỗ, không request nào được gửi đi và không tốn đồng nào:
- text rỗng;
- text trên 50000 ký tự;
- speed ngoài khoảng 0.5–2.0;
- một định dạng không phải WAV đi kèm text vượt ngưỡng. Route text dài chỉ trả về được WAV, nên trường hợp này bị từ chối chứ không âm thầm hạ cấp.
Output
JSON của mỗi item mang:
| Field | Ý nghĩa |
|---|---|
route | sync hoặc async — cách duy nhất đáng tin để biết route nào đã chạy |
engine, voiceId | thứ đã được yêu cầu, hoặc null nếu dùng mặc định |
textLength | số ký tự thô |
billedCharacterBasis | số ký tự theo NFC với sàn 50 ký tự |
billingNote | nói rõ rằng con số cơ sở này là trước hệ số engine và phụ phí AI refine |
format | định dạng thực sự được tạo ra; luôn là wav trên route text dài |
mimeType | đọc từ phản hồi, không phải từ request — xem bên dưới |
billedCharacterBasis là cơ sở của khoản phí, không phải tổng số token.
Backend áp bốn hệ số theo đúng thứ tự này: cơ sở ký tự (sàn 50 ký tự), rồi phụ
phí AI refine nếu nó bật, rồi hệ số engine, rồi một hệ số token toàn cục —
một thiết lập của quản trị viên, mặc định là 1, mà không route /v1 nào báo lại.
Chính hệ số cuối đó là lý do không con số nào node in ra là một báo giá. Hãy đọc
hệ số engine trực tiếp từ Engine: Get Many, và coi mọi ngân sách bạn tính ra chỉ
là ước lượng.
Audio ra ở đâu
Audio được gắn vào binary property có tên do Put Output File in Field đặt —
là data nếu bạn không đổi. JSON của cùng item đó cũng có thêm fileName và
fileSize.
Hãy đổi tên field khi một node phía trên đã chiếm data, hoặc khi một node phía
sau cần một tên cụ thể.
Mime type và tên file được đọc từ phản hồi, không bao giờ từ request:
- header
X-Output-Format, nếu nó nêu một định dạng đã biết; - nếu không thì
Content-Type; - tên file lấy từ
Content-Dispositionkhi có, nếu không thìspeech.<ext>—wav,mp3,oggcho Opus, vàrawchopcmvàulawvốn không có header.
Sự gián tiếp đó là có chủ ý: route text ngắn có thể lùi về WAV khi gặp một worker
cũ, và các header này là dấu hiệu duy nhất cho biết điều đó đã xảy ra. Gắn nhãn
cho binary theo request sẽ đưa cho node phía sau một mớ byte WAV mang nhãn
audio/mpeg.
Trên route text dài, tên mặc định là speech-<jobId>.wav.
Đưa cho node kế tiếp. Mọi node tiêu thụ file đều hỏi binary property theo
tên: đưa nó data, hoặc tên bạn đã đổi thành. Kích thước và tên cũng nằm trên
JSON của item dưới dạng {{ $json.fileSize }} và {{ $json.fileName }}, nên một
IF phía sau kiểm được chúng mà không đụng vào byte nào. Để giữ một file WAV lớn
khỏi bộ nhớ workflow khi bạn chỉ cần cái link, hãy tắt Download Audio trên
route text dài và chuyền {{ $json.audioUrl }} đi tiếp — nó còn hiệu lực trong
một khoảng thời gian giới hạn, và JSON báo lại khoảng đó ở audioUrlExpiresIn.
Lỗi hiện ra thế nào
Mọi thất bại từ API đều thành một NodeApiError với thông báo:
VieNeu API error 403 while synthesizing speech — <detail> (request id <x-request-id>)
Phần văn xuôi nằm ở description. Chỗ để rẽ nhánh là failure.cause máy đọc
được của n8n. Mỗi status nghĩa là gì ở phía API thì xem
Lỗi; bảng dưới đây là phần ánh xạ mà node thêm
vào bên trên đó:
| Status | failure.cause | Chuyện gì đã xảy ra |
|---|---|---|
| 400 | configuration-invalid | Thường là một giọng thuộc engine kia — nhưng cũng có thể là một giọng clone_… trên route text ngắn, text không phải tiếng Việt, và một key vn_test_ vượt trần 100 từ của nó. |
| 401 | credential-invalid | Key bị từ chối. Dán một key còn hiệu lực vào credential. |
| 403, thông báo có nhắc tới một engine | configuration-invalid | Gói của bạn không bao gồm engine đó. |
| 403, các trường hợp còn lại | quota-exhausted | Token grant đã cạn hoặc hết hạn. Không gợi ý thời gian chờ nào được đặt cả — nó không tự hết. |
| 404 | configuration-invalid | Không có gì dưới id đó. Một jobId của key khác đọc lên y hệt một jobId chưa từng tồn tại; một giọng đã bị gỡ trả về 404, không phải 400. |
| 413 | configuration-invalid | Payload quá lớn. |
| 422 | configuration-invalid | Kiểm duyệt từ chối văn bản. Chỉ chạm tới được khi bật AI Refine. Hãy viết lại; thử lại y nguyên thì vẫn hỏng. |
| 429 | xem bên dưới | Ba bộ giới hạn khác nhau. |
| 503 | temporarily-unavailable | Không có worker nào rảnh cho engine đó. Tạm thời thôi. |
| 5xx khác | temporarily-unavailable | Thử lại sau chốc lát. |
| mọi thứ khác | node-defect | Phản hồi ngoài dự kiến. |
VieNeu trả 403 khi hết token — không bao giờ 402. Logic retry bám vào 402 sẽ không bao giờ chạy.
Các thất bại về DNS, TLS, reset và timeout không bao giờ vào tới bảng đó; chúng
thành một lỗi temporarily-unavailable ghi "Could not reach VieNeu while …", với
phần mô tả bảo bạn kiểm tra xem Base URL có kèm /api/v1 hay không.
Bị rate limit hay đã cạn quota
Một lỗi 429 có thể đến từ ba nơi đòi hỏi ba cách phản ứng trái ngược nhau. Node đọc header của phản hồi để phân biệt — và đó là lý do nó tự soi status thay vì để n8n ném lỗi ra rồi làm mất chúng.
| Dấu hiệu trên phản hồi | Bộ giới hạn nào | failure.cause | Gợi ý chờ | Phải làm gì |
|---|---|---|---|---|
Có Retry-After | Throttler của ứng dụng, đếm theo từng API key | rate-limited | retryAfterMs, lấy từ header | Chờ đúng khoảng thời gian nêu ra rồi thử lại. Số dư không có vấn đề gì. |
Không có Retry-After, body khớp token limit reached / resets at / quota | Hạn ngạch token — một trần theo ngày hoặc theo tuần trên grant | quota-exhausted | resetsAtEpochMs, parse ra từ nội dung thông báo | Đừng thử lại. Chờ tới mốc reset đã nêu, hoặc nạp thêm. |
Không có Retry-After, cũng không có câu chữ đó (HTML của nginx) | Biên nginx, đếm theo IP nguồn | rate-limited | không có | Lùi lại thật mạnh. Trên n8n Cloud, IP đó dùng chung với những khách hàng chẳng liên quan gì tới bạn. |
Hai trong ba trường hợp gộp về rate-limited. Phân biệt chúng bằng việc
retryAfterMs có được đặt hay không: có nghĩa là throttler của ứng dụng và một
khoảng chờ đã biết; không có nghĩa là biên và chẳng có gợi ý chờ nào cả.
Mốc reset của quota phải parse ra từ văn xuôi vì không còn cách nào khác:
controller chỉ giữ lại chuỗi thông báo, nên đoạn text đó là dấu vết cuối cùng còn
sót lại của field resetAt có cấu trúc.
Để ý là hai status khác nhau cùng ánh xạ về quota-exhausted: lỗi 429 ở trên, có
kèm mốc reset, và lỗi 403 do cạn grant, chẳng kèm gì.
Những thất bại không phải là thất bại HTTP
- Một job hỏng trả về HTTP 200 kèm
status: "failed". Speech: Generate nâng nó lên thành lỗi; Job: Get trả nó về như dữ liệu, nên một vòng lặp Wait + IF nhìn thấy được trạng thái kết thúc và thoát ra được. - Poll hết giờ sinh ra một lỗi hình dạng 504 nói rằng job vẫn đang chạy và
đã bị tính phí rồi, kèm theo
jobIdcủa nó. Hãy lấy nó về sau bằng Job: Get. Giữ Job Timeout thấp hơn timeout thực thi của chính n8n, nếu không n8n giết cả lần chạy và jobId đi theo luôn. - Body audio rỗng, hoặc một lần submit không trả về
jobId, sinh ra một lỗi hình dạng 502. - Một phản hồi 200 với danh sách giọng rỗng và một field
error— tức là lần đọc danh mục thất bại — được nâng lên thành lỗi thay vì hiện ra một dropdown rỗng.
Khi node được đặt tiếp tục chạy khi lỗi thay vì dừng lại, thất bại trở thành JSON
của item: error với thông báo đã redact, cộng jobId chỉ khi một job đã thực
sự được gửi đi. Trên item đó không có gì khác — đặc biệt là không có binary
nào. Một thất bại ở text ngắn, và một thất bại async xảy ra ngay lúc submit, mang
theo error mà chẳng có jobId nào cả.
Workflow ví dụ: đọc text dài, cứu lại những job hết giờ
Bảy phút audio có thể dài hơn cả cửa sổ poll, mà job thì bị tính phí ngay lúc submit. Workflow này giữ lại những job đó thay vì trả tiền để lấy về con số không.
Nếu bạn nhận được request HTTPS đi vào, một webhook cùng một node trigger Webhook của n8n tránh được toàn bộ vấn đề này — không còn cửa sổ poll nào để mà vượt quá. Chỉ dựng cái này khi bạn không làm được như vậy.
- Manual Trigger — "When clicking 'Execute workflow'". Hãy thay nó bằng thứ
sinh ra item của bạn; mỗi item cần một field
text. - VieNeu — Resource Speech, Operation Generate.
- Text:
={{ $json.text }} - Engine: chọn một cái, để danh sách giọng được giới hạn theo nó.
- Voice: From List, tìm giọng bạn muốn.
- Put Output File in Field:
data - Options → Format:
WAV(text dài không trả về gì khác). - Options → Job Timeout (Seconds):
900, thấp hơn timeout thực thi n8n của bạn. - Mở tab Settings của node và đổi On Error từ Stop Workflow sang lựa chọn tiếp tục bằng output thường của node — không phải lựa chọn thêm một connector output lỗi riêng, vì cái đó sẽ đẩy các item hỏng xuống một nhánh mà IF ở bước 3 không bao giờ nhìn thấy. Không làm vậy thì một job hết giờ sẽ huỷ cả lần chạy và jobId mất theo.
- Text:
- IF — "Failed?". Điều kiện:
={{ $json.error }}is not empty. True nghĩa là có gì đó hỏng; false nghĩa là item đã có audio ởdata. - Nhánh true → IF — "Recoverable?". Điều kiện:
={{ $json.jobId }}is not empty. True nghĩa là job đã bị tính phí và nhiều khả năng vẫn đang chạy. False nghĩa là thất bại xảy ra trước khi có job nào — key sai, giọng sai, một lỗi 429 — nên chẳng có gì để lấy về: hãy đẩy nó ra một output lỗi, một tin nhắn Slack, hay bất cứ đâu bạn muốn nhìn thấy nó. Đừng gộp nó vào bước 8; nó không có binary và sẽ không bao giờ có. - Recoverable → Wait — 60 giây. Poll không tốn token và không tốn ngân sách rate limit, nhưng ở biên thì nó không miễn phí.
- Wait → VieNeu — Resource Job, Operation Get.
- Job ID:
={{ $json.jobId }} - Download Audio: bật (mặc định nó tắt trên operation này)
- Put Output File in Field:
data
- Job ID:
- IF — "Terminal?". Điều kiện:
={{ $json.status }}bằngcompleted. True → bước 8. False → một IF trên={{ $json.status }}bằngfailed, nhánh true của nó kết thúc nhánh (một node No Operation) còn nhánh false thì quay về node Wait ở bước 5. Một job đã vượt quá 900 giây poll thì không chắc chắn là xong sau 60 giây nữa, và nếu không có vòng lặp này thì item rơi tuột qua mà chẳng có binary nào đính kèm. - Gộp các item đã hoàn tất — những cái từ nhánh false của bước 3 và những cái từ nhánh true của bước 7 — vào thứ sẽ tiêu thụ file: upload, email, lưu trữ. Cả hai đều mang cùng tên binary property, nên các node phía sau không cần biết một item đã đi đường nào.
Cùng hình dạng đó chạy được theo từng item trên một bảng tính. Nhớ rằng mỗi dòng là một lần tổng hợp bị tính phí riêng.
Không muốn cài node?
Mọi operation đều là một lời gọi HTTPS, và node HTTP Request có sẵn của n8n gọi được tất cả. Đường này chạy trên n8n Cloud, không cần bước build, và không cần gì từ VieNeu ngoài một key.
Hai workflow làm sẵn đi kèm gói, nằm dưới n8n-nodes-vieneu/templates/:
| File | Dựng ra cái gì |
|---|---|
vieneu-speech-http-request.json | Manual trigger → một HTTP Request tới POST /audio/speech → MP3 trên binary field data. |
vieneu-long-text-http-request.json | Submit tới POST /tts, rồi Wait → poll → IF cho tới khi job kết thúc, rồi tải audio presigned về. |
Nhập cái nào cũng được bằng Workflows → Import from File. Cả hai đều gắn key qua một credential Bearer Auth, không bao giờ qua một header gõ tay vào tham số node. Lưu ý là chưa cái nào được chạy thật trên một n8n đang chạy — chúng được kiểm theo cấu trúc.
Nếu bạn không có gói, các công thức bên dưới dựng lại đúng hai workflow đó bằng tay. Về hợp đồng request nằm dưới, xem Tổng quan Cloud API.
Tìm một voice id trước. Cả hai lời gọi dưới đây đều không cần key. Từ 2026-09-24,
Cloud API chỉ còn một engine, V4, với id là tên hiển thị (Ngọc Lan); các slug
vieneu-… của danh mục V3 đã ngừng không còn tra ra nữa và trả 400 ở voiceId,
nên hãy nạp lại bất kỳ id nào bạn lưu từ trước đó.
# Which engine is the default? Look for "isDefault": true.
curl -s https://api.vieneu.io/api/v1/engines
# Then list that engine's voices only.
curl -s "https://api.vieneu.io/api/v1/voices?engine=v4" | head -c 400
Kiểm tra key của bạn chạy được, và nghe thử kết quả, trước khi nối dây bất cứ thứ gì:
curl -X POST https://api.vieneu.io/api/v1/audio/speech \
-H "Authorization: Bearer $VIENEU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"Xin chào Việt Nam.","response_format":"mp3","speed":1}' \
--output speech.mp3
Text ngắn: một node HTTP Request
| Thiết lập | Giá trị |
|---|---|
| Method | POST |
| URL | https://api.vieneu.io/api/v1/audio/speech |
| Authentication | Generic Credential Type → Bearer Auth, giữ key vn_sk_… của bạn |
| Send Body | bật, JSON |
| Response → Format | File, output property data |
Body, viết dưới dạng expression để text lấy được từ item đi vào:
{{ JSON.stringify({
input: $json.text || 'Xin chào Việt Nam! Đây là giọng đọc tiếng Việt của VieNeu.',
voice: $json.voiceId || undefined,
response_format: 'mp3',
speed: 1
}) }}
Dùng một credential Bearer Auth, đừng gõ header Authorization thẳng vào
node. Credential được tham chiếu theo id và không nằm trong workflow JSON đã
export — đó là khác biệt giữa một workflow bạn dán được lên issue và một workflow
làm lộ key của bạn ngay khi bạn dán.
response_format ở đây nhận mp3, wav, opus, pcm, ulaw. Route này chặn
suốt cả quá trình tổng hợp, nên hãy để dành nó cho text ngắn. Một giọng clone_…
bị từ chối ở đây bằng một lỗi 400, y như khi đi qua node.
Text dài: submit, poll, tải về
Quá khoảng 500 ký tự, hãy xếp job vào hàng đợi thay vì giữ một kết nối mở.
Trước khi bạn dựng vòng lặp: nếu n8n của bạn nhận được request HTTPS đi vào, một
webhook thay các bước 2 đến 5 bằng một node trigger
Webhook duy nhất. VieNeu POST một sự kiện có chữ ký ngay khi job chạm trạng
thái kết thúc, và POST /tts là route duy nhất sinh ra những sự kiện đó. Chỉ
poll khi bạn không có sẵn một endpoint nhận vào.
Bản dùng poll, khớp với vieneu-long-text-http-request.json, gồm tám node: một
Manual Trigger và bảy node dưới đây.
-
HTTP Request — "Submit TTS job":
POST https://api.vieneu.io/api/v1/tts, credential Bearer Auth, body JSON:{{ JSON.stringify({
text: $json.text,
voiceId: $json.voiceId || undefined,
speed: 1
}) }}Để ý tên field trên route này:
textvàvoiceId, không phảiinputvàvoice. Nó không nhận tham số format nào và luôn trả về WAV. Nó phản hồi kèm mộtjobId, và text bị tính phí ngay tại đây, trước khi job được xếp hàng. -
Wait — 3 giây.
-
HTTP Request — "Check job status":
GET https://api.vieneu.io/api/v1/tts/{{ $json.jobId }}(dưới dạng expression), cùng credential Bearer Auth đó. -
IF — "Completed?":
={{ $json.status }}bằngcompleted. True → tải về. False → bước 5. -
IF — "Failed?":
={{ $json.status }}bằngfailed. True → bước 6. False → quay lại node Wait. Một job hỏng trả về HTTP 200, nên hãy rẽ nhánh theo body, đừng bao giờ theo mã trạng thái. -
No Operation — "Job failed": kết thúc nhánh hỏng.
-
HTTP Request — "Download audio":
GET {{ $json.audioUrl }}, Response Format File, output propertydata, và không credential, không xác thực. URL đó là một link S3 presigned mang chữ ký của chính nó — S3 từ chối thẳng request nếu có một headerAuthorizationđi kèm.
Lỗi trên đường này về ở dạng thô, không có phần phân loại của node. Các quy tắc
vẫn giữ nguyên: một lỗi 429 kèm Retry-After là throttler theo key; một lỗi 429
mà thông báo nhắc tới trần token hoặc một mốc reset là hạn ngạch; một lỗi 429
không có cả hai thì đến từ biên và được đếm theo IP nguồn. Hết sạch token thì là
một lỗi 403.