SSE API 總覽
注意:本文件中的網址(
vas-poc.vurbo.ai)為預計部署網址,正式上線後將另行通知。
目錄
- 目錄
- 連線資訊
- Broadcast SSE API
- 浮動字幕 SSE — 詳見獨立規格
- GET /api/v1/sse/history/transcribe/{taskId}(取得歷史對話紀錄)
- GET /api/v1/sse/retranslate/{taskId}(重新翻譯全文)
- GET /api/v1/sse/recordings/{taskId}/entries/{sid}/retranslate(單句重翻,v1.4.0 新增)
- GET /api/v1/sse/retranslate/summary/{taskId}(重新翻譯摘要)
- 重新生成摘要(GET 預覽 / POST 存檔)
- POST /api/v1/sse/summary(Ad-hoc 摘要,v1.9.1 新增)
- POST /api/v1/sse/summary/translate(摘要翻譯,v1.17.0 新增)
- GET /api/v1/sse/audio/{taskId}(音訊串流播放) — 詳見獨立規格
- GET /api/v1/sse/tts/{taskId}(TTS 語音串流)
- GET /api/v1/sse/imports/{importId}/progress(匯入進度串流)
連線資訊
| 項目 | 值 |
|---|---|
| 基礎路徑 | https://vas-poc.vurbo.ai/api/v1/sse |
| 協定 | HTTP + Server-Sent Events (SSE) |
| 資料格式 | text/event-stream |
| 認證方式 | Header X-API-Key: {KEY} |
認證方式
需認證的 SSE API 接受兩種傳送 API Key 的方式(兩者皆支援):
# 方式 A:HTTP Header(推薦,安全性較佳)
X-API-Key: vas_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 方式 B:Query string(瀏覽器原生 EventSource fallback)
?api_key=vas_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
注意:瀏覽器原生 EventSource API 不支援自訂 Header,可改用
?api_key=query string,或改用 fetch API 搭配 ReadableStream / 支援 Header 的 SSE 客戶端套件。Query string 模式 API Key 會出現在 URL,請避免將完整 URL 寫入 server log 或截圖外洩。
浮動字幕 SSE
錄音進行中的「浮動字幕」唯讀逐字稿串流(來源語言原文+目標語言翻譯),以獨立連線訂閱、支援跨裝置/視窗,基礎路徑為 https://vas-poc.vurbo.ai(即時服務)。先以 POST /api/v1/auth/tasks/{taskId}/subtitle-feed-token 換取 feed_token,再連線 GET /tasks/{task_id}/subtitle?feed_token=...。
亦可由擁有者開啟分享,讓現場其他觀眾憑分享密鑰連線(唯讀、不另計費;人數上限以伺服器設定為準、預設 10,不含擁有者);連線中以 viewers 事件回報目前觀看人數。
詳見 浮動字幕 SSE 規格。
Broadcast SSE API
Broadcast SSE API 提供即時字幕串流功能,讓觀眾可以透過分享連結觀看即時轉錄和翻譯內容。
注意:Broadcast SSE 的基礎路徑為
https://vas-poc.vurbo.ai/broadcast,與其他 SSE API 不同。
GET /broadcast/{token}/text(觀眾即時字幕串流)
功能說明
觀眾透過分享 Token 連線,接收即時轉錄和翻譯的 SSE 串流。
使用場景
- 觀眾觀看即時字幕
- 多語言翻譯字幕顯示
- TTS 語音播放
認證方式
Token 認證(無需 API Key):透過 URL 路徑中的 {token} 進行驗證。
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
token | string | 是 | 廣播分享 Token(4 字元短碼 a-z0-9,路徑參數) |
lang | string | 否 | 篩選特定翻譯語言(如 en-US) |
tts | boolean | 否 | 是否啟用 TTS(true / false,預設 false) |
viewer_access_token | string | 條件 | 觀眾存取 Token(密碼保護廣播時必填) |
密碼保護說明:當廣播設定為密碼保護時,觀眾必須先透過密碼驗證 API 取得
viewer_access_token,再將此 Token 帶入 SSE 連線的 Query Parameter 中。
請求範例
// 接收所有語言
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text'
);
// 只接收英文翻譯
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US'
);
// 接收英文翻譯並啟用 TTS
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US&tts=true'
);
事件類型
| 事件 | 說明 | 備註 |
|---|---|---|
connected | 連線確認 | - |
queued | 已加入等待佇列 | 排隊機制 |
admitted | 從佇列進入直播 | 排隊機制 |
origin | 原文(STT) | - |
translation | 翻譯結果 | - |
tts_ready | TTS 音訊就緒 | - |
paused | 廣播暫停 | 主講者手動暫停(主辦斷線另送 host_disconnected) |
resumed | 廣播恢復 | 主講者恢復 |
ended | 廣播結束 | - |
kicked | 被踢除 | 觀眾管理 |
error | 錯誤 | - |
speaker_renamed | 說話者重命名 | - |
speaker_reassigned | 單句說話者修改 | - |
speakers_merged | 語者合併 | - |
recording_started | 新錄音開始 | 觀眾應清除畫面上的舊字幕 |
max_viewers_changed | 觀眾上限變更 | - |
host_disconnected | 主辦暫時離線、重連中 | 廣播凍結但未結束 |
host_reconnected | 主辦已重新連線 | data.resumed 指出是否同時恢復播放 |
standby | 預備階段通知 | - |
phase_changed | 階段變更通知 | - |
announcement | 主講者公告 | - |
事件格式
connected:
{
"available_langs": ["en-US", "ja-JP"],
"tts_languages": ["en-US"],
"phase": "standby",
"recognition_mode": "single"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
available_langs | array | 可用的翻譯語言列表 |
tts_languages | array | 有啟用 TTS 的語言列表(空陣列表示無 TTS) |
phase | string | 廣播階段:standby(預備)或 live(正式) |
recognition_mode | string | 辨識模式:single(單人)或 multi_speaker(多人語者分離) |
connected事件不含轉錄語言清單。頻道的公開資訊(名稱、transcription_languages、translation_languages、TTS 語音)請改用GET /api/v1/viewer/broadcasts/{token}取得,見 觀眾端 API。
queued:
{
"position": 3,
"estimated_wait": "約 2 分鐘"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
position | number | 佇列中的位置(1 = 下一個) |
estimated_wait | string | 預估等待時間 |
admitted:
{
"message": "已進入直播"
}
多個廣播事件的
message/estimated_wait是伺服器固定回傳的顯示字串(如已進入直播、廣播已暫停、廣播已恢復、廣播已結束、廣播已開始)。請直接呈現或改用自家文案,不要解析、也不要拿來做條件判斷。
origin:
{
"sid": 1,
"text": "大家好",
"speaker_id": "Guest-1",
"speaker_label": "Guest-1",
"start_time": "00:05",
"is_final": true
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 句子 ID |
text | string | 原文內容 |
speaker_id | string | 可選。原始說話者 ID(不可變);僅多人語者分離模式帶此欄位,單人模式不帶 |
speaker_label | string | 顯示標籤(套用 speaker_aliases 後;無 alias 時等於 speaker_id) |
start_time | string | 開始時間(mm:ss),從開播(live)起算,由 00:00 開始。預備階段的內容不會送給觀眾 |
is_final | boolean | 是否為最終結果 |
translation:
{
"sid": 1,
"language": "en-US",
"text": "Hello everyone",
"speaker_id": "Guest-1",
"speaker_label": "Royx",
"is_final": true
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 對應的句子 ID |
language | string | 翻譯語言 |
text | string | 翻譯內容 |
speaker_id | string | 原始說話者 ID(多人對話模式;不可變) |
speaker_label | string | 顯示標籤(套用 speaker_aliases 後) |
is_final | boolean | 是否為最終結果 |
tts_ready:
{
"sid": 1,
"language": "en-US",
"transcript": "你好,大家好",
"text": "Hello everyone",
"audio": "//uQxAAAAAANIAAAAAExBTUUzLjEwMFVVVV...",
"format": "mp3",
"duration_ms": 2340,
"boundaries": [
{"offset_ms": 0, "duration_ms": 320, "text": "Hello", "text_offset": 0, "word_length": 5},
{"offset_ms": 320, "duration_ms": 280, "text": "everyone", "text_offset": 6, "word_length": 8}
]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 對應的句子 ID |
language | string | TTS 語言 |
transcript | string | 原始逐字稿(原文) |
text | string | 翻譯後的文字 |
audio | string | Base64 編碼的 MP3 音訊 |
format | string | 音訊格式,固定為 "mp3" |
duration_ms | number | 音訊時長(毫秒) |
boundaries | array | Word Boundaries(可選,見下表) |
Word Boundaries 欄位(boundaries 陣列中的每個物件):
| 欄位 | 類型 | 說明 |
|---|---|---|
offset_ms | number | 該單字在音訊中的開始時間(ms) |
duration_ms | number | 該單字的發音時長(ms) |
text | string | 單字文字 |
text_offset | number | 該單字在文字中的起始位置 |
word_length | number | 該單字的字元長度 |
注意:
- 主講者需在
start指令中透過tts_config參數指定哪些語言啟用 TTS - 只有訂閱該語言且啟用 TTS 的觀眾會收到此事件
- 只在
live階段發送,standby階段不會發送 TTS
paused:
{
"message": "廣播已暫停"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息 |
主辦端斷線與重新連上是獨立事件(
host_disconnected/host_reconnected),不是paused的原因值。
resumed:
{
"message": "廣播已恢復"
}
ended:
{
"message": "廣播已結束"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息 |
這三個事件都只帶
message,不含結束原因或時間戳。message的文字僅供顯示,請勿據以判斷狀態 —— 狀態請以事件名稱本身為準。
kicked:
{
"message": "Kicked by host"
}
message僅供顯示、請勿解析。主講者手動踢除時為固定值Kicked by host;因存取類型或密碼變更而被踢除時,為說明變更原因的中文提示。
error:
{
"error_code": "broadcast_session_ended",
"severity": "error",
"message": "Broadcast session ended",
"context": "broadcast",
"request_id": "req_abc123xyz789",
"timestamp": "2025-12-05T10:30:45.123Z"
}
句子級錯誤(如某語言的翻譯失敗)會額外帶上 sid 與 translation_language,方便前端標示某句的某個語言失敗:
{
"error_code": "llm_content_filtered",
"severity": "warning",
"message": "Content filtered",
"context": "translation",
"sid": 5,
"translation_language": "ja-JP",
"timestamp": "2026-04-26T10:30:45.123Z"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
error_code | string | 錯誤碼 |
severity | string | 嚴重度:warning / error / fatal |
message | string | 錯誤訊息(伺服器回傳的固定英文顯示文字,僅供顯示、請勿解析) |
context | string | 錯誤發生的上下文(如 broadcast、translation) |
sid | int | 可選。句子級錯誤的句子編號(如該句翻譯失敗) |
translation_language | string | 可選。翻譯失敗的目標語言(觀眾可依此判斷是否該句的某個語言失敗) |
request_id | string | 可選。請求追蹤 ID。僅連線階段錯誤(HTTP 狀態碼非 200)帶出;句子級/會話級錯誤不帶 |
timestamp | string | 錯誤發生時間(ISO 8601) |
speaker_renamed:
多人對話模式專用。當主播執行全域重命名說話者時發送。
{
"speaker_id": "Guest-1",
"new_label": "Royx",
"affected_sids": [1, 3, 5, 7]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
speaker_id | string | 解析後的原始語者 ID(即使輸入是顯示標籤,事件回傳仍是原始 ID) |
new_label | string | 新顯示標籤(如 Royx) |
affected_sids | array | 受影響的句子 ID 列表 |
speaker_reassigned:
多人對話模式專用。當主播修改單句的說話者時發送。
{
"sid": 3,
"old_speaker_id": "Guest-1",
"new_speaker_id": "Guest-2",
"new_speaker_label": "Amy"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 被修改的句子 ID |
old_speaker_id | string | 原始語者 ID(如 Guest-1) |
new_speaker_id | string | 新的原始語者 ID(如 Guest-2) |
new_speaker_label | string | 新語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於原始 ID) |
speakers_merged:
多人對話模式專用。當主播合併語者時發送。合併後,該語者的所有句子會歸屬到目標語者。
{
"source_speaker_id": "Guest-2",
"target_speaker_id": "Guest-1",
"target_speaker_label": "王經理",
"affected_sids": [3, 5, 7]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
source_speaker_id | string | 被合併的原始語者 ID(如 Guest-2) |
target_speaker_id | string | 合併目標的原始語者 ID(如 Guest-1) |
target_speaker_label | string | 目標語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於原始 ID) |
affected_sids | array | 受影響的句子 ID 列表:原屬來源語者的句子,加上顯示名稱因合併而改變的目標語者原有句子(例如來源語者的自訂名稱轉給目標語者時) |
standby:
當觀眾在預備階段連線時,會在
connected事件後立即收到此事件,表示廣播尚未正式開始。 主講者可透過 WebSocketset_standby_messageaction 動態更新預備訊息,更新後所有觀眾會收到新的standby事件。
{
"message": "演講即將開始,請稍候...",
"translations": {
"en-US": "The presentation is about to begin, please wait...",
"ja-JP": "プレゼンテーションがまもなく始まります。お待ちください..."
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 預備階段顯示訊息(原文) |
translations | object | 翻譯結果(可選),key 為語言代碼,value 為翻譯文字 |
phase_changed:
當廣播從預備階段切換到正式階段時發送。
{
"phase": "live",
"message": "廣播已開始"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
phase | string | 新階段:live(正式階段) |
message | string | 階段變更訊息 |
announcement:
主講者發送的公告訊息,所有觀眾都會收到。
{
"message": "會議將在 5 分鐘後結束",
"translations": {
"en-US": "The meeting will end in 5 minutes",
"ja-JP": "会議は5分後に終了します"
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 公告訊息內容(原文) |
translations | object | 翻譯結果(可選),key 為語言代碼,value 為翻譯文字 |
心跳機制
SSE 連線使用心跳保持連線活躍:
- 間隔:15 秒
- 格式:SSE 註解(以
:開頭) - 前端無需處理,瀏覽器會自動忽略
: heartbeat
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
broadcast_session_not_found | 404 | 找不到廣播 | 確認 Token 正確 |
broadcast_session_ended | 410 | 廣播已結束 | 提示使用者廣播結束 |
broadcast_capacity_exceeded | 503 | 觀眾人數已達上限 | 加入等待佇列 |
注意:SSE 端點若發生未預期內部異常,可能會回傳
internal_error(與 WebSocket 的單則訊息錯誤處理一致);可預期的領域錯誤則回傳對應錯誤碼(如sse_translation_failed等)。
前端範例
function connectBroadcast(token, lang = null) {
let url = `https://vas-poc.vurbo.ai/broadcast/${token}/text`;
if (lang) {
url += `?lang=${lang}`;
}
const eventSource = new EventSource(url);
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log(`已連線,階段:${data.phase},辨識模式:${data.recognition_mode}`);
console.log(`可用翻譯:${data.available_langs.join(', ')}`);
});
eventSource.addEventListener('queued', (e) => {
const data = JSON.parse(e.data);
console.log(`排隊中,位置:${data.position},預估等待:${data.estimated_wait}`);
});
eventSource.addEventListener('admitted', (e) => {
console.log('已進入直播');
});
eventSource.addEventListener('origin', (e) => {
const data = JSON.parse(e.data);
console.log(`[${data.start_time}] ${data.text}`);
});
eventSource.addEventListener('translation', (e) => {
const data = JSON.parse(e.data);
console.log(`翻譯 (${data.language}): ${data.text}`);
});
eventSource.addEventListener('tts_ready', (e) => {
const data = JSON.parse(e.data);
// 將 Base64 音訊解碼並播放
const byteCharacters = atob(data.audio);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const blob = new Blob([new Uint8Array(byteNumbers)], { type: 'audio/mpeg' });
const audio = new Audio(URL.createObjectURL(blob));
audio.play();
});
eventSource.addEventListener('paused', (e) => {
const data = JSON.parse(e.data);
console.log(`直播暫停:${data.message}`);
});
eventSource.addEventListener('resumed', (e) => {
console.log('直播已恢復');
});
eventSource.addEventListener('ended', (e) => {
const data = JSON.parse(e.data);
console.log(`直播結束:${data.message}`);
eventSource.close();
});
eventSource.addEventListener('kicked', (e) => {
console.log('您已被移除');
eventSource.close();
});
eventSource.addEventListener('speaker_renamed', (e) => {
const data = JSON.parse(e.data);
console.log(`說話者重命名:${data.speaker_id} → ${data.new_label}`);
console.log(`受影響的句子:${data.affected_sids.join(', ')}`);
// 更新所有受影響句子的說話者顯示名稱
});
eventSource.addEventListener('speaker_reassigned', (e) => {
const data = JSON.parse(e.data);
console.log(`句子 ${data.sid} 的說話者從 ${data.old_speaker_id} 改為:${data.new_speaker_label}`);
// 更新該句子的說話者顯示名稱
});
eventSource.addEventListener('standby', (e) => {
const data = JSON.parse(e.data);
// 根據觀眾選擇的語言顯示對應翻譯
const displayLang = 'en-US'; // 觀眾選擇的語言
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`預備階段:${displayMessage}`);
// 顯示等待畫面
});
eventSource.addEventListener('phase_changed', (e) => {
const data = JSON.parse(e.data);
console.log(`階段變更:${data.phase} - ${data.message}`);
// 移除等待畫面,開始顯示字幕
});
eventSource.addEventListener('announcement', (e) => {
const data = JSON.parse(e.data);
// 根據觀眾選擇的語言顯示對應翻譯
const displayLang = 'en-US'; // 觀眾選擇的語言
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`公告:${displayMessage}`);
// 顯示公告訊息
});
eventSource.addEventListener('error', (e) => {
if (e.data) {
const error = JSON.parse(e.data);
console.error(`錯誤 [${error.error_code}]: ${error.message}`);
}
eventSource.close();
});
return eventSource;
}
另有 REST API:頻道公開資訊(名稱、語言清單、TTS 語音)請參見 觀眾端 API。
GET /api/v1/sse/history/transcribe/{taskId}(取得歷史對話紀錄)
功能說明
載入指定任務的完整對話紀錄,包含所有句子和摘要。透過 SSE 串流逐條發送。
使用場景
- 查看錄音詳情頁
- 載入歷史逐字稿
認證方式
Header:X-API-Key: YOUR_API_KEY
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 錄音 ID(路徑參數) |
請求範例
// 使用 fetch API(因 EventSource 不支援 Header)
async function connectSSE(taskId, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/history/transcribe/${taskId}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
// ... 處理 SSE 事件
}
事件序列
1. connected → 連線確認
2. init_metadata → 發送任務元資料
3. init_sentence → 逐條發送句子(重複 N 次)
4. init_summary → 發送摘要
5. init_done → 初始化完成
事件格式
connected:
{"message": "歷史紀錄服務已連線 (taskId: xxx)"}
init_metadata:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "會議記錄",
"created_at": "2025-12-17T10:00:00Z",
"type": "transcribe",
"has_speaker_diarization": true,
"transcription_languages": ["zh-TW"],
"translation_languages": ["en-US"],
"summary_template": "general",
"summary_language": "zh-TW",
"speaker_aliases": {"speaker_1": "王經理"}
}
speaker_aliases 為「原始說話者 ID → 顯示名」的映射;無別名時為 {}(空物件,非陣列)。前端可用此映射做說話者重命名前的撞名預檢(v1.3.12 新增)。
init_sentence:
{
"sid": 1,
"origin": "你好,很高興認識你",
"translations": {
"en-US": "Hello, nice to meet you"
},
"start_time": "00:05",
"speaker_id": "speaker_1",
"speaker_label": "王經理"
}
句子若有翻譯失敗,會額外帶 translation_errors 欄位(僅有失敗時出現),供前端區分「該語言未排程翻譯」(translations 缺 key)vs「翻過但失敗」(translation_errors 有 key);同一個語言可能同時有舊譯文與失敗記錄(重翻失敗時先前的譯文會保留),判讀時兩欄要一起看:
{
"sid": 5,
"origin": "敏感詞句子",
"translations": {
"en-US": "Sensitive sentence"
},
"translation_errors": {
"ja-JP": "llm_content_filtered"
},
"start_time": "00:25",
"speaker_id": "speaker_1",
"speaker_label": "王經理"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | int | 句子編號 |
origin | string | 原文 |
translations | object | 翻譯結果(可選),key 為語言代碼,value 為翻譯文字 |
translation_errors | object | 可選。翻譯失敗錯誤碼,key 為語言代碼,value 為 error_code(如 llm_content_filtered) |
channel_id | number | 可選。(多聲道限定)本句來自哪一路實體聲道;僅多聲道錄音出現,單路錄音不帶此欄位 |
start_time | string | 起始時間(mm:ss 格式) |
speaker_id | string|null | 原始說話者 ID(不可變,如 speaker_1);PATCH /speakers/reassign 的 target_speaker_id 來源(v1.5.3 翻轉:原為顯示名) |
speaker_label | string|null | 顯示標籤(套 speaker_aliases 後的人類可讀名稱,如 王經理);無 alias 時等同 speaker_id(v1.5.3 新增取代原 speaker_id 顯示語意) |
init_summary:
{
"text": "這是一段會議記錄的摘要...",
"mode": "builtin",
"template": "meeting",
"plain_text": false,
"summary_language": "zh-TW"
}
mode/template/plain_text/summary_language固定會出現(summary_language為這份摘要的語言,沒有摘要時為null);custom模式另帶prompt_snapshot,自動降級時另帶fallback_level/dropped_segments。完整欄位見 history 端點。
init_done:
{"totalSentences": 10}
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
recording_not_found | 404 | 找不到錄音 | 確認 taskId 正確 |
sse_transcript_not_found | 404 | 找不到逐字稿 | 錄音可能尚未處理完成 |
前端範例
// 使用 fetch API 處理 SSE(需自行解析 event-stream)
async function loadHistory(taskId, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/history/transcribe/${taskId}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// 解析 SSE 格式:event: xxx\ndata: {...}\n\n
const events = parseSSE(text);
for (const event of events) {
if (event.type === 'init_metadata') {
console.log('任務資訊:', event.data.title);
} else if (event.type === 'init_sentence') {
console.log(`[${event.data.start_time}] ${event.data.origin}`);
if (event.data.translations) {
console.log(`翻譯: ${event.data.translations['en-US']}`);
}
} else if (event.type === 'init_done') {
console.log('載入完成');
}
}
}
}
GET /api/v1/sse/retranslate/{taskId}(重新翻譯全文)
功能說明
將指定任務的所有句子重新翻譯為目標語言。透過 SSE 串流逐條發送翻譯結果。
使用場景
- 切換顯示語言
- 更新翻譯內容
認證方式
Header:X-API-Key: YOUR_API_KEY
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 錄音 ID(路徑參數) |
targetLang | string | 是 | 目標語言代碼 |
segmented | string | 否 | 帶 1 表示客戶端支援分段續翻(v1.18.0) |
fromSid | number | 否 | 只能和 segmented=1 一起使用:從這一句(含)開始翻(v1.18.0) |
expectedRevision | number | 否 | 逐字稿版本;不符時不翻譯、不扣點,回 transcript_revision_conflict(v1.18.0) |
長逐字稿請用分段續翻:沒帶
segmented=1時一次翻完整份;逐字稿太長、一次請求翻不完時,串流開始前回 HTTP 422retranslate_segmentation_required(不扣點)。帶segmented=1時每次請求翻一段,done帶revision,被截斷時再帶truncated: true與nextSid,以fromSid={nextSid}接著送下一段,直到done不再帶truncated。每一段各自儲存、各自扣點。完整規則見 reference/sse/retranslate.md。
請求範例
// 使用 fetch API(因 EventSource 不支援 Header)
async function retranslateSSE(taskId, targetLang, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/retranslate/${taskId}?targetLang=${targetLang}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
// ... 處理 SSE 事件
}
事件格式
translation:
{"sid": 1, "text": "Hello, nice to meet you", "is_final": true}
done:
{"totalUpdated": 10, "characters_billed": 12700, "charged": "6.4", "billed": true}
計費三欄僅在本次實際產生消耗時出現。判斷是否計費請以 billed 為準(billed 為 true 才是計費)——此判準適用所有帶計費欄位的端點。
charged 為本次依費率計算的消耗點數,反映用量——吃到飽方案已涵蓋的用量,此欄位仍回報消耗量。
代呼叫並向終端用戶計價的整合方可直接採用此值,不必自行推算。
error(per-sid 句子翻譯失敗):
當某句翻譯失敗時,不發 translation 而是發 event: error 帶 sid + error_code,與 translation 事件交錯出現。失敗事件的格式與 WebSocket 即時翻譯一致,前端可共用同一套錯誤處理:
event: error
data: {"error_code": "sse_translation_failed", "severity": "error", "message": "SSE translation failed", "context": "sse", "sid": 5, "request_id": "req_abc123xyz789", "timestamp": "2026-04-27T10:30:45.123Z", "details": {"translation_language": "ja-JP", "original_error": "..."}}
| 欄位 | 類型 | 說明 |
|---|---|---|
error_code | string | 錯誤碼:sse_translation_failed 或 llm_content_filtered |
severity | string | sse_translation_failed 為 error;llm_content_filtered 為 warning |
message | string | 人類可讀訊息 |
context | string | sse_translation_failed 為 sse;llm_content_filtered 為 translation |
sid | int | 失敗的句子編號 |
request_id | string | 請求追蹤 ID |
timestamp | string | 錯誤發生時間(ISO 8601) |
details | object | 含 translation_language、original_error 等 debug 資訊 |
失敗的句子會被儲存為翻譯錯誤記錄(見 history-playback 指南),下次載入歷史時可看到失敗標記。完整規範詳見 reference/sse/retranslate.md。
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
sse_translation_failed | 500 | 翻譯失敗(per-sid) | 失敗的單句仍透過 event: error 通知,整體流程不中斷 |
llm_content_filtered | 400 | 該句內容無法翻譯(per-sid) | 重試無效;請修改該句原文後再試。該句不計入 totalUpdated,也不產生消耗 |
storage_upload_failed | 500 | 逐字稿儲存失敗 | 本次重翻全部作廢、不計費;稍後重試。收到此碼後不會再收到 done |
transcript_revision_conflict | 409 | 同一份逐字稿正有其他寫入在進行,或逐字稿在處理期間被改動,或 expectedRevision 與目前的版本不符 | 本次重翻全部作廢、不計費;版本不符時重新載入逐字稿,其他情況稍後重試即可。收到此碼後不會再收到 done |
retranslate_segmentation_required | 422 | 沒帶 segmented=1,而逐字稿太長、一次請求翻不完(details 帶 sentenceCount、maxSentences)(v1.18.0) | 串流開始前的 JSON 回應,不扣點;改帶 segmented=1 分段續翻 |
前端範例
// 使用 fetch API 處理 SSE
async function retranslate(taskId, targetLang, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/retranslate/${taskId}?targetLang=${targetLang}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const events = parseSSE(decoder.decode(value));
for (const event of events) {
if (event.type === 'translation') {
console.log(`句子 ${event.data.sid}: ${event.data.text}`);
} else if (event.type === 'error') {
console.warn(`句子 ${event.data.sid} 翻譯失敗: ${event.data.error_code}`);
} else if (event.type === 'done') {
console.log(`完成,共更新 ${event.data.totalUpdated} 句`);
}
}
}
}
GET /api/v1/sse/recordings/{taskId}/entries/{sid}/retranslate(單句重翻,v1.4.0 新增)
功能說明
重新翻譯單一句子。最常見場景:使用者透過 PATCH /api/v1/tasks/{id}/entries/{sid} 編輯原文後,呼叫此端點將該句的所有翻譯重做。
與全文重翻 (/retranslate/{taskId}) 的差異:
- 全文重翻:所有句子翻成單一目標語言
- 單句重翻:只翻一句,可同時翻該句曾翻譯過或曾翻譯失敗過的所有語言;支援樂觀鎖
認證方式
Query:api_key(瀏覽器 EventSource 不支援 Header)
請求參數
| 參數 | 位置 | 類型 | 必填 | 說明 |
|---|---|---|---|---|
taskId | path | string | 是 | 錄音 ID(UUID) |
sid | path | number | 是 | 句子 ID(1-based) |
targetLang | query | string | 否 | 目標語言代碼。省略時會重翻該句所有「曾翻譯過或曾翻譯失敗過」的語言,也就是譯文與翻譯錯誤記錄兩者的聯集 |
expectedRevision | query | number | 否 | 樂觀鎖:當前 transcript revision;不符回 transcript_revision_conflict |
api_key | query | string | 是 | API Key |
事件格式
事件序列:connected → progress / translated / error ×N → done
// progress(每語言開始翻譯時)
{ "sid": 5, "lang": "en-US", "status": "translating" }
// translated(每語言成功完成)
{ "sid": 5, "lang": "en-US", "text": "Hello world", "tokens_used": 25 }
// done(全部完成;翻譯成功的語言列在 languages_translated)
{
"sid": 5,
"revision": 6,
"original_text_edited_at": "2026-05-06T10:30:00.000000Z",
"languages_translated": ["en-US"],
"languages_failed": ["ja-JP"]
}
錯誤回應
| 錯誤碼 | HTTP | 說明 |
|---|---|---|
recording_not_found | 404 | 錄音不存在或不屬於該使用者 |
recording_not_completed | 422 | 錄音尚未完成處理 |
entry_not_found | 404 | 找不到指定的句子 |
entry_text_empty | 422 | 該句原文為空(只有空白字元也算) |
sse_translation_failed | 500 | 某個目標語言翻譯失敗(per-lang) |
llm_content_filtered | 400 | 某個目標語言的內容無法翻譯(per-lang),重試無效 |
transcript_revision_conflict | 409 | revision 不符,或同一份逐字稿正有其他寫入在進行 |
storage_upload_failed | 500 | 逐字稿儲存失敗 |
完整事件格式、樂觀鎖搭配 PATCH 的工作流範例見 reference/sse/retranslate.md。
init_sentence 編輯標記欄位(v1.4.0 新增)
historyTranscribe 對被使用者編輯過的句子會在 init_sentence 事件加上兩個欄位(僅在編輯後出現):
{
"sid": 7,
"origin": "修正後的文字",
"original_text_raw": "原本的 STT 輸出",
"original_text_edited_at": "2026-05-06T10:30:00.000000Z",
"translations": { "en-US": "Corrected text" }
}
前端 detection:以欄位存在性判斷('original_text_raw' in data),不要比對 origin === original_text_raw — 使用者可能編輯後又改回相同字串,那種情況下文字相等但仍應顯示「已編輯」標記。詳見 reference/sse/history.md。
GET /api/v1/sse/retranslate/summary/{taskId}(重新翻譯摘要)
功能說明
將指定任務的摘要重新翻譯為目標語言。透過 SSE 串流逐段發送翻譯結果。
使用場景
- 切換摘要顯示語言
- 獲取不同語言的摘要
認證方式
Header:X-API-Key: YOUR_API_KEY
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 錄音 ID |
targetLang | string | 是 | 目標語言代碼 |
請求範例
// 使用 fetch API(因 EventSource 不支援 Header)
async function retranslateSummarySSE(taskId, targetLang, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/retranslate/summary/${taskId}?targetLang=${targetLang}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
// ... 處理 SSE 事件
}
事件格式
summary_translation:
{"text": "累積的翻譯結果...", "is_final": false}
done:
{"totalUpdated": 1}
重新翻譯的摘要不會儲存,已儲存的摘要與其語言都不變。要把摘要換成其他語言並保存,請使用重新生成摘要的儲存端點(POST,會計費)。
譯文不完整時,
done會多帶truncated: true(v1.17.0 新增)。處理時間與逾時規則同 摘要翻譯。
本端點不計費,
done不含characters_billed/charged/billed欄位。
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
sse_summary_not_found | 404 | 找不到摘要 | 該錄音沒有摘要 |
sse_summary_translation_failed | 500 | 摘要翻譯失敗。details.original_error 為 Translation timed out 時表示逾時 | 稍後重試 |
llm_content_filtered | 400 | 摘要內容無法翻譯 | 重試無效;請調整摘要內容後再試 |
重新生成摘要(GET 預覽 / POST 存檔)
拆兩個端點 + mode-aware。完整 schema 請參考 reference/sse/regenerate-summary.md,此處為快速摘要。
| 方法 | 端點 | 儲存結果 | 儲存逐字稿 | 計費 | 用途 |
|---|---|---|---|---|---|
| GET | /api/v1/sse/regenerate/summary/{taskId} | 否 | 否 | 是 | 預覽(試跑) |
| POST | /api/v1/sse/regenerate/summary/{taskId} | 是 | 是(並遞增 revision) | 是 | 存檔(正式儲存) |
已知限制:GET 也計費 — LLM 真實消耗 token,不能讓 GET 端點白嫖。
共用參數(GET 走 query string、POST 走 JSON body)
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId (path) | string | 是 | 錄音 UUID |
mode | string | 是 | 摘要模式 enum:builtin / custom |
template | string | builtin 必填 / custom 禁帶 | 內建模板 slug |
prompt | string | custom 必填 / builtin 禁帶 | 客戶完整 prompt(完整取代內建模板,≤3000 字元) |
promptSlug | string | custom 必填 / builtin 禁帶 | 客戶自家識別碼(≤64 Unicode 字元,禁控制字元) |
language | string | 否 | 輸出語言(預設 transcription 第一個語言) |
plainText | boolean | 否 | 是否要求純文字輸出(預設 false) |
互斥規則:違反屬參數驗證失敗(HTTP 200 + error 事件,data 只有 message、不含 error_code)。
請求範例
# 預覽 builtin(不保存結果)
curl -N "https://vas-poc.vurbo.ai/api/v1/sse/regenerate/summary/550e8400-...?mode=builtin&template=meeting&language=zh-TW&plainText=true" \
-H "X-API-Key: YOUR_API_KEY"
# 存檔 custom
curl -N -X POST "https://vas-poc.vurbo.ai/api/v1/sse/regenerate/summary/550e8400-..." \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"custom","prompt":"請強調 KPI","promptSlug":"acme-v2","plainText":true}'
事件序列
1. connected → 連線確認(含 mode=builtin|custom、endpoint=preview|persist)
2. summary_regeneration → 串流摘要片段(累積式,is_final=true 為最後一筆)
3. done → 完成,含 final_content / mode / template(effective) / prompt_snapshot(custom 才有)
或
3. error → 生成失敗(sse_summary_regeneration_failed;兩個端點都可能出現;不儲存、不會送出 done、不計費)
或
3. error → 儲存失敗或同時有其他寫入(僅存檔端點;流程中止、不會送出 done、不計費)
done event
{
"task_id": "550e8400-...",
"tokens_used": 123,
"final_content": "本次會議...",
"mode": "custom",
"template": "acme-v2",
"plain_text": true,
"persisted": true,
"summary_language": "zh-TW",
"characters_billed": 12700,
"charged": "1.3",
"billed": true,
"prompt_snapshot": "請強調 KPI"
}
mode:摘要模式(builtin / custom)template:effective slug — builtin → 內建模板 slug;custom → 客戶 slugpersisted:本次摘要是否已正式儲存(GET 為false、POST 為true)summary_language:本次摘要實際使用的語言。有帶language時為該值,未帶時為第一個轉錄語言;預覽(GET)與儲存(POST)都會帶,一定有值characters_billed/charged/billed:本次消耗量。僅在實際產生消耗時出現(生成失敗即不出現);判斷是否計費請以billed為準。預覽(GET)與儲存(POST)都會計費。charged反映用量——吃到飽方案已涵蓋的用量,此欄位仍回報消耗量prompt_snapshot:僅 custom mode 出現,為客戶原樣傳入的prompt內容(強制 snapshot,是唯一重建依據)truncated:僅在摘要不完整時出現(值恆為true):摘要長度達到輸出上限,或生成時間達到處理時間上限(只回傳已完成的部分)。照常計費,存檔端點也會儲存這份摘要
錯誤碼
| 錯誤碼 | HTTP | 說明 |
|---|---|---|
recording_not_found | 404 | 找不到錄音 |
sse_template_not_found | 404 | 找不到摘要模板 |
sse_transcript_not_found | 404 | 找不到逐字稿 |
summary_text_empty | 400 | 逐字稿無內容 |
summary_text_too_long | 400 | 逐字稿超過 200,000 字元上限 |
sse_summary_regeneration_failed | 500 | 重新生成失敗(回應不含內部錯誤細節。內容過濾、串流中途停住或沒有正常結束也歸此碼;不儲存、不計費) |
參數驗證失敗沒有錯誤碼。
mode不合法、欄位組合不符、prompt或promptSlug超長/含控制字元等,都在參數驗證階段被擋下,回應是error事件但data只有message一欄、不含error_code。請以message呈現給使用者,不要嘗試比對錯誤碼。(即時錄音的
set_summary走另一條路徑,那裡有對應的錯誤碼,詳見 WebSocket API 文件。)
前端範例
async function regenerateSummary(taskId, body, apiKey, { persist = false } = {}) {
const url = `https://vas-poc.vurbo.ai/api/v1/sse/regenerate/summary/${taskId}`;
const init = persist
? { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
: { method: 'GET', headers: { 'X-API-Key': apiKey } };
if (!persist) {
const params = new URLSearchParams(body);
return fetch(`${url}?${params}`, init);
}
return fetch(url, init);
}
POST /api/v1/sse/summary(Ad-hoc 摘要,v1.9.1 新增)
對請求自帶的任意文字內容生成摘要(不綁定錄音)。完整 schema 請參考 reference/sse/adhoc-summary.md,此處為快速摘要。
| 方法 | 端點 | 是否保存結果 | 計費 | 用途 |
|---|---|---|---|---|
| POST | /api/v1/sse/summary | 否 | 是 | 以請求帶入的 content 生成摘要(多段合併全文、編輯後全文等服務端沒有的內容);結果不儲存 |
- 必填:
content(≤200,000 字元)、idempotency_key(≤64 字元,A-Z a-z 0-9 . _ -)、mode(builtin / custom,欄位互斥規則與重新生成摘要相同) - 計費:0.1 點/每 1,000
content字元(與摘要同費率),生成成功才計費 - 重複請求保證(
idempotency_key只在同一把 API Key 內有效):以整份請求內容(content+所有摘要參數)判斷是否為重試。同一識別碼+完全相同請求重試不重複計費(但會重新生成,不重播結果);同一識別碼+任一欄位不同回 409summary_idempotency_key_conflict;失敗不佔用識別碼 - 錯誤契約與其他 SSE 端點不同:串流開始前一律回真實 HTTP 狀態碼(401/403 / 422 / 404 / 400 / 402 / 409),且認證僅接受 Header、不接受
?api_key=;串流開始後才用 SSEerror事件 - 事件序列:
connected→summary_regeneration×N →done(done 無task_id/persisted,另多characters_billed/charged/idempotency_key/billed四欄;summary_language與重新生成摘要相同,未帶language時為zh-TW)。摘要不完整時done另帶truncated: true(照常計費);生成失敗時改送error(sse_summary_regeneration_failed),不計費 - 計費欄位:
characters_billed與charged恆會出現;billed在免費重試與生成結果為空時為false,對帳請以此欄為準。charged反映用量——吃到飽方案已涵蓋的用量,此欄位仍回報消耗量
POST /api/v1/sse/summary/translate(摘要翻譯,v1.17.0 新增)
把請求自帶的摘要文字翻譯成指定語言,不綁定錄音。完整規格請參考 reference/sse/summary-translate.md,此處為快速摘要。
| 方法 | 端點 | 是否保存結果 | 計費 | 用途 |
|---|---|---|---|---|
| POST | /api/v1/sse/summary/translate | 否 | 是 | 翻譯請求帶入的 content,例如合併後、編輯過等服務端沒有的摘要;結果不儲存 |
- 必填:
content:不超過 30,000 字元target_languageidempotency_key:不超過 64 字元,只能用A-Z a-z 0-9 . _ -
- 選填:
source_language。不帶時自動判斷;跟target_language相同時回 422。 - 計費:每 200 個
content字元 0.1 點,不滿 200 字元以 200 計,跟全文重新翻譯同費率。翻譯成功才計費。 - 重複請求:規則跟 Ad-hoc 摘要相同。
- 同一個識別碼、完全相同的請求重送,不重複計費。
- 同一個識別碼、但任一欄位不同,回 409
summary_idempotency_key_conflict。
- 錯誤契約:串流開始前的錯誤,一律回真實 HTTP 狀態碼(401/403、422、402、409、429);串流開始後,才用 SSE
error事件。 - 事件序列:
connected→summary_translation×N(累積全文)→done。- 內容較長時,串流常會一次送出一大段,兩則之間也可能停頓數秒。
done的欄位:tokens_used、source_language、target_language、characters_billed、charged、idempotency_key、billed。- 譯文不完整時會多帶
truncated: true:達到處理時間或長度上限而被截斷(照常計費),或判定沒有翻完(不計費)。有沒有實際扣點以billed為準。
- 譯文不完整時會多帶
- 處理時間:一次請求上限約 230 秒。途中停頓超過 60 秒會送
error,錯誤碼是sse_summary_translation_failed,details.original_error為Translation timed out。
GET /api/v1/sse/tts/{taskId}(TTS 語音串流)
功能說明
將歷史錄音的翻譯內容轉換為 TTS 語音,透過 SSE 串流逐句發送。前端可控制每次請求回傳的句子數量。
使用場景
- 歷史錄音的翻譯語音播放
- 卡拉 OK 效果(配合 Word Boundary)
- 翻譯內容的語音朗讀
認證方式
Header:X-API-Key: YOUR_API_KEY
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 錄音 ID(路徑參數) |
language | string | 是 | TTS 輸出語言(如 en-US) |
voice | string | 否 | 指定語音名稱(如 en-US-JennyNeural) |
sid | int | 否 | 起始句子 ID(預設 1,從第 1 句開始) |
length | int | 否 | 回傳句子數量(預設 1,最大 20) |
注意:
length最大值由伺服器設定控制(預設 20)。超過最大值時會自動截斷。
請求範例(單句播放)
// 使用 fetch API(因 EventSource 不支援 Header)
async function playTTSSingle(taskId, language, sid, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}?language=${language}&sid=${sid}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
// ... 處理 SSE 事件
}
請求範例(多句播放)
// 播放第 5、6、7 句(共 3 句)
async function playTTSMultiple(taskId, language, sid, length, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}?language=${language}&sid=${sid}&length=${length}`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
// ... 處理 SSE 事件
}
事件序列
1. connected → 連線確認
2. tts_audio → 逐句發送 TTS 音訊(重複 N 次,N = length)
3. tts_done → 播放完成
事件格式
connected:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"language": "en-US",
"voice": "en-US-JennyNeural",
"start_sid": 5,
"length": 3
}
tts_audio:
{
"sid": 5,
"transcript": "你好,很高興認識你",
"text": "Hello, nice to meet you",
"audio": "Base64EncodedMP3...",
"duration_ms": 2500,
"boundaries": [
{"offset_ms": 0, "duration_ms": 350, "text_offset": 0, "word_length": 5, "text": "Hello"},
{"offset_ms": 350, "duration_ms": 100, "text_offset": 5, "word_length": 1, "text": ","},
{"offset_ms": 500, "duration_ms": 250, "text_offset": 7, "word_length": 4, "text": "nice"},
{"offset_ms": 750, "duration_ms": 200, "text_offset": 12, "word_length": 2, "text": "to"},
{"offset_ms": 950, "duration_ms": 350, "text_offset": 15, "word_length": 4, "text": "meet"},
{"offset_ms": 1300, "duration_ms": 300, "text_offset": 20, "word_length": 3, "text": "you"}
],
"characters_used": 23
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | int | 句子 ID |
transcript | string | 原始逐字稿(STT 識別結果) |
text | string | 翻譯文字(TTS 合成來源) |
audio | string | Base64 編碼的 MP3 音訊 |
duration_ms | int | 音訊時長(毫秒) |
boundaries | array | Word Boundary 陣列 |
characters_used | int | 本句合成消耗的字元數;命中快取時為 0(tts_done.total_characters_used 即為此欄位加總) |
Word Boundary 欄位說明
| 欄位 | 類型 | 說明 |
|---|---|---|
offset_ms | int | 該字詞在音訊中的起始時間(毫秒) |
duration_ms | int | 該字詞持續時間(毫秒) |
text_offset | int | 在原文字串中的位置(字元索引) |
word_length | int | 字詞長度(字元數) |
text | string | 字詞內容 |
tts_done:
{
"sentences_sent": 3,
"total_duration_ms": 7500,
"total_characters_used": 142
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sentences_sent | int | 實際發送的句子數量 |
total_duration_ms | int | 所有句子的總音訊時長(毫秒) |
total_characters_used | int | 本次 TTS 合成的總字元數(用於配額計算) |
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
recording_not_found | 404 | 找不到錄音 | 確認 taskId 正確 |
tts_synthesis_failed | 500 | TTS 合成失敗 | 稍後重試 |
前端範例
// 使用 fetch API 處理 TTS SSE
async function playTTS(taskId, language, apiKey, startSid = 1, length = 1) {
const url = new URL(`https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}`);
url.searchParams.set('language', language);
url.searchParams.set('sid', startSid);
url.searchParams.set('length', length);
const response = await fetch(url, {
headers: {
'X-API-Key': apiKey
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const events = parseSSE(decoder.decode(value));
for (const event of events) {
if (event.type === 'connected') {
console.log(`TTS 連線成功,語音:${event.data.voice}`);
} else if (event.type === 'tts_audio') {
console.log(`句子 ${event.data.sid}: ${event.data.text}`);
// 播放音訊
const audioBlob = base64ToBlob(event.data.audio, 'audio/mp3');
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
// 設定卡拉 OK 效果
setupKaraoke(audio, event.data.boundaries, event.data.text);
audio.play();
} else if (event.type === 'tts_done') {
console.log(`播放完成,共 ${event.data.sentences_sent} 句`);
}
}
}
}
// Base64 轉 Blob
function base64ToBlob(base64, mimeType) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
// 卡拉 OK 效果
function setupKaraoke(audio, boundaries, text) {
const updateHighlight = () => {
const currentTimeMs = audio.currentTime * 1000;
const currentWord = boundaries.find((b, i) => {
const nextOffset = boundaries[i + 1]?.offset_ms ?? Infinity;
return currentTimeMs >= b.offset_ms && currentTimeMs < nextOffset;
});
if (currentWord) {
// 高亮當前字詞
highlightWord(text, currentWord.text_offset, currentWord.word_length);
}
};
const interval = setInterval(updateHighlight, 50);
audio.addEventListener('ended', () => clearInterval(interval));
}
GET /api/v1/sse/imports/{importId}/progress(匯入進度串流)
功能說明
即時追蹤音檔匯入的處理進度。連線後透過 SSE 串流持續推送進度更新,直到匯入完成、失敗或連線超時。
使用場景
- 上傳音檔後即時顯示處理進度條
- 追蹤音檔轉換、轉錄、翻譯、摘要等各階段進展
認證方式
Header:X-API-Key: YOUR_API_KEY
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
importId | string | 是 | 匯入任務 ID(UUID,路徑參數) |
請求範例
curl -N "https://vas-poc.vurbo.ai/api/v1/sse/imports/550e8400-e29b-41d4-a716-446655440000/progress" \
-H "X-API-Key: vas_aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW"
事件序列
情境一:匯入尚在處理中
1. connected → 連線確認
2. progress → 發送目前進度
3. progress ×N → 進度有變化時持續推送
heartbeat ×N → 每 15 秒無進度變化時發送心跳
4. completed → 匯入成功,連線結束
或 failed → 匯入失敗,連線結束
或 timeout → 超過 15 分鐘,連線結束
情境二:匯入已完成(終態)
1. connected → 連線確認
2. progress → 發送最終進度
3. completed → 直接發送完成事件並結束
或 failed → 直接發送失敗事件並結束
事件格式
connected:
{"message": "匯入進度服務已連線 (importId: xxx)"}
progress:
{
"import_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"stage": "transcribing",
"progress": 45,
"message": "轉錄中..."
}
| 欄位 | 類型 | 說明 |
|---|---|---|
import_id | string | 匯入任務 ID(UUID) |
status | string | 匯入狀態:pending / processing / completed / failed |
stage | string / null | 目前處理階段 |
progress | integer | 進度百分比(0-100) |
message | string | 可讀的進度訊息 |
階段(stage)值與對應進度範圍:
| 值 | 說明 | 進度範圍 |
|---|---|---|
converting | 音檔格式轉換 | 0% - 10% |
transcribing | 語音轉文字 | 10% - 60% |
translating | 文字翻譯 | 60% - 85% |
summarizing | 產生摘要 | 85% - 100% |
completed | 匯入完成 | 100% |
null | 尚未開始 | — |
completed:
{
"import_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"task_id": "abc123-e29b-41d4-a716-446655440000",
"message": "處理完成"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
import_id | string | 匯入任務 ID |
status | string | 固定為 completed |
task_id | string | 產生的任務 ID,可用於後續查詢 |
message | string | 固定為 處理完成 |
failed:
{
"import_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"error_code": "import_invalid_format",
"error_message": "不支援的音檔格式"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
import_id | string | 匯入任務 ID |
status | string | 固定為 failed |
error_code | string | 錯誤代碼 |
error_message | string | 可讀的錯誤訊息(一般說明,不含內部細節;排查時請提供 import_id) |
heartbeat:
每 15 秒在進度無變化時發送,用於保持連線活躍。
{"timestamp": 1708761600}
timeout:
超過 15 分鐘未完成時發送,連線將自動結束。
{"message": "連線超時"}
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
import_not_found | 404 | 找不到指定的匯入任務 | 確認 importId 正確 |
前端範例
async function trackImportProgress(importId, apiKey) {
const response = await fetch(
`https://vas-poc.vurbo.ai/api/v1/sse/imports/${importId}/progress`,
{
headers: {
'X-API-Key': apiKey
}
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop();
for (const eventStr of events) {
if (!eventStr.trim()) continue;
const lines = eventStr.split('\n');
let eventType = '';
let eventData = '';
for (const line of lines) {
if (line.startsWith('event: ')) eventType = line.slice(7);
else if (line.startsWith('data: ')) eventData = line.slice(6);
}
if (!eventType || !eventData) continue;
const data = JSON.parse(eventData);
switch (eventType) {
case 'connected':
console.log('已連線:', data.message);
break;
case 'progress':
console.log(`[${data.stage}] ${data.progress}% - ${data.message}`);
updateProgressBar(data.progress, data.stage, data.message);
break;
case 'completed':
console.log('匯入完成! 錄音 ID:', data.task_id);
navigateToRecording(data.task_id);
break;
case 'failed':
console.error('匯入失敗:', data.error_code, data.error_message);
showError(data.error_message);
break;
case 'timeout':
console.warn('連線超時:', data.message);
break;
}
}
}
}
版本:V1.24.1 最後更新:2026-09-28