API 文件

Sse Api

注意:本文件中的網址(vas-poc.vurbo.ai)為預計部署網址,正式上線後將另行通知。


目錄


連線資訊

項目
基礎路徑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} 進行驗證。

請求參數

參數類型必填說明
tokenstring廣播分享 Token(4 字元短碼 a-z0-9,路徑參數)
langstring篩選特定翻譯語言(如 en-US
ttsboolean是否啟用 TTS(true / false,預設 false)
viewer_access_tokenstring條件觀眾存取 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_readyTTS 音訊就緒-
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_idstring廣播會話 ID
source_langstring原文語言(主講者設定)
subscribed_langstring觀眾訂閱的篩選語言(若未指定則為 null
available_langsarray可用的翻譯語言列表
tts_languagesarray有啟用 TTS 的語言列表(空陣列表示無 TTS)
phasestring廣播階段:standby(預備)或 live(正式)
recognition_modestring辨識模式:single(單人)或 multi_speaker(多人語者分離)
client_idstring客戶端 ID

queued:

{
  "position": 3,
  "estimated_wait": "約 2 分鐘"
}
欄位類型說明
positionnumber佇列中的位置(1 = 下一個)
estimated_waitstring預估等待時間

admitted:

{
  "message": "已進入直播"
}

origin:

{
  "sid": 1,
  "text": "大家好",
  "speaker_id": "Guest-1",
  "speaker_label": "Guest-1",
  "start_time": "00:05",
  "is_final": true
}
欄位類型說明
sidnumber句子 ID
textstring原文內容
speaker_idstring原始說話者 ID(不可變;單人模式為 "0" 或互譯模式為 "1"/"2"
speaker_labelstring顯示標籤(套用 speaker_aliases 後;無 alias 時等於 speaker_id
start_timestring開始時間(mm:ss);standby 階段不送此欄位,進 live 後從 00:00 起算
is_finalboolean是否為最終結果

translation:

{
  "sid": 1,
  "language": "en-US",
  "text": "Hello everyone",
  "speaker_id": "Guest-1",
  "speaker_label": "Royx",
  "is_final": true
}
欄位類型說明
sidnumber對應的句子 ID
languagestring翻譯語言
textstring翻譯內容
speaker_idstring原始說話者 ID(多人對話模式;不可變)
speaker_labelstring顯示標籤(套用 speaker_aliases 後)
is_finalboolean是否為最終結果

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}
  ]
}
欄位類型說明
sidnumber對應的句子 ID
languagestringTTS 語言
transcriptstring原始逐字稿(原文)
textstring翻譯後的文字
audiostringBase64 編碼的 MP3 音訊
formatstring音訊格式,固定為 "mp3"
duration_msnumber音訊時長(毫秒)
boundariesarrayWord Boundaries(可選,見下表)

Word Boundaries 欄位boundaries 陣列中的每個物件):

欄位類型說明
offset_msnumber該單字在音訊中的開始時間(ms)
duration_msnumber該單字的發音時長(ms)
textstring單字文字
text_offsetnumber該單字在文字中的起始位置
word_lengthnumber該單字的字元長度

注意

  • 主講者需在 start 指令中透過 tts_config 參數指定哪些語言啟用 TTS
  • 只有訂閱該語言且啟用 TTS 的觀眾會收到此事件
  • 只在 live 階段發送,standby 階段不會發送 TTS

paused:

{
  "reason": "host_paused",
  "message": "直播暫停中",
  "paused_at": "2025-12-23T10:30:45.123Z"
}
欄位類型說明
reasonstring暫停原因:host_paused / host_disconnected
messagestring提示訊息
paused_atstring暫停時間(ISO 8601)

resumed:

{
  "message": "直播已恢復",
  "resumed_at": "2025-12-23T10:32:15.456Z"
}

ended:

{
  "reason": "session_stopped",
  "duration_ms": 3600000
}
欄位類型說明
reasonstring結束原因
duration_msnumber廣播持續時間(毫秒)

結束原因:

reason說明
session_stopped主講者正常結束
token_revokedToken 被撤銷
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"
}

