# Canto-Gen (港嘢) API Documentation

廣東話文字轉語音 API：支援預設聲線、聲音設計（design）與聲音複製（clone），所有預設聲線為廣東話原生。

- Base URL: `https://canto-gen.com`
- Auth: `Authorization: Bearer $CANTOGEN_API_KEY`
- 互動式參考：/v1/docs/reference · OpenAPI：/v1/openapi.json · llms.txt：/llms.txt

兩種生成方式 — 短文字用**同步**，其餘用**非同步**（兩者接受相同的請求欄位）：

## 取得 API 金鑰

1. 登入後前往 帳戶 → API 金鑰（`/account/api-keys`）建立金鑰。
2. 完整金鑰**只會顯示一次** — 請立即複製。伺服器端僅儲存 SHA-256 hash；遺失的金鑰只能撤銷後重新建立。
3. 將金鑰匯出為環境變數，供下方示例使用：

```bash
export CANTOGEN_API_KEY="sk_live_..."
```

| 前綴 | 用途 |
|---|---|
| `sk_live_…` | 唯一的金鑰格式。免費方案金鑰受每日生成上限約束；付費方案用量計入該金鑰的每月 credits 上限。 |

| Scope | 開放權限 |
|---|---|
| `tts:generate` | 預設。開放 voice 及 design 生成，以及所有讀取 endpoint（聲線目錄、任務查詢）。 |
| `tts:clone` | clone 模式（以參考樣本複製聲音）必須使用此 scope。 |
| `tts:design` | design 模式（gender + age 合成聲線）的細粒度 scope — 已由 tts:generate 涵蓋，兩者其一即可。 |
| `voices:manage` | 個人聲線管理（MCP 的 list / create / delete my voices）必須使用此 scope。 |

金鑰同時附帶每分鐘 rate limit（預設 60）及可選的每月 credits 上限。

**免費方案**：免費方案用戶同樣可以建立 API 金鑰（一律 `sk_live_` 格式），每日上限 50 個生成請求（輪詢與下載不計入）。升級至輕量方案後，同一金鑰即自動解鎖全速 API，無需重建。

## 定價

API 存取為**付費方案（輕量／入門／專業／工作室）專屬**，全速無每日上限。API 用量與網頁 Playground 共用同一個帳戶 credits 池，按帳單週期（訂閱周年日）重置 — 非同步 API 1 字 = 1 credit，無附加費；實時語音（同步 `POST /v1/audio/speech`）按 1.3 倍計量。credits 池用完可購買 Credit Pack。`X-Credits-Charged` 回應 header 顯示每個請求的實扣 credits 數（已含倍數）。方案詳情請見 /pricing。

免費方案用戶亦可建立 API 金鑰試用，詳情見「取得 API 金鑰」。

## 授權 — 商業使用

音頻權利取決於生成當刻嘅方案：**付費期間**（訂閱、字符包或 API 用量）生成嘅音頻，權利歸屬您，可自由用於個人或商業用途，且**終身有效**（取消訂閱後仍然有效）；**免費方案**生成嘅音頻僅供個人非商業用途，升級唔會追溯授權。預設聲線與聲音模型嘅知識產權屬於港嘢 — 您獲授權嘅係生成音頻檔，而唔係聲線本身。唔准將聲線、聲線庫或引擎轉售／再包裝為獨立語音合成服務、經單一帳戶轉介第三方生成，或以輸出訓練競爭模型。全文：/terms（版本 1.2，2026-11-04）。

## 詞彙表

- **每日生成上限** — 免費方案金鑰每日 50 個生成請求（輪詢與下載不計）；付費方案無每日上限。
- **金鑰每月 credits 上限** — 每把金鑰本月（UTC）累計計費 credits 的上限，超出回 403 monthly_cap_reached；預設不設上限。
- **帳戶 credits 池** — 帳戶層級的 credits 餘額，網頁 Playground 與 API 共用，按帳單週期重置；不足時回 402 character_limit_reached。
- **Credit Pack** — 為帳戶 credits 池一次性增值，永不過期。
- **待處理任務上限** — 同一帳戶可同時 pending 的任務數，按方案分級（3／4／5／10／20／20）；超限回 429 in_flight_limit_reached。

## 快速入門

先列出聲線目錄，再以目錄中的任何 `slug` 提交語音任務：

```bash
curl https://canto-gen.com/v1/voices \
  -H "Authorization: Bearer $CANTOGEN_API_KEY"
```

```json
{
  "voices": [
    {
      "slug": "sum",
      "name": "心妍",
      "gender": "female",
      "age": "adult",
      "styles": [],
      "preview_url": "https://canto-gen.com/api/voice-clip?slug=sum"
    }
  ]
}
```

> 聲線目錄為即時資料 — 請務必從你自己的 `GET /v1/voices` 回應中挑選 `slug`。此處的 `"sum"`（心妍）僅為文件示例。

提交語音任務（`model` 可省略 — `"cantogen-tts"` 是唯一值）：

```bash
# Async by design — submit returns 202 with a job_id, then poll.
export CANTOGEN_API_KEY="sk_live_…"

curl -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"
  }'
```

回應 — **`202 Accepted`**：

```http
HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "job_id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "status": "queued",
  "poll_url": "/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "srt_requested": false
}
```

每次成功的非同步提交均回傳 `202`，附帶 `job_id` / `status` / `poll_url` / `srt_requested`。

批次：傳入 `{"items": [ …最多 25 個請求內容… ]}` — 驗證為全有或全無（任何一項無效即回傳 `400 invalid_items`）；入隊後，各項結果在 `202 {"jobs":[…]}` 中逐項回報。超過 25 項回傳 `400 too_many_items`。批量生成為入門／專業／工作室方案功能。並行上限按方案分級（免費 3／輕量 4／入門 5／專業 10／工作室 20／企業 20 個 pending 任務），超上限回 `429 in_flight_limit_reached`，待現有任務完成後即可再提交。

### 同步快捷方式 — POST /v1/audio/speech

POST /v1/audio/speech 是同步快捷方式：在同一個請求內等待生成完成，直接以 200 OK 回傳原始音訊 bytes（通常 5–15 秒內完成，附 X-Audio-Duration / X-Credits-Charged headers）。限制：文字上限 300 個計費 credits，僅支援 voice（預設）或 design 模式 — clone 回傳 400 clone_not_supported_sync，srt 回傳 400 srt_not_supported_sync（兩者請改用非同步 endpoint）。本 endpoint 為實時語音，按 1.3 倍計量；`X-Credits-Charged` 已含倍數，超時轉 202 後不會重複扣費。

```bash
curl -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": "你好，歡迎使用港嘢！",
    "response_format": "mp3"
  }' --output speech.mp3
```

### 冷池 — 忙時回 503

同步隊列繁忙（有任何生成進行中）時，此 endpoint 以 503 pool_cold 直接拒絕 — 不扣費，亦不會靜默轉入排隊；請改用 POST /v1/audio/jobs。若請求已受理但等待超過約 45 秒，任務會被 cooperative cancel 並於數秒內退款，endpoint 回 503 sync_timeout（帶 Retry-After）— 保證交付請改用非同步（2026-10-20 起）。

```http
HTTP/1.1 503 Service Unavailable

{ "error": "pool_cold", "hint": "use POST /v1/audio/jobs (async)" }
```

## 聲音模式

三種聲音模式互斥：每次請求只可傳入 voice、design 或 clone 其中一項 — 否則回傳 400 voice_mode_conflict。設計及複製的聲線僅對該次請求生效，不會儲存為可重用的聲線資產。

### voice — 圖庫聲線

以 slug 直接選用廣東話優化圖庫中的現成聲線。

- 瀏覽 GET /v1/voices — 所有預設聲線均以廣東話為母語，附試聽片段。
- 將 slug 作為 voice 參數傳入。文件示例為 sum（心妍）— 請務必從你自己的回應中挑選 slug。slug 為 ASCII 穩定識別符（行業標準做法），中文名作介面顯示之用。

```bash
# Async by design — submit returns 202 with a job_id, then poll.
export CANTOGEN_API_KEY="sk_live_…"

curl -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"
  }'
```

### design — 設計聲線

以純文字描述一個合成聲線 — 指定 gender 與 age，無需上載任何音訊檔。

