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 接受兩種傳送 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} 進行驗證。

請求參數

參數類型必填說明
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廣播暫停主講者手動暫停(主辦斷線另送 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_langsarray可用的翻譯語言列表
tts_languagesarray有啟用 TTS 的語言列表(空陣列表示無 TTS)
phasestring廣播階段:standby(預備)或 live(正式)
recognition_modestring辨識模式:single(單人)或 multi_speaker(多人語者分離)

connected 事件不含轉錄語言清單。頻道的公開資訊(名稱、transcription_languages、translation_languages、TTS 語音)請改用 GET /api/v1/viewer/broadcasts/{token} 取得,見 觀眾端 API。

queued:

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

admitted:

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

多個廣播事件的 message / estimated_wait 是伺服器固定回傳的顯示字串(如 已進入直播、廣播已暫停、廣播已恢復、廣播已結束、廣播已開始)。請直接呈現或改用自家文案,不要解析、也不要拿來做條件判斷。

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(不可變);僅多人語者分離模式帶此欄位,單人模式不帶
speaker_labelstring顯示標籤(套用 speaker_aliases 後;無 alias 時等於 speaker_id)
start_timestring開始時間(mm:ss),從開播(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:

{
  "message": "廣播已暫停"
}
欄位類型說明
messagestring提示訊息

主辦端斷線與重新連上是獨立事件(host_disconnected / host_reconnected),不是 paused 的原因值。

resumed:

{
  "message": "廣播已恢復"
}

ended:

{
  "message": "廣播已結束"
}
欄位類型說明
messagestring提示訊息

這三個事件都只帶 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_codestring錯誤碼
severitystring嚴重度:warning / error / fatal
messagestring錯誤訊息(伺服器回傳的固定英文顯示文字,僅供顯示、請勿解析)
contextstring錯誤發生的上下文(如 broadcast、translation)
sidint可選。句子級錯誤的句子編號(如該句翻譯失敗)
translation_languagestring可選。翻譯失敗的目標語言(觀眾可依此判斷是否該句的某個語言失敗)
request_idstring可選。請求追蹤 ID。僅連線階段錯誤(HTTP 狀態碼非 200)帶出;句子級/會話級錯誤不帶
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 的單則訊息錯誤處理一致);可預期的領域錯誤則回傳對應錯誤碼(如 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

請求參數

參數類型必填說明
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": "歷史紀錄服務已連線 (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": "王經理"
}
欄位類型說明
sidint句子編號
originstring原文
translationsobject翻譯結果(可選),key 為語言代碼,value 為翻譯文字
translation_errorsobject可選。翻譯失敗錯誤碼,key 為語言代碼,value 為 error_code(如 llm_content_filtered)
channel_idnumber可選。(多聲道限定)本句來自哪一路實體聲道;僅多聲道錄音出現,單路錄音不帶此欄位
start_timestring起始時間(mm:ss 格式)
speaker_idstring|null原始說話者 ID(不可變,如 speaker_1);PATCH /speakers/reassign 的 target_speaker_id 來源(v1.5.3 翻轉:原為顯示名)
speaker_labelstring|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_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.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

請求參數

參數類型必填說明
taskIdstring是錄音 ID(路徑參數)
targetLangstring是目標語言代碼
segmentedstring否帶 1 表示客戶端支援分段續翻(v1.18.0)
fromSidnumber否只能和 segmented=1 一起使用:從這一句(含)開始翻(v1.18.0)
expectedRevisionnumber否逐字稿版本;不符時不翻譯、不扣點,回 transcript_revision_conflict(v1.18.0)

長逐字稿請用分段續翻:沒帶 segmented=1 時一次翻完整份;逐字稿太長、一次請求翻不完時,串流開始前回 HTTP 422 retranslate_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_codestring錯誤碼:sse_translation_failed 或 llm_content_filtered
severitystringsse_translation_failed 為 error;llm_content_filtered 為 warning
messagestring人類可讀訊息
contextstringsse_translation_failed 為 sse;llm_content_filtered 為 translation
sidint失敗的句子編號
request_idstring請求追蹤 ID
timestampstring錯誤發生時間(ISO 8601)
detailsobject含 translation_language、original_error 等 debug 資訊

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

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
sse_translation_failed500翻譯失敗(per-sid)失敗的單句仍透過 event: error 通知,整體流程不中斷
llm_content_filtered400該句內容無法翻譯(per-sid)重試無效;請修改該句原文後再試。該句不計入 totalUpdated,也不產生消耗
storage_upload_failed500逐字稿儲存失敗本次重翻全部作廢、不計費;稍後重試。收到此碼後不會再收到 done
transcript_revision_conflict409同一份逐字稿正有其他寫入在進行,或逐字稿在處理期間被改動,或 expectedRevision 與目前的版本不符本次重翻全部作廢、不計費;版本不符時重新載入逐字稿,其他情況稍後重試即可。收到此碼後不會再收到 done
retranslate_segmentation_required422沒帶 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)

請求參數

參數位置類型必填說明
taskIdpathstring是錄音 ID(UUID)
sidpathnumber是句子 ID(1-based)
targetLangquerystring否目標語言代碼。省略時會重翻該句所有「曾翻譯過或曾翻譯失敗過」的語言,也就是譯文與翻譯錯誤記錄兩者的聯集
expectedRevisionquerynumber否樂觀鎖:當前 transcript revision;不符回 transcript_revision_conflict
api_keyquerystring是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_found404錄音不存在或不屬於該使用者
recording_not_completed422錄音尚未完成處理
entry_not_found404找不到指定的句子
entry_text_empty422該句原文為空(只有空白字元也算)
sse_translation_failed500某個目標語言翻譯失敗(per-lang)
llm_content_filtered400某個目標語言的內容無法翻譯(per-lang),重試無效
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}

