> 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，通过麦克风或手动切分音频来实时转录。

客户端实现与服务器端有以下不同：

* 需要单次使用令牌：这是一种临时令牌，可在不暴露 API 密钥的情况下连接 API。
* 可将麦克风音频直接传输到 API 进行转录，无需手动切分音频。

如需从 URL 流式传输音频，请参阅[服务器端流式传输](/docs/zh/eleven-api/guides/how-to/speech-to-text/realtime/server-side-streaming)指南。

## 快速入门

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

#### 安装 SDK

**`React`**

```bash title="React"
npm install @elevenlabs/react @elevenlabs/elevenlabs-js
```

**`JavaScript`**

```bash title="JavaScript"
npm install @elevenlabs/client @elevenlabs/elevenlabs-js
```

#### 创建令牌

要使用客户端 SDK，需要创建单次使用令牌。这是一种临时令牌，可在不暴露 API 密钥的情况下连接 API。可通过服务器端的 ElevenLabs API 创建。

> **Warning**
>
> 切勿将 API 密钥暴露给客户端。

```typescript
// Node.js server
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

app.get("/scribe-token", yourAuthMiddleware, async (req, res) => {
  const token = await elevenlabs.tokens.singleUse.create("realtime_scribe");

  res.json(token);
});
```

> **Info**
>
> 单次使用令牌会在 15 分钟后自动过期。

#### 启动转录会话

可通过麦克风或手动切分音频进行转录。音频可以是文件或流。

> **Info**
>
> 如需了解 API 支持的全部参数和选项，请参阅 [API 参考文档](/docs/zh/api-reference/speech-to-text/v-1-speech-to-text-realtime)。

#### 麦克风

**`React`**

```typescript title="React"
import { useScribe } from "@elevenlabs/react";

function MyComponent() {
  const scribe = useScribe({
    modelId: "scribe_v2_realtime",
    onPartialTranscript: (data) => {
      console.log("Partial:", data.text);
    },
    onCommittedTranscript: (data) => {
      console.log("Committed:", data.text);
    },
    onCommittedTranscriptWithTimestamps: (data) => {
      console.log("Committed with timestamps:", data.text);
      console.log("Timestamps:", data.words);
    },
  });

  const handleStart = async () => {
    // Fetch a single use token from the server
    const token = await fetchTokenFromServer();

    await scribe.connect({
      token,
      microphone: {
        echoCancellation: true,
        noiseSuppression: true,
      },
    });
  };

  return (
    <div>
      <button onClick={handleStart} disabled={scribe.isConnected}>
        Start Recording
      </button>
      <button onClick={scribe.disconnect} disabled={!scribe.isConnected}>
        Stop
      </button>

      {scribe.partialTranscript && <p>Live: {scribe.partialTranscript}</p>}

      <div>
        {scribe.committedTranscripts.map((t) => (
          <p key={t.id}>{t.text}</p>
        ))}
      </div>
    </div>
  );
}
```

**`JavaScript`**

```typescript title="JavaScript"
// Client side
import { Scribe, RealtimeEvents } from "@elevenlabs/client";

// Ensure you have authentication headers set up
const response = await fetch("/scribe-token", yourAuthHeaders);
const { token } = await response.json();

const connection = Scribe.connect({
  token,
  modelId: "scribe_v2_realtime",
  includeTimestamps: true,
  microphone: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
  },
});

// Set up event handlers

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

// Partial transcripts (interim results), use this in your UI to show the live transcript
connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (data) => {
  console.log("Partial:", data.text);
});

// Committed transcripts
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (data) => {
  console.log("Committed:", data.text);
});

// Committed transcripts with word-level timestamps. Only received when includeTimestamps is set to true.
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (data) => {
  console.log("Committed:", data.text);
  console.log("Timestamps:", data.words);
});

// Errors - will catch all errors, both server and websocket specific errors
connection.on(RealtimeEvents.ERROR, (error) => {
  console.error("Error:", error);
});

// Connection opened
connection.on(RealtimeEvents.OPEN, () => {
  console.log("Connection opened");
});

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

// When you are done, close the connection
connection.close();
```

