目錄(29 節)
- 一分鐘睇完:批量生成係咩、唔係咩
- 三種提交方式,幾時用邊種?
- 核心流程:提交 → poll → 收貨
- 實戰:由單句改成批量(附可抄 code)
- 第一步:本地驗稿(唔好跳)
- 第二步:分輪 + 並行提交
- 第三步:poll 收結果
- 第四步:錯誤處理同重試
- 第五步:下載(同一個陷阱)
- 完整 Node.js 例子
- Python 版本(精簡)
- 批量注意事項:五個一定要知嘅坑
- 一、提交限流係 60 次/分鐘(per key)
- 二、額度同成本:批量會令你嘅月配額蒸發得好快
- 三、Job 管理:三個要記住嘅規則
- 四、失敗重試:分清「提交失敗」同「任務失敗」
- 五、同步捷徑唔可靠,批量一定要行 async
- 配額同成本:批量開跑之前先計清楚
- 單價對照(四個方案)
- 實際規模對照(用上面單價實算)
- 常見問題 FAQ
- 一次過最多可以提交幾多條?
- 批量生成係唔係一定要升級?
- 60 次/分鐘嘅限流,係計「請求」定計「item」?
- 有幾條失敗咗,會唔會照扣我字符?
- Poll 會唔會食咗我嘅限流額度?
- 額度用完會點?會唔會自動扣錢?
- 開錯 batch,可唔可以中途取消?
- 由一個 10 條嘅小批開始
批量生成粵語語音:由單句到上萬段嘅 workflow
批量生成唔係獨立 endpoint,而係一套「本地拆稿 → 分輪 bulk 提交(一請求最多 25 項)→ poll 收結果 → 只補跑失敗項」嘅 workflow。同步 endpoint 唔可靠,生產 pipeline 一定要行 async。
一句答你:批量生成唔係一個獨立 endpoint,而係一套「本地拆稿 → 分輪 bulk 提交(一個請求最多 25 項)→ poll 收結果 → 只補跑失敗項」嘅 workflow;同步 endpoint 唔可靠(會回
503 pool_cold),所以生產 pipeline 一定要行 async。
一條稿、兩條稿,貼去 playground 就搞掂。但當你要出嘅係一整套課程旁白、一個 podcast 季度、或者過萬條 voice memo,你就唔可以再靠人手複製貼上 — 你要嘅係一條可以重複跑、可以中途死而唔會前功盡廢、可以只補跑失敗部分嘅 pipeline。
呢篇就係講呢條 pipeline 點砌。全文用港嘢 Canto-Gen 嘅公開 API 做例子(POST /v1/audio/jobs + GET /v1/audio/jobs/{id}),code 可以直接抄去用。如果你連基本 API 流程都未行過,建議先睇《粵語 TTS API 快速上手》,再返嚟睇呢篇;想由頭認識廣東話 TTS,就去《廣東話 TTS 完全指南》。
Key Takeaways
- 批量嘅核心係 async + bulk:
POST /v1/audio/jobs一個請求可以帶 最多 25 項({"items":[…]}),回202加每項嘅job_id- Bulk 驗證係全有或全無:任何一項唔合法 → 成個請求
400 invalid_items、零提交、零扣費;所以一定要本地先驗稿- 提交同讀取係兩套限流:提交限 60 次/分鐘(per key),撞到回
429+Retry-After;GET /v1/audio/jobs/{id}(poll)唔計限流- 唔好靠同步捷徑:
POST /v1/audio/speech而家恆回503 pool_cold,批量一定要行 async- 失敗唔等於白花:入隊前失敗零扣費、任務失敗會自動退款;pipeline 只需記錄失敗項再補跑
- 批量燃燒額度好快 — 1,000 條 × 100 字符 = 100,000 字符 ≈ 7 個鐘語音,專業方案等效 HK$56.33;要計清楚先開跑
一分鐘睇完:批量生成係咩、唔係咩
批量生成就係「一次提交多條獨立任務,然後用程式收結果」。 佢唔係一個神秘嘅「批量按鈕」,而係三個動作嘅組合:
| 動作 | 你要做嘅事 | 對應 API |
|---|---|---|
| 提交 | 將一批稿分輪(每輪 ≤25 項)POST 出去 | POST /v1/audio/jobs(body 帶 items) |
| 追蹤 | 記住每項嘅 job_id,輪詢狀態 | GET /v1/audio/jobs/{id} |
| 收貨 | completed → 下載 audio_url;失敗 → 入補跑清單 | GET(pre-signed URL,直接 GET 下載) |
搞清楚一個概念先:批量唔等於「一條大 job」。 25 項入去,係 25 條獨立任務、25 個獨立 job_id、各自獨立成敗。呢個設計對你有利 — 一條失敗唔會拖冧其餘 24 條。
三種提交方式,幾時用邊種?
| 方式 | Endpoint | 一次過可以帶幾多 | 適用 |
|---|---|---|---|
| 單句 async | POST /v1/audio/jobs(單一 body) | 1 條 | 互動式、要即刻知 job_id |
| 批量 async(本文重點) | POST /v1/audio/jobs({"items":[…]}) | 最多 25 項 | 生產 pipeline、離線批量 |
| 同步捷徑 | POST /v1/audio/speech | 1 條,≤300 字符,只支援 voice/design | ⚠️ 目前恆回 503 pool_cold,唔好用 |
第三行要特別講:同步 endpoint 喺專用池未暖嘅時候會如實失敗(fail closed,唔會靜默轉 async)。所以無論你係單句定批量,生產 pipeline 都應該以 async 為主。呢點喺官方文檔寫得好清楚,唔係本文嘅推測。
核心流程:提交 → poll → 收貨
┌─────────────────────────────────────────────┐
│ ① 本地拆稿+驗稿 │
│ 每條 text 1–500 字符、恰好一種 voice mode │
└───────────────────┬─────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ ② 分輪提交(每輪 ≤25 項 items) │
│ POST /v1/audio/jobs │
│ → 202 { jobs:[{index, job_id, status,…}] }│
│ → 400 invalid_items(全批拒絕、零扣費) │
└───────────────────┬─────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ ③ poll 每條 job(GET 唔計 rate limit) │
│ queued → processing → completed │
│ ↘ failed / cancelled │
└───────────────────┬─────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ ④ 收貨:completed → 下載 audio_url │
│ 其餘 → 寫入 failed.json,下一輪補跑 │
└─────────────────────────────────────────────┘
任務狀態生命週期係 queued → processing → completed | failed | cancelled。呢五個狀態嘅處理方式完全唔同:
| 狀態 | 意思 | 你應該點做 |
|---|---|---|
queued | 已入隊,未開始 | 繼續 poll(建議每 2–5 秒一次) |
processing | 生成中 | 繼續 poll |
completed | 完成,有 audio_url(要求過就有 srt) | 即刻下載(URL 會過期) |
failed | 終態,唔會再好 | 記錄落補跑清單,唔好無限重試同一條 |
cancelled | 你取消咗,或已被取消 | 記錄,需要就重新提交 |
實戰:由單句改成批量(附可抄 code)
以下係一條完整、可執行嘅 Node.js pipeline(Node 18+,用內建 fetch,零依賴)。想直接理解流程,可以一路跟住五個步驟睇。
第一步:本地驗稿(唔好跳)
呢步係整條 pipeline 最重要嘅一步,原因係 bulk 驗證係全有或全無:任何一項唔合法,成個請求會回 400 invalid_items,25 項一齊被拒、零提交、零扣費。
所以本地先驗,成本最低:
text必須有值,長度 1–500 字符(超限係拒絕,唔係截斷)- 恰好一種 voice mode:
voice(slug)/design(gender + age)/clone(參考音頻)三選一,同時傳多過一個會回400 voice_mode_conflict response_format只可以mp3或wav- 每輪 item 數 ≤ 25(超過 25 項回
400 too_many_items)
第二步:分輪 + 並行提交
一個請求最多 25 項。要交 1,000 條,就有 40 輪。你可以同時開幾輪去食盡提交限流,但唔好開到失控 — 用一個固定大小嘅 worker pool 就夠。
第三步:poll 收結果
GET /v1/audio/jobs/{id} 唔受每分鐘限流限制(限流只針對提交),所以 poll 可以放心 poll。但都唔好癲到 100ms 一次 — 每 2–5 秒一次、配少少退避已經好夠。
第四步:錯誤處理同重試
呢步係「業餘」同「生產級」嘅分界線。錯誤要分類處理,唔可以一刀切「retry 到成功為止」:
| HTTP / 情況 | error | 要唔要重試? | 正確做法 |
|---|---|---|---|
400 | invalid_items / too_many_items | ❌ | 修 payload(本地驗稿做得唔夠) |
400 | voice_mode_conflict / text_too_long | ❌ | 修 payload |
401 | invalid_or_expired_token | ❌ | 換 key |
402 | character_limit_reached | ❌ | 加額度(買字符包或升級) |
403 | batch_requires_pro | ❌ | 批量係專業/工作室專屬功能 |
403 | monthly_cap_reached | ❌ | 等 UTC 月份重置或調高 key 上限 |
403 | insufficient_scope | ❌ | 檢查 key scope |
429 | rate_limit_exceeded | ✅ | 等 Retry-After 秒再試(body 亦有 retry_after) |
429 | in_flight_limit_reached | ✅ | 等現有任務完成再提交 |
503 | jobs_unavailable / voice_gallery_unavailable | ✅ | 指數退避重試(後端暫時性) |
單項 status: "failed" | 逐項回報 | ⚠️ | 唔好即刻重試 — 記錄落 failed.json,最後補跑一次 |
一句記住:4xx 修 payload,429 睇 Retry-After,5xx 退避,已入隊嘅單項失敗留返最後補跑。
第五步:下載(同一個陷阱)
audio_url 係 pre-signed URL,會過期:由素材庫出嘅係 1 小時,由生成 worker 直出嘅係 7 日。所以 completed 之後即刻下載;如果 URL 已過期,重新 poll 一次就會攞到新簽嘅 URL(唔使重新生成、唔會再扣費)。
完整 Node.js 例子
// batch.mjs — Node 18+(內建 fetch);零依賴
import { writeFile, mkdir } from "node:fs/promises";
const BASE = "https://canto-gen.com";
const KEY = process.env.CANTOGEN_API_KEY;
if (!KEY) throw new Error("未設定 CANTOGEN_API_KEY");
const VOICE = "sum"; // 由 GET /v1/voices 揀最新 slug;"sum" 只係文檔示例
const BULK_MAX = 25; // 一個 bulk 請求上限 25 項
const CONCURRENCY = 2; // 同時開幾輪提交(受 60/min 提交限流約束)
const POLL_MS = 3000; // poll 間隔
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** 提交一輪(≤25 項)。429 / 503 自動退避重試;其餘錯誤即刻拋。 */
async function submitBulk(scripts, attempt = 0) {
const res = await fetch(`${BASE}/v1/audio/jobs`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
items: scripts.map((s) => ({
model: "cantogen-tts",
voice: VOICE,
text: s.text, // 1–500 字符
response_format: "mp3", // 或 "wav"
})),
}),
});
if (res.status === 202) return (await res.json()).jobs;
// 提交限流:60/min per key。Retry-After 係秒數,body 亦有 retry_after。
if (res.status === 429) {
if (attempt >= 5) throw new Error("429 重試次數用盡(等一分鐘再跑)");
const waitSec = Number(res.headers.get("retry-after") ?? 5);
await sleep(waitSec * 1000 + 500); // 加 buffer,避開臨界
return submitBulk(scripts, attempt + 1);
}
// 後端暫時性問題 → 指數退避
if (res.status === 503) {
if (attempt >= 5) throw new Error("503 重試次數用盡");
await sleep(2 ** attempt * 1000);
return submitBulk(scripts, attempt + 1);
}
// 400 invalid_items / 402 / 403:重試都唔會好,即刻拋等上層處理
throw new Error(`submit ${res.status}: ${await res.text()}`);
}
/** poll 到終態為止(GET 唔計 rate limit)。 */
async function pollJob(jobId) {
const deadline = Date.now() + POLL_TIMEOUT_MS;
let backoff = 1000;
while (Date.now() < deadline) {
const res = await fetch(`${BASE}/v1/audio/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (res.status === 503) { // 讀取暫時失敗 → 退避再試
await sleep(backoff);
backoff = Math.min(backoff * 2, 10_000);
continue;
}
if (!res.ok) throw new Error(`poll ${res.status} (${jobId}): ${await res.text()}`);
const job = await res.json();
if (job.status === "completed") return job;
if (job.status === "failed" || job.status === "cancelled") return job; // 終態,交返上層
await sleep(POLL_MS); // queued / processing
}
throw new Error(`poll timeout: ${jobId}`);
}
async function main(scripts) {
await mkdir("out", { recursive: true });
// ① 本地驗稿 — bulk 係 all-or-nothing,一條唔合法會拖冧成批
for (const s of scripts) {
if (!s.text || s.text.length > 500) throw new Error(`${s.id} 文字無效(1–500 字符)`);
}
// ② 分輪(每輪 ≤25 項)
const rounds = [];
for (let i = 0; i < scripts.length; i += BULK_MAX) rounds.push(scripts.slice(i, i + BULK_MAX));
// ③ 並行提交(worker pool:同時最多 CONCURRENCY 輪)
const submitted = []; // { id, text, job_id }
const failed = []; // 入隊前/入隊後失敗項
let cursor = 0;
async function worker() {
while (cursor < rounds.length) {
const round = rounds[cursor++];
const jobs = await submitBulk(round);
for (const r of jobs) {
const src = round[r.index];
if (r.job_id) submitted.push({ ...src, job_id: r.job_id });
else failed.push({ ...src, stage: "submit", error: r.error }); // 逐項失敗
}
}
}
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
// ④ poll + 收貨
let ok = 0;
for (const item of submitted) {
const job = await pollJob(item.job_id);
if (job.status === "completed") {
const audio = Buffer.from(await (await fetch(job.audio_url)).arrayBuffer());
await writeFile(`out/${item.id}.mp3`, audio); // 即刻落檔,唔好等
ok += 1;
} else {
failed.push({ ...item, stage: job.status, error: job.error ?? job.status });
}
}
console.log(`完成 ${ok}/失敗 ${failed.length}`);
if (failed.length) {
await writeFile("out/failed.json", JSON.stringify(failed, null, 2));
console.log("失敗清單已寫入 out/failed.json — 修好之後只需重跑呢批");
}
}
await main([
{ id: "ep01", text: "第一集開場白,各位聽眾你好。" },
{ id: "ep02", text: "今日會講三個重點。" },
]);
Python 版本(精簡)
如果你用 Python,流程完全一樣,換個 client 就得:
# batch.py — pip install httpx
import os, json, time, httpx
BASE = "https://canto-gen.com"
H = {"Authorization": f"Bearer {os.environ['CANTOGEN_API_KEY']}"}
BULK_MAX, POLL_S = 25, 3
def submit_bulk(items, attempt=0):
r = httpx.post(
f"{BASE}/v1/audio/jobs", headers=H, timeout=60,
json={"items": [
{"model": "cantogen-tts", "voice": "sum",
"text": it["text"], "response_format": "mp3"} for it in items
]},
)
if r.status_code == 202:
return r.json()["jobs"] # 逐項 {index, job_id, ...}
if r.status_code in (429, 503): # 限流/後端暫時性
if attempt >= 5:
raise RuntimeError(f"重試用盡:{r.status_code}")
wait = float(r.headers.get("retry-after", 2 ** attempt))
time.sleep(wait + 0.5)
return submit_bulk(items, attempt + 1)
raise RuntimeError(f"submit {r.status_code}: {r.text}") # 4xx:修 payload
def poll(job_id):
deadline = time.time() + 600
while time.time() < deadline:
j = httpx.get(f"{BASE}/v1/audio/jobs/{job_id}", headers=H, timeout=30).json()
if j["status"] in ("completed", "failed", "cancelled"):
return j
time.sleep(POLL_S) # GET 唔計 rate limit
raise TimeoutError(job_id)
scripts = [{"id": "ep01", "text": "第一集開場白。"}, {"id": "ep02", "text": "今日講三個重點。"}]
for s in scripts: # 本地先驗
assert s["text"] and len(s["text"]) <= 500, s["id"]
failed = []
for i in range(0, len(scripts), BULK_MAX):
batch = scripts[i:i + BULK_MAX]
for r in submit_bulk(batch): # 一輪 ≤25 項
src = batch[r["index"]]
if not r.get("job_id"):
failed.append({**src, "stage": "submit", "error": r.get("error")})
continue
j = poll(r["job_id"])
if j["status"] == "completed":
with open(f"out/{src['id']}.mp3", "wb") as f:
f.write(httpx.get(j["audio_url"], timeout=60).content)
else:
failed.append({**src, "stage": j["status"], "error": j.get("error")})
json.dump(failed, open("out/failed.json", "w"), ensure_ascii=False)
兩個版本嘅重點都一樣:本地驗稿 → 分輪 bulk → poll 到終態 → 失敗項寫檔留底。
voice 用邊個 slug,去 GET /v1/voices 攞(聲線目錄連試聽 clip);其他生成參數(speed、pitch、volume、emotion、style、language、cfg_value)同單句一樣,喺每個 item 入面各自指定 — 即係話同一批稿可以有唔同聲線、唔同情緒。參數細節見《點樣用 API 生成地道粵語語音》。
批量注意事項:五個一定要知嘅坑
一、提交限流係 60 次/分鐘(per key)
POST /v1/audio/jobs 有一個滑動 60 秒窗口嘅限流,預設 60 次/分鐘(per key),key 建立時可以調整。撞到會回 429,附 Retry-After header(秒)同 retry_after body 欄位,另有 X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset 三條 header 喺 202 同 429 兩邊都帶。
429 唔係邊間公司自創嘅碼 — 佢係 IETF 喺 RFC 6585 正式定義嘅 "Too Many Requests",Retry-After 亦係 RFC 一齊規定嘅配套 header。實務上點讀呢兩樣嘢、點設計退避,MDN 嘅 HTTP 429 條目 有清楚說明,可以同下面嘅重試策略對照。
呢度有一個對批量極之重要嘅細節:限流係按「請求」計,唔係按「item」計。 一個帶 25 項嘅 bulk 請求 = 1 次請求。所以同樣食 60 次/分鐘,bulk 嘅提交容量係單句嘅 25 倍 — 呢個就係批量一定要用 items 而唔係「loop 25 次單句提交」嘅根本原因。
另外,GET /v1/audio/jobs/{id} 同 GET /v1/voices 唔受限流,所以 poll 唔會偷用你嘅提交額度。
純由限流推算嘅提交上限:60 請求/分鐘 × 25 項 = 每分鐘 1,500 項提交容量。呢個係限流層嘅理論天花板,唔係實際生成速度 — 實際完成時間取決於引擎消化速度,本文未做過公開可重現嘅實測,所以唔會報任何實際吞吐量或每條耗時。
二、額度同成本:批量會令你嘅月配額蒸發得好快
呢個係最多人開跑之後才發現嘅事。批量嘅成本唔係「貴」,而係快:
- API 用量同 playground 1:1 共用同一個月配額 — 你用 API 跑嘅每個字符,都會扣同一個 pool
- 站方估算 ≈ 240 字符/分鐘,所以 100,000 字符 ≈ 7 個鐘語音;1,500,000 字符 ≈ 104 個鐘
- 冇超額收費:配額用完就係用完(請求會回
402/403 monthly_cap_reached),唔會偷偷碌你卡;要加量就要自己買字符包(HK$0.80/1K,最小 HK$50 = 62.5K)
具體數字同「升級 vs 買包」嘅決策,下面獨立一節拆。想睇完整收費結構,可以睇《廣東話 TTS 價錢點計》。
三、Job 管理:三個要記住嘅規則
規則一:job_id 就係你嘅帳。 每項嘅 job_id 要即刻寫入自己嘅清單(配你自己嘅稿 id)。程式中途死咗、想知跑到邊 — 靠嘅就係呢份對照表。唔好靠記憶,亦唔好只靠 console log。
規則二:已入隊嘅可以取消。 DELETE /v1/audio/jobs/{id} 有三種結果,對批量收尾好有用:
| 情況 | 回應 | 結果 |
|---|---|---|
| 任務未開始 | 200 | 即時取消,全額退款 |
| 生成中 | 202 | 收線中;poll 會見到取消cancelling,之後變 cancelled`(已完成部分照計費) |
| 已屬終態(completed/failed/cancelled) | 409 already_settled | 唔可以再取消,亦唔退款 |
所以「開錯 batch」係可以補救嘅 — 趁未開始就 DELETE,唔會白花。
規則三:唔好重複提交同一條稿。 重複提交 = 重複扣費。做法好簡單:本地先過濾一次,只提交未有 job_id、或者已確認失敗嘅項。
四、失敗重試:分清「提交失敗」同「任務失敗」
呢兩種失敗嘅正確處理係唔同嘅:
- 提交階段失敗(成個請求 4xx/5xx):4xx 係你嘅 payload 或權限問題,重試無用;5xx 同 429 就退避重試。因為 bulk 係全有或全無,呢類失敗零扣費。
- 入隊後逐項失敗(
202body 入面{index, status:"failed", error}):其他項照跑,唔會因為一項死而全批死。 - 任務層失敗(poll 到
failed):係終態,同一條唔會自己變好。唔好喺 poll loop 入面即時重試 — 應該收集埋一齊,跑完之後(通常係修完稿之後)一次過補跑。咁樣可以避免「同一條爛稿無限燒額度」。
任務失敗會自動退款;入隊失敗亦已經即時退款 — 所以「失敗」唔應該令你嘅字符配額白白消失。
五、同步捷徑唔可靠,批量一定要行 async
POST /v1/audio/speech(≤300 字符即回音頻 bytes)睇落好吸引,但佢而家喺專用池未暖嘅時候恆回 503 pool_cold,而且只支援 voice/design,唔支援 srt,亦唔支援 clone。批量場合更加唔啱:
| 對比 | Async(/v1/audio/jobs) | Sync(/v1/audio/speech) |
|---|---|---|
| 文字長度 | 任意(單項 1–500 字符) | ≤300 字符 |
| 批量 items | ✅ 最多 25 項 | ❌ |
srt 字幕 | ✅ | ❌ 400 srt_not_supported_sync |
clone 模式 | ✅ | ❌ 400 clone_not_supported_sync |
| 現況 | 穩定主路徑 | ⚠️ 恆回 503 pool_cold |
順帶一提:想批量同時出字幕,喺每個 item 加 "srt": true 就得,完成後 poll response 會多一個 srt 欄位。呢條 pipeline 嘅細節見《TTS 加 SRT 字幕》。
配額同成本:批量開跑之前先計清楚
單價對照(四個方案)
| 方案 | 月費 | 字符額度 | 每千字符單價(月繳) |
|---|---|---|---|
| 免費 Free | HK$0 | 5,000(每月重置) | — |
| 入門 Starter | HK$128 | 200,000 | HK$0.64 |
| 專業 Pro | HK$338 | 600,000 | HK$0.5633 |
| 工作室 Studio | HK$688 | 1,400,000 | HK$0.4914 |
- 字符包:四檔同價 HK$0.80/1K,最小 HK$50 = 62.5K
- API(連批量)係專業/工作室專屬:免費同入門 plan 冇 API;帶
items入去會回403 batch_requires_pro,而且零提交、零扣費 - 讀音風險要計入成本:批量出錯係批量錯,貼稿前值得先用《廣東話多音字攻略》嘅清單掃一次
實際規模對照(用上面單價實算)
| 批量規模 | 總字符 | ≈ 語音時長 | 專業(600K 額度) | 工作室(1.4M 額度) |
|---|---|---|---|---|
| 100 條 × 50 字符 | 5,000 | ≈ 21 分鐘 | HK$2.82 | HK$2.46 |
| 1,000 條 × 100 字符 | 100,000 | ≈ 7 小時 | HK$56.33 | HK$49.14 |
| 5,000 條 × 150 字符 | 750,000 | ≈ 52 小時 | 超額度 → 600K +150K 包 = HK$458 | HK$368.55(額度內) |
| 10,000 條 × 150 字符 | 1,500,000 | ≈ 104 小時 | 超額度 → 600K +900K 包 = HK$1,058 | 1.4M +100K 包 = HK$768 |
點讀呢個表:「專業/工作室」兩欄係等效成本(規模 × 每千字符單價),方便你比較規模。實際帳單係月費(額度內)+ 字符包(超額部分),唔會自動加價。當規模超出方案額度,買包嘅 HK$0.80/1K 明顯貴過工作室額度內嘅 HK$0.4914/1K — 所以批量越大,升級方案通常比買包抵。呢個就係表入面最後兩行想講嘅事。
一個實務建議:先用小批試跑。 攞 10–20 條真稿跑一次完整 pipeline(提交 → poll → 落檔),確認讀音、聲線、時長都啱,先至開大批。呢個習慣可以幫你慳返大量「跑完 5,000 條才發現聲線揀錯」嘅成本。想知免費額度實際做到啲咩,可以睇《免費廣東話 TTS 點玩》。
常見問題 FAQ
一次過最多可以提交幾多條?
單一個請求最多 25 項({"items":[…]});超過會回 400 too_many_items。要交更多就分輪:1,000 條 = 40 輪,用 worker pool 控制同時開幾輪。另外單一項嘅 text 上限係 500 字符,超限係直接拒絕,唔會自動截斷。
批量生成係唔係一定要升級?
係。批量生成(items)係專業/工作室方案專屬功能,免費同入門 plan 帶 items 入去會回 403 {"error":"batch_requires_pro"},而且零提交、零扣費。如果你只係偶爾出一兩句,用 playground 就得,唔使為批量升級。
60 次/分鐘嘅限流,係計「請求」定計「item」?
計請求,唔計 item。 一個帶 25 項嘅 bulk 請求只算 1 次。所以 bulk 嘅提交容量係單句提交嘅 25 倍 — 呢個係批量場合一定要用 items 嘅原因。撞到 429 嘅時候,等 Retry-After header 標明嘅秒數再試。
有幾條失敗咗,會唔會照扣我字符?
唔會。三種情況都唔會白花:入隊前失敗(400 invalid_items、403 batch_requires_pro 等)係零提交零扣費;入隊後逐項失敗其他項照跑,失敗項唔收費;任務層失敗(poll 到 failed)會自動退款。唯一要小心嘅係「重複提交同一條稿」— 嗰個係真係會重複計費嘅。
Poll 會唔會食咗我嘅限流額度?
唔會。每分鐘 60 次嘅限流只針對提交(POST /v1/audio/jobs);GET /v1/audio/jobs/{id} 同 GET /v1/voices 兩個讀取 endpoint 係唔受限流嘅。所以 poll 可以放心 poll,建議每 2–5 秒一次配少少退避。
額度用完會點?會唔會自動扣錢?
唔會自動扣錢,亦冇超額收費。 配額用完就係用完:提交會失敗(402 character_limit_reached 或 per-key 嘅 403 monthly_cap_reached)。要繼續跑就要自己買字符包(HK$0.80/1K,最小 HK$50 = 62.5K),或者升級方案(額度內單價明顯低過包裝)。留意 API 用量同 playground 係共用同一份月配額。
開錯 batch,可唔可以中途取消?
可以。DELETE /v1/audio/jobs/{id} 視乎狀態有三種結果:任務未開始 → 200 即時取消、全額退款;生成中 → 202,之後 poll 會見到 cancelling 再變 cancelled(已完成部分照計費);已屬終態 → 409 already_settled,唔可以再取消亦唔退款。所以開錯 batch 要趁早取消,越早越慳。
由一個 10 條嘅小批開始
批量生成其實冇魔法:本地驗稿 → 分輪 bulk(≤25)→ poll 到終態 → 失敗留底補跑。四個動作砌埋,就係一條可以跑一萬次、可以中途死、可以只補跑失敗部分嘅 pipeline。
最快嘅試法:攞 10 條真稿,抄上面嘅 code 跑一次。 你會即刻知自己嘅拆分邏輯、命名、落檔位有冇問題 — 呢啲嘢喺小批階段修,成本最低。
- 想即場試音(唔寫 code)→ 去 playground
- 由 API 入手 → 《粵語 TTS API 快速上手》+《點樣用 API 生成地道粵語語音》
- 要連字幕一齊出 → 《TTS 加 SRT 字幕》
- 想計清楚成本 → 《廣東話 TTS 價錢點計》
- 由頭認識廣東話 TTS → 《廣東話 TTS 完全指南》
一次提交,25 條起跳。批量係一條 pipeline 嘅事,唔係人手嘅事。