- gender: male | female · age: child | teenage | adult | midage | elder — 兩項均必填。
- 適合產品人設；可自由搭配 emotion 及 srt 參數。

```bash
curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "cantogen-tts",
    "design": { "gender": "female", "age": "adult" },
    "text": "今日天氣幾好，一齊去行山啦。",
    "style": "brisk",
    "emotion": "happy",
    "srt": true
  }'
```

### clone — 複製聲線

上載參考樣本 — 平台在內部轉錄（ASR）並自動套用逐字稿，客戶端無需額外呼叫。

- reference_audio: data URL 或 raw base64 — 支援 audio/mpeg、wav、webm、mp4/m4a、ogg。單一請求上限 4.4 MB（base64 編碼後音訊約 ≤3 MB）。
- 樣本至少 8 秒（不足會被拒絕）；處理時按句界自動裁至約 15–20 秒、上限 30 秒 — 乾淨樣本效果最佳。
- 只需 reference_audio；舊版 prompt_text 欄位已退役（送出會被忽略）。
- 需要具備 tts:clone scope 的 API 金鑰。

```bash
B64=$(base64 -w0 sample.wav)

curl -X POST https://canto-gen.com/v1/audio/jobs \
  -H "Authorization: Bearer $CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"model\": \"cantogen-tts\",
    \"clone\": {
      \"reference_audio\": \"data:audio/wav;base64,$B64\"
    },
    \"text\": \"用我把聲講呢句嘢。\"
  }"
```

其他生成參數：

- `style` — 演繹風格 — 建議使用英文 slug：節奏 steady（平穩） / composed（從容） / rapid（急促） / slow（慢板） / drawl（拖長） / brisk（明快） / dynamic（起伏） / staccato（頓挫）；音色 mellow（圓潤） / cute（奶音） / sharp（尖細） / lazy（懶音） / breathy（氣聲） / hoarse（沙啞） / deep（洪厚） / crisp（清脆） / feeble（無力） / trembling（顫音）；場景／角色 slug 同樣接受（corporate／promo／…／storytelling（講古）／sports 等）。原有中文 preset 值（明快／圓潤 等）仍作別名接受 — 兩者內部會正規化到同一 canonical 值；其他自由文字原樣接受。
- `emotion` — 9 個支援值其中之一（見下方 chips）。注意：API enum 使用英文值（happy／excited 等），並以英文原樣傳入引擎；與 Playground 介面的中文情緒 preset（雀躍／冷靜 等）屬兩套不同體系 — 中文情緒 preset 僅限 Playground 使用，API emotion 參數不接受中文值。
- `instruction` — 自由文字演繹指示，上限 500 字。優先次序：style → emotion → instruction — instruction 有最終決定權。
- `pitch` — 已棄用（2026-09-20）：原音交付下不生效（接受參數但忽略其值）。歷史範圍 −12…+12 半音，預設 0。
- `volume` — 已棄用（2026-09-20）：原音交付下不生效（接受參數但忽略其值）。歷史範圍 0–10，預設 1。
- `response_format` — mp3（預設）/ wav。
- `srt` — true → 完成後的任務附帶 srt 欄位（以較高 credits 費率計費）。全方案適用。
- `emotion` 可選值：`happy` / `sad` / `angry` / `fearful` / `disgusted` / `surprised` / `neutral` / `excited` / `gentle`

## 語氣標籤（inline tone tags）

`text` 支援 inline 語氣標籤 — 在台詞中插入 `[標籤]`，該句即帶對應的動作／語氣演繹。閉集共 15 條：

| 標籤 | token | 位置 |
|---|---|---|
| `笑聲` | `laughing` | head 或 tail |
| `嘆氣` | `sigh` | head 或 tail |
| `深呼吸` | `breath` | head（句首） |
| `遲疑` | `Uhm` | head（句首） |
| `噓` | `Shh` | head（句首） |
| `唓` | `Dissatisfaction-hnn` | head（句首） |
| `啊?` | `Question-ah` | tail（句尾） |
| `誒?` | `Question-ei` | tail（句尾） |
| `嗯?` | `Question-en` | tail（句尾） |
| `哦?` | `Question-oh` | tail（句尾） |
| `嘩!` | `Surprise-wa` | head（句首） |
| `唷!` | `Surprise-yo` | tail（句尾） |
| `呀!` | `Surprise-ah` | tail（句尾） |
| `哦!` | `Surprise-oh` | tail（句尾） |
| `嗯哼` | `Confirmation-en` | tail（句尾） |

位置規則：`head` 標籤置於句首（開口前動作／感嘆詞），`tail` 標籤置於句尾（粵語語氣詞）；問句標籤（`啊?` `嗯?` `哦?` `誒?`）只配以 `？`/`?` 收尾的台詞。方括號內不屬閉集的 token 原樣保留（永不刪字）。標籤字元按正常文字計費。

示例：`"text": "[笑聲]今日天氣好好[唷!]"` — 句首一個笑聲、句尾一個語氣詞。

## 輪詢與 SRT

狀態生命週期：`queued` → `processing` → `completed` | `failed` | `cancelled`；取消生成中的任務時會先出現暫態 `cancelling`（見下文「取消任務」）。以 2–5 秒間隔（含退避）查詢 `GET /v1/audio/jobs/{id}`：

```bash
curl https://canto-gen.com/v1/audio/jobs/b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b \
  -H "Authorization: Bearer $CANTOGEN_API_KEY"
```

當 `status` 為 `"completed"`，回應會附上預簽名的 `audio_url`：

```json
{
  "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/..."
}
```

以普通 GET 下載（無需 Authorization header — 連結已預簽名）：

```bash
curl -sL -o speech.mp3 "$AUDIO_URL"
```

> audio_url 為預簽名連結，設有有效期：由素材音訊庫直接供應時有效期為 1 小時，由生成節點即時供應時為 7 天。請盡早下載；連結過期後重新查詢該任務即可取得新連結。任務在 queued/processing 期間以及 failed/cancelled 任務，均不附帶 audio_url 欄位。

格式錯誤的任務 id 回傳 `400 {"error":"invalid_job_id"}`；不存在的 id 回傳 `404 {"error":"job_not_found"}`。

**字幕（SRT）：** 提交時傳入 `"srt": true`，完成後任務的查詢回應會附上 `srt` 欄位，內容為字幕。計費細節：任務的帳戶 credit 包含 SRT 附加費（srt: true 時 billedCharacters 會高於基本 credits 數），而每金鑰每月上限的帳本則按提交文字長度直接計算。SRT 字幕輸出全方案適用。

**Rate limit 與上限：** 每月 credits 上限 — 金鑰本月至今（UTC）的計費 credits 數必須低於其上限（null = 無限制）。超限會回傳 403 monthly_cap_reached 並附 used 數字，直至該月重設。X-RateLimit-* headers 附於 POST /v1/audio/jobs 的 202 及 429 回應、以及 POST /v1/audio/speech 的 429 回應（200 回應附帶 X-Credits-Charged 等 usage headers）；讀取 endpoint（GET /v1/audio/jobs/{id}、GET /v1/voices）不受 rate limit 限制。兩個計量器均以「故障時放行」為設計原則 — 計量系統故障絕不會令請求被拒。

### 取消任務 — DELETE /v1/audio/jobs/{id}

不再需要的任務可以取消 — 對同一個 `job_id` 發出 `DELETE /v1/audio/jobs/{id}`：

