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(詳見 認證機制

請求參數

參數位置類型必填說明
taskIdpathstring錄音 ID(UUID)
languagequerystringTTS 輸出語言(如 en-US
voicequerystring指定語音名稱(如 en-US-JennyNeural
sidqueryint起始句子 ID(預設 1,從第 1 句開始)
lengthqueryint回傳句子數量(預設 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_idstring任務 ID(UUID)
languagestringTTS 輸出語言
voicestring使用的語音名稱
start_sidnumber起始句子 ID
lengthnumber請求的句子數量

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"
    }
  ]
}
欄位類型說明
sidnumber句子 ID
transcriptstring原始逐字稿(STT 識別結果)
textstring翻譯文字(TTS 合成來源)
audiostringBase64 編碼的 MP3 音訊
duration_msnumber音訊時長(毫秒)
boundariesarrayWord Boundary 陣列

Word Boundary 欄位說明boundaries 陣列中的每個物件):

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

tts_done

{
  "sentences_sent": 3,
  "total_duration_ms": 7500,
  "total_characters_used": 120
}
欄位類型說明
sentences_sentnumber實際發送的句子數量
total_duration_msnumber所有句子的總音訊時長(毫秒)
total_characters_usednumberTTS 合成消耗的總字元數(用量統計)

tts_error

當 TTS 合成過程中發生錯誤時發送。

{
  "error": "tts_synthesis_failed",
  "message": "TTS 合成失敗"
}
欄位類型說明
errorstring錯誤碼
messagestring錯誤訊息

特有錯誤碼

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

前端範例

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

Copyright © 2026