# EVA Gateway WebSocket API

EVA Gateway 提供基于 WebSocket 的 ASR 双向流式接口，可在持续发送音频的同时接收实时识别结果。该接口兼容 OpenAI Realtime API 的转录事件子集；模型、音频格式、采样率等参数与 HTTP ASR 接口保持一致，具体参数取值和模型限制请参阅 [ASR 参数说明](./models-and-parameters#asr)。

> **当前仅支持 ASR，暂不支持 TTS。** 仅部分 ASR 模型支持 WebSocket 实时识别，请在[模型广场](/models)中查看模型信息、接口能力和音频参数。

## 连接与鉴权

连接地址：

```text
wss://eva-gateway.autoarkai.com/v1/realtime
```

建立 WebSocket 连接时，须在握手请求头中携带 API Key：

```http
Authorization: Bearer <你的 API_KEY>
```

## 会话流程

1. 建立 WebSocket 连接，等待服务端返回 `session.created`。
2. 发送 `session.update`，配置模型、音频格式和服务端 VAD。
3. 收到 `session.updated` 后，通过 `input_audio_buffer.append` 持续发送 Base64 编码的音频块。
4. 服务端返回实时识别结果，并通过 `server_vad` 自动完成分句。
5. 音频发送完毕并收到最终结果后，由客户端关闭连接；若长时间没有语音输入，服务端会在静音超时后主动断开。

当前仅支持 `turn_detection: {"type": "server_vad"}` 自动分句，不支持客户端通过 `input_audio_buffer.commit` 或 `input_audio_buffer.clear` 手动控制分句。

## 静音超时

若 60 秒内没有识别出任何文本，服务端会发送 `error`（`code: "51005"`）并主动断开连接。

> **注意**：部分上游服务商在一定时间内未收到音频帧时会主动断开连接，请客户端自行处理此类问题，例如发送静音音频帧、监听连接关闭事件并按需重连等。

## 时序

```mermaid
sequenceDiagram
    participant C as 客户端
    participant G as EVA Gateway

    C->>G: WebSocket Upgrade + Authorization
    G-->>C: session.created
    C->>G: session.update
    G-->>C: session.updated
    loop 持续发送音频
        C->>G: input_audio_buffer.append(audio: Base64)
        G-->>C: conversation.item.input_audio_transcription.delta
    end
    G-->>C: input_audio_buffer.committed
    G-->>C: conversation.item.input_audio_transcription.completed
    C->>G: 关闭连接
```

每条消息均为 JSON 文本帧。`event_id` 由 Gateway 生成，用于定位事件；客户端不应依赖其具体格式。

## 客户端事件

### `session.update`

收到 `session.created` 后，客户端应发送此事件以初始化转录会话。模型通过 `session.audio.input.transcription.model` 指定。

```json
{
  "type": "session.update",
  "session": {
    "type": "transcription",
    "audio": {
      "input": {
        "format": {
          "audio_format": "pcm",
          "sample_rate": 16000,
          "channels": 1
        },
        "transcription": {
          "model": "<支持实时 ASR 的模型 ID>",
          "keywords": "奇多多,无界方舟"
        },
        "turn_detection": {
          "type": "server_vad"
        }
      }
    },
    "provider_params": "{\"key\": \"value\"}"
  }
}
```

| 参数                                         | 类型         | 说明                                                                                                                                                                                                             |
| -------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.type`                               | string       | 固定为 `transcription`。当前不支持完整的 `realtime` 会话。                                                                                                                                                       |
| `session.audio.input.transcription.model`    | string       | 模型广场中标记为支持实时 WebSocket 的 ASR 模型 ID。不同模型的音频格式和采样率要求可能不同。                                                                                                                      |
| `session.audio.input.transcription.keywords` | string，可选 | 热词提示，逗号分隔字符串（与 HTTP 转录接口的 `hotwords` 参数一致），按模型能力生效。                                                                                                                             |
| `session.audio.input.format.audio_format`    | string       | 音频编码类型。仅支持可按字节精确计时的 PCM 格式(`pcm*` 及其容器 `wav`);`opus/flac/aac` 等压缩格式因无法按转发字节计费而会被拒绝。经 `provider_params` 重写的格式同样受限。                                       |
| `session.audio.input.format.sample_rate`     | integer      | 必填。音频采样率，单位 Hz，必须为正整数并符合所选模型要求。                                                                                                                                                      |
| `session.audio.input.format.channels`        | integer      | 声道数。示例使用单声道 `1`。                                                                                                                                                                                     |
| `session.audio.input.turn_detection.type`    | string       | 当前固定为 `server_vad`，由模型服务自动判断分句结束。                                                                                                                                                            |
| `session.provider_params`                    | string，可选 | 原生参数透传通道，用于传入上表未覆盖的模型专属参数（与 HTTP 转录接口的 `provider_params` 参数一致），其值为一个 JSON 字符串。当某项参数同时通过上表字段设置时，以 `provider_params` 中的值为准（`model` 除外）。 |

### `input_audio_buffer.append`

发送一段音频。`audio` 必须是原始音频字节经 Base64 编码后得到的字符串。

```json
{
  "type": "input_audio_buffer.append",
  "audio": "<Base64 编码的音频字节>"
}
```

建议客户端按照音频实际速率发送时长约为 100-200 ms 的音频块。当前不支持 `input_audio_buffer.commit` 和 `input_audio_buffer.clear`，发送后会收到 `error` 事件。

## 服务端事件

### `session.created`

连接建立后，Gateway 发送以下事件：

```json
{
  "event_id": "evt_abc12345_1",
  "type": "session.created",
  "session": {
    "id": "abc12345-...",
    "object": "realtime.session"
  }
}
```

### `session.updated`

配置成功后，Gateway 返回规范化后的会话配置：

```json
{
  "event_id": "evt_abc12345_2",
  "type": "session.updated",
  "session": {
    "id": "abc12345-...",
    "object": "realtime.session",
    "audio": {
      "input": {
        "format": {
          "audio_format": "pcm",
          "sample_rate": 16000,
          "channels": 1
        },
        "transcription": {
          "model": "<请求的模型 ID>",
          "keywords": "奇多多,无界方舟"
        },
        "turn_detection": {
          "type": "server_vad"
        }
      }
    }
  }
}
```

### `conversation.item.input_audio_transcription.delta`

返回实时识别候选文本：

```json
{
  "event_id": "evt_abc12345_8",
  "type": "conversation.item.input_audio_transcription.delta",
  "item_id": "item_turn_001",
  "content_index": 0,
  "delta": "你好，欢迎使用"
}
```

`delta` 的内容取决于具体模型，可能是当前分句的增量片段，也可能是当前分句的完整候选文本，客户端不要无条件拼接所有 `delta`。

服务端 VAD 确定分句后，事件顺序为 `committed` → `completed`：

### `input_audio_buffer.committed`

```json
{
  "event_id": "evt_abc12345_9",
  "type": "input_audio_buffer.committed",
  "item_id": "item_turn_001",
  "previous_item_id": null
}
```

### `conversation.item.input_audio_transcription.completed`

```json
{
  "event_id": "evt_abc12345_10",
  "type": "conversation.item.input_audio_transcription.completed",
  "item_id": "item_turn_001",
  "content_index": 0,
  "transcript": "你好，欢迎使用 EVA Gateway。",
  "usage": {
    "duration_ms": 1840
  },
  "credit": 123
}
```

### `error`

```json
{
  "event_id": "evt_abc12345_11",
  "type": "error",
  "error": {
    "code": "40003",
    "type": "client",
    "message": "input_audio_buffer.commit is not supported; turns are detected automatically (server_vad)",
    "param": null
  }
}
```

## Python 示例

```python
import asyncio
import base64
import json

import websockets

async def main():
    async with websockets.connect(
        "wss://eva-gateway.autoarkai.com/v1/realtime",
        additional_headers={"Authorization": "Bearer <你的 API_KEY>"},
    ) as ws:
        await ws.recv()  # session.created

        # 配置会话
        await ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "type": "transcription",
                "audio": {
                    "input": {
                        "format": {"audio_format": "pcm", "sample_rate": 16000, "channels": 1},
                        "transcription": {"model": "<支持实时 ASR 的模型 ID>"},
                        "turn_detection": {"type": "server_vad"},
                    }
                }
            }
        }))
        await ws.recv()  # session.updated

        # 发送音频（16kHz 单声道 PCM，按实际速率每 200ms 发送 200ms 音频）
        CHUNK_BYTES = 6400  # 200ms @ 16kHz/16bit/mono
        with open("audio.pcm", "rb") as f:
            while data := f.read(CHUNK_BYTES):
                await ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": base64.b64encode(data).decode(),
                }))
                await asyncio.sleep(0.2)

        # 音频发完后追加 2s 静音，让 server_vad 判定句子结束、下发 completed
        SILENCE = b"\x00" * (16000 * 4)  # 2s 静音（16kHz/16bit/单声道）
        for i in range(0, len(SILENCE), CHUNK_BYTES):
            await ws.send(json.dumps({
                "type": "input_audio_buffer.append",
                "audio": base64.b64encode(SILENCE[i:i + CHUNK_BYTES]).decode(),
            }))
            await asyncio.sleep(0.2)

        # 接收识别结果
        async for raw in ws:
            event = json.loads(raw)
            if event["type"] == "conversation.item.input_audio_transcription.delta":
                print("实时：", event["delta"])
            elif event["type"] == "conversation.item.input_audio_transcription.completed":
                print("最终：", event["transcript"])
                break

asyncio.run(main())
```

运行前请将 `API_KEY` 替换为你的密钥，并按所选模型的要求准备音频文件（上述示例对应 16kHz / 单声道 PCM）。