```bash
curl -X DELETE https://canto-gen.com/v1/audio/jobs/<job_id> \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

四種結果：

- 未開始（`queued`）→ **`200`** `{ "ok": true, "status": "cancelled", "refunded": 1234 }` — 任務即時取消並全額退款（`refunded` 為實際退回的 credits 數）。
- 生成中（`processing`）→ **`202`** `{ "ok": true, "status": "cancelling" }` — 取消已受理，任務收尾中；其後輪詢會顯示 `status: "cancelling"`，最終收斂為 `"cancelled"`。
- 已終態（`completed` / `failed` / `cancelled`）→ **`409`** `{ "error": "already_settled" }` — 任務無法再取消，亦不會退款。
- id 不存在（或屬於另一金鑰）→ **`404`** `{ "error": "job_not_found" }`。

## 文件轉寫

文件轉寫 — 上載音檔，同一請求內同步回傳文字。Canonical path：`POST /v1/asr/transcribe`；以 multipart/form-data 傳送。欄位：`file`（必填）、`model`（必填）、`language`（選填，ISO-639-1）、`response_format`（`json` 預設／`text`）。`srt`／`verbose_json` 需要段級時間標記，暫不支援（回 400 unsupported_response_format）。可選 `vocabulary`（上限 50 詞，關鍵詞偏置，`asr_vocabulary` 權限限定；無權限靜默忽略）— 冇帶呢個欄位時（API key 請求）自動套用你帳戶儲存嘅詞庫（`PUT /v1/asr/vocabulary`），帶咗（包括空陣列）就以請求為準。現階段轉寫以廣東話為本。

**計費：** 按音頻時長計費（預付 HK$ 餘額；1 HK$ = 1,000 credits）— 文件轉寫 800 credits／小時（HK$0.80／小時）、實時聽寫 1,400 credits／小時（HK$1.40／小時，1.75 倍），按秒計費、不進位。請求受理前先按偵測到的時長預扣，完成後按實際時長結算；失敗、超時或服務不可用一律全額退回（只扣成功）。餘額不足回 402 insufficient_balance — 請前往 /account/billing 儲值後重試。費率與餘額（credits／HKD 兩版）可用 GET /v1/balance 查詢。

上限：

- `file size` — 同步 route 單檔實際上限 **4.5 MB**（平台 request body 上限；超出回平台層 413 — 建議先壓縮：`ffmpeg -i in.* -vn -ac 1 -ar 16000 -c:a libopus -b:a 16k out.ogg`，16kHz mono Opus 16k 單檔約載 33–40 分鐘）。
- `audio duration` — 同步交付上限 **8 分鐘（480 秒）**：probe 到超過 → `400 audio_too_long`（帶 `duration_seconds`＋`async_endpoint`，處理前拒、零扣費）— 長音檔改用 `POST /v1/asr/jobs`（非同步，上限 4 小時／500MB）。單請求音頻政策上限 4 小時（超出回 413）。
- `daily cap` — **冇每日音訊上限**（2026-10-30 起對所有 plan 一視同仁，包括免費）— 用量只由預付餘額 bound（credit 池 ＋ 錢包 hold）。

示例：

```bash
curl -X POST https://canto-gen.com/v1/asr/transcribe \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -F file=@sample.wav \
  -F model=cantogen-asr \
  -F response_format=json
```

回應 — **`200`**：

```json
{
  "id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
  "object": "transcription",
  "text": "今日天氣幾好，一齊去行山啦。",
  "duration_seconds": 1830,
  "charged_hkd": 0.4067
}
```

> 同步交付 — 短至中等長度音檔（≤8 分鐘）直接回 200 文字。超過同步交付上限（480 秒，C3 2026-10-20）→ `400 audio_too_long`（帶 `duration_seconds`＋`async_endpoint`，處理前拒、零扣費）— 同一 route 只有一種成功形狀，行為由檔案時長話事、唔受伺服器負載影響。長音檔正路：`POST /v1/asr/jobs`（非同步，上限 4 小時／500MB）— 提交即 `202 queued`，`GET /v1/asr/transcriptions/{id}` poll 文字；同一錢包計費語義。

### 查詢餘額 — GET /v1/balance

查詢預付餘額（HKD；1 HK$ = 1,000 credits — 回應同時帶 `balance_credits` credits 版餘額）、文件轉寫／實時聽寫費率（credits／HKD 兩版：800／1,400 credits 每音頻小時）與自動儲值設定。測試金鑰期間回傳測試環境餘額（與正式環境互相隔離）。餘額不足請前往 /account/billing 儲值（可開啟自動儲值 — 餘額跌低門檻時自動增值）。

```bash
curl https://canto-gen.com/v1/balance \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

```json
{
  "object": "balance",
  "currency": "HKD",
  "balance_hkd": 50,
  "balance_credits": 50000,
  "asr": {
    "batch_rate_hkd_per_hour": 0.8,
    "realtime_rate_hkd_per_hour": 1.4,
    "batch_rate_credits_per_hour": 800,
    "realtime_rate_credits_per_hour": 1400,
    "billed_unit": "audio_second"
  },
  "auto_topup": { "enabled": true, "threshold_hkd": 20, "amount_hkd": 100, "threshold_credits": 20000, "amount_credits": 100000 }
}
```

### 用量明細 — GET /v1/usage

查詢預付錢包嘅 ledger 明細（append-only 唯一真相）— 每一筆入帳／扣帳都帶 kind（top_up／hold／hold_release／refund／adjustment…）、有號金額（4dp）＋ credits 版金額（`amount_credits`／`balance_after_credits`；1 HK$ = 1,000 credits）、扣後餘額，用量類仲有單位／數量／當時費率，自我解釋。游標分頁：`limit`（1–100，預設 20）＋ `starting_after`（上一頁最後一筆嘅 id）。`hold`＋`hold_release` 成對出現（預扣 → 按實際時長結算找差額／失敗全退）。金額 4dp 原樣輸出 — 0.0025 級 sub-cent 扣減都審到。

**用量摘要（`summary`）— 對帳用**：唔使逐筆加總就知本期用咗幾多。`period_start` 係當前帳單週期嘅開始日（付費方案 = 訂閱周年日，**唔係**曆月初 1 號；免費方案 = 當月 1 號），`used_chars` 係本期已用 credits（方案配額 + credit 消耗同一本帳），`limit_chars` 係方案本期配額，`remaining_chars` 係本期剩餘（配額剩額 + credit 池），`credit_chars` 係 credit 池（credits），`balance_hkd`／`balance_hkd_exact` 係預付錢包餘額（2dp + 4dp 審數），`balance_credits` 係同一個餘額嘅 credits 版（1 HK$ = 1,000 credits）。對賬語意：摘要同 ledger 係兩本帳 —— ledger 記「錢包」每筆出入，摘要記「credits」本期用量；兩者都係同一個帳戶、同一個帳單週期口徑，所以用嚟核對「本期用咗幾多 credits／仲有幾多餘額」。

**轉錄用量（批次／實時）喺 `asr_seconds_by_operation` 睇**：ASR 唔按 credits 計費，係按音頻秒數（`audio_second`）喺錢包預付 —— 每一筆轉錄收費都係一條 ledger row（`unit` = `audio_second`、`quantity` = 秒數、`unit_rate_hkd_per_hour` = 當時費率）。摘要嘅 `asr_seconds_by_operation` 就係**本期已計費秒數，分開兩個 operation**（`asr_batch` 批次轉寫、`asr_realtime` 實時聽寫）—— 兩者費率唔同（800 vs 1,400 credits／音頻小時 — 即 HK$0.80 vs HK$1.40／音頻小時），所以**唔會**混埋一個合計數；摘要同 ledger 係同一批收口後嘅 row，唔好再加埋。

