Chuyển tới nội dung chính

Node n8n

Trạng thái phát hành: đã dựng xong, chưa công bố

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.cause má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.
Đừng npm install n8n-nodes-vieneu

Cá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ầuTừ đâu ra
Node 20.19 trở lênengines.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ênphiê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/custom khô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_EXTENSIONS nhậ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.

Đường cài này chưa được thử trên một n8n đang chạy

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.

Không phải tool cho AI Agent

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.

FieldTên trong workflow JSONBắt buộcMặc định
API KeyapiKeyCó— (che như mật khẩu, placeholder vn_sk_…)
Base URLbaseUrlCó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 apiKey cả. Chính n8n tự dựng Authorization: Bearer {{$credentials.apiKey}}, bên trong httpRequestWithAuthentication, từ khối authenticate củ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_… hay sk_… đều thành ***REDACTED*** trước khi n8n lưu chúng vào dữ liệu thực thi.

Resource và operation​

ResourceOperationGọiTham số
SpeechGeneratePOST /audio/speech hoặc POST /tts + pollText, Engine, Voice, Put Output File in Field, Options
JobGetGET /tts/{jobId}Job ID, Download Audio, Put Output File in Field
VoiceGet ManyGET /voicesEngine, Search, Return All, Limit
EngineGet ManyGET /engineskhô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ếuVì 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/:voiceIdPhá 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 /dialogueChỉ 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 clone bên trong nhóm facet cho bất kỳ giọng nào API trả về với kind: "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.

Giọng clone_… không chạy trên text ngắn

Route 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ểuMặc địnhGhi chú
Textstring (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 IDoptionsmặ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á.
Voiceresource locatormặc định của engineGiới hạn theo Engine.
Put Output File in FieldstringdataBắt buộc. Binary property mà audio rơi vào.
Optionscollection{}Chín option, bên dưới.

Options​

OptionTênMặc địnhGhi chú
AI RefineaiRefinefalseChạ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 IDemotionmặc định của engineNạ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 NamefileNamesuy raĐể trống thì tên được suy ra từ định dạng mà API thực sự tạo ra.
Formatformatwavwav, mp3, opus, pcm, ulaw.
Download AudiodownloadAudiotrueChỉ áp cho route text dài — xem bên dưới.
Job Timeout (Seconds)jobTimeout600Tối thiểu 5. Poll một job text dài trong bao lâu.
Poll Interval (Seconds)pollInterval2Tối thiểu 0.5, và bị kẹp về 500 ms trong code bất kể workflow JSON ghi gì.
Speedspeed10.5–2.0.
Synchronous Route ThresholdsyncMaxChars500Tố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ó.

Có thể bạn chẳng cần đến Job Timeout

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ắnText dài
Điều kiệnđộ dài ≤ syncMaxChars (mặc định 500)dài hơn
EndpointPOST /audio/speech (chặn)POST /tts, rồi poll GET /tts/{jobId}
Field trong bodyinput, response_format, speed, và voice / engine / emotion / aiRefine khi có đặttext, speed, và voiceId / engine / emotion / aiRefine khi có đặt
Định dạngcả nămchỉ WAV — route này không có tham số format
Giọng nhân bảnbị từ chối bằng 400được nhận (của bạn và clone do admin công bố)
Download Audiobị bỏ qua; byte về thẳng trong phản hồicó tác dụng
JSON ra thêmsampleRate, requestIdjobId, 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
routesync hoặc async — cách duy nhất đáng tin để biết route nào đã chạy
engine, voiceIdthứ đã được yêu cầu, hoặc null nếu dùng mặc định
textLengthsố ký tự thô
billedCharacterBasissố ký tự theo NFC với sàn 50 ký tự
billingNotenó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:

  1. header X-Output-Format, nếu nó nêu một định dạng đã biết;
  2. nếu không thì Content-Type;
  3. tên file lấy từ Content-Disposition khi có, nếu không thì speech.<ext> — wav, mp3, ogg cho Opus, và raw cho pcm và ulaw vố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 đó:

Statusfailure.causeChuyện gì đã xảy ra
400configuration-invalidThườ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ó.
401credential-invalidKey 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 engineconfiguration-invalidGói của bạn không bao gồm engine đó.
403, các trường hợp còn lạiquota-exhaustedToken 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.
404configuration-invalidKhô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.
413configuration-invalidPayload quá lớn.
422configuration-invalidKiể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.
429xem bên dướiBa bộ giới hạn khác nhau.
503temporarily-unavailableKhông có worker nào rảnh cho engine đó. Tạm thời thôi.
5xx kháctemporarily-unavailableThử lại sau chốc lát.
mọi thứ khácnode-defectPhả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ồiBộ giới hạn nàofailure.causeGợi ý chờPhải làm gì
Có Retry-AfterThrottler của ứng dụng, đếm theo từng API keyrate-limitedretryAfterMs, lấy từ headerChờ đú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 / quotaHạn ngạch token — một trần theo ngày hoặc theo tuần trên grantquota-exhaustedresetsAtEpochMs, 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ồnrate-limitedkhô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 jobId củ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.

  1. 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.
  2. 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.
  3. 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.
  4. 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ó.
  5. 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í.
  6. 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
  7. IF — "Terminal?". Điều kiện: ={{ $json.status }} bằng completed. True → bước 8. False → một IF trên ={{ $json.status }} bằng failed, 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.
  8. 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/:

FileDựng ra cái gì
vieneu-speech-http-request.jsonManual trigger → một HTTP Request tới POST /audio/speech → MP3 trên binary field data.
vieneu-long-text-http-request.jsonSubmit 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ậpGiá trị
MethodPOST
URLhttps://api.vieneu.io/api/v1/audio/speech
AuthenticationGeneric Credential Type → Bearer Auth, giữ key vn_sk_… của bạn
Send Bodybật, JSON
Response → FormatFile, 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.

  1. 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: text và voiceId, không phải input và 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ột jobId, và text bị tính phí ngay tại đây, trước khi job được xếp hàng.

  2. Wait — 3 giây.

  3. 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 đó.

  4. IF — "Completed?": ={{ $json.status }} bằng completed. True → tải về. False → bước 5.

  5. IF — "Failed?": ={{ $json.status }} bằng failed. 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.

  6. No Operation — "Job failed": kết thúc nhánh hỏng.

  7. HTTP Request — "Download audio": GET {{ $json.audioUrl }}, Response Format File, output property data, 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 header Authorization đ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.