使用指南

History Playback

目錄

  1. 概述
  2. 取得任務列表
  3. 載入歷史逐字稿
  4. 音訊回放
  5. 重新翻譯
  6. 摘要重新翻譯
  7. TTS 語音播放
  8. 完整流程圖
  9. 相關文件

概述

VAS 歷史紀錄功能讓您載入過往的語音辨識結果,包括逐字稿、翻譯、摘要,以及原始音訊回放和重新翻譯。

歷史紀錄的資料來源有兩種:

  • 即時語音翻譯:透過 WebSocket 完成錄音後產生的 Task
  • 音檔匯入:透過 REST API 上傳音檔處理完成後產生的 Task

兩者完成後都會產生 task_id,後續操作方式完全相同。

涉及的 API

API用途
GET /api/v1/tasks取得任務列表
GET /api/v1/sse/history/transcribe/{taskId}載入歷史逐字稿(SSE 串流)
GET /api/v1/sse/audio/{taskId}音訊串流播放(支援 Range Request)
GET /api/v1/sse/retranslate/{taskId}重新翻譯全文(SSE 串流)
GET /api/v1/sse/retranslate/summary/{taskId}重新翻譯摘要(SSE 串流)
GET /api/v1/sse/tts/{taskId}TTS 語音串流播放
GET /api/v1/tasks/{taskId}/audio/export下載原始音檔(離線保存)
GET /api/v1/tasks/{taskId}/transcript/export下載逐字稿(TXT / SRT / SBV / VTT / CSV)

認證方式

所有 API 透過 Header X-API-Key 認證。詳見 認證機制

注意:瀏覽器原生 EventSource API 不支援自訂 Header,SSE API 需使用 fetch API 搭配 ReadableStream 讀取。


取得任務列表

首先取得使用者的所有任務,找到想要回放的 task_id

請求

curl -X GET "https://vas-poc.vurbo.ai/api/v1/tasks" \
  -H "X-API-Key: YOUR_API_KEY"

回應

{
  "tasks": [
    {
      "task_id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "產品規劃會議",
      "type": "transcribe",
      "duration_ms": 3600000,
      "duration_formatted": "60:00",
      "source_lang": "zh-TW",
      "target_lang": "en-US",
      "created_at": "2026-02-20T10:00:00Z",
      "is_pinned": false,
      "is_unread": true
    }
  ]
}

重點欄位

欄位說明
task_id任務 ID(UUID),後續所有操作的 key
title任務標題
type錄音類型:transcribeconversationrecordbroadcast
duration_ms錄音時長(毫秒)
source_lang來源語言
target_lang目標語言
is_pinned是否已釘選
is_unread是否未讀

相關操作

操作API說明
刪除任務DELETE /api/v1/tasks/{taskId}軟刪除
釘選任務PUT /api/v1/tasks/{taskId}/pin標記重要
標記已讀PUT /api/v1/tasks/{taskId}/read清除未讀標記
更新名稱PATCH /api/v1/tasks/{taskId}/name自訂任務標題

載入歷史逐字稿

透過 SSE 串流載入指定任務的完整逐字稿,包含原文、翻譯和摘要。

請求