```bash
curl "https://canto-gen.com/v1/usage?limit=20" \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

```json
{
  "object": "list",
  "summary": {
    "period_start": "2026-10-10",
    "used_chars": 2339,
    "limit_chars": 370000,
    "remaining_chars": 367661,
    "credit_chars": 5765,
    "currency": "HKD",
    "balance_hkd": 49.98,
    "balance_hkd_exact": 49.9775,
    "balance_credits": 49977.5,
    "asr_seconds_by_operation": {
      "asr_batch": 120.5,
      "asr_realtime": 300
    }
  },
  "data": [
    {
      "id": "3fcf4ca5-a996-813f-9714-c45e59549a24",
      "object": "wallet_ledger_entry",
      "created_at": "2026-10-17T10:34:00.000Z",
      "kind": "hold_release",
      "amount_hkd": 0.0001,
      "balance_after_hkd": 49.9775,
      "amount_credits": 0.1,
      "balance_after_credits": 49977.5,
      "unit": "audio_second",
      "quantity": 11.46,
      "rate_hkd_per_hour": 0.8,
      "note": null
    },
    {
      "id": "3f9f4ca5-a996-8149-a631-d7c4351b7bb3",
      "object": "wallet_ledger_entry",
      "created_at": "2026-10-17T10:33:58.000Z",
      "kind": "hold",
      "amount_hkd": -0.0026,
      "balance_after_hkd": 49.9774,
      "amount_credits": -2.6,
      "balance_after_credits": 49977.4,
      "unit": "audio_second",
      "quantity": 11.46,
      "rate_hkd_per_hour": 0.8,
      "note": null
    }
  ],
  "has_more": true,
  "next_cursor": "3f9f4ca5-a996-8149-a631-d7c4351b7bb3"
}
```

### 非同步提交 — POST /v1/asr/jobs

非同步提交（upfront async）— 提交即回 202 queued，唔使等同步窗。三種進場：multipart `file`（≤4.5MB 平台上限）、JSON `audio_url`（server 側下載，繞過 4.5MB；https only、上限 500MB／4 小時、拒內網位址）或 JSON `storage_path`（只限 browser session — playground 大檔 lane；API key caller 送 → 400 `storage_path_session_only`，跨用戶 path → 403）。之後 poll `GET /v1/asr/transcriptions/{id}`：queued → processing → completed（`text`／`duration_seconds` 在場）／failed（`error` 在場）。第一次見 completed 按實際時長結算錢包 hold（冪等，重複 poll 唔會重收）。取回面同 transcribe-202 共用。可選 `vocabulary`（上限 50 詞）— 關鍵詞偏置，`asr_vocabulary` 權限限定；無權限靜默忽略，有權限時取回面交出 `text` 前自動剝朗誦殘渣。冇帶呢個欄位時（API key 請求）自動套用你帳戶儲存嘅詞庫（`PUT /v1/asr/vocabulary`），帶咗（包括空陣列）就以請求為準。

```bash
# multipart 上載
curl -X POST https://canto-gen.com/v1/asr/jobs \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -F file=@audio.ogg \
  -F model=cantogen-asr

# 或 JSON audio_url（server 側下載，繞過 4.5MB 上載上限）
curl -X POST https://canto-gen.com/v1/asr/jobs \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"audio_url": "https://example.com/long-audio.ogg"}'
```

```json
{
  "id": "1cabbd7b-741d-46a1-b83e-ed259247a57c",
  "object": "transcription",
  "status": "queued",
  "retrieval": "/v1/asr/transcriptions/1cabbd7b-741d-46a1-b83e-ed259247a57c"
}
```

## ASR 關鍵詞詞庫

關鍵詞詞庫（keyword biasing）— 一本 per-account 嘅詞表，幫 ASR 認出專有名詞、機構名同英文縮寫。三個 method：`GET` 讀取、`PUT` 整本覆寫、`DELETE` 清空（等效 `PUT {"terms": []}`）。同一帳戶嘅所有金鑰共用同一本詞庫（per-account，唔係 per-key）；詞表會喺 server 端清理（trim／去重／丟超過 30 字嘅詞／最多 50 個）之後先儲存，回應會附上實際儲存咗嘅內容。

清理規則：

- `terms` — 最多 50 個詞（超出嘅由頭截斷）
- `term length` — 每個詞最多 30 字（超出嘅詞唔會儲存）
- `cleanup` — trim 空白、Unicode NFC 收斂、去掉重複詞（保留第一次出現嘅）

### 讀取 — GET /v1/asr/vocabulary

```bash
curl https://canto-gen.com/v1/asr/vocabulary \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

```json
{
  "scope": "api",
  "terms": ["證監會", "庫務局", "Canto-Gen"],
  "updated_at": "2026-10-28T09:12:33.000Z"
}
```

### 覆寫 — PUT /v1/asr/vocabulary

```bash
curl -X PUT https://canto-gen.com/v1/asr/vocabulary \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"terms": ["證監會", "庫務局", "Canto-Gen"]}'
```

傳 `{"terms": []}` 就係清空（等效 `DELETE`）。

### 清空 — DELETE /v1/asr/vocabulary

```bash
curl -X DELETE https://canto-gen.com/v1/asr/vocabulary \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

### 每次請求帶 `vocabulary`

每次請求都可以帶 `vocabulary`（字串陣列）做即時偏置 — `POST /v1/asr/transcribe`、`POST /v1/asr/jobs` 同 `POST /v1/asr/realtime/token`（實時轉寫）都收。**API key 同 browser session 客戶一樣可以帶**（`sk_` 憑證或 browser session）。空陣列係**明確覆蓋**（今次唔做任何偏置）；冇帶呢個欄位就係「用預設」語意（下一段）。

### 儲存詞庫幾時會自動套用

**儲存嘅詞庫只係 API key 請求嘅預設**：`sk_` 客戶嘅轉寫請求如果冇帶 `vocabulary` 欄位，就會自動套用本帳戶儲存嘅詞庫；帶咗參數（包括空陣列）就以請求為準，**永遠唔會** fallback 去儲存嗰份。Browser session（Playground）客戶唔受呢條預設影響 — session 請求冇帶參數就係冇偏置。

### 權限

關鍵詞詞庫功能需要帳戶開通（`asr_vocabulary` 權限）。未開通嘅話，三個 method 一律回 `403 {"error":"asr_vocabulary_not_granted"}`，並附一句點做嘅提示（聯絡 support 開通功能後再試）。開通咗之後，轉寫請求帶住 `vocabulary` 但仍然冇呢個權限 → 該次請求會忽略詞庫（唔會影響轉寫本身）。

## 實時 WebSocket（Realtime）

除 REST 外，港嘢另提供 WebSocket 實時端點：在單一長連線上傳送文字並接收串流 PCM 音訊，適合自帶 LLM 的語音應用。協議以 **OpenAI Realtime** 為基礎；港嘢新增的事件與欄位一律標明為 extension。

`wss://api.canto-gen.com/v1/audio/realtime`

### 認證

認證方式與 REST 相同：`sk_` API 金鑰，所需 scope 為 `tts:generate`。優先使用 `Authorization: Bearer sk_…` header；瀏覽器 WebSocket API 無法設定自訂 header，可改用 query 參數 `?api_key=sk_…`（別名 `?token=`）。query 參數會寫入 URL、伺服器日誌與瀏覽器歷史，存在洩漏風險，**僅限瀏覽器環境**使用；伺服器端請求一律使用 header。

### 事件 — client → server

| 事件 | 來源 | 用途 |
|---|---|---|
| `session.update` | OpenAI 原生 | 建立／更新 session 設定：voice、language、output_audio_format、max_session_chars、tts.segmentation（`max_session_chars` 亦可於連線時用 query `?max_session_chars=` 指定，一律被 clamp 落 plan 上限）。 |
| `conversation.item.create` | OpenAI 原生 | 提交一段明確文字（input_text）；觸發合成。 |
| `input_text.append` | 港嘢 extension | 串流傳送原始 token delta。 |
| `input_text.commit` | 港嘢 extension | 強制段落邊界（立即 flush）。 |
| `response.create` | OpenAI 原生 | 觸發合成。 |
| `response.cancel` | OpenAI 原生 | barge-in：停止當前句子並退款。 |
| `session.close` | 港嘢 extension | 主動關閉連線並結算。 |

### 事件 — server → client

| 事件 | 來源 | 用途 |
|---|---|---|
| `session.created` | OpenAI 原生 | 連線建立；附伺服器簽發（server-minted）的 session id，同**生效值** `max_session_chars`（已被 clamp 落 plan 上限）。 |
| `session.updated` | OpenAI 原生 | 設定更新確認。 |
| `response.created` | OpenAI 原生 | 單句開始合成。 |
| `response.output_audio.delta` | OpenAI 原生 | base64 PCM16 @ 48 kHz 音訊 chunk。 |
| `response.output_audio.done` | OpenAI 原生 | 單句音訊完成。 |
| `response.done` | OpenAI 原生 | 單句結束；usage 附 input_chars／billed_chars／remaining（extension 欄位）。 |
| `error` | OpenAI 原生 | {type, code, message, http_status}。 |

`tts.segmentation`（extension）控制切句方式：`{"mode":"server"}`（預設）由伺服器按標點自動切句並逐句合成 — client 只需持續 append token；`{"mode":"client"}` 則由 client 透過 `conversation.item.create` / `input_text.commit` 自行決定段落邊界。可選 `min_chars` / `max_chars` / `punctuation`。