句子級錯誤(如某語言的翻譯失敗)會額外帶上 sidtranslation_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_codestring錯誤碼
severitystring嚴重度:warning / error / fatal
messagestring錯誤訊息
contextstring錯誤發生的上下文(如 broadcasttranslation
sidint可選。句子級錯誤的句子編號(如該句翻譯失敗)
translation_languagestring可選。翻譯失敗的目標語言(觀眾可依此判斷是否該句的某個語言失敗)
request_idstring請求追蹤 ID
timestampstring錯誤發生時間(ISO 8601)

speaker_renamed:

多人對話模式專用。當主播執行全域重命名說話者時發送。

{
  "speaker_id": "Guest-1",
  "new_label": "Royx",
  "affected_sids": [1, 3, 5, 7]
}
欄位類型說明
speaker_idstring解析後的原始語者 ID(即使輸入是顯示標籤,事件回傳仍是原始 ID)
new_labelstring新顯示標籤(如 Royx
affected_sidsarray受影響的句子 ID 列表

speaker_reassigned:

多人對話模式專用。當主播修改單句的說話者時發送。

{
  "sid": 3,
  "old_speaker_id": "Guest-1",
  "new_speaker_id": "Guest-2",
  "new_speaker_label": "Amy"
}
欄位類型說明
sidnumber被修改的句子 ID
old_speaker_idstring原始語者 ID(如 Guest-1
new_speaker_idstring新的原始語者 ID(如 Guest-2
new_speaker_labelstring新語者顯示標籤(套用 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_idstring被合併的原始語者 ID(如 Guest-2
target_speaker_idstring合併目標的原始語者 ID(如 Guest-1
target_speaker_labelstring目標語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於原始 ID)
affected_sidsarray受影響的句子 ID 列表

standby:

當觀眾在預備階段連線時,會在 connected 事件後立即收到此事件,表示廣播尚未正式開始。 主講者可透過 WebSocket set_standby_message action 動態更新預備訊息,更新後所有觀眾會收到新的 standby 事件。

{
  "message": "演講即將開始,請稍候...",
  "translations": {
    "en-US": "The presentation is about to begin, please wait...",
    "ja-JP": "プレゼンテーションがまもなく始まります。お待ちください..."
  }
}
欄位類型說明
messagestring預備階段顯示訊息(原文)
translationsobject翻譯結果(可選),key 為語言代碼,value 為翻譯文字

phase_changed:

當廣播從預備階段切換到正式階段時發送。

{
  "phase": "live",
  "message": "廣播已開始"
}
欄位類型說明
phasestring新階段:live(正式階段)
messagestring階段變更訊息

announcement:

主講者發送的公告訊息,所有觀眾都會收到。

{
  "message": "會議將在 5 分鐘後結束",
  "translations": {
    "en-US": "The meeting will end in 5 minutes",
    "ja-JP": "会議は5分後に終了します"
  }
}
欄位類型說明
messagestring公告訊息內容(原文)
translationsobject翻譯結果(可選),key 為語言代碼,value 為翻譯文字

心跳機制

SSE 連線使用心跳保持連線活躍:

  • 間隔:15 秒
  • 格式:SSE 註解(以 : 開頭)
  • 前端無需處理,瀏覽器會自動忽略
: heartbeat

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
broadcast_session_not_found404找不到廣播確認 Token 正確
broadcast_session_ended410廣播已結束提示使用者廣播結束
broadcast_capacity_exceeded503觀眾人數已達上限加入等待佇列

注意: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

請求參數

參數類型必填說明
taskIdstring錄音 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": "王經理"
}
欄位類型說明
sidint句子編號
originstring原文
translationsobject翻譯結果(可選),key 為語言代碼,value 為翻譯文字
translation_errorsobject可選。翻譯失敗錯誤碼,key 為語言代碼,value 為 error_code(如 llm_content_filtered
start_timestring起始時間(mm:ss 格式)
speaker_idstring|null原始說話者 ID(不可變,如 speaker_1);PATCH /speakers/reassigntarget_speaker_id 來源(v1.5.3 翻轉:原為顯示名)
speaker_labelstring|null顯示標籤(套 speaker_aliases 後的人類可讀名稱,如 王經理);無 alias 時等同 speaker_id(v1.5.3 新增取代原 speaker_id 顯示語意)

init_summary:

{"text": "這是一段會議記錄的摘要..."}

init_done:

{"totalSentences": 10}

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
recording_not_found404找不到錄音確認 taskId 正確
sse_transcript_not_found404找不到逐字稿錄音可能尚未處理完成

前端範例

// 使用 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

請求參數

參數類型必填說明
taskIdstring錄音 ID(路徑參數)
targetLangstring目標語言代碼

請求範例

// 使用 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: errorsid + 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_codestring錯誤碼,目前固定為 sse_translation_failed
severitystringerror
messagestring人類可讀訊息
contextstringsse(依 ErrorContextEnum 前綴自動匹配規則)
sidint失敗的句子編號
request_idstring請求追蹤 ID
timestampstring錯誤發生時間(ISO 8601)
detailsobjecttranslation_languageoriginal_error 等 debug 資訊

失敗的句子會被儲存為翻譯錯誤記錄(見 history-playback 指南),下次載入歷史時可看到失敗標記。完整規範詳見 reference/sse/retranslate.md

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
sse_missing_target_lang422缺少目標語言參數提供 targetLang
sse_unsupported_language422不支援的目標語言使用有效的語言代碼
sse_translation_failed500翻譯失敗(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)

請求參數

參數位置類型必填說明
taskIdpathstring錄音 ID(UUID)
sidpathnumber句子 ID(1-based)
targetLangquerystring目標語言代碼。省略時會重翻該句已存在 translated_texts 中的所有語言
expectedRevisionquerynumber樂觀鎖:當前 transcript revision;不符回 transcript_revision_conflict
api_keyquerystringAPI 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_found404錄音不存在或不屬於該使用者
recording_not_completed422錄音尚未完成處理
entry_not_found404找不到指定的句子
entry_text_empty422該句原文為空
transcript_revision_conflict409revision 不符(已被其他請求修改)
storage_upload_failed500逐字稿儲存失敗

完整事件格式、樂觀鎖搭配 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

請求參數

參數類型必填說明
taskIdstring錄音 ID
targetLangstring目標語言代碼

請求範例

// 使用 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_found404找不到摘要該錄音沒有摘要
sse_summary_translation_failed500摘要翻譯失敗稍後重試

重新生成摘要(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
modestring摘要模式 enum:builtin / custom
templatestringbuiltin 必填 / custom 禁帶內建模板 slug
promptstringcustom 必填 / builtin 禁帶客戶完整 prompt(取代 IPEVO 三層,≤2000 字元)
promptSlugstringcustom 必填 / builtin 禁帶客戶自家識別碼(≤64 Unicode 字元,禁控制字元)
languagestring輸出語言(預設 transcription 第一個語言)
plainTextboolean是否要求純文字輸出(預設 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 → 客戶 slug
  • persisted:本次摘要是否已正式儲存(GET 為 false、POST 為 true
  • prompt_snapshot僅 custom mode 出現,為客戶原樣傳入的 prompt 內容(強制 snapshot,是唯一重建依據)

錯誤碼

錯誤碼HTTP說明
recording_not_found404找不到錄音
sse_template_not_found404找不到摘要模板
sse_transcript_not_found404找不到逐字稿
summary_text_empty400逐字稿無內容
summary_text_too_long400逐字稿超過 100,000 字元上限
sse_summary_regeneration_failed500重新生成失敗(已 sanitize raw error)
summary_invalid_mode422mode 不是 builtin / custom
summary_mode_field_mismatch422mode 與欄位組合不符(必填缺漏 / 禁帶被帶入)
summary_prompt_too_long422prompt 超過 2000 字元
summary_prompt_slug_too_long422promptSlug 超過 64 字元
summary_prompt_slug_invalid422promptSlug 含控制字元(\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

請求參數

參數類型必填說明
taskIdstring錄音 ID(路徑參數)
languagestringTTS 輸出語言(如 en-US
voicestring指定語音名稱(如 en-US-JennyNeural
sidint起始句子 ID(預設 1,從第 1 句開始)
lengthint回傳句子數量(預設 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"}
  ]
}
欄位類型說明
sidint句子 ID
transcriptstring原始逐字稿(STT 識別結果)
textstring翻譯文字(TTS 合成來源)
audiostringBase64 編碼的 MP3 音訊
duration_msint音訊時長(毫秒)
boundariesarrayWord Boundary 陣列

Word Boundary 欄位說明

欄位類型說明
offset_msint該字詞在音訊中的起始時間(毫秒)
duration_msint該字詞持續時間(毫秒)
text_offsetint在原文字串中的位置(字元索引)
word_lengthint字詞長度(字元數)
textstring字詞內容

tts_done:

{
  "sentences_sent": 3,
  "total_duration_ms": 7500,
  "total_characters_used": 142
}
欄位類型說明
sentences_sentint實際發送的句子數量
total_duration_msint所有句子的總音訊時長(毫秒)
total_characters_usedint本次 TTS 合成的總字元數(用於配額計算)

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
recording_not_found404找不到錄音確認 taskId 正確
sse_missing_target_lang422缺少語言參數提供 language 參數
sse_unsupported_language422不支援的語言使用有效的語言代碼
tts_translation_not_found400找不到該語言翻譯確認該語言的翻譯存在
tts_synthesis_failed500TTS 合成失敗稍後重試
tts_quota_exceeded402TTS 使用量已達上限稍後重試

前端範例

// 使用 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

請求參數

參數類型必填說明
importIdstring匯入任務 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_idstring匯入任務 ID(UUID)
statusstring匯入狀態:pending / processing / completed / failed
stagestring / null目前處理階段
progressinteger進度百分比(0-100)
messagestring可讀的進度訊息

階段(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_idstring匯入任務 ID
statusstring固定為 completed
task_idstring產生的錄音 ID(recording_id),可用於後續查詢
messagestring固定為 處理完成

failed:

{
  "import_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "error_code": "import_invalid_format",
  "error_message": "不支援的音檔格式"
}
欄位類型說明
import_idstring匯入任務 ID
statusstring固定為 failed
error_codestring錯誤代碼
error_messagestring可讀的錯誤訊息

heartbeat:

每 15 秒在進度無變化時發送,用於保持連線活躍。

{"timestamp": 1708761600}

timeout:

超過 15 分鐘未完成時發送,連線將自動結束。

{"message": "連線超時"}

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
import_not_found404找不到指定的匯入任務確認 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

Copyright © 2026