> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# 服务端流式传输

> **Note**
>
> **操作指南** · 假设你已完成[文本转语音 快速入门](/docs/zh/eleven-api/guides/cookbooks/speech-to-text)。

## 概述

ElevenLabs 实时文本转语音 API 使用 Scribe Realtime v2 模型，以极低延迟实时转录音频流。无论是构建语音助手、转录服务，还是任何需要实时语音识别的应用，这个基于 WebSocket 的 API 都会在你说话时提供部分转录，并在语音片段完成后提供已提交转录。

Scribe v2 Realtime 可在服务端实现实时音频转录，音频可来自 URL、文件或自有音频流。

服务端实现与客户端有以下区别：

* 使用 ElevenLabs API 密钥，而非一次性令牌。
* 支持直接从 URL 流式传输，无需手动分割音频块。

如需直接从麦克风流式传输音频，请参阅[客户端流式传输](/docs/zh/eleven-api/guides/how-to/speech-to-text/realtime/client-side-streaming)指南。

## 快速入门

> **Note**
>
> 本指南假设你已[设置 API 密钥和 SDK](/docs/zh/eleven-api/quickstart)。如尚未完成，请先完成快速入门。

#### 配置 SDK

SDK 提供两种实时转录音频的方式：从 URL 流式传输，或手动分割文件或自有音频流中的音频。

> **Info**
>
> 有关 API 支持的完整参数和选项列表，请参阅 [API 参考文档](/docs/zh/api-reference/speech-to-text/v-1-speech-to-text-realtime)。

#### 从 URL 流式传输

此示例展示如何使用官方 SDK 从 URL 流式传输音频文件。