重新翻譯的摘要不會儲存,已儲存的摘要與其語言都不變。要把摘要換成其他語言並保存,請使用重新生成摘要的儲存端點(POST,會計費)。

譯文不完整時,done 會多帶 truncated: true(v1.17.0 新增)。處理時間與逾時規則同 摘要翻譯。

本端點不計費,done 不含 characters_billed / charged / billed 欄位。

錯誤回應

錯誤碼HTTP 狀態碼說明處理建議
sse_summary_not_found404找不到摘要該錄音沒有摘要
sse_summary_translation_failed500摘要翻譯失敗。details.original_error 為 Translation timed out 時表示逾時稍後重試
llm_content_filtered400摘要內容無法翻譯重試無效;請調整摘要內容後再試

重新生成摘要(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
modestring是摘要模式 enum:builtin / custom
templatestringbuiltin 必填 / custom 禁帶內建模板 slug
promptstringcustom 必填 / builtin 禁帶客戶完整 prompt(完整取代內建模板,≤3000 字元)
promptSlugstringcustom 必填 / builtin 禁帶客戶自家識別碼(≤64 Unicode 字元,禁控制字元)
languagestring否輸出語言(預設 transcription 第一個語言)
plainTextboolean否是否要求純文字輸出(預設 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 → 客戶 slug
  • persisted:本次摘要是否已正式儲存(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_found404找不到錄音
sse_template_not_found404找不到摘要模板
sse_transcript_not_found404找不到逐字稿
summary_text_empty400逐字稿無內容
summary_text_too_long400逐字稿超過 200,000 字元上限
sse_summary_regeneration_failed500重新生成失敗(回應不含內部錯誤細節。內容過濾、串流中途停住或沒有正常結束也歸此碼;不儲存、不計費)

參數驗證失敗沒有錯誤碼。 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+所有摘要參數)判斷是否為重試。同一識別碼+完全相同請求重試不重複計費(但會重新生成,不重播結果);同一識別碼+任一欄位不同回 409 summary_idempotency_key_conflict;失敗不佔用識別碼
  • 錯誤契約與其他 SSE 端點不同:串流開始前一律回真實 HTTP 狀態碼(401/403 / 422 / 404 / 400 / 402 / 409),且認證僅接受 Header、不接受 ?api_key=;串流開始後才用 SSE error 事件
  • 事件序列: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_language
    • idempotency_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

請求參數

參數類型必填說明
taskIdstring是錄音 ID(路徑參數)
languagestring是TTS 輸出語言(如 en-US)
voicestring否指定語音名稱(如 en-US-JennyNeural)
sidint否起始句子 ID(預設 1,從第 1 句開始)
lengthint否回傳句子數量(預設 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
}
欄位類型說明
sidint句子 ID
transcriptstring原始逐字稿(STT 識別結果)
textstring翻譯文字(TTS 合成來源)
audiostringBase64 編碼的 MP3 音訊
duration_msint音訊時長(毫秒)
boundariesarrayWord Boundary 陣列
characters_usedint本句合成消耗的字元數;命中快取時為 0(tts_done.total_characters_used 即為此欄位加總)

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 正確
tts_synthesis_failed500TTS 合成失敗稍後重試

前端範例

// 使用 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%
completed匯入完成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,可用於後續查詢
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可讀的錯誤訊息(一般說明,不含內部細節;排查時請提供 import_id)

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.24.1 最後更新:2026-09-28

Copyright © 2026