History Playback
目錄
概述
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 認證。詳見 認證機制。
注意:瀏覽器原生
EventSourceAPI 不支援自訂 Header,SSE API 需使用fetchAPI 搭配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 | 錄音類型:transcribe、conversation、record、broadcast |
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
| 順序 | 事件 | 說明 | 次數 |
|---|---|---|---|
| 1 | connected | 連線確認 | 1 次 |
| 2 | init_metadata | 任務元資料 | 1 次 |
| 3 | init_sentence | 逐句推送(原文+翻譯) | N 次 |
| 4 | init_summary | 摘要內容 | 0~1 次 |
| 5 | init_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 } }
);
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 任務 ID(路徑參數) |
targetLang | string | 是 | 目標語言代碼(如 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 } }
);
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 任務 ID(路徑參數) |
targetLang | string | 是 | 目標語言代碼 |
事件序列
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 } }
);
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
taskId | string | 是 | 任務 ID(路徑參數) |
language | string | 是 | TTS 輸出語言(如 en-US) |
voice | string | 否 | 指定語音名稱(如 en-US-JennyNeural) |
sid | int | 否 | 起始句子 ID(預設 1) |
length | int | 否 | 播放句子數量(預設 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 合成來源) |
audio | Base64 編碼的 MP3 音訊 |
duration_ms | 音訊時長(毫秒) |
boundaries | Word 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 串流 Reference | TTS 語音合成 SSE 完整規格 |
| 即時語音翻譯 | 即時語音翻譯指南 |
| 音檔匯入 | 音檔匯入指南 |
版本:V1.5.7 最後更新:2026-05-20