控制台概览
账户资源与调用情况
最近用量
| 服务 | 模型 | 用量 | 费用 | 时间 |
|---|
服务状态
新语音调试
输入 Prompt、选择音色,直接调试千阶 S2S 实时对话
调试配置
配置只用于本次调试,不会覆盖智能语音的正式配置。
点击按钮并允许使用麦克风
实时语音
API Key 直接建立 S2S WebSocket,不需要临时 token
浏览器调试
连接地址
wss://your-domain/developer/realtime?api_key=sk-qj-...
Authorization: Bearer sk-qj-...
启动消息
{"type":"start","voice_id":"你的千阶音色ID","mode":"prompt","prompt":"系统提示词"}
API Key
密钥用于实时语音和免费向量接口
| 名称 | 前缀 | 状态 | 最近使用 | 创建时间 |
|---|
声音复刻
上传一段清晰人声,或直接录音,生成属于当前租户的专属音色。
创建我的音色
支持 mp3、wav、m4a 格式
音色归属
系统音色和当前租户创建的克隆音色会统一出现在实时语音的选择列表中。其他租户的音色不会展示。
可用音色
| 名称 | 来源 | 状态 | 语速 | 创建时间 |
|---|
知识与向量
引导知识和 QA 问答知识按现有 kg_entity 隔离,S2S 会话按 kg_id 自动加载
新增 QA 问答
QA 问答知识
| 问题 | 答案 | 状态 | 创建时间 |
|---|
充值与账单
积分用于 AI 对话/语音/创作;美元与人民币用于 API 调用,两账户独立、不可互转。
积分充值(AI 对话 / 语音 / 创作专用)
充值汇率 1000 积分 = 1 元;积分不可退款、不可转赠、不可兑换为 API 账户余额。
微信 Native 充值(API 账户 USD / CNY)
选择美元时按下单瞬间的实时汇率换算人民币应付金额;汇率不可用则不会创建订单。
充值订单
| 订单号 | 币种 | 到账 | 微信应付 | 状态 | 时间 |
|---|
钱包流水
| 标题 | 金额 | 余额 | 时间 |
|---|
全部用量
| 服务 | 模型 | 数量 | 单位 | 费用 | 请求 ID | 时间 |
|---|
千阶实时语音 · 接入手册
S2S (Speech-to-Speech) 双向流式 PCM 实时语音 API 文档
1. 概述
千阶实时语音(S2S,Speech-to-Speech)是基于大模型的双向流式语音对话服务。与传统的 ASR → LLM → TTS 级联方案不同,S2S 在单条 WebSocket 连接上完成语音输入理解、大模型推理和语音合成输出,实现更低延迟和更自然的交互体验。
核心能力
- 双向流式 PCM:用户边说、AI 边听边回复,无需等待一轮完整说完
- 实时打断:识别用户有效开口后立即停止当前 TTS 播放并重新进入理解
- 智能抢话:理解停顿、换气和犹豫,判断用户是否真正结束表达
- 企业 RAG:会话自动加载租户知识库,无需客户端传临时 context
- 意图与决策引擎:识别用户意图、拒绝原因、状态变化,输出话术级控制信号
- 400B+ 模型:默认挂载大尺寸模型,支持复杂理解和多轮推理
适用场景
- 专业外呼(销售、回访、通知)
- 智能客服与呼入接待
- 催收、提醒等业务语音场景
- 教育辅导、咨询等对话型应用
2. 接入准备
获取 API Key
登录千阶控制台,在「API Key」页面创建访问令牌。API Key 以 sk-qj- 前缀开头,创建后可随时在列表中点击「查看」获取明文。
余额要求
账户需要有足够余额(千阶币或 CNY)才能发起语音会话。新注册用户赠送 1000 千阶币 + ¥3 CNY 体验额度。余额不足时会话将被拒绝。
3. 建立 WebSocket 连接
连接 URL
wss://trial.qjchat.com/developer/realtime?api_key=sk-qj-xxxxxxxx
将 trial.qjchat.com 替换为你的实际接入域名。API Key 直接通过 query 参数传递。
通过 Header 鉴权(可选)
如果客户端支持在 WebSocket 握手时设置自定义 Header,也可以使用 Authorization 头:
Authorization: Bearer sk-qj-xxxxxxxx
Token Bootstrap(可选)
API Key 本身即可作为 Token 直接使用,通常不需要额外的 bootstrap 步骤。如需获取临时 Token:
POST /developer/realtime/token
Authorization: Bearer sk-qj-xxxxxxxx
# 响应
{"token": "sk-qj-xxxxxxxx", "expires_in": 3600}
4. 会话指令
连接建立后,客户端需要发送 JSON 文本帧来控制会话。
start 指令
发送 start 指令开启一次语音会话:
{
"type": "start",
"model": "s2s-realtime",
"voice_id": "你的音色ID",
"mode": "prompt",
"prompt": "你是专业客服,请友好回答用户问题"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 固定 "start" |
model | string | 否 | 默认 s2s-realtime |
voice_id | string | 是 | 音色 ID,在「声音管理」页获取 |
mode | string | 否 | prompt 或 script,默认 prompt |
prompt | string | 否 | Prompt 模式下的系统提示词 |
script_id | string | 否 | 脚本模式下使用,在「知识管理」页创建脚本后获取 ID |
stop 指令
发送 stop 指令结束会话:
{"type": "stop"}
也可以直接关闭 WebSocket 连接来结束会话,服务端会自动清理资源。
5. 音频格式
音频通过 WebSocket 二进制帧传输,格式为原始 PCM(无文件头、无编码)。
| 方向 | 采样率 | 位深 | 声道 | 传输方式 |
|---|---|---|---|---|
| 用户音频(客户端 → 服务端) | 16000 Hz | 16-bit | 单声道 | WebSocket 二进制帧 |
| AI 音频(服务端 → 客户端) | 24000 Hz | 16-bit | 单声道 | WebSocket 二进制帧 |
帧大小建议
- 每帧建议 20–40ms 音频(320–640 samples,即 640–1280 bytes)
- 帧过大增加延迟,帧过小增加协议开销
- 连续发送,不要等待前一帧确认
字节序
PCM 数据使用小端序(Little-Endian),与 WAV 文件的 PCM data chunk 格式一致。
6. 事件流
一次完整的语音会话事件流如下:
客户端 服务端
│ │
│── WebSocket 连接 ────────────▶│ (鉴权通过)
│ │
│── start 指令 (JSON 文本帧) ──▶│ (加载音色/模型/知识)
│ │
│── 用户音频 (二进制帧) ───────▶│ (持续发送)
│◀── AI 音频 (二进制帧) ────────│ (持续接收)
│◀── AI 音频 (二进制帧) ────────│
│── 用户音频 (二进制帧) ───────▶│
│ ... 双向传输 ... │
│ │
│── stop 指令 / 关闭连接 ──────▶│ (清理资源)
│ │
录音
通话过程中服务端自动录音,生成以下文件:
caller.wav— 用户侧音频(16kHz 单声道)bot.wav— AI 侧音频(24kHz 单声道)merged.wav— 合并后的 24kHz 立体声 WAV(左声道=用户,右声道=AI)meta.json— 通话元数据与对话记录
录音可在管理端回放,S2S 录音的 source 字段标记为 "s2s"。
7. 计费
计费方式
实时语音按会话时长(分钟)计费,精确到秒,不足 1 秒按 1 秒计算。
| 项目 | 价格 | 说明 |
|---|---|---|
| 成本价 | ¥0.09 / 分钟 | 上游语音服务成本 |
| 零售价 | 由加价倍率推导 | 成本 × 倍率,倍率可在管理端配置 |
| 千阶币扣费 | 按 voiceCoinsPerSecond 计算 | 从千阶币余额扣除 |
扣费公式
扣币数 = 成本Cny × 加价倍率 × 汇率系数 × 1000
其中:
成本Cny = 秒数 / 60 × 0.09
汇率系数 = 1.5 (1000币 = ¥1.5,可配)
加价倍率 = 1.4 (默认,可配)
扣费时机
- 千阶币账户:会话结束时按实际秒数一次性扣费
- API CNY/USD 账户:API 调用时扣费,CNY 优先、USD 兜底
- 余额不足时会话将被拒绝或中途中断
8. 代码示例
JavaScript(浏览器)
const ws = new WebSocket(
"wss://trial.qjchat.com/developer/realtime?api_key=sk-qj-xxxx"
);
ws.onopen = () => {
ws.send(JSON.stringify({
type: "start",
model: "s2s-realtime",
voice_id: "你的音色ID",
mode: "prompt",
prompt: "你是专业客服"
}));
startMicCapture();
};
ws.onmessage = async (event) => {
if (typeof event.data === "string") {
const msg = JSON.parse(event.data);
console.log("收到事件:", msg);
} else {
const pcm = await event.data.arrayBuffer();
playAudio(pcm, 24000);
}
};
async function startMicCapture() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 16000, channelCount: 1 }
});
const ctx = new AudioContext({ sampleRate: 16000 });
const source = ctx.createMediaStreamSource(stream);
const processor = ctx.createScriptProcessor(512, 1, 1);
source.connect(processor);
processor.connect(ctx.destination);
processor.onaudioprocess = (e) => {
const float32 = e.inputBuffer.getChannelData(0);
const int16 = float32ToInt16(float32);
if (ws.readyState === WebSocket.OPEN) {
ws.send(int16.buffer);
}
};
}
function float32ToInt16(float32) {
const int16 = new Int16Array(float32.length);
for (let i = 0; i < float32.length; i++) {
const s = Math.max(-1, Math.min(1, float32[i]));
int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
return int16;
}
function playAudio(pcmBuffer, sampleRate) {
const ctx = new AudioContext({ sampleRate });
const int16 = new Int16Array(pcmBuffer);
const float32 = new Float32Array(int16.length);
for (let i = 0; i < int16.length; i++) {
float32[i] = int16[i] / 0x8000;
}
const buffer = ctx.createBuffer(1, float32.length, sampleRate);
buffer.getChannelData(0).set(float32);
const src = ctx.createBufferSource();
src.buffer = buffer;
src.connect(ctx.destination);
src.start();
}
Python
import asyncio
import json
import websockets
import pyaudio
API_KEY = "sk-qj-xxxx"
WS_URL = f"wss://trial.qjchat.com/developer/realtime?api_key={API_KEY}"
USER_SAMPLE_RATE = 16000
CHUNK = 1024
async def voice_session():
async with websockets.connect(WS_URL) as ws:
await ws.send(json.dumps({
"type": "start",
"model": "s2s-realtime",
"voice_id": "你的音色ID",
"mode": "prompt",
"prompt": "你是专业客服"
}))
pa = pyaudio.PyAudio()
stream_in = pa.open(format=pyaudio.paInt16, channels=1,
rate=USER_SAMPLE_RATE, input=True, frames_per_buffer=CHUNK)
stream_out = pa.open(format=pyaudio.paInt16, channels=1,
rate=24000, output=True, frames_per_buffer=CHUNK)
async def send_audio():
while True:
data = stream_in.read(CHUNK, exception_on_overflow=False)
await ws.send(data)
async def recv_audio():
while True:
msg = await ws.recv()
if isinstance(msg, bytes):
stream_out.write(msg)
else:
print("事件:", json.loads(msg))
await asyncio.gather(send_audio(), recv_audio())
asyncio.run(voice_session())
curl — Token Bootstrap
curl -X POST https://trial.qjchat.com/developer/realtime/token \
-H "Authorization: Bearer sk-qj-xxxx" \
-H "Content-Type: application/json"
9. 声音复刻
声音复刻(Voice Clone)允许你上传一段参考音频,训练生成自定义音色,用于 S2S 会话或其他 TTS 场景。
克隆流程
- 1. 上传参考音频:准备 10–30 秒清晰人声录音(WAV/MP3,16kHz 以上)
- 2. 调用克隆接口:提交参考音频,服务端异步训练
- 3. 获取 voice_id:训练完成后返回
voice_id - 4. 用于 S2S:在 start 指令中使用该
voice_id
默认提供 100+ 真人音色,可在「声音管理」页查看和选择。
10. 常见问题
连接不上 WebSocket?
- 检查 API Key 是否正确(
sk-qj-前缀) - 检查账户余额是否充足
- 确认 URL 协议是
wss://(不是ws://) - 检查域名是否正确,生产环境使用
trial.qjchat.com
没有声音输出?
- 检查 AI 音频帧是否为二进制帧(
ArrayBuffer),而非文本帧 - 确认播放器采样率设置为 24000 Hz
- 确认 PCM 数据为 16-bit 小端序
- 浏览器需要用户交互后才能播放音频(autoplay 策略)
AI 听不到我说?
- 检查麦克风音频是否为 16kHz / 16bit / 单声道 PCM
- 确认发送的是二进制帧(
ws.send(arrayBuffer)),而非 Base64 编码 - 检查麦克风权限是否已授权
- 确认 Float32 到 Int16 转换正确
延迟高?
- 检查网络延迟(建议 RTT < 100ms)
- 减小音频帧大小(20–40ms / 帧)
- 确认网络带宽足够(16kHz PCM ≈ 32KB/s 上行)
- 检查客户端是否在发送前等待了不必要的确认
会话被中断?
- 检查账户余额是否在通话过程中耗尽
- 确认 WebSocket 连接未被客户端意外关闭
- 网络异常断开后需重新建立连接并发送 start