const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/history/transcribe/${taskId}`,
  {
    headers: { 'X-API-Key': apiKey }
  }
);

注意transcribe 端點適用於所有錄音類型(transcribe、conversation、record),不僅限於 transcribe 類型。

事件序列

SSE 串流會依序推送以下事件:

connected → init_metadata → init_sentence × N → init_summary → init_done
順序事件說明次數
1connected連線確認1 次
2init_metadata任務元資料1 次
3init_sentence逐句推送(原文+翻譯)N 次
4init_summary摘要內容0~1 次
5init_done初始化完成1 次

事件格式

connected

event: connected
data: {"message": "歷史紀錄服務已連線 (recordingId: xxx)"}

init_metadata

event: init_metadata
data: {"task_id": "550e8400...", "title": "會議記錄", "created_at": "2026-02-20T10:00:00Z", "type": "transcribe", "has_speaker_diarization": false, "transcription_languages": ["zh-TW"], "translation_languages": ["en-US"], "summary_template": "general", "summary_language": "zh-TW"}
欄位說明
task_id任務 ID
title任務標題
type錄音類型
has_speaker_diarization是否有語者分離(多人辨識模式)
transcription_languages轉錄語言陣列(BCP 47,如 ["zh-TW"]),最多 2 個
translation_languages翻譯語言陣列(BCP 47,如 ["en-US"]),最多 8 個
summary_template摘要模板 slug,未指定時為 null
summary_language摘要輸出語言(BCP 47),未指定時為 null

init_sentence

event: init_sentence
data: {"sid": 1, "origin": "你好,很高興認識你", "translations": {"en-US": "Hello, nice to meet you"}, "start_time": "00:05", "speaker_id": "0"}

句子若有翻譯失敗(內容過濾、provider error 等),會額外帶 translation_errors 欄位(僅有失敗時出現):

event: init_sentence
data: {"sid": 5, "origin": "敏感詞句子", "translations": {"en-US": "Sensitive sentence"}, "translation_errors": {"ja": "llm_content_filtered"}, "start_time": "00:25", "speaker_id": "0"}
欄位說明
sid句子編號
origin原文(辨識結果)
translations翻譯結果 map(可能為 null
translation_errors可選。翻譯失敗錯誤碼 map,前端可區分「該語言未排程翻譯」(缺 key)vs「翻過但失敗」(有 key)
start_time句子開始時間(mm:ss
speaker_id說話者 ID

init_summary

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

init_done

event: init_done
data: {"totalSentences": 42}

前端範例

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();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // 解析 SSE 格式(以雙換行分隔事件)
    const events = buffer.split('\n\n');
    buffer = events.pop(); // 最後一段可能不完整

    for (const eventStr of events) {
      const lines = eventStr.split('\n');
      let eventType = '';
      let eventData = '';

      for (const line of lines) {
        if (line.startsWith('event: ')) eventType = line.slice(7);
        if (line.startsWith('data: ')) eventData = line.slice(6);
      }

      if (!eventType || !eventData) continue;
      const data = JSON.parse(eventData);

      switch (eventType) {
        case 'init_metadata':
          console.log(`任務: ${data.title} (${data.type})`);
          break;
        case 'init_sentence':
          console.log(`[${data.start_time}] ${data.origin}`);
          if (data.translation) {
            console.log(`  → ${data.translation}`);
          }
          break;
        case 'init_summary':
          console.log(`摘要: ${data.text}`);
          break;
        case 'init_done':
          console.log(`載入完成,共 ${data.totalSentences} 句`);
          break;
      }
    }
  }
}

音訊回放

透過 Audio API 播放任務的錄音檔案,支援 HTTP Range Request 實現拖曳播放。

基本播放

async function playAudio(taskId, apiKey) {
  const response = await fetch(
    `https://vas-poc.vurbo.ai/api/v1/sse/audio/${taskId}`,
    { headers: { 'X-API-Key': apiKey } }
  );
  const blob = await response.blob();
  const audioUrl = URL.createObjectURL(blob);
  const audio = new Audio(audioUrl);
  audio.play();
}

回應格式

情境HTTP 狀態碼說明
完整檔案200回傳完整音訊
部分檔案206回傳指定範圍的音訊(Range Request)

回應 Header:

Content-Type: audio/mp4      (所有錄音音檔一律以 M4A 容器回傳)
Content-Length: 1234567
Accept-Ranges: bytes

Range Request(拖曳播放)

使用 HTML5 <audio> 標籤可自動處理 Range Request:

const audio = document.createElement('audio');
audio.src = `https://vas-poc.vurbo.ai/api/v1/sse/audio/${taskId}`;
// 瀏覽器會自動帶入 X-API-Key... 但需要額外處理

