Sse Api
注意:本文件中的網址(
vas-poc.vurbo.ai)為預計部署網址,正式上線後將另行通知。
目錄
- SSE API
- 目錄
- 連線資訊
- Broadcast SSE API
- 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 存檔)
- 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 接受兩種傳送方式(後端 VerifyApiKeyQuery middleware 同時支援):
# 方式 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 或截圖外洩。
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 | 廣播暫停 | 主講者暫停或斷線 |
resumed | 廣播恢復 | 主講者恢復 |
ended | 廣播結束 | - |
kicked | 被踢除 | 觀眾管理 |
error | 錯誤 | - |
speaker_renamed | 說話者重命名 | - |
speaker_reassigned | 單句說話者修改 | - |
speakers_merged | 語者合併 | - |
standby | 預備階段通知 | - |
phase_changed | 階段變更通知 | - |
announcement | 主講者公告 | - |
事件格式
connected:
{
"session_id": "abc123",
"source_lang": "zh-TW",
"subscribed_lang": "en-US",
"available_langs": ["en-US", "ja-JP"],
"tts_languages": ["en-US"],
"phase": "standby",
"recognition_mode": "single",
"client_id": "client_xyz"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
session_id | string | 廣播會話 ID |
source_lang | string | 原文語言(主講者設定) |
subscribed_lang | string | 觀眾訂閱的篩選語言(若未指定則為 null) |
available_langs | array | 可用的翻譯語言列表 |
tts_languages | array | 有啟用 TTS 的語言列表(空陣列表示無 TTS) |
phase | string | 廣播階段:standby(預備)或 live(正式) |
recognition_mode | string | 辨識模式:single(單人)或 multi_speaker(多人語者分離) |
client_id | string | 客戶端 ID |
queued:
{
"position": 3,
"estimated_wait": "約 2 分鐘"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
position | number | 佇列中的位置(1 = 下一個) |
estimated_wait | string | 預估等待時間 |
admitted:
{
"message": "已進入直播"
}
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(不可變;單人模式為 "0" 或互譯模式為 "1"/"2") |
speaker_label | string | 顯示標籤(套用 speaker_aliases 後;無 alias 時等於 speaker_id) |
start_time | string | 開始時間(mm:ss);standby 階段不送此欄位,進 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:
{
"reason": "host_paused",
"message": "直播暫停中",
"paused_at": "2025-12-23T10:30:45.123Z"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
reason | string | 暫停原因:host_paused / host_disconnected |
message | string | 提示訊息 |
paused_at | string | 暫停時間(ISO 8601) |
resumed:
{
"message": "直播已恢復",
"resumed_at": "2025-12-23T10:32:15.456Z"
}
ended:
{
"reason": "session_stopped",
"duration_ms": 3600000
}
| 欄位 | 類型 | 說明 |
|---|---|---|
reason | string | 結束原因 |
duration_ms | number | 廣播持續時間(毫秒) |
結束原因:
| reason | 說明 |
|---|---|
session_stopped | 主講者正常結束 |
token_revoked | Token 被撤銷 |
host_timeout | 主講者斷線超時 |
capacity_exceeded | 排隊超時 |
kicked:
{
"message": "已被主講者移除"
}
error:
{
"error_code": "broadcast_session_ended",
"severity": "error",
"message": "廣播已結束",
"context": "broadcast",
"request_id": "req_abc123xyz789",
"timestamp": "2025-12-05T10:30:45.123Z"
}
句子級錯誤(如某語言的翻譯失敗)會額外帶上 sid 與 translation_language,方便前端標示某句的某個語言失敗:
{
"error_code": "llm_content_filtered",
"severity": "warning",
"message": "LLM 內容被過濾",
"context": "translation",
"sid": 5,
"translation_language": "ja",
"request_id": "req_abc123xyz789",
"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 |
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 per-message panic recover 機制);可預期的領域錯誤則回傳對應錯誤碼(如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.source_lang}`);
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.reason}`);
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:查詢廣播資訊的端點
GET /broadcast/{token}/info請參見 REST API - Broadcasts 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": "歷史紀錄服務已連線 (recordingId: 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": "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) |
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": "這是一段會議記錄的摘要..."}
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.translation) {
console.log(`翻譯: ${event.data.translation}`);
}
} 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 | 是 | 目標語言代碼 |
請求範例
// 使用 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}
error(per-sid 句子翻譯失敗):
當某句翻譯失敗(如 LLM provider error、內容過濾),不發 translation 而是發 event: error 帶 sid + error_code,與 translation 事件交錯出現。前端可用同一套 translationError.ts 攔截器處理(與 WebSocket spec 對齊):
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", "original_error": "..."}}
| 欄位 | 類型 | 說明 |
|---|---|---|
error_code | string | 錯誤碼,目前固定為 sse_translation_failed |
severity | string | error |
message | string | 人類可讀訊息 |
context | string | sse(依 ErrorContextEnum 前綴自動匹配規則) |
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_missing_target_lang | 422 | 缺少目標語言參數 | 提供 targetLang |
sse_unsupported_language | 422 | 不支援的目標語言 | 使用有效的語言代碼 |
sse_translation_failed | 500 | 翻譯失敗(per-sid) | 失敗的單句仍透過 event: error 通知,整體流程不中斷 |
前端範例
// 使用 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/recordings/{id}/entries/{sid} 編輯原文後,呼叫此端點將該句的所有翻譯重做。
與全文重翻 (/retranslate/{taskId}) 的差異:
- 全文重翻:所有句子翻成單一目標語言
- 單句重翻:只翻一句,可同時翻所有已存在的目標語言;支援樂觀鎖
認證方式
Query:api_key(瀏覽器 EventSource 不支援 Header)
請求參數
| 參數 | 位置 | 類型 | 必填 | 說明 |
|---|---|---|---|---|
taskId | path | string | 是 | 錄音 ID(UUID) |
sid | path | number | 是 | 句子 ID(1-based) |
targetLang | query | string | 否 | 目標語言代碼。省略時會重翻該句已存在 translated_texts 中的所有語言 |
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 | 該句原文為空 |
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}
錯誤回應
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
sse_summary_not_found | 404 | 找不到摘要 | 該錄音沒有摘要 |
sse_summary_translation_failed | 500 | 摘要翻譯失敗 | 稍後重試 |
重新生成摘要(GET 預覽 / POST 存檔)
拆兩個端點 + mode-aware。完整 schema 請參考 reference/sse/regenerate-summary.md,此處為快速摘要。
| 方法 | 端點 | 寫 DB | 儲存逐字稿 | 計費 | 用途 |
|---|---|---|---|---|---|
| GET | /api/v1/sse/regenerate/summary/{taskId} | ❌ | ❌ | ✅ | 預覽(試跑) |
| POST | /api/v1/sse/regenerate/summary/{taskId} | ✅ | ✅ + bump 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(取代 IPEVO 三層,≤2000 字元) |
promptSlug | string | custom 必填 / builtin 禁帶 | 客戶自家識別碼(≤64 Unicode 字元,禁控制字元) |
language | string | 否 | 輸出語言(預設 transcription 第一個語言) |
plainText | boolean | 否 | 是否要求純文字輸出(預設 false) |
互斥規則:違反 → 422 summary_mode_field_mismatch。
請求範例
# 預覽 builtin(不寫 DB / blob)
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 才有)
done event
{
"task_id": "550e8400-...",
"tokens_used": 123,
"final_content": "本次會議...",
"mode": "custom",
"template": "acme-v2",
"plain_text": true,
"persisted": true,
"prompt_snapshot": "請強調 KPI"
}
mode:業務 mode(builtin / custom)template:effective slug — builtin → 內建模板 slug;custom → 客戶 slugpersisted:本次摘要是否已正式儲存(GET 為false、POST 為true)prompt_snapshot:僅 custom mode 出現,為客戶原樣傳入的prompt內容(強制 snapshot,是唯一重建依據)
錯誤碼
| 錯誤碼 | HTTP | 說明 |
|---|---|---|
recording_not_found | 404 | 找不到錄音 |
sse_template_not_found | 404 | 找不到摘要模板 |
sse_transcript_not_found | 404 | 找不到逐字稿 |
summary_text_empty | 400 | 逐字稿無內容 |
summary_text_too_long | 400 | 逐字稿超過 100,000 字元上限 |
sse_summary_regeneration_failed | 500 | 重新生成失敗(已 sanitize raw error) |
summary_invalid_mode | 422 | mode 不是 builtin / custom |
summary_mode_field_mismatch | 422 | mode 與欄位組合不符(必填缺漏 / 禁帶被帶入) |
summary_prompt_too_long | 422 | prompt 超過 2000 字元 |
summary_prompt_slug_too_long | 422 | promptSlug 超過 64 字元 |
summary_prompt_slug_invalid | 422 | promptSlug 含控制字元(\n / \r / \t / \0 等) |
前端範例
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);
}
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最大值由後端環境變數TTS_SSE_MAX_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"}
]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | int | 句子 ID |
transcript | string | 原始逐字稿(STT 識別結果) |
text | string | 翻譯文字(TTS 合成來源) |
audio | string | Base64 編碼的 MP3 音訊 |
duration_ms | int | 音訊時長(毫秒) |
boundaries | array | Word Boundary 陣列 |
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 正確 |
sse_missing_target_lang | 422 | 缺少語言參數 | 提供 language 參數 |
sse_unsupported_language | 422 | 不支援的語言 | 使用有效的語言代碼 |
tts_translation_not_found | 400 | 找不到該語言翻譯 | 確認該語言的翻譯存在 |
tts_synthesis_failed | 500 | TTS 合成失敗 | 稍後重試 |
tts_quota_exceeded | 402 | 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% |
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(recording_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 | 可讀的錯誤訊息 |
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.5.7 最後更新:2026-05-20