目錄(27 節)
- SRT 係咩?30 秒搞懂
- 點解要「TTS 直接出字幕」?
- 一條 pipeline 出聲又出字幕:整體流程
- 實際 API 用法:srt=true 行 async 軌
- 第一步:提交 job
- 第二步:poll 攞結果(連 srt 欄位)
- 第三步:寫入 .srt 檔
- 完整 Node.js 例子(提交 → poll → 落檔)
- ⚠️ 大陷阱:sync 捷徑唔支援 SRT
- 權限同計費
- SRT 輸出格式實例:逐段拆解
- 實際應用場景
- 1. 影片字幕(YouTube/短片/課程)
- 2. 有聲書/Podcast 章節
- 3. 無障礙(Accessibility)
- 4. 批量生成字幕
- 5. 語氣同字幕配合
- 常見錯誤排查
- 常見問題 FAQ
- TTS 出 SRT 字幕,係咪可以用 sync 捷徑?
- 點解 poll response 冇 srt 欄位?
- srt_requested 同 srt 有咩分別?
- SRT 同 WebVTT 有咩分別?我應該用邊個?
- 一條 job 最長幾多字?長片旁白點算?
- SRT 會唔會另外收費?
- 批量出幾十條片嘅字幕,要注意咩?
- 結語:兩份產物,一次提交
TTS 加 SRT 字幕:一條 pipeline 出聲又出字幕
廣東話 TTS 可以一次過出聲同字幕:喺 POST /v1/audio/jobs 加 "srt": true,job 完成後 poll response 就會多一個 srt 欄位,入面係成段 SRT 文字。附完整 poll code 同格式拆解。
一句答你:港嘢 Canto-Gen 支援一次過出聲同字幕 — 你只要喺
POST /v1/audio/jobs加"srt": true,等 job 完成之後,poll response 就會多一個srt欄位,入面就係成段 SRT 文字。
做影片、做有聲書、做無障礙,最煩嘅通常唔係「整把聲」,而係跟住要自己打字幕、自己對時間軸。一段 10 分鐘旁白,聲音好快生成好,字幕卻可能要人手打足 2 個鐘。呢篇就係講點樣將「出聲」同「出字幕」壓縮成一條 pipeline:提交一次、poll 一次、攞到兩份嘢。
如果你連基本 API 流程都未行過,建議先睇《粵語 TTS API 快速上手》,再返嚟睇呢篇。
Key Takeaways
- SRT 係async 專屬:
"srt": true只可以行POST /v1/audio/jobs,sync 捷徑POST /v1/audio/speech會回400 srt_not_supported_sync- Job 完成後,poll response 嘅
srt欄位就係完整 SRT 文字(用\n分行),直接寫入.srt檔即可用srt_requested恆在場,但唔等於字幕已就緒 — 判斷準則係status: "completed"+srt欄位出現- 需要嘅 scope 係
tts:generate(preset/design 生成通用),唔使額外開權限- 計費有兩個獨立計法:帳戶 credit 會含 SRT 附加費,而 key 嘅月度字符上限係按提交文字長度平均計
- 攞到
audio_url要即刻下載 — pre-signed URL 由素材庫出嘅係 1 小時,由生成 worker 直出嘅係 7 日
SRT 係咩?30 秒搞懂
SRT(SubRip Text)係最通用嘅字幕格式:一條 cue 由序號、時間軸、字幕文字三部分組成,中間用空行分隔。 副檔名 .srt,幾乎所有播放器、剪片軟件、影片平台都食。
一個最基本的 SRT 長成咁:
1
00:00:00,000 --> 00:00:02,400
你好,歡迎使用港嘢!
三個部分:
| 部分 | 例子 | 說明 |
|---|---|---|
| 序號 | 1 | 由 1 開始遞增 |
| 時間軸 | 00:00:00,000 --> 00:00:02,400 | 時:分:秒,毫秒,箭嘴兩邊係開始/結束 |
| 字幕文字 | 你好,歡迎使用港嘢! | 呢段時間要顯示嘅字 |
留意毫秒用逗號 , 分隔,唔係句號。呢個係 SRT 同 WebVTT 其中一個最常見嘅混淆位(WebVTT 用小數點,仲要有 WEBVTT 開頭行)。WebVTT 係 W3C 制定嘅 web 字幕標準,完整格式見 W3C WebVTT 規範;如果你要將字幕直接放上網頁播,HTML 嘅 <track> 元素 就係掛載 .vtt 檔嘅原生方法。
點解要「TTS 直接出字幕」?
因為時間軸係 TTS 自己最清楚嘅嘢。把聲係佢合成嘅,每句由第幾毫秒講到第幾毫秒,佢本來就有。你搵第三方 ASR 回聽一次再對時間軸,係多一重工序、多一重誤差。
對比一下兩條路:
| 做法 | 工序 | 時間軸準確度來源 |
|---|---|---|
| 先出音頻,再另外做 ASR 對齊 | 生成 → 下載 → 送 ASR → 對齊 → 校對 | ASR 回聽推算 |
| 出音頻同時出 SRT | 提交 → poll → 攞兩份嘢 | 生成時本身嘅句子時間 |
如果你關心嘅係「字幕同把聲夾唔夾」,第二條路明顯少一層。
一條 pipeline 出聲又出字幕:整體流程
流程同一般 async 生成一樣,只係多一個欄位、多一份收穫:
① POST /v1/audio/jobs (body 加 "srt": true)
↓
② 202 { job_id, status, poll_url, srt_requested }
↓
③ GET /v1/audio/jobs/{id} 每 2–5 秒 poll 一次
↓
④ status: "completed"
→ audio_url (音頻,pre-signed)
→ srt (字幕文字,可直接寫檔)
重點:SRT 唔會另開一個 endpoint,佢係同一條 job 帶出嚟嘅第二份產物。 所以你要做嘅改動,基本上就係 body 入面加四個字。
實際 API 用法:srt=true 行 async 軌
第一步:提交 job
curl -s -X POST https://canto-gen.com/v1/audio/jobs \
-H "Authorization: Bearer $CANTOGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "cantogen-tts",
"voice": "sum",
"text": "你好,歡迎使用港嘢!今日天氣幾好,一齊去行山啦。",
"response_format": "mp3",
"srt": true
}'
成功會回 202 Accepted:
{
"job_id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "queued",
"poll_url": "/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
"srt_requested": false
}
⚠️ 唔好用 202 嗰刻嘅
srt_requested做判斷。 呢個欄位喺 job 剛入列時未反映請求;要知字幕有冇就緒,睇status同srt。
voice 用邊個 slug,去 GET /v1/voices 攞(聲線目錄連試聽)。本文例子用 sum(心妍,female,adult)— 但目錄係動態嘅,記住由自己嘅 response 揀。其他生成參數(speed、pitch、volume、emotion、style、language、cfg_value)同平時一樣,同 srt 可以並用;詳情見《點樣用 API 生成地道粵語語音》。
第二步:poll 攞結果(連 srt 欄位)
curl -s https://canto-gen.com/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $CANTOGEN_API_KEY"
完成後嘅 response:
{
"job_id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "completed",
"poll_url": "/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
"srt_requested": true,
"srt": "1\n00:00:00,000 --> 00:00:02,400\n你好,歡迎使用港嘢!",
"audio_url": "https://canto-gen.com/storage/v1/object/sign/audio_history/..."
}
狀態生命週期係 queued → processing → completed | failed | cancelled。建議每 2–5 秒 poll 一次。
兩個欄位嘅分工:
srt_requested— 恆在場,話你「呢條 job 有冇要求過字幕」srt— 只喺「有要求」+「字幕已就緒」時出現;佢缺席唔會影響其他欄位
所以最穩陣嘅寫法係:唔好靠 srt_requested 判斷有冇字幕,直接睇 srt 存唔存在。
第三步:寫入 .srt 檔
srt 係一個 JSON 字串,入面嘅換行係 \n。將佢原樣寫入檔案,就係一個合法嘅 .srt:
import { writeFile } from "node:fs/promises";
await writeFile("narration.srt", job.srt, "utf8");
唔需要自己補 header、唔需要改逗號做句號。呢個係一個好常見嘅誤會 — 有人以為要轉格式,結果親手整壞個檔。
完整 Node.js 例子(提交 → poll → 落檔)
以下係一條可以直接抄去用嘅最小 pipeline(Node 18+,用內建 fetch):
import { writeFile } from "node:fs/promises";
const BASE = "https://canto-gen.com";
const KEY = process.env.CANTOGEN_API_KEY;
async function ttsWithSrt(text, voice = "sum") {
// ① 提交
const submit = await fetch(`${BASE}/v1/audio/jobs`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "cantogen-tts",
voice,
text,
response_format: "mp3",
srt: true,
}),
});
if (submit.status !== 202) {
throw new Error(`submit failed: ${submit.status} ${await submit.text()}`);
}
const { job_id } = await submit.json();
// ② poll(每 3 秒一次,最多試 60 次)
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 3000));
const res = await fetch(`${BASE}/v1/audio/jobs/${job_id}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
const job = await res.json();
if (job.status === "completed") return job;
if (job.status === "failed" || job.status === "cancelled") {
throw new Error(`job ${job.status}`);
}
}
throw new Error("poll timeout");
}
// ③ 用
const job = await ttsWithSrt(
"你好,歡迎使用港嘢!今日天氣幾好,一齊去行山啦。"
);
await writeFile("narration.mp3", Buffer.from(
await (await fetch(job.audio_url)).arrayBuffer()
));
await writeFile("narration.srt", job.srt, "utf8");
console.log("done:", job.srt.split("\n")[0], "cue(s) ready");
幾個實作注意:
audio_url要即刻用。 由素材庫出嘅 pre-signed URL 有效期 1 小時;由生成 worker 直出、未入庫嘅係 7 日。擺過夜就可能過期,到時重新 poll 一次攞新 URL 就得。- read endpoint 冇 rate limit。 每分鐘 60 次嘅限制係針對提交(POST);
GET /v1/audio/jobs/{id}唔計,所以 poll 放心 poll(但都唔好癲到 100ms 一次)。 - 失敗要分開處理。
failed/cancelled嘅 job 冇audio_url、冇srt,唔好當佢係「未準備好」而無限重試。
⚠️ 大陷阱:sync 捷徑唔支援 SRT
好多人見到有 sync 端點 POST /v1/audio/speech(≤300 字符、即回音頻 bytes)就想抄近路 — 唔得:
# 唔好咁做,會失敗
curl -s -X POST https://canto-gen.com/v1/audio/speech \
-H "Authorization: Bearer $CANTOGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "cantogen-tts", "voice": "sum", "text": "你好。", "srt": true }'
會回:
{ "error": "srt_not_supported_sync", "message": "..." }
HTTP 400。 原因好簡單:字幕需要按句切分同時間軸,係一個後處理步驟,sync 軌嘅設計係「即時回音頻」;同理 clone 模式亦唔支援 sync(400 clone_not_supported_sync)。
| 軌道 | Endpoint | 支援 srt: true? | 失敗時 |
|---|---|---|---|
| Async(字幕只可以行呢條) | POST /v1/audio/jobs | ✅ 支援 | — |
| Sync 捷徑 | POST /v1/audio/speech | ❌ 唔支援 | 400 srt_not_supported_sync |
順帶一提:sync 端點 POST /v1/audio/speech 目前會 fail closed,回 503 pool_cold。所以就算你唔要字幕,生產 pipeline 都應該以 async 為主。
權限同計費
- Scope:
tts:generate就夠(preset/design 生成+所有讀取 endpoint 都用呢個)。要 clone 先需要tts:clone。 - 計費兩個計法要分清楚:帳戶 credit 係按引擎計費,
srt: true時會含 SRT 附加費(即billedCharacters會大過 base 字數);而 key 嘅月度字符上限 ledger 係按提交嘅text長度平計(唔計附加費)。 - 實際數字喺 app 嘅帳單頁面為準 — poll response 唔會回帶計費數字。
SRT 輸出格式實例:逐段拆解
返去上面嗰個 srt 欄位,JSON 入面睇係一行,實際內容係:
1
00:00:00,000 --> 00:00:02,400
你好,歡迎使用港嘢!
再多幾句嘅話,就會係一連串 cue,中間空一行分隔:
1
00:00:00,000 --> 00:00:02,400
你好,歡迎使用港嘢!
2
00:00:02,400 --> 00:00:05,120
今日天氣幾好。
3
00:00:05,120 --> 00:00:08,000
一齊去行山啦。
| 你要留意嘅位 | 正確做法 |
|---|---|
| 換行 | JSON 字串入面係 \n(同 \r\n 唔同,但主流播放器兩者都食) |
| 毫秒分隔 | 逗號 ,(WebVTT 先用句號) |
| cue 分隔 | 空行 |
| 編碼 | UTF-8(廣東話用字必須) |
| 寫檔 | 原樣寫,唔好自己加工 |
如果要 WebVTT:SRT 轉
.vtt係好機械化嘅事(加WEBVTT開頭、時間軸逗號改句號)。但呢個轉換係你自己 pipeline 嘅事,API 出嘅係 SRT。
實際應用場景
1. 影片字幕(YouTube/短片/課程)
最直接:出聲 + 出 .srt,兩份嘢一齊交去剪片或者上載。因為字幕係跟生成時嘅句子時間,你唔使再入剪片軟件逐句對位。
一個實務建議:字幕檔本身係有價值嘅文字資產(可被搜尋、可做逐字稿、可做章節)。所以出稿前,讀音要啱先值得配字幕 — 呢點可以睇《廣東話多音字攻略》。
2. 有聲書/Podcast 章節
cue 嘅時間軸可以直接用嚟做邊聽邊高亮嘅閱讀器:音頻播到幾多毫秒,就 highlight 對應嗰句。因為時間軸係生成時就有,唔使做完 ASR 對齊先做得到。
同樣邏輯可以延伸去「章節標記」— 將長篇拆做多條 job,每條 job 一份 SRT,就自然有段落級嘅時間對照。
3. 無障礙(Accessibility)
字幕係無障礙嘅基本功,唔止服務聽障觀眾:
- 靜音環境:地鐵、辦公室睇片唔開聲
- 語言可及性:字幕 + 語音雙軌,理解門檻最低
- 內容檢索:有逐字稿先可以被全文搜尋
呢個位值得留意:無障礙用途對字幕準確度嘅要求最高(同教材一樣)。所以出街前應該抽樣人手核對,特別係專名、數字、多音字。
4. 批量生成字幕
一次過要出幾十條片嘅字幕,就係「批量 + SRT」嘅組合。批量提交嘅時候,每條 job 各自帶 srt: true,之後逐條 poll。批量提交同輪詢嘅設計(並行度、backoff、失敗重試)可以睇《TTS 批量生成 workflow》。
5. 語氣同字幕配合
同一句「你真係好嘢」,配唔同情緒,字幕可能想跟住調整標點。emotion 同 style 可以同 srt: true 一齊用,做法見《TTS 情緒控制指南》。
常見錯誤排查
| 症狀 | 可能原因 | 解決 |
|---|---|---|
400 srt_not_supported_sync | 你打咗去 POST /v1/audio/speech | 改用 POST /v1/audio/jobs |
400 invalid_srt | srt 唔係 boolean | 傳 true/false,唔好傳 "true" 字串 |
400 text_too_long | 文字超 500 字 | 拆句/拆 job;超限係拒絕唔係截斷 |
403 insufficient_scope | key 冇 tts:generate | 喺 playground「API 金鑰」入面重新開 key |
403 monthly_cap_reached | key 月度上限用盡 | 等下月 UTC 重置,或調高上限 |
429 rate_limit_exceeded | 提交太密(預設 60/min) | 讀 Retry-After,等夠秒數先重試 |
404 job_not_found | job id 錯/屬另一個 key | 核對 id;job 有過期 |
503 jobs_unavailable | 後端短暫問題 | 用 backoff 重試 |
srt 欄位一直唔出現 | job 未 completed,或生成失敗 | 睇 status;failed/cancelled 唔會有字幕 |
常見問題 FAQ
TTS 出 SRT 字幕,係咪可以用 sync 捷徑?
唔可以。srt: true 只行 async 軌。你打去 POST /v1/audio/speech(sync 捷徑,≤300 字符)會即回 400 srt_not_supported_sync,要用 POST /v1/audio/jobs。同樣道理,clone 模式亦唔支援 sync(400 clone_not_supported_sync)。
點解 poll response 冇 srt 欄位?
三個可能:① job 未 completed(仲 queued/processing);② job failed 或 cancelled;③你提交嗰陣冇帶 "srt": true。srt 係additive 欄位 — 只喺「要求過」+「字幕就緒」先出現,佢缺席唔影響其他欄位。
srt_requested 同 srt 有咩分別?
srt_requested 係 boolean,恆在場,話你「呢條 job 有冇要求字幕」;srt 係字幕文字本身,有要求+已就緒先出現。所以判斷字幕有冇,應該睇 srt 存唔存在,而唔係睇 srt_requested。
SRT 同 WebVTT 有咩分別?我應該用邊個?
兩者都係純文字字幕格式。SRT 最通用(播放器、剪片軟件幾乎全食),時間軸用逗號分隔毫秒;WebVTT 係 web 標準,開頭要有 WEBVTT,時間軸用句號。港嘢 API 出嘅係 SRT;要 WebVTT 就自己喺 pipeline 轉,轉換係機械式嘅。
一條 job 最長幾多字?長片旁白點算?
text 上限 500 字(超限回 400 text_too_long,係拒絕唔係截斷)。長片做法係按段落/章節拆 job,每條 job 各自 srt: true,最後再將多份 SRT 順序接埋。接嘅時候記得將後面 cue 嘅序號同時間軸做 offset。
SRT 會唔會另外收費?
計費有兩個計法:帳戶 credit 係按引擎計費,srt: true 時會含 SRT 附加費(billedCharacters 會大過 base 字數);而 key 嘅月度字符上限係按提交嘅 text 長度平計,唔計附加費。實際數字以 app 帳單面為準。
批量出幾十條片嘅字幕,要注意咩?
三個位:① 提交受 60/min rate limit 限制(read endpoint 唔受限);② 每條 job 各自 poll,建議用 2–5 秒 backoff,唔好死循環密 poll;③ failed/cancelled 要當終態處理並重試提交,唔好當「未準備好」。完整做法見批量生成 workflow 篇。
結語:兩份產物,一次提交
字幕呢件事,最痛唔係「打」,而係「對時間」。當時間軸係生成時本來就有嘅嘢,你就唔應該再花人手去估。
最快嘅試法:攞一句你想配字幕嘅稿,加 "srt": true 行一次。 睇下 poll 返嚟嘅 srt 有幾乾淨。
- 想即場試音(唔寫 code)→ 去 playground
- 由 API 入手 → 《粵語 TTS API 快速上手》+《點樣用 API 生成地道粵語語音》
- 要出大量 → 《TTS 批量生成 workflow》
- 想知揀邊個工具 → 《2026 廣東話 TTS 大比拼》
- 由頭認識廣東話 TTS → 《廣東話 TTS 完全指南》
有聲,有字,先至係一條完整嘅 pipeline。