#### 手动切分音频

**`React`**

```typescript title="React"
import { useScribe, AudioFormat } from "@elevenlabs/react";

function FileTranscription() {
  const [file, setFile] = useState<File | null>(null);
  const scribe = useScribe({
    modelId: "scribe_v2_realtime",
    audioFormat: AudioFormat.PCM_16000,
    sampleRate: 16000,
  });

  const transcribeFile = async () => {
    if (!file) return;

    // Fetch a single use token from the server
    const token = await fetchToken();
    await scribe.connect({ token });

    // Decode audio file
    const arrayBuffer = await file.arrayBuffer();
    const audioContext = new AudioContext({ sampleRate: 16000 });
    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

    // Convert to PCM16
    const channelData = audioBuffer.getChannelData(0);
    const pcmData = new Int16Array(channelData.length);

    for (let i = 0; i < channelData.length; i++) {
      const sample = Math.max(-1, Math.min(1, channelData[i]));
      pcmData[i] = sample < 0 ? sample * 32768 : sample * 32767;
    }

    // Send in chunks
    const chunkSize = 4096;
    for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
      const chunk = pcmData.slice(offset, offset + chunkSize);
      const bytes = new Uint8Array(chunk.buffer);
      const base64 = btoa(String.fromCharCode(...bytes));

      scribe.sendAudio(base64);
      await new Promise((resolve) => setTimeout(resolve, 50));
    }

    // Commit transcription
    scribe.commit();
  };

  return (
    <div>
      <input
        type="file"
        accept="audio/*"
        onChange={(e) => setFile(e.target.files?.[0] || null)}
      />
      <button onClick={transcribeFile} disabled={!file || scribe.isConnected}>
        Transcribe
      </button>

      {scribe.committedTranscripts.map((transcript) => (
        <div key={transcript.id}>{transcript.text}</div>
      ))}
    </div>
  );
}
```

**`JavaScript`**

```typescript title="JavaScript"
import { Scribe, AudioFormat, RealtimeEvents, CommitStrategy } from "@elevenlabs/client";

// Ensure you have authentication headers set up
const response = await fetch("/scribe-token", yourAuthHeaders);
const { token } = await response.json();

const connection = Scribe.connect({
  token,
  modelId: "scribe_v2_realtime",
  includeTimestamps: true,
  audioFormat: AudioFormat.PCM_16000,
  sampleRate: 16000,
  commitStrategy: CommitStrategy.MANUAL,
});

// Set up event handlers
connection.on(RealtimeEvents.SESSION_STARTED, () => {
  console.log("Session started");
  sendAudio();
});

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

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

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

// Committed transcripts with word-level timestamps. Only received when includeTimestamps is set to true.
connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (data) => {
  console.log("Committed:", data.text);
  console.log("Timestamps:", data.words);

  // Committed transcript received, close the connection
  connection.close();
});

async function sendAudio() {
  // Get file from input element
  const fileInput = document.querySelector('input[type="file"]');
  const audioFile = fileInput.files[0];

  // Read file as ArrayBuffer
  const arrayBuffer = await audioFile.arrayBuffer();
  const audioData = new Uint8Array(arrayBuffer);

  // Convert to base64 and send in chunks
  const chunkSize = 8192; // 8KB chunks
  for (let i = 0; i < audioData.length; i += chunkSize) {
    const chunk = audioData.slice(i, i + chunkSize);
    const base64 = btoa(String.fromCharCode(...chunk));

    // Send audio chunk
    connection.send({ audioBase64: base64 });

    // Optional: Add delay to simulate real-time streaming
    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  // Signal end of audio segment
  connection.commit();
}
```

## 后续步骤

#### [服务器端流式传输](/docs/zh/eleven-api/guides/how-to/speech-to-text/realtime/server-side-streaming)

使用相同的 WebSocket API 在服务器端转录音频流。

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

控制转录文本的确认时机及部分结果的处理方式。