> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gravitex.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 实时语音（Realtime）

> WebSocket 实时语音对话，兼容 OpenAI Realtime API

## 简介

通过 **WebSocket** 建立低延迟实时语音会话，兼容 [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime)。连接后按事件协议收发音频与文本，适用于语音助手、实时翻译等场景。

Base URL：`wss://api.gravitex.ai/v1/realtime`

<Note>
  必须使用 WebSocket 协议，不是普通 HTTP POST。`model` 必须作为 **查询参数** 传入，否则网关返回 400。
</Note>

## 认证

建立 WebSocket 连接时在请求头携带：

<ParamField header="Authorization" type="string" required>
  Bearer Token，如 `Bearer sk-xxxxxxxxxx`
</ParamField>

<ParamField header="OpenAI-Beta" type="string" required>
  固定为 `realtime=v1`
</ParamField>

浏览器环境无法自定义 WebSocket Header，可使用 OpenAI 子协议传 Key（与 Header 方式等价，由网关解析）：

```
Sec-WebSocket-Protocol: realtime, openai-insecure-api-key.sk-xxxxxxxxxx, openai-beta.realtime-v1
```

## 连接

```
wss://api.gravitex.ai/v1/realtime?model=gpt-realtime-1.5
```

<ParamField query="model" type="string" required>
  实时模型 ID，如 `gpt-realtime-1.5`、`gpt-realtime-2`。须与当前 API Key 可用模型列表一致。
</ParamField>

连接成功时服务端返回 **101 Switching Protocols**。随后会收到 `session.created`，客户端应发送 `session.update` 完成会话配置。

## 会话配置

连接成功后发送 `session.update` 设置指令、输出模态与音频格式：

```json theme={null}
{
  "type": "session.update",
  "session": {
    "type": "realtime",
    "instructions": "You are a concise assistant. Reply in Chinese. 回答用中文回答",
    "output_modalities": ["audio"],
    "audio": {
      "input":  { "format": { "type": "audio/pcm", "rate": 24000 } },
      "output": { "format": { "type": "audio/pcm", "rate": 24000 }, "voice": "sage" }
    }
  }
}
```

| 字段                    | 说明                                       |
| --------------------- | ---------------------------------------- |
| `session.type`        | 固定为 `realtime`                           |
| `instructions`        | 系统指令，控制助手行为与回复语言                         |
| `output_modalities`   | `["audio"]` 输出语音；`["text"]` 仅输出文字（不播放音频） |
| `audio.input.format`  | 输入音频格式，**PCM16、24 kHz、单声道**              |
| `audio.output.format` | 输出音频格式，同上                                |
| `audio.output.voice`  | 音色，见下方可选值                                |

**可选音色**（`output_modalities` 含 `audio` 时有效）：

`alloy` · `ash` · `ballad` · `coral` · `echo` · `sage` · `shimmer` · `verse` · `marin`

等待 `session.updated` 后即可开始对话。

## 发送消息

每轮对话分两步：

1. `conversation.item.create` — 写入用户消息
2. `response.create` — 触发模型生成回复

### 文字输入

```json theme={null}
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "user",
    "content": [
      { "type": "input_text", "text": "你好，请用中文介绍一下自己" }
    ]
  }
}
```

### 音频输入

录音须为 **PCM16 raw bytes**（无 WAV 头），Base64 编码后发送：

```json theme={null}
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "user",
    "content": [
      { "type": "input_audio", "audio": "<base64_pcm16>" }
    ]
  }
}
```

同一轮可组合多种输入，例如图片 + 文字、图片 + 录音：

```json theme={null}
{
  "type": "conversation.item.create",
  "item": {
    "type": "message",
    "role": "user",
    "content": [
      { "type": "ms_image", "image": "data:image/jpeg;base64,<base64>" },
      { "type": "input_text", "text": "这张图里有什么？" }
    ]
  }
}
```