// 推薦:使用 Blob URL 方式
const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/audio/${taskId}`,
  { headers: { 'X-API-Key': apiKey } }
);
const blob = await response.blob();
audio.src = URL.createObjectURL(blob);
audio.controls = true;
document.body.appendChild(audio);

常見錯誤

錯誤碼說明處理方式
recording_not_found找不到錄音確認 taskId 正確
recording_audio_not_ready音檔尚未上傳完成稍後重試

重新翻譯

將任務的所有句子重新翻譯為指定的目標語言。適用於切換顯示語言或更新翻譯。

請求

GET /api/v1/sse/retranslate/{taskId}?targetLang=ja-JP
const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/retranslate/${taskId}?targetLang=ja-JP`,
  { headers: { 'X-API-Key': apiKey } }
);
參數類型必填說明
taskIdstring任務 ID(路徑參數)
targetLangstring目標語言代碼(如 ja-JP

事件序列

translation × N → done

translation 事件

event: translation
data: {"sid": 1, "text": "こんにちは、お会いできて嬉しいです", "is_final": true}
欄位說明
sid句子編號(對應原始逐字稿的 sid)
text新的翻譯結果
is_final是否為最終結果

done 事件

event: done
data: {"totalUpdated": 42}

前端範例

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();
  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) {
      const lines = eventStr.split('\n');
      let eventType = '';
      let eventData = '';

      for (const line of lines) {
        if (line.startsWith('event: ')) eventType = line.slice(7);
        if (line.startsWith('data: ')) eventData = line.slice(6);
      }

      if (!eventType || !eventData) continue;
      const data = JSON.parse(eventData);

      if (eventType === 'translation') {
        // 更新 UI 中對應 sid 的翻譯
        updateTranslation(data.sid, data.text);
      } else if (eventType === 'done') {
        console.log(`重新翻譯完成,共更新 ${data.totalUpdated} 句`);
      }
    }
  }
}

常見錯誤

錯誤碼說明
sse_missing_target_lang缺少 targetLang 參數
sse_unsupported_language不支援的目標語言
sse_translation_failed翻譯服務失敗,稍後重試

摘要重新翻譯

將任務的摘要重新翻譯為指定語言。

請求

GET /api/v1/sse/retranslate/summary/{taskId}?targetLang=ja-JP
const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/retranslate/summary/${taskId}?targetLang=ja-JP`,
  { headers: { 'X-API-Key': apiKey } }
);
參數類型必填說明
taskIdstring任務 ID(路徑參數)
targetLangstring目標語言代碼

事件序列

summary_translation × N → done

summary_translation 事件

event: summary_translation
data: {"text": "累積的翻譯結果...", "is_final": false}

摘要翻譯採用串流方式推送,is_final: false 表示翻譯仍在進行,is_final: true 或收到 done 事件表示完成。

done 事件

event: done
data: {"totalUpdated": 1}

常見錯誤

錯誤碼說明
sse_summary_not_found該任務沒有摘要
sse_summary_translation_failed摘要翻譯失敗,稍後重試

TTS 語音播放

將歷史錄音的翻譯內容轉換為 TTS 語音播放。支援單句或連續多句播放。

請求

// 單句播放
const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}?language=en-US&sid=1`,
  { headers: { 'X-API-Key': apiKey } }
);