### 限制與逾時

- `billing` — 逐句（per utterance）reserve→complete／退款；實扣 = ceil(輸入字符 × 1.5)（2026-11-08 起 WS realtime 倍數，SSOT = constants.ts WS_REALTIME_CHARACTER_MULTIPLIER）。失敗／barge-in 自動退款。`response.done` usage：`input_chars` = raw 輸入、`billed_chars` = 實扣（含倍數）、`remaining` = 餘額。
- `idle_timeout` — 閒置 120 秒（無任何 app-level 活動）→ 伺服器關閉連線並結算。
- `max_session_duration` — free plan 單一 session 最長 5 分鐘；付費 plan 無時長上限（達全域上限時關閉）。
- `max_sessions_per_user` — 同一用戶的同時 session 上限按 plan 階梯：free 1／lite 2／starter 3／pro 4／studio 5／enterprise 5；未知 plan 一律視為 free。
- `max_sessions_global` — 全平台同時最多 8 個 session。
- `max_session_chars` — 每個 session **輸入**字符上限（**免費 2,000 字／付費 20,000 字**；按輸入字符量度 — 實扣另乘 WS ×1.5 倍數，見 billing）；server 端 clamp — client 經 `session.update`（或連線時 query `?max_session_chars=`）只可以要求更低，唔可以高過 plan 上限；超出回 session_char_limit_exceeded。生效值會喺 `session.created` 直接回傳（唔使行多次 `session.update` 先知）。
- `max_in_flight` — 每 session 同時最多 3 句未結算。
- `raw_ingress` — 每 session 原始輸入上限 20,000 字、每分鐘 5,000 字（標點與空白亦計）；超出回 raw_limit_exceeded，即時拒絕。
- `lease` — 每句合成有 60 秒結算租約；逾時未完成會自動退款。
- `rate_limit / monthly_cap` — 每用戶每分鐘 rate limit（同一帳戶嘅所有金鑰加埋計 — 開多把金鑰唔會突破總額；單把金鑰另外有較細上限）；每月 credits 上限按金鑰計。用量帳本逐句記錄（per utterance）。
- `daily_generation_limit` — 免費方案每日 50 個生成句（與 REST 共用同一計數，輪詢與下載不計）；超出回 sandbox_daily_limit（429），付費方案無每日上限。
- `frame_size` — 單一 WS 訊息上限 64 KiB。
- `heartbeat` — 伺服器定期 ping；心跳不計入 app 活動。

### Node 範例 client

```js
// npm i ws   — one long-lived connection: stream tokens in, stream PCM out.
import WebSocket from "ws";

const ws = new WebSocket("wss://api.canto-gen.com/v1/audio/realtime", {
  headers: { Authorization: `Bearer ${process.env.CANTOGEN_API_KEY}` },
});

ws.on("open", () => {
  ws.send(JSON.stringify({
    type: "session.update",
    session: { voice: "sum", language: "yue", output_audio_format: "pcm16", max_session_chars: 500 },
  }));
  // One explicit utterance — or keep sending input_text.append deltas.
  ws.send(JSON.stringify({
    type: "conversation.item.create",
    item: { type: "message", content: [{ type: "input_text", text: "你好，歡迎使用港嘢！" }] },
  }));
});

const chunks = [];
ws.on("message", (raw) => {
  const ev = JSON.parse(raw.toString());
  if (ev.type === "session.created") console.log("session", ev.session.id);
  if (ev.type === "response.output_audio.delta") chunks.push(Buffer.from(ev.delta, "base64"));
  if (ev.type === "response.done") {
    // PCM16 @ 48 kHz — feed an AudioWorklet or write the chunks to disk.
    console.log("billed", ev.response.usage.billed_chars, "remaining", ev.response.usage.remaining);
    ws.close();
  }
  if (ev.type === "error") console.error(ev.error.code, ev.error.message);
});
```

### 錯誤碼

| code | HTTP | 原因 |
|---|---|---|
| `invalid_api_key` | 401 | 金鑰缺失、無效、過期或 scope 不符。 |
| `rate_limit_exceeded` | 429 | 觸及每用戶每分鐘上限（同一帳戶嘅所有金鑰加埋計）；附 retry_after。 |
| `sandbox_daily_limit` | 429 | 免費方案每日 50 個生成句已用完（與 REST 共用計數）；明日再試或升級方案。 |
| `monthly_cap_reached` | 429 | 已達金鑰本月 credits 上限。 |
| `character_limit_reached` | 402 | 帳戶 credits 池不足，該句不會合成。 |
| `concurrency_limit_reached` | 429 | 同時 session 或同時未結算句數已達上限。 |
| `session_char_limit_exceeded` | 403 | 已達本次 session 字數上限。 |
| `raw_limit_exceeded` | 413 | 原始輸入量（含標點／空白）超出上限。 |
| `text_too_long` | 400 | 單段文字超出上限。 |
| `invalid_session` | 400 | session 已關閉或狀態無效。 |
| `engine_unavailable` | 503 | 實時引擎暫時不可用；請重試。 |

## 錯誤碼

錯誤回應使用穩定的機器可讀 `error` 代碼，另附可選的人類可讀 `message`，例如 `{"error": "text_too_long", "message": "…"}`。

