SSE API
Tts
連線資訊
| 項目 | 值 |
|---|---|
| 基礎路徑 | https://vas-poc.vurbo.ai/api/v1/sse |
| 協定 | HTTP + Server-Sent Events (SSE) |
| 資料格式 | text/event-stream |
| 認證方式 | Header X-API-Key: {KEY} |
注意:瀏覽器原生 EventSource API 不支援自訂 Header,需使用 fetch API 搭配 ReadableStream,或使用支援 Header 的 SSE 客戶端套件。
端點總覽
| 方法 | 端點 | 說明 |
|---|---|---|
| GET | /api/v1/sse/tts/{taskId} | TTS 語音合成串流 |
GET /api/v1/sse/tts/{taskId}
功能說明
將歷史錄音的翻譯內容轉換為 TTS 語音,透過 SSE 串流逐句發送。前端可控制每次請求回傳的句子數量。
使用場景
- 歷史錄音的翻譯語音播放
- 卡拉 OK 效果(配合 Word Boundary)
- 翻譯內容的語音朗讀
認證方式
Header:X-API-Key(詳見 認證機制)
請求參數
| 參數 | 位置 | 類型 | 必填 | 說明 |
|---|---|---|---|---|
taskId | path | string | 是 | 錄音 ID(UUID) |
language | query | string | 是 | TTS 輸出語言(如 en-US) |
voice | query | string | 否 | 指定語音名稱(如 en-US-JennyNeural) |
sid | query | int | 否 | 起始句子 ID(預設 1,從第 1 句開始) |
length | query | int | 否 | 回傳句子數量(預設 1,最大 20) |
注意:
length最大值由後端環境變數TTS_SSE_MAX_LENGTH控制(預設 20)。超過最大值時會自動截斷。
請求範例
單句播放:
curl -N "https://vas-poc.vurbo.ai/api/v1/sse/tts/550e8400-e29b-41d4-a716-446655440000?language=en-US&sid=1" \
-H "X-API-Key: vas_aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW"
// 使用 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 → 播放完成
* tts_error → 合成失敗時發送(取代 tts_done)
事件格式
connected
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"language": "en-US",
"voice": "en-US-JennyNeural",
"start_sid": 5,
"length": 3
}
| 欄位 | 類型 | 說明 |
|---|---|---|
task_id | string | 任務 ID(UUID) |
language | string | TTS 輸出語言 |
voice | string | 使用的語音名稱 |
start_sid | number | 起始句子 ID |
length | number | 請求的句子數量 |
tts_audio
{
"sid": 5,
"transcript": "原文",
"text": "翻譯",
"audio": "Base64EncodedMP3...",
"duration_ms": 2500,
"boundaries": [
{
"offset_ms": 0,
"duration_ms": 350,
"text_offset": 0,
"word_length": 5,
"text": "Hello"
}
]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 句子 ID |
transcript | string | 原始逐字稿(STT 識別結果) |
text | string | 翻譯文字(TTS 合成來源) |
audio | string | Base64 編碼的 MP3 音訊 |
duration_ms | number | 音訊時長(毫秒) |
boundaries | array | Word Boundary 陣列 |
Word Boundary 欄位說明(boundaries 陣列中的每個物件):
| 欄位 | 類型 | 說明 |
|---|---|---|
offset_ms | number | 該字詞在音訊中的起始時間(毫秒) |
duration_ms | number | 該字詞持續時間(毫秒) |
text_offset | number | 在原文字串中的位置(字元索引) |
word_length | number | 字詞長度(字元數) |
text | string | 字詞內容 |
tts_done
{
"sentences_sent": 3,
"total_duration_ms": 7500,
"total_characters_used": 120
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sentences_sent | number | 實際發送的句子數量 |
total_duration_ms | number | 所有句子的總音訊時長(毫秒) |
total_characters_used | number | TTS 合成消耗的總字元數(用量統計) |
tts_error
當 TTS 合成過程中發生錯誤時發送。
{
"error": "tts_synthesis_failed",
"message": "TTS 合成失敗"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
error | string | 錯誤碼 |
message | string | 錯誤訊息 |
特有錯誤碼
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
recording_not_found | 404 | 找不到錄音 | 確認 taskId 正確 |
sse_missing_target_lang | 422 | 缺少語言參數 | 提供 language 參數 |
sse_unsupported_language | 422 | 不支援的語言 | 使用有效的語言代碼 |
tts_translation_not_found | 400 | 找不到該語言翻譯 | 確認該語言的翻譯存在 |
tts_synthesis_failed | 500 | TTS 合成失敗 | 稍後重試 |
tts_quota_exceeded | 402 | TTS 使用量已達上限 | 稍後重試 |
前端範例
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));
}
版本:V1.5.7 最後更新:2026-05-20