// 多句播放(從第 5 句開始,播放 3 句)
const response = await fetch(
  `https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}?language=en-US&sid=5&length=3`,
  { headers: { 'X-API-Key': apiKey } }
);
參數類型必填說明
taskIdstring任務 ID(路徑參數)
languagestringTTS 輸出語言(如 en-US
voicestring指定語音名稱(如 en-US-JennyNeural
sidint起始句子 ID(預設 1)
lengthint播放句子數量(預設 1,最大 20)

事件序列

connected → tts_audio × N → tts_done

tts_audio 事件

event: tts_audio
data: {"sid": 5, "transcript": "你好", "text": "Hello", "audio": "Base64...", "duration_ms": 2500, "boundaries": [...]}
欄位說明
sid句子 ID
transcript原始逐字稿
text翻譯文字(TTS 合成來源)
audioBase64 編碼的 MP3 音訊
duration_ms音訊時長(毫秒)
boundariesWord Boundary 陣列(可用於卡拉 OK 效果)

tts_done 事件

event: tts_done
data: {"sentences_sent": 3, "total_duration_ms": 7500}

前端播放範例

async function playTTS(taskId, language, sid, length, apiKey) {
  const url = new URL(`https://vas-poc.vurbo.ai/api/v1/sse/tts/${taskId}`);
  url.searchParams.set('language', language);
  url.searchParams.set('sid', sid);
  url.searchParams.set('length', length);

  const response = await fetch(url, {
    headers: { 'X-API-Key': apiKey }
  });

  // 解析 SSE 事件後播放音訊
  // ...(SSE 解析邏輯同上)

  // 收到 tts_audio 事件時:
  function handleTTSAudio(data) {
    const binaryString = atob(data.audio);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    const blob = new Blob([bytes], { type: 'audio/mp3' });
    const audio = new Audio(URL.createObjectURL(blob));
    audio.play();
  }
}

完整流程圖

                ┌──────────────────┐
                │  GET /api/v1/tasks │  取得任務列表
                └────────┬─────────┘
                         │
                    選擇 task_id
                         │
        ┌────────────────┼────────────────┐
        │                │                │
  ┌─────▼──────┐   ┌────▼─────┐   ┌─────▼──────┐
  │ 載入逐字稿  │   │ 音訊回放  │   │ 重新翻譯   │
  │ SSE History │   │ Audio API│   │SSE Retrans.│
  └─────┬──────┘   └────┬─────┘   └─────┬──────┘
        │                │                │
        │          ┌─────▼──────┐         │
        │          │  HTTP 200  │         │
        │          │  音訊串流   │         │
        │          │ (Range OK) │         │
        │          └────────────┘         │
        │                                 │
  ┌─────▼──────────────────┐   ┌─────────▼────────┐
  │ SSE 事件序列:          │   │ SSE 事件序列:     │
  │                        │   │                   │
  │ 1. connected           │   │ translation × N   │
  │ 2. init_metadata       │   │ done              │
  │ 3. init_sentence × N   │   └───────────────────┘
  │ 4. init_summary        │
  │ 5. init_done           │         ┌──────────────────┐
  └────────────────────────┘         │ 摘要重新翻譯      │
                                     │ SSE Retrans/Summary│
                                     └────────┬─────────┘
                                              │
                                     summary_translation × N
                                     done
                         │
                ┌────────▼─────────┐
                │  TTS 語音播放     │
                │  SSE /tts/{id}   │
                └────────┬─────────┘
                         │
                connected → tts_audio × N → tts_done

典型使用流程

1. 呼叫 GET /api/v1/tasks 取得任務列表
2. 使用者選擇一個任務
3. 同時呼叫:
   a. SSE History API 載入逐字稿(init_sentence 逐句渲染)
   b. Audio API 準備音訊播放
4. 使用者可以:
   - 播放/拖曳音訊
   - 切換翻譯語言(呼叫 SSE Retranslate)
   - 切換摘要語言(呼叫 SSE Retranslate Summary)
   - 切換摘要模板重新生成(呼叫 SSE Regenerate Summary)
   - 播放翻譯 TTS 語音(呼叫 SSE TTS)

相關文件

文件說明
認證機制API Key 認證詳細說明
Tasks API Reference任務管理 API 完整規格
歷史紀錄 SSE Reference歷史逐字稿 SSE 完整規格
重新翻譯 SSE Reference全文/摘要重新翻譯 SSE 完整規格
重新生成摘要 SSE Reference切換模板重新生成摘要 SSE 完整規格
音訊串流 Reference音訊播放 API 完整規格
TTS 串流 ReferenceTTS 語音合成 SSE 完整規格
即時語音翻譯即時語音翻譯指南
音檔匯入音檔匯入指南

版本:V1.5.7 最後更新:2026-05-20

Copyright © 2026