| HTTP | error | 原因／處理方法 |
|---|---|---|
| 400 | `invalid_model` | model 必須是 "cantogen-tts"（或省略）。 省略 model，或設為 "cantogen-tts"。 |
| 400 | `voice_mode_conflict` | 同時傳入了多於一個 voice / design / clone — 三種模式互斥。 每次請求只傳入一種聲音模式。 |
| 400 | `text_required` | text 缺失或空白。 傳入非空 text（1–500 credits）。 |
| 400 | `text_too_long` | text 超過 500 credits。 請拆分文字 — 超限請求會被拒絕，不會截斷處理。 |
| 400 | `unsupported_format` | response_format 必須是 mp3 或 wav。 使用 mp3 或 wav。 |
| 400 | `invalid_pitch` | pitch 必須介乎 −12 至 +12 半音。 將 pitch 調整至有效範圍。 |
| 400 | `invalid_volume` | volume 必須介乎 0 至 10。 將 volume 調整至有效範圍。 |
| 400 | `invalid_emotion` | emotion 不在 9 個支援值之內。 使用 9 個支援的情緒值之一。 |
| 400 | `invalid_srt` | srt 必須是 boolean。 使用 true / false。 |
| 400 | `reference_audio_required` | clone.reference_audio 缺失。 附上 base64 參考樣本。 |
| 400 | `invalid_reference_audio` | clone.reference_audio 不是有效的 base64 音訊。 傳入 data URL（data:audio/…;base64,…) 或 raw base64。 |
| 400 | `invalid_reference_audio_size` | clone.reference_audio 音頻超過約 3 MB（JSON API 單一請求上限）。 縮短樣本（至少 8 秒即可）再試。 |
| 400 | `spoken_text_required` | text 無可朗讀內容 — 計費 credits 為 0（只剩標點／空白／emoji／tone tags）。 傳入含可讀文字的 text。 |
| 400 | `design_gender_and_age_required` | design.gender 與 design.age 兩項均必填。 同時提供兩個欄位。 |
| 400 | `invalid_job_id` | 查詢或取消所用的 id 不是 UUID。 檢查 job_id。 |
| 400 | `invalid_request` | body 不是有效 JSON，或 Content-Type 非 JSON（/v1 僅接受 application/json）；文件轉寫則為 multipart 缺 `file`／`model`、音檔為空或音檔格式不支援。 以 Content-Type: application/json 傳送合法 JSON body。 |
| 413 | `payload_too_large` | 請求超過大小上限 — JSON API 4.4 MB（clone 參考樣本）；文件轉寫同步 4.5 MB（平台 request-body 上限）；非同步 jobs audio_url／storage_path 500 MB。 同步：壓縮成 16kHz mono Opus 16k（≈33–40 分鐘/4.5MB）或改用 POST /v1/asr/jobs（audio_url／storage_path 至 500MB）。 |
| 401 | `invalid_or_expired_token` | API 金鑰缺失、無效、已過期或已被撤銷。 建立新金鑰並檢查 Bearer header。 |
| 402 | `character_limit_reached` | 帳戶 credits 池不足以完成本次生成（回應內含 remaining）。 購買 Credit Pack 或等待月度重置。 |
| 403 | `insufficient_scope` | 金鑰缺少所需 scope（複製聲音需要 tts:clone）；回應內含 "scope"。 建立具備所需 scope 的金鑰。 |
| 403 | `monthly_cap_reached` | 金鑰的每月 credits 上限已達；回應內含 "used"。 等候 UTC 月份重設，或調高上限。 |
| 404 | `job_not_found` | 沒有此 id 的任務（或任務屬於另一金鑰）。 檢查 job_id 及提交時所用的金鑰。 |
| 409 | `already_settled` | 任務已屬終態（completed/failed/cancelled），無法再取消，亦不會退款。 改以 GET /v1/audio/jobs/{id} 查詢最終狀態。 |
| 409 | `duplicate_request` | 同一個 Idempotency-Key（alias x-idempotency-key）已提交過（重試安全：不會重複扣費）。 沿用原本的 job_id 輪詢，或以新 key 提交新任務。 |
| 404 | `not_found` | 沒有此 endpoint（路徑有誤）。 對照 /v1/openapi.json 的路徑。 |
| 403 | `batch_requires_pro` | 批量生成（items）為入門／專業／工作室方案功能。單句任務的並行上限按方案分級（free 3／輕量 4／入門 5／專業 10／工作室 20／企業 20 個 pending），超上限回 429 in_flight_limit_reached。 待現有任務完成後再提交，或升級至入門／專業／工作室方案以解鎖批量生成與更高並行上限。 |
| 429 | `capacity_exceeded` | 公開 API 生成池到頂（全域並發上限）。 等候 Retry-After 秒再試，或改用非同步 `POST /v1/audio/jobs`。 |
| 429 | `rate_limit_exceeded` | 已觸及每用戶每分鐘上限（同一帳戶嘅所有金鑰加埋計）。 等候 Retry-After 秒（回應內容亦以 retry_after 標示），然後重試。 |
| 503 | `service_unavailable` | 語音引擎暫時離線（維護或後端未就緒）— 請求在受理前已被拒，未扣費。回應帶 Retry-After。 等候 Retry-After 秒（預設 60）後重試；失敗不扣費。 |
| 503 | `rate_limit_unavailable` | 速率限制服務暫時不可用 — 為避免放行未受管流量，請求在受理前已被拒，未扣費。回應帶 Retry-After。 等候 Retry-After 秒（預設 5）後重試。 |
| 429 | `sandbox_daily_limit` | 免費方案每日 50 個生成請求上限已用完；輪詢與下載不受此限。 明日再試，或升級至輕量方案解鎖全速 API。 |
| 429 | `api_busy` | API 生成隊列已達全域容量上限（受理前即拒，不扣費）。 稍候數秒重試。 |
| 502 | `audio_delivery_failed` | 生成已完成，但音訊提取失敗。 改以 GET /v1/audio/jobs/{id} 輪詢並下載 audio_url。 |
| 503 | `sync_generation_failed` | 同步等待期間任務失敗或被取消（已自動退款）。 改用 POST /v1/audio/jobs（非同步）重試。 |
| 503 | `reference_upload_failed` | clone 參考音頻暫時儲存失敗。 直接重試。 |
| 503 | `voice_gallery_unavailable / jobs_unavailable` | 後端暫時性問題。 以指數退避重試。 |
| 400 | `unsupported_response_format` | 文件轉寫的 response_format 暫只支援 json／text — srt／verbose_json 需要段級時間標記，暫未提供。 使用 json 或 text。 |
| 402 | `insufficient_balance` | 預付餘額不足以預扣本次文件轉寫費用（未開始處理、零扣費）。 前往 /account/billing 儲值後重試（失敗不扣費）。 |
| 503 | `engine_unavailable` | 文件轉寫服務暫時不可用（音檔時長偵測失敗、計費系統、轉寫管線故障，或同步窗內引擎排隊／退化 — 屬瞬態）。已預扣金額全額退回。 等候 Retry-After 秒重試；不會產生費用。 |
| 400 | `audio_too_long` | 音頻超過同步交付上限 8 分鐘（480 秒）— 處理前拒、零扣費（回應帶 duration_seconds＋async_endpoint）。 改用 POST /v1/asr/jobs（非同步，上限 4 小時／500MB）。 |
| 400 | `storage_path_session_only` | API key caller 送 `storage_path` — 該進場只限 browser session（playground 大檔 lane）。 改用 audio_url 或 multipart file 進場。 |
| 403 | `storage_path_forbidden` | `storage_path` 唔屬於認證用戶（防跨用戶閘）。 只可以用 /api/asr/upload-url 回傳嘅自己 path。 |
| 404 | `storage_path_not_found` | `storage_path` 指住嘅物件唔存在（上載未完成或已過 TTL — free 24 小時／paid 7 日）。 重新經 /api/asr/upload-url 上載後再提交。 |
| 503 | `sync_timeout` | 語音同步快線等待超過約 45 秒 — 任務已取消並於數秒內退款（回應帶 Retry-After；X-Request-Id 留作對帳）。 稍後重試，或改用 POST /v1/audio/jobs（非同步）。 |

## 實時轉寫 WebSocket（ASR Realtime）

`wss://api.canto-gen.com/v1/asr/realtime`

單一長連線串入 PCM 音訊（16 kHz／mono／s16LE，base64），串出轉寫 — partial `transcription.delta` ＋ final `transcription.done`。**HK$1.40／音頻小時**、按秒計費、只收成功音訊。

### 認證

伺服器端：`Authorization: Bearer sk_…`。瀏覽器：**永遠唔准將永久 API key 放入 URL** — 先喺伺服器端叫 `POST /v1/asr/realtime/token` 簽一枚短命 ephemeral token（`cst_…`，60–600 秒，預設 300），再用 `?token=<cst_…>` 開 WS（canonical；`?api_key=` 仍收做過渡 alias — 2026-10-21 gateway 正名，e2e 兩式實測 PASS）。

```
curl -X POST https://canto-gen.com/v1/asr/realtime/token \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{"expires_in_seconds":300}'
```

### 協議次序

```
gateway ▶ client   {"type":"gateway.welcome","rate_hkd_per_hour":1.4,...}
engine  ▶ client   {"type":"session.created",...}
client ▶ gateway   {"type":"session.update","model":"asr"}
client ▶ gateway   {"type":"input_audio_buffer.commit","final":false}　（開 stream）
client ▶ gateway   {"type":"input_audio_buffer.append","audio":"<b64>"} × N
gateway ▶ client   {"type":"transcription.delta","delta":"…"}　（partial）
client ▶ gateway   {"type":"input_audio_buffer.commit","final":true}　（收尾）
gateway ▶ client   {"type":"transcription.done","text":"…"}　（全文）
```

音訊 = PCM16LE mono 16 kHz base64（32,000 bytes = 1 秒）；單 frame ≤640KB、平均 ≤640KB/s。送錯 sample rate 會得到垃圾轉寫並按收到的 bytes 計費 — 轉檔責任在 client（業界標準）：瀏覽器用 `new AudioContext({ sampleRate: 16000 })` 取 mic；Node／檔案用 `ffmpeg -i in.any -f s16le -ar 16000 -ac 1 out.pcm`。關閉前會收 `gateway.close`（code＋reason）。

### 計費與上限

連線前驗餘額（唔夠一個 tick → 4002）；session 中每 60 秒 in-arrears 預扣（唔夠 → 4009）；收線按實際音頻秒結算（billed == audio）。上限：同時 session global 10／每 key free 2／paid 4；**冇單 session 長度上限**（保護靠 60 秒計費 tick ＋ 120 秒 idle timeout ＋ 冇錢即 close）。實時用量同批次轉寫用量分開計。連線 query 可宣告格式：`?sample_rate=8000–48000&channels=1|2`（計費秒數按宣告 bps 折算；唔宣告 = 16kHz mono 契約）。錯送容器檔（WAV／MP3／Ogg／FLAC）→ 4010 invalid_audio_format（零計費）— 要 raw PCM16LE。

### WS close codes