<Warning>
  **图片输入限制**：部分上游（如 Azure OpenAI Realtime）目前不支持图片，仅支持 `input_text` 与 `input_audio`，发送图片可能触发服务端 500 错误。Vision 能力仅在非 Realtime 的 Chat Completions / Responses 接口中可用。详见 [Microsoft 官方说明](https://learn.microsoft.com/en-us/answers/questions/5706094/how-to-use-input-image-function-in-azure-gpt-realt)。图片 Base64 长度建议不超过 **1,048,576** 字符（约 750 KB 原图）。
</Warning>

写入消息后发送触发回复：

```json theme={null}
{ "type": "response.create" }
```

## 接收回复

常见服务端事件：

| 事件类型                                                                         | 说明                                    |
| ---------------------------------------------------------------------------- | ------------------------------------- |
| `response.text.delta` / `response.output_text.delta`                         | 文字回复增量（`output_modalities: ["text"]`） |
| `response.audio_transcript.delta` / `response.output_audio_transcript.delta` | 音频回复的文字转写增量                           |
| `response.audio.delta` / `response.output_audio.delta`                       | 音频 PCM16 增量（Base64），需解码后播放            |
| `response.done`                                                              | 本轮结束，含 `usage` 用量                     |
| `error`                                                                      | 错误；部分上游错误后连接会被关闭，需重连                  |

音频增量字段可能是 `audio` 或 `delta`，客户端应兼容两种写法。

## 用量说明

每次 `response.done` 返回的是**本次 Response 的用量**，不是会话累计。官方原文：*"The tokens used for a Response can be read from the response.done event"*。多轮对话时建议同时记录每轮用量与会话累计用量。

`usage` 结构示例：

```json theme={null}
{
  "total_tokens": 320,
  "input_tokens": 180,
  "output_tokens": 140,
  "input_token_details": {
    "text_tokens": 50,
    "audio_tokens": 130,
    "image_tokens": 0,
    "cached_tokens": 20,
    "cached_tokens_details": {
      "text_tokens": 8,
      "audio_tokens": 12
    }
  },
  "output_token_details": {
    "text_tokens": 40,
    "audio_tokens": 100
  }
}
```

### 字段含义

| 字段                                   | 说明                                                           |
| ------------------------------------ | ------------------------------------------------------------ |
| `total_tokens`                       | `input_tokens` + `output_tokens`                             |
| `input_tokens`                       | 发给模型的全部输入（**含对话历史**）                                         |
| `input_token_details.text_tokens`    | 输入中的文字 token                                                 |
| `input_token_details.audio_tokens`   | 输入中的音频 token                                                 |
| `input_token_details.image_tokens`   | 输入中的图片 token                                                 |
| `input_token_details.cached_tokens`  | `input_tokens` 的**子集**（不是额外叠加），命中 prompt cache 的部分；计费时按缓存价打折 |
| `cached_tokens_details.text_tokens`  | 缓存中属于文字的部分（OpenAI 原生返回）                                      |
| `cached_tokens_details.audio_tokens` | 缓存中属于音频的部分（OpenAI 原生返回）                                      |
| `output_tokens`                      | 本轮生成的 token                                                  |
| `output_token_details.text_tokens`   | 输出中的文字 token，即音频回复的 transcript                               |
| `output_token_details.audio_tokens`  | 输出中的音频 token                                                 |

<Note>
  1. **output.text\_tokens**：即使选择音频输出，也会因自动 transcript 产生文字 token，属正常计费。详见 [OpenAI Realtime 计费说明](https://developers.openai.com/api/docs/guides/realtime-costs)。
  2. **input.cached\_tokens**：是 `input_tokens` 的子集，表示命中缓存的部分，不是额外叠加。OpenAI 原生会返回 `cached_tokens_details`（拆分文字/音频缓存）；部分上游（如 Azure GA）不返回时，网关会按文字/音频输入比例估算。
  3. **多轮会话成本**：每次 Response 都会把整个对话历史发给模型，越到后面 `input_tokens` 越大；但前几轮内容大概率命中 prompt cache，折扣很高（如 `gpt-realtime-2` 音频缓存 $0.40/1M，相比音频输入 $32/1M 折扣 98.75%），实际增量成本远小于原始输入价格。官方原文：*"turns later in the session will be more expensive"*。
</Note>

## Python 完整示例

依赖：`pip install websockets pyaudio`

```python theme={null}
import asyncio
import base64
import json
import websockets

BASE_URL = "wss://api.gravitex.ai/v1/realtime"
API_KEY  = "sk-xxxxxxxxxx"
MODEL    = "gpt-realtime-1.5"

async def main():
    url = f"{BASE_URL}?model={MODEL}"

    async with websockets.connect(url, additional_headers={
        "Authorization": f"Bearer {API_KEY}",
        "OpenAI-Beta": "realtime=v1",
    }) as ws:
        # 1. 配置会话
        await ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "type": "realtime",
                "instructions": "You are a concise assistant. Reply in Chinese.",
                "output_modalities": ["audio"],
                "audio": {
                    "input":  {"format": {"type": "audio/pcm", "rate": 24000}},
                    "output": {"format": {"type": "audio/pcm", "rate": 24000}, "voice": "sage"},
                },
            },
        }))

        # 2. 等待会话就绪
        async for raw in ws:
            evt = json.loads(raw)
            if evt.get("type") in ("session.created", "session.updated"):
                break

        # 3. 发送文字并触发回复
        await ws.send(json.dumps({
            "type": "conversation.item.create",
            "item": {
                "type": "message",
                "role": "user",
                "content": [{"type": "input_text", "text": "你好"}],
            },
        }))
        await ws.send(json.dumps({"type": "response.create"}))

        # 4. 接收回复
        async for raw in ws:
            evt = json.loads(raw)
            t = evt.get("type", "")
            if t in ("response.text.delta", "response.output_text.delta",
                     "response.audio_transcript.delta", "response.output_audio_transcript.delta"):
                print(evt.get("delta", ""), end="", flush=True)
            elif t in ("response.audio.delta", "response.output_audio.delta"):
                audio_b64 = evt.get("audio", "") or evt.get("delta", "")
                if audio_b64:
                    # 解码 Base64 后播放 PCM16 音频
                    pass
            elif t == "response.done":
                print()
                resp = evt.get("response", evt)  # GA 可能结构不同
                usage = resp.get("usage")
                if usage:
                    print("本轮用量:", usage)
                break
            elif t == "error":
                print("错误:", evt)
                break

asyncio.run(main())
```

完整交互脚本（含麦克风录音、扬声器播放、图片输入、每轮用量与累计用量统计）可参考 `test_realtime.py`。

## 常见问题

* **连接 400**：检查 URL 是否包含 `?model=...` 查询参数。
* **连接 401**：API Key 无效或未授权；可先调用 `GET /v1/models` 验证 Key 与模型是否可用。
* **发送后无响应**：确认已收到 `session.updated` 且已发送 `response.create`。
* **服务端 500 后断连**：上游错误会导致 WebSocket 关闭，需重新建立连接。
* **音频无声**：确认采样率为 24 kHz、PCM16 单声道，且正确解码 Base64 增量后播放。

更多事件类型与字段说明请参考 [OpenAI Realtime API 文档](https://platform.openai.com/docs/guides/realtime)。
