> 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.

# Speech Engine 快速入门

本指南将带你使用 Speech Engine 构建语音智能体。你需要设置一个服务器，将 LLM 连接到 ElevenLabs，然后接入浏览器客户端，让用户能与你的智能体进行语音对话。

> **Tip**
>
> 使用 [ElevenLabs Speech Engine skill](https://github.com/elevenlabs/skills/tree/main/speech-engine) 为聊天智能体添加语音功能：
>
> ```bash
> npx skills add elevenlabs/skills --skill speech-engine
> ```

## Speech Engine 的工作原理

Speech Engine 将 LLM 连接到 ElevenLabs，让用户能与智能体交谈并听到其回复。ElevenLabs 负责语音转文本和文本转语音；服务器提供 LLM 逻辑。

```mermaid
sequenceDiagram
    participant Browser
    participant ElevenLabs

    box Your Server
        participant SDK as Speech Engine SDK
        participant LLM
    end

    Browser->>ElevenLabs: User speaks (audio)
    ElevenLabs->>SDK: Transcript (WebSocket)
    SDK->>LLM: Conversation history
    LLM->>SDK: Streamed response
    SDK->>ElevenLabs: Text chunks
    ElevenLabs->>Browser: Agent speaks (audio)
```

每个 WebSocket 连接代表一次对话。用户说话时，ElevenLabs 会转写音频并将转写文本发送到服务器。服务器将其传给 LLM，再将回复流式传回。ElevenLabs 将文本转换为语音，并在浏览器中播放。SDK 会处理连接管理、轮次控制和打断检测。

## 前提条件

本教程使用 OpenAI 的 API 作为 LLM。需要在 `OPENAI_API_KEY` 环境变量中设置 OpenAI API 密钥。

## 服务器设置

#### 创建 API 密钥

[在控制台中创建 API 密钥](https://elevenlabs.io/app/api/api-keys)，用于安全地[访问 API](/docs/zh/api-reference/authentication)。

将密钥作为受管理的机密信息存储，并根据偏好，通过 `.env` 文件将其作为环境变量传递给 SDK，或直接写入应用配置。

**`.env`**

```js title=".env"
ELEVENLABS_API_KEY=<your_api_key_here>
```

#### 安装依赖

```python
pip install elevenlabs openai python-dotenv
```

```typescript
npm install @elevenlabs/elevenlabs-js openai
```

#### 公开服务器

Speech Engine 需要可从公网访问的 URL。使用 [ngrok](https://ngrok.com) 公开本地服务器。服务器尚未构建完成，但 ngrok 必须先运行，以便在下一步中使用该 URL。

```bash
ngrok http 3001
```

复制转发 URL（例如 `https://abc123.ngrok.io`）。

#### 创建 Speech Engine 实例

使用 SDK 创建 Speech Engine 实例，并将附加了 `/ws` 路径的 ngrok URL 作为 WebSocket URL 传入。

**`create_engine.py`**

```python title="create_engine.py"
import asyncio
from dotenv import load_dotenv
from elevenlabs import AsyncElevenLabs

load_dotenv()

elevenlabs = AsyncElevenLabs(
    api_key=os.getenv("ELEVENLABS_API_KEY"),
)


async def main():
    engine = await elevenlabs.speech_engine.create(
        name="My Speech Engine",
        speech_engine={
            # Note we use the wss protocol instead of https
            "ws_url": "wss://abc123.ngrok.io/ws",
        },
    )

    print(f"Speech Engine ID: {engine.engine_id}")


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

**`create-engine.mts`**

```typescript title="create-engine.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

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

const engine = await elevenlabs.speechEngine.create({
  name: "My Speech Engine",
  speechEngine: {
    // Note we use the wss protocol instead of https
    wsUrl: "wss://abc123.ngrok.io/ws",
  },
});

console.log("Speech Engine ID:", engine.engineId);
```

运行此脚本，并复制 Speech Engine ID（例如 `seng_8k3m9xr4hjnfg983brhmhkd98n6`），用于下一步。

#### 创建服务器

创建名为 `server.py` 或 `server.mts` 的文件，并填入以下内容。这会设置服务器，在 `/ws` 路径附加 Speech Engine，并使用 OpenAI 生成回复。

**`server.py`**

```python maxLines=0 title="server.py"
import asyncio
import os

from dotenv import load_dotenv
from openai import AsyncOpenAI
from elevenlabs import AsyncElevenLabs

load_dotenv()

# Replace with your Speech Engine ID from step 4
SPEECH_ENGINE_ID = "seng_8k3m9xr4hjnfg983brhmhkd98n6"

openai = AsyncOpenAI(
  api_key=os.getenv("OPENAI_API_KEY"),
)
elevenlabs = AsyncElevenLabs(
  api_key=os.getenv("ELEVENLABS_API_KEY"),
)


def on_init(conversation_id, session):
    print(f"Session started: {conversation_id}")


async def on_transcript(transcript, session):
    stream = await openai.responses.create(
        model="gpt-4o",
        instructions="You are a helpful voice assistant. Keep responses concise and conversational.",
        input=[
            {"role": "assistant" if m.role == "agent" else m.role, "content": m.content}
            for m in transcript
        ],
        stream=True,
    )

    await session.send_response(stream)


def on_close(session):
    print(f"Session ended: {session.conversation_id}")


def on_error(err, session):
    print(f"Error: {err}")


async def main():
    engine = await elevenlabs.speech_engine.get(SPEECH_ENGINE_ID)

    await engine.serve(
        port=3001,
        path="/ws",
        debug=True,
        on_init=on_init,
        on_transcript=on_transcript,
        on_close=on_close,
        on_error=on_error,
    )


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

**`server.mts`**

```typescript maxLines=0 title="server.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { createServer } from "node:http";
import OpenAI from "openai";
import "dotenv/config";

// Replace with your Speech Engine ID from step 4
const SPEECH_ENGINE_ID = "seng_8k3m9xr4hjnfg983brhmhkd98n6";

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

const httpServer = createServer();

await elevenlabs.speechEngine.attach(SPEECH_ENGINE_ID, httpServer, "/ws", {
  debug: true,

  onInit(conversationId) {
    console.log("Session started:", conversationId);
  },

  async onTranscript(transcript, signal, session) {
    const response = await openai.responses.create(
      {
        model: "gpt-4o",
        instructions:
          "You are a helpful voice assistant. Keep responses concise and conversational.",
        input: transcript.map((m) => ({
          role: m.role === "agent" ? "assistant" : m.role,
          content: m.content,
        })),
        stream: true,
      },
      { signal },
    );

    session.sendResponse(response);
  },

  onClose(session) {
    console.log("Session ended:", session.conversationId);
  },

  onError(err) {
    console.error("Error:", err);
  },
});

httpServer.listen(3001, () => {
  console.log("Speech Engine server listening on port 3001");
});
```

`onTranscript` / `on_transcript` 回调会接收完整对话历史和当前会话。TypeScript SDK 还提供 `AbortSignal`，当用户在回复过程中打断时会触发。将 `signal` 传给 OpenAI 调用，即可在打断时自动取消 LLM 请求。

`sendResponse()` / `send_response()` 可接受字符串、异步可迭代对象，或来自 OpenAI、Anthropic 或 Google Gemini 的流。SDK 会自动提取文本内容。

> **Warning**
>
> 上述示例会将用户的完整转写文本传给 LLM。在生产环境中，应添加防护措施，防止任何提示词注入或操纵尝试。

#### 启动服务器

```python
python server.py
```

```typescript
npx tsx server.mts
```

## 客户端设置

#### 安装客户端 SDK

#### React

```bash
npm install @elevenlabs/react
```

#### JavaScript

```bash
npm install @elevenlabs/client
```

#### 创建令牌端点

添加一个服务器端端点来生成对话令牌。这样可避免将 API 密钥暴露在浏览器中，并使用 WebRTC 获得最佳音频质量。

**`token_server.py`**

```python title="token_server.py"
import os

from dotenv import load_dotenv
from flask import Flask, jsonify
from elevenlabs import ElevenLabs

load_dotenv()

app = Flask(__name__)
elevenlabs = ElevenLabs(
    api_key=os.getenv("ELEVENLABS_API_KEY"),
)


@app.route("/api/token")
def get_token():
    # Replace with your Speech Engine ID from step 4 of the server setup
    speech_engine_id = "seng_8k3m9xr4hjnfg983brhmhkd98n6"

    response = elevenlabs.conversational_ai.conversations.get_webrtc_token(
        agent_id=speech_engine_id,
    )

    return jsonify(token=response.token)


if __name__ == "__main__":
    app.run(port=3002)
```

**`token-server.mts`**

```typescript title="token-server.mts"
import express from "express";
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

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

app.get("/api/token", async (req, res) => {
  // Replace with your Speech Engine ID from step 4 of the server setup
  const speechEngineId = "seng_8k3m9xr4hjnfg983brhmhkd98n6";

  const response = await elevenlabs.conversationalAi.conversations.getWebrtcToken({
    agentId: speechEngineId,
  });

  res.json({ token: response.token });
});

app.listen(3002, () => {
  console.log("Token server listening on port 3002");
});
```

#### 构建对话 UI

从服务器获取对话令牌，并用它启动会话。

#### React

**`App.tsx`**

```tsx title="App.tsx"
import { useConversation } from "@elevenlabs/react";
import { useCallback } from "react";

async function getToken(): Promise<string> {
  const response = await fetch("/api/token");
  if (!response.ok) {
    throw Error("Failed to get conversation token");
  }
  const data = await response.json();
  return data.token;
}

export default function App() {
  const conversation = useConversation({
    onConnect: () => console.log("Connected"),
    onDisconnect: () => console.log("Disconnected"),
    onError: (error: Error) => console.error("Error:", error),
  });

  const startConversation = useCallback(async () => {
    await navigator.mediaDevices.getUserMedia({ audio: true });
    const token = await getToken();
    await conversation.startSession({ conversationToken: token });
  }, [conversation]);

  const stopConversation = useCallback(async () => {
    await conversation.endSession();
  }, [conversation]);

  return (
    <div>
      <p>Status: {conversation.status}</p>
      <button onClick={startConversation} disabled={conversation.status === "connected"}>
        Start conversation
      </button>
      <button onClick={stopConversation} disabled={conversation.status !== "connected"}>
        End conversation
      </button>
    </div>
  );
}
```

#### JavaScript

**`main.ts`**

```typescript title="main.ts"
import { Conversation } from "@elevenlabs/client";

let conversation: Conversation | null = null;

async function getToken(): Promise<string> {
  const response = await fetch("/api/token");
  if (!response.ok) throw Error("Failed to get conversation token");
  const data = await response.json();
  return data.token;
}

document.getElementById("start")!.addEventListener("click", async () => {
  await navigator.mediaDevices.getUserMedia({ audio: true });
  const token = await getToken();

  conversation = await Conversation.startSession({
    conversationToken: token,
    onConnect: () => {
      document.getElementById("status")!.textContent = "Connected";
      (document.getElementById("start") as HTMLButtonElement).disabled = true;
      (document.getElementById("stop") as HTMLButtonElement).disabled = false;
    },
    onDisconnect: () => {
      document.getElementById("status")!.textContent = "Disconnected";
      (document.getElementById("start") as HTMLButtonElement).disabled = false;
      (document.getElementById("stop") as HTMLButtonElement).disabled = true;
    },
    onError: (error) => console.error("Error:", error),
  });
});

document.getElementById("stop")!.addEventListener("click", () => {
  if (conversation) conversation.endSession();
});
```

#### 试用

确保以下 3 个进程正在运行：

1. **ngrok** - 转发到端口 3001
2. **Speech Engine 服务器** - `python server.py` 或 `npx tsx server.mts`
3. **令牌服务器** - `npx tsx token-server.mts` 或 `python token_server.py`

在浏览器中打开客户端应用，然后点击 **开始对话**。系统提示时授予麦克风访问权限，然后开始说话。你应该能通过扬声器听到智能体的回复。

如果在服务器上启用了 `debug: true`，控制台会记录传入的转写文本和传出的回复。

## 会话事件

| 事件                | TypeScript 回调  | Python 回调       | 说明                     |
| ----------------- | -------------- | --------------- | ---------------------- |
| `user_transcript` | `onTranscript` | `on_transcript` | 用户语音已转写。包含完整对话历史和中止信号。 |
| `init`            | `onInit`       | `on_init`       | 会话已使用对话 ID 初始化。        |
| `close`           | `onClose`      | `on_close`      | 已与 ElevenLabs 正常断开连接。  |
| `disconnected`    | `onDisconnect` | `on_disconnect` | WebSocket 意外断开。        |
| `error`           | `onError`      | `on_error`      | 协议或 WebSocket 错误。      |

## 配置智能体的首条消息

默认情况下，智能体会等待用户先说话。若要让智能体在对话开始时问候用户，请在启动会话时，在客户端的 `overrides` 选项中设置首条消息。

要允许智能体先说话，需要更新 Speech Engine 资源，以允许从客户端进行此设置。

```python
engine = await elevenlabs.speech_engine.update(
    speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
    overrides={
      "first_message": True,
    },
)
```

```typescript
const engine = await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  overrides: {
    firstMessage: true,
  },
});
```

然后在客户端 SDK 中配置首条消息。

#### React

```tsx
conversation.startSession({
  conversationToken: token,
  overrides: {
    agent: {
      firstMessage: "Hello! How can I help you today?",
    },
  },
});
```

#### JavaScript

```typescript
const conversation = await Conversation.startSession({
  conversationToken: token,
  overrides: {
    agent: {
      firstMessage: "Hello! How can I help you today?",
    },
  },
});
```

连接建立后，智能体会立即说出首条消息。它不会触发服务器上的 `onTranscript` 回调，而是完全由 ElevenLabs 端处理。

## 配置最长对话时长

默认情况下，对话会在 600 秒（10 分钟）后结束。Speech Engine 支持最长 2,400 秒（40 分钟）的对话。若要允许更长的对话，请使用更高的 `max_duration_seconds` 更新 Speech Engine 资源：

```python
engine = await elevenlabs.speech_engine.update(
    speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
    conversation={
        "max_duration_seconds": 2400,
    },
)
```

```typescript
const engine = await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  conversation: {
    maxDurationSeconds: 2400,
  },
});
```

你也可以在控制台中设置：打开 Speech Engine，前往 **高级** 标签页，然后在 **对话行为** 下设置 **最长对话时长**。

> **Warning**
>
> 不支持超过 40 分钟的对话，即使
> `max_duration_seconds` 设置得更高，也可能会断开连接。如需更长的对话，请[联系我们的 团队](https://elevenlabs.io/contact-sales)。

## 后续步骤

#### [JavaScript SDK 参考文档](/docs/zh/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

JavaScript SDK 的类、方法和事件。

#### [Python SDK 参考文档](/docs/zh/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Python SDK 的类、方法和事件。

#### [API 参考文档](/docs/zh/api-reference/speech-engine/create)

查看所有 Speech Engine 参数和响应格式。

#### [Next.js 示例应用](https://github.com/elevenlabs/examples/tree/main/speech-engine/nextjs/quickstart)

在本地运行完整的 Speech Engine 快速入门应用。