| Code | Reason | 意思 |
|---|---|---|
| 1000 | `completed` | 正常完（done 已收）。 |
| 1001 | `idle_timeout` | 逾時無任何 client frame（120s）。 |
| 1008 | `protocol_violation` | 壞 protocol／binary frame／base64 壞／超速。 |
| 1009 | `message_too_big` | 單 frame > 640KB。 |
| 1011 | `engine_lost` | 引擎中途收線。 |
| 1013 | `engine_unavailable` | 引擎連唔上。 |
| 4001 | `invalid_or_expired_token` | key 無效／過期／撤銷。 |
| 4002 | `insufficient_balance` | 連線前餘額唔夠。 |
| 4008 | `capacity_exceeded` | 同時 session 爆 cap（global 10／per-key free 2／paid 4）。 |
| 4009 | `insufficient_balance` | session 中餘額唔夠下一筆 hold。 |
| 4010 | `invalid_audio_format` | 送咗容器檔（WAV／Ogg／FLAC／MP3）— 要 raw PCM16LE mono 16kHz（ffmpeg -ac 1 -ar 16000 -f s16le）。 |

### Node 範例 client

```
// npm i ws
const WebSocket = require("ws");
const ws = new WebSocket("wss://api.canto-gen.com/v1/asr/realtime", {
  headers: { Authorization: `Bearer ${process.env.CANTOGEN_API_KEY}` },
});
ws.on("open", () => {
  ws.send(JSON.stringify({ type: "session.update", model: "asr" }));
  ws.send(JSON.stringify({ type: "input_audio_buffer.commit", final: false }));
  ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: pcmChunk.toString("base64") }));
});
ws.on("message", (raw) => {
  const ev = JSON.parse(raw.toString());
  if (ev.type === "transcription.delta") process.stdout.write(ev.delta);
  if (ev.type === "transcription.done") { console.log(ev.text); ws.close(); }
});
```

## 基礎建設

API 語音負載運行於專用 GPU 池 — 與互動 Playground 完全隔離，正式流量不會與任何瀏覽器工作階段爭奪算力。

## 帳戶與用量面

純 API 客戶唔使登入 web console 都用得嘅三個帳戶面入口 — 查返自己嘅生成紀錄、開關匿名使用統計，同埋（不可逆嘅）刪除自己個帳戶。三個都係 owner-scoped：只會顯示／影響你同一個帳戶嘅嘢。

### 生成紀錄清單 — GET /v1/audio/jobs

列出**你自己**嘅生成紀錄（最新先）— 純 API 客戶唔使登入 web UI 就查得返自己提交過乜、做到咗幾耐。每條紀錄都帶一個有界嘅文字預覽（最多 120 字）；要全文就用 `GET /v1/audio/jobs/{id}`，或者自己保留原文。金鑰要帶 `tts:generate` 或 `tts:design` scope（唔夠 → `403 insufficient_scope`）。

```bash
curl "https://canto-gen.com/v1/audio/jobs?limit=20" \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

```json
{
  "jobs": [
    {
      "id": "b7e6f2a1-4c3d-4e5f-8a9b-0c1d2e3f4a5b",
      "status": "done",
      "created_at": "2026-10-29T09:12:00.000Z",
      "voice_slug": "sum",
      "duration_ms": 4200,
      "text_preview": "你好，歡迎使用港嘢！"
    }
  ],
  "next_before": null
}
```

參數同回應欄位：

- `limit` — 每頁行數（1–100，預設 20）
- `before` — 游標 — 上一頁最後一條紀錄嘅 `id`（要 uuid）
- `status` — 可選過濾：queued／processing／cancelling／done／error／cancelled
- `next_before` — 下一頁嘅游標；`null` = 冇下一頁
- `text_preview` — 生成文字嘅有界預覽（最多 120 字）
- `duration_ms` — 音訊時長（毫秒；未生成完成 = `null`）

> ⚠️ `status` 用**任務實際狀態值**（`done`／`error`／…）；`GET /v1/audio/jobs/{id}` 嘅輪詢面用公開狀態名（`completed`／`failed`／…）—— 兩套詞彙唔同，睇清自己讀緊邊個 endpoint。

### 匿名使用統計 — GET / PUT /v1/account/telemetry

讀／寫本帳戶嘅**匿名使用統計**（質素訊號）設定。`telemetry_opt_out: true` = 關閉**自動行為遙測**（例如你嘅使用行為記錄）；你主動提交嘅評分／回報**唔受影響** — 嗰啲係你明示嘅意圖，唔當成遙測。每個帳戶一個設定（同一帳戶嘅所有金鑰共用 `user_profiles.telemetry_opt_out`）。

```bash
curl https://canto-gen.com/v1/account/telemetry \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

```json
{
  "telemetry_opt_out": false
}
```

```bash
curl -X PUT https://canto-gen.com/v1/account/telemetry \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"telemetry_opt_out": true}'
```

```json
{
  "ok": true,
  "telemetry_opt_out": true
}
```

### 刪除帳戶 — POST /v1/account/deletion

刪除**你自己**嘅帳戶（自我服務刪除，純 API 客戶唔使登入 web UI 就用得）。**兩步確認**：body `{ confirmEmail, confirmWord }` — `confirmEmail` 大小寫不敏感地等於你嘅帳戶電郵，`confirmWord` 大小寫敏感地等於 `DELETE`。成功 → 帳戶**即時停用**（登入被封、現有 session 全部失效），個人資料由每日清理程序清走、**30 日內永久清除**（`purge_by` 係完成清除嘅上限時刻）。

```bash
curl -X POST https://canto-gen.com/v1/account/deletion \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirmEmail": "you@example.com", "confirmWord": "DELETE"}'
```

```json
{
  "ok": true,
  "purge_by": "2026-11-02T03:00:00.000Z"
}
```

清除範圍：生成紀錄、聲線、參考音頻等個人資料一律清走；**加密備份隨供應商嘅輪替週期移除**（唔會即刻清）；**帳務／用量紀錄依法保留**（唔會因為刪戶而清走）。**團隊帳戶要先解散** — 你係一個仲有其他成員嘅工作區擁有者會回 `409 team_owner`（要先解散／轉讓）；仲有未完成嘅生成工作會回 `409 active_jobs`。重複請求係冪等嘅（同一個帳戶只有一條刪除申請）。

### 錯誤碼

| HTTP | error | 原因 |
|---|---|
| 400 | `invalid_limit` | `limit` 唔係 1–100 嘅整數。 |
| 400 | `invalid_cursor` | `before` 唔係一個 uuid，或者唔屬於你嘅任何一條紀錄。 |
| 400 | `invalid_status` | `status` 唔喺值集內（queued／processing／cancelling／done／error／cancelled）。 |
| 400 | `invalid_telemetry_opt_out` | `telemetry_opt_out` 唔係 boolean。 |
| 400 | `confirm_missing` | 刪戶 body 冇帶 `confirmEmail` 或 `confirmWord`。 |
| 400 | `confirm_email_mismatch` | `confirmEmail` 同你嘅帳戶電郵唔一致（大小寫唔敏感比較）。 |
| 400 | `confirm_word_mismatch` | `confirmWord` 唔係 `DELETE`（大小寫敏感）。 |
| 401 | `invalid_or_expired_token` | API 金鑰缺失、無效、已過期或已被撤銷。 |
| 403 | `insufficient_scope` | 金鑰缺少 `tts:generate` 或 `tts:design` scope（生成紀錄清單面）。 |
| 409 | `active_jobs` | 仲有未完成嘅生成工作（queued／processing／cancelling）— 等佢哋完成先再刪戶。 |
| 409 | `team_owner` | 你係一個仲有其他成員嘅工作區擁有者 — 要先解散／轉讓工作區。 |
| 503 | `jobs_unavailable` | 生成紀錄清單暫時讀唔到（`GET /v1/audio/jobs`）。 |
| 503 | `telemetry_read_failed` | 匿名使用統計設定暫時讀唔到。 |
| 503 | `telemetry_update_failed` | 匿名使用統計設定暫時儲存唔到。 |
| 503 | `deletion_request_failed` | 刪除申請暫時寫唔入 — 稍後重試（重試係冪等嘅）。 |

## 數據保留與刪除