> **Warning**
>
> 从 URL 流式传输时需要 `ffmpeg` 工具。请访问[其网站](https://ffmpeg.org/download.html)获取安装说明。

根据所选语言，新建名为 `example.py` 或 `example.mts` 的文件，并添加以下代码：

```python
from dotenv import load_dotenv
import os
import asyncio
from elevenlabs import ElevenLabs, RealtimeEvents, RealtimeUrlOptions

load_dotenv()

async def main():
    elevenlabs = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))

    # Create an event to signal when to stop
    stop_event = asyncio.Event()

    # Connect to a streaming audio URL
    connection = await elevenlabs.speech_to_text.realtime.connect(RealtimeUrlOptions(
        model_id="scribe_v2_realtime",
        url="https://npr-ice.streamguys1.com/live.mp3",
        include_timestamps=True,
    ))

    # Set up event handlers
    def on_session_started(data):
        print(f"Session started: {data}")

    def on_partial_transcript(data):
        print(f"Partial: {data.get('text', '')}")

    def on_committed_transcript(data):
        print(f"Committed: {data.get('text', '')}")

    # Committed transcripts with word-level timestamps. Only received when include_timestamps is set to True.
    def on_committed_transcript_with_timestamps(data):
        print(f"Committed with timestamps: {data.get('words', '')}")

    # Errors - will catch all errors, both server and websocket specific errors
    def on_error(error):
        print(f"Error: {error}")
        # Signal to stop on error
        stop_event.set()

    def on_close():
        print("Connection closed")

    # Register event handlers
    connection.on(RealtimeEvents.SESSION_STARTED, on_session_started)
    connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, on_partial_transcript)
    connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, on_committed_transcript)
    connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, on_committed_transcript_with_timestamps)
    connection.on(RealtimeEvents.ERROR, on_error)
    connection.on(RealtimeEvents.CLOSE, on_close)

    print("Transcribing audio stream... (Press Ctrl+C to stop)")

    try:
        # Wait until error occurs or connection closes
        await stop_event.wait()
    except KeyboardInterrupt:
        print("\nStopping transcription...")
    finally:
        await connection.close()

if __name__ == "__main__":
    asyncio.run(main())
```

```typescript
import "dotenv/config";
import { ElevenLabsClient, RealtimeEvents } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient();

const connection = await elevenlabs.speechToText.realtime.connect({
  modelId: "scribe_v2_realtime",
  url: "https://npr-ice.streamguys1.com/live.mp3",
  includeTimestamps: true,
});

connection.on(RealtimeEvents.SESSION_STARTED, (data) => {
  console.log("Session started", data);
});

connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (transcript) => {
  console.log("Partial transcript", transcript);
});

connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (transcript) => {
  console.log("Committed transcript", transcript);
});

connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (transcript) => {
  console.log("Committed with timestamps", transcript);
});

connection.on(RealtimeEvents.ERROR, (error) => {
  console.log("Error", error);
});

connection.on(RealtimeEvents.CLOSE, () => {
  console.log("Connection closed");
});

```

#### 手动分割音频

使用 Scribe 转录音频最简单的方式是使用官方 SDK。如果无法使用 SDK，也可以直接使用 WebSocket API。请参阅下方 WebSocket 示例，了解如何使用 WebSocket API。

此示例模拟对音频文件进行实时转录。

```python
import asyncio
import base64
import os
from dotenv import load_dotenv
from pathlib import Path
from elevenlabs import AudioFormat, CommitStrategy, ElevenLabs, RealtimeEvents, RealtimeAudioOptions
from pydub import AudioSegment
import sys

load_dotenv()

async def main():
    # Initialize the ElevenLabs client
    elevenlabs = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))

    # Create an event to signal when transcription is complete
    transcription_complete = asyncio.Event()

    # Connect with manual audio chunk mode
    connection = await elevenlabs.speech_to_text.realtime.connect(RealtimeAudioOptions(
        model_id="scribe_v2_realtime",
        audio_format=AudioFormat.PCM_16000,
        sample_rate=16000,
        commit_strategy=CommitStrategy.MANUAL,
        include_timestamps=True,
    ))

    # Set up event handlers
    def on_session_started(data):
        print(f"Session started: {data}")
        # Start sending audio once session is ready
        asyncio.create_task(send_audio())

    def on_partial_transcript(data):
        transcript = data.get('text', '')
        if transcript:
            print(f"Partial: {transcript}")

    def on_committed_transcript(data):
        transcript = data.get('text', '')
        print(f"\nCommitted transcript: {transcript}")

    def on_committed_transcript_with_timestamps(data):
        print(f"Timestamps: {data.get('words', '')}")
        print("-" * 50)
        # Signal that transcription is complete
        transcription_complete.set()

    def on_error(error):
        print(f"Error: {error}")
        transcription_complete.set()

    def on_close():
        print("Connection closed")
        transcription_complete.set()

    # Register event handlers
    connection.on(RealtimeEvents.SESSION_STARTED, on_session_started)
    connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, on_partial_transcript)
    connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, on_committed_transcript)
    connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, on_committed_transcript_with_timestamps)
    connection.on(RealtimeEvents.ERROR, on_error)
    connection.on(RealtimeEvents.CLOSE, on_close)

    # Convert audio file to PCM format if necessary
    def load_and_convert_audio(audio_path: str | Path, target_sample_rate: int = 16000) -> bytes:
        try:
            if str(audio_path).lower().endswith('.pcm'):
                with open(audio_path, 'rb') as f:
                    return f.read()

            audio = AudioSegment.from_file(audio_path)
            if audio.channels > 1:
                audio = audio.set_channels(1)
            if audio.frame_rate != target_sample_rate:
                audio = audio.set_frame_rate(target_sample_rate)
            audio = audio.set_sample_width(2)
            return audio.raw_data
        except Exception as e:
            print(f"Error loading audio: {e}")
            sys.exit(1)

    async def send_audio():
        """Send audio chunks from an audio file"""
        audio_file_path = Path("/path/to/audio.mp3")

        try:
            # Read the audio file
            audio_data = load_and_convert_audio(audio_file_path)

            # Split into chunks (1 second of audio = 32000 bytes at 16kHz, 16-bit)
            chunk_size = 32000
            chunks = [audio_data[i:i + chunk_size] for i in range(0, len(audio_data), chunk_size)]

            # Send each chunk
            for i, chunk in enumerate(chunks):
                chunk_base64 = base64.b64encode(chunk).decode('utf-8')
                await connection.send({"audio_base_64": chunk_base64, "sample_rate": 16000})

                # Wait 1 second between chunks (simulating real-time)
                if i < len(chunks) - 1:
                    await asyncio.sleep(1)

            # Small delay before committing to let last chunk process
            await asyncio.sleep(0.5)

            # Commit to finalize segment and get committed transcript
            await connection.commit()

        except Exception as e:
            print(f"Error sending audio: {e}")
            transcription_complete.set()

    try:
        # Wait for transcription to complete
        await transcription_complete.wait()
    except KeyboardInterrupt:
        print("\nStopping...")
    finally:
        await connection.close()

if __name__ == "__main__":
    asyncio.run(main())

```

```typescript
import "dotenv/config";
import * as fs from "node:fs";
import { ElevenLabsClient, RealtimeEvents, AudioFormat } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient();

const connection = await elevenlabs.speechToText.realtime.connect({
  modelId: "scribe_v2_realtime",
  audioFormat: AudioFormat.PCM_16000,
  sampleRate: 16000,
  includeTimestamps: true,
});

connection.on(RealtimeEvents.SESSION_STARTED, (data) => {
  console.log("Session started", data);
  sendAudio();
});

connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (transcript) => {
  console.log("Partial transcript", transcript);
});

connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (transcript) => {
  console.log("Committed transcript", transcript);
});

connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (transcript) => {
  console.log("Committed with timestamps", transcript);
});

connection.on(RealtimeEvents.ERROR, (error) => {
  console.log("Error", error);
});

connection.on(RealtimeEvents.CLOSE, () => {
  console.log("Connection closed");
});

async function sendAudio() {
  const pcmFilePath = "/path/to/audio.pcm";

  const chunkSize = 32000; // 1 second of 16kHz audio (16000 samples * 2 bytes per sample)

  // Read the entire file into a buffer
  const audioBuffer = fs.readFileSync(pcmFilePath);

  // Split the buffer into chunks of exactly chunkSize bytes
  const chunks: Buffer[] = [];
  for (let i = 0; i < audioBuffer.length; i += chunkSize) {
    const chunk = audioBuffer.subarray(i, i + chunkSize);
    chunks.push(chunk);
  }

  // Send each chunk via websocket payload
  for (let i = 0; i < chunks.length; i++) {
    const chunk = chunks[i];
    const chunkBase64 = chunk.toString("base64");

    connection.send({
      audioBase64: chunkBase64,
      sampleRate: 16000,
    });

    // Wait 1 second between chunks to simulate real-time streaming
    // (each chunk contains 1 second of audio at 16kHz)
    if (i < chunks.length - 1) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }

  // Small delay before final commit to let the last chunk process
  await new Promise(resolve => setTimeout(resolve, 500));

  // send final commit
  connection.commit();
}
```

**`Python WebSocket 示例`**

```python title="Python WebSocket 示例"
# Use this example if you are unable to use the SDK
import asyncio
import base64
import json
import websockets
from dotenv import load_dotenv
import os

load_dotenv()

async def send_audio(ws, audio_data):
    """Send audio chunks to the websocket"""
    chunk_size = 32000  # 1 second of 16kHz audio

    for i in range(0, len(audio_data), chunk_size):
        chunk = audio_data[i : i + chunk_size]
        await ws.send(
            json.dumps(
                {
                    "message_type": "input_audio_chunk",
                    "audio_base_64": base64.b64encode(chunk).decode(),
                    "commit": False,
                    "sample_rate": 16000,
                }
            )
        )
        # Wait 1 second between chunks to simulate real-time streaming
        await asyncio.sleep(1)

    # Small delay before final commit
    await asyncio.sleep(0.5)

    # Send final commit
    await ws.send(
        json.dumps(
            {
                "message_type": "input_audio_chunk",
                "audio_base_64": "",
                "commit": True,
                "sample_rate": 16000,
            }
        )
    )

async def receive_transcripts(ws):
    """Receive and process transcripts from the websocket"""
    while True:
        try:
            # Wait for 10 seconds for a message
            # Adjust the timeout in cases where audio files have more than 10 seconds before speech starts, or if the audio is longer than 10 seconds.
            message = await asyncio.wait_for(ws.recv(), timeout=10.0)
            data = json.loads(message)

            if data["message_type"] == "partial_transcript":
                print(f"Partial: {data['text']}")
            elif data["message_type"] == "committed_transcript":
                print(f"Committed: {data['text']}")
            elif data["message_type"] == "committed_transcript_with_timestamps":
                print(f"Committed with timestamps: {data['words']}")
                break
            elif data["message_type"] == "input_error":
                print(f"Error: {data}")
        except asyncio.TimeoutError:
            print("Timeout waiting for transcript")


async def transcribe():
    url = "wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime"
    headers = {"xi-api-key": os.getenv("ELEVENLABS_API_KEY")}

    async with websockets.connect(url, additional_headers=headers) as ws:
        # Connection established, wait for session_started
        session_msg = await ws.recv()
        print(f"Session started: {session_msg}")

        # Read audio file (16 kHz, mono, 16-bit PCM, little-endian)
        with open("/path/to/audio.pcm", "rb") as f:
            audio_data = f.read()

        # Run sending and receiving concurrently
        await asyncio.gather(
            send_audio(ws, audio_data),
            receive_transcripts(ws)
        )


asyncio.run(transcribe())
```

**`TypeScript WebSocket 示例`**

```typescript title="TypeScript WebSocket 示例"
    // Use this example if you are unable to use the SDK
    import "dotenv/config";
    import * as fs from "node:fs";
    // Make sure to install the "ws" library beforehand
    import WebSocket from "ws";

    const uri = "wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime";
    const websocket = new WebSocket(uri, {
      headers: {
        "xi-api-key": process.env.ELEVENLABS_API_KEY,
      },
    });

    websocket.on("open", async () => {
      console.log("WebSocket opened");
    });

    // Listen to the incoming message from the websocket connection
    websocket.on("message", function incoming(event) {
      const data = JSON.parse(event.toString());

      switch (data.message_type) {
        case "session_started":
          console.log("Session started", data);
          sendAudio();
          break;
        case "partial_transcript":
          console.log("Partial:", data);
          break;
        case "committed_transcript":
          console.log("Committed:", data);
          break;
        // Committed transcripts with word-level timestamps. Only received when "include_timestamps=true" is included in the query parameters
        case "committed_transcript_with_timestamps":
          console.log("Committed with timestamps:", data);
          websocket.close();
          break;
        default:
          console.log(data);
          break;
      }

    });

    async function sendAudio() {
      // 16 kHz, mono, 16-bit PCM, little-endian
      const pcmFilePath = "/path/to/audio.pcm";

      const chunkSize = 32000; // 1 second of 16kHz audio (16000 samples * 2 bytes per sample)

      // Read the entire file into a buffer
      const audioBuffer = fs.readFileSync(pcmFilePath);

      // Split the buffer into chunks of exactly chunkSize bytes
      const chunks: Buffer[] = [];
      for (let i = 0; i < audioBuffer.length; i += chunkSize) {
        const chunk = audioBuffer.subarray(i, i + chunkSize);
        chunks.push(chunk);
      }

      // Send each chunk via websocket payload
      for (let i = 0; i < chunks.length; i++) {
        const chunk = chunks[i];
        const chunkBase64 = chunk.toString("base64");

        websocket.send(JSON.stringify({
          message_type: "input_audio_chunk",
          audio_base_64: chunkBase64,
          commit: false,
          sample_rate: 16000,
        }));

        // Wait 1 second between chunks to simulate real-time streaming
        // (each chunk contains 1 second of audio at 16kHz)
        if (i < chunks.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 1000));
        }
      }

      // Small delay before final commit to let the last chunk process
      await new Promise(resolve => setTimeout(resolve, 500));

      // send final commit
      websocket.send(JSON.stringify({
        message_type: "input_audio_chunk",
        audio_base_64: "",
        commit: true,
        sample_rate: 16000,
      }));
    }
```

#### 执行代码

```python
python example.py
```

```typescript
npx tsx example.mts
```

控制台将输出音频文件的部分转录和已提交转录。

## 后续步骤

#### [转录与提交策略](/docs/zh/eleven-api/guides/how-to/speech-to-text/realtime/transcripts-and-commit-strategies)

控制转录的提交时机以及如何处理部分结果。

#### [事件参考](/docs/zh/eleven-api/guides/how-to/speech-to-text/realtime/event-reference)

实时 STT API 的完整事件和错误类型列表。