以下規則適用於你經 API 提交嘅所有內容。系統保留期（唔係你主動刪除嘅嘢）一律 90 日；你主動刪除嘅資料即時失效、並喺 30 日內永久清除。

| 資料類型 | 保留／刪除 |
|---|---|
| 生成內容（生成工作紀錄連文字、生成音檔、ASR 轉錄紀錄） | 一律保留 90 日，之後由每日自動清理刪走 |
| 你主動刪除嘅資料（刪除聲線、刪除個別紀錄、刪除轉錄或音檔） | 即時失效、不設復原，30 日內永久清除 |
| 未註冊／冇任何工作引用嘅參考音頻（上載咗但冇綁定） | 24 小時寬限期，之後最長 30 日內清走 |
| 已註冊聲線嘅參考音頻 | 聲線仲喺度就保留；刪除聲線即時失效，檔案 30 日內清走 |
| 會議記錄（會議錄音、會議逐字稿） | 唔喺自動清理範圍 — 跟帳戶生命週期，只喺你明確刪除或刪除帳戶時清 |
| 帳戶刪除 | 即時停用（唔可以再登入或呼叫 API），30 日內完成永久清除 |
| 備份 | 加密備份隨供應商嘅輪替週期移除 |

刪除一律**先刪資料庫紀錄、後刪音檔**，資料即時失效、唔設復原；刪走之後唔會再喺任何生成或轉寫流程入面用到。

資料中心位於南韓（首爾），備份由同區託管管理。次處理者：Supabase（託管資料庫同儲存）、Stripe（付款處理，唔經手你嘅內容）。語音生成由我們自家嘅雲端基礎建設執行，唔涉額外第三方。

你上載嘅文字同音頻（包括參考音頻）以及由其產生嘅聲線，在任何情況下都**唔會**用於訓練、微調或改進我哋嘅模型；我哋生成嘅音頻輸出同樣唔會用於訓練。模型改進只依據你主動提交嘅評分同錯誤回報。

### 刪除一條轉錄紀錄

刪除一條 ASR 轉錄紀錄（連你上載嘅音檔）：`DELETE /v1/asr/transcriptions/{id}`。個 `id` 就係 `POST /v1/asr/transcribe`／`POST /v1/asr/jobs` 回應帶返嘅 `id`。只可以刪自己金鑰嘅紀錄；搵唔到或者唔屬於你，一律回 404（唔會洩露 id 存在與否）。

```bash
curl -X DELETE https://canto-gen.com/v1/asr/transcriptions/<id> \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

### 刪除你的聲線

刪除你自己嘅聲線：`DELETE /v1/voices/{id}`（`{id}` 係聲線嘅 `slug` 或者 id）。聲線即刻喺所有清單消失、唔可以再用於生成；參考音檔同準備好嘅參考音頻副本即時清走，唔設復原。官方預設聲線唔可以刪（同一個 404，唔會洩露存在與否）；金鑰要帶 `voices:manage` scope。如果有生成工作仲引用緊呢把聲線，會回 `409 voice_busy`（唔會刪，等個工作完成之後再刪）。

```bash
curl -X DELETE https://canto-gen.com/v1/voices/<voice-slug> \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

### 刪除一條生成紀錄

刪除一條生成紀錄：`DELETE /v1/audio/jobs/{id}`（`{id}` 係 `POST /v1/audio/jobs` 回應帶返嘅 `job_id`）。如果個 job 仲未完成（queued／processing），呢條 route 照取消佢（退款語義唔變）；如果已經完成／失敗／取消咗，就會刪走工作紀錄（連你生成嘅文字）、音檔同字幕副檔。刪除一律先刪資料庫紀錄、後刪音檔，資料即時失效、唔設復原。

```bash
curl -X DELETE https://canto-gen.com/v1/audio/jobs/<job_id> \
  -H "Authorization: Bearer \$CANTOGEN_API_KEY"
```

刪除成個帳戶（自我服務）→ 見「帳戶與用量面」一節嘅 `POST /v1/account/deletion`：兩步確認；帳戶即時停用、個人資料 30 日內永久清除、加密備份隨輪替移除、帳務／用量紀錄依法保留、團隊帳戶要先解散。

## 參考與下載

### Endpoints

- `POST /v1/audio/speech` — 短文字同步生成 — 直接回傳 200 音訊 bytes（voice/design 模式，≤300 credits）。
- `POST /v1/audio/jobs` — 提交語音任務（單一或批次 ≤25；批次需入門方案或以上）。回傳 202 及 job_id 與 poll_url。
- `GET /v1/audio/jobs/{id}` — 查詢任務狀態；完成後回應會附上預簽名的 audio_url。
- `GET /v1/audio/jobs` — 列出你自己嘅生成紀錄（最新先）。Owner-scoped：`limit`（1–100，預設 20）＋ `before` 游標分頁，`status` 可過濾；回 `id`／`status`／`created_at`／`voice_slug`／`duration_ms`／`text_preview`（≤120 字）。
- `GET /v1/voices` — 列出廣東話優化聲線目錄，附試聽片段。此 endpoint 可匿名存取。
- `DELETE /v1/voices/{id}` — 刪除你自己嘅聲線（slug 或 id）— 即時失效、唔可以再用於生成，檔案即時清走。官方預設聲線唔可以刪。
- `DELETE /v1/audio/jobs/{id}` — 仲未完成嘅任務照取消（退款）；已終態嘅任務就係刪紀錄（工作紀錄 ＋ 音檔 ＋ 字幕副檔）。
- `POST /v1/asr/transcribe` — 文件轉寫 — 上載音檔同步回傳文字；按音頻時長計費 — 文件轉寫 800 credits／小時（HK$0.80／小時；1 HK$ = 1,000 credits），只扣成功。
- `POST /v1/asr/jobs` — 非同步提交轉寫任務 — 即回 202 queued；multipart 上載、audio_url（server 下載）或 storage_path（browser session 大檔 lane，至 500MB）。長音檔首選。
- `GET /v1/asr/transcriptions/{id}` — 取回轉寫結果／狀態（queued → processing → completed／failed）；completed 附文字稿。
- `GET /v1/balance` — 查詢預付餘額（HKD）、文件轉寫／實時聽寫費率與自動儲值設定。
- `GET /v1/asr/vocabulary` — 讀取本帳戶嘅 ASR 關鍵詞詞庫（一本，per-account）。`terms` 係已清理嘅詞列表，`updated_at` 係上次更新時間（未建立過 → null）。
- `PUT /v1/asr/vocabulary` — 整本覆寫本帳戶嘅 ASR 關鍵詞詞庫（唔係增量）。`terms` 會先清理（trim／去重／丟超過 30 字嘅詞／最多 50 個）再儲存；回應係實際儲存咗嘅內容。
- `DELETE /v1/asr/vocabulary` — 清空本帳戶嘅 ASR 關鍵詞詞庫（等效 `PUT {"terms": []}`）。之後 API key 客戶嘅轉寫請求如果冇帶 `vocabulary` 參數，就唔會再套任何詞。
- `GET /v1/account/telemetry` — 讀本帳戶嘅匿名使用統計（質素訊號）設定 → `{ telemetry_opt_out }`。每個帳戶一個設定（`user_profiles.telemetry_opt_out`）。
- `PUT /v1/account/telemetry` — 開／關本帳戶嘅匿名使用統計（body `{ telemetry_opt_out: boolean }`）。`true` = 關閉自動行為遙測；你主動提交嘅評分／回報唔受影響。
- `POST /v1/account/deletion` — 刪除你自己嘅帳戶（自我服務）。兩步確認 body `{ confirmEmail, confirmWord }`；成功回 `{ ok, purge_by }` — 帳戶即時停用，個人資料 30 日內永久清除。

- 互動式參考（Scalar）：https://canto-gen.com/v1/docs/reference
- OpenAPI 3.1 規格：https://canto-gen.com/v1/openapi.json — 可直接生成所用語言的 typed client
- 本頁純文字 Markdown 版：https://canto-gen.com/v1/docs.md
- Agent 索引：https://canto-gen.com/llms.txt
- AI agent 適用 MCP：local stdio server（npm 套件 `cantogen-mcp`，執行 `npx -y cantogen-mcp`；設定指示見 /llms.txt）

---

港嘢 Canto-Gen — Built in Hong Kong.
