AI 语音对话(文字 / 语音输入)
通过 WebSocket 与 AI 进行实时语音对话,支持文字输入和语音输入两种方式,AI 以语音形式回复。每次连接为独立的一轮对话。
接口信息
- 接口路径:
/api/v1/audio/conversation/stream-text - 通信协议:WebSocket
- 认证方式:URL 参数
api_key
连接示例
const ws = new WebSocket(
'wss://api.vuilabs.cn/api/v1/audio/conversation/stream-text?api_key=YOUR_KEY'
);
ws.binaryType = 'arraybuffer';
认证参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| api_key | string | 是 | 应用密钥,拼在 WebSocket URL 的 query 参数中 |
注意:api_key 用于验证账户信息,进入 控制台 → 项目空间,选择对应项目,左侧导航进入「API Key 管理」即可创建/查看。
一、客户端发送消息
1. Meta 消息(必须,连接后第一条)
连接建立后立即以 JSON 文本消息发送会话元信息:
{
"meta": {
"session_id": "",
"voice_id": "qinyao",
"audio_format": "pcm16",
"text_input": "你好,请介绍一下你自己"
}
}
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| session_id | string | 否 | 会话 ID;每次连接均为全新一轮,留空即可 |
| voice_id | string | 是 | 角色 ID,见下方角色列表 |
| audio_format | string | 是 | 音频格式,固定填 pcm16(16kHz / 16bit / 单声道) |
| text_input | string | 否 | 文字输入时填写内容;留空则进入语音输入模式 |
2. 音频帧(语音输入模式)
text_input 留空时,发送 PCM 二进制消息:
| 属性 | 值 |
|---|---|
| 采样率 | 16000 Hz |
| 位深 | 16-bit Signed Little-Endian |
| 声道数 | 1(单声道) |
| 建议帧长 | 约 100ms |
说完后发送结束标记:
{ "is_final": true }
二、推送事件
session_created — 会话创建
{ "type": "session_created", "session_id": "abc123..." }
| 字段 | 类型 | 说明 |
|---|---|---|
| session_id | string | 本轮会话 ID |
asr_partial — 实时识别中间结果(语音输入模式)
{ "type": "asr_partial", "asr_text": "你好" }
| 字段 | 类型 | 说明 |
|---|---|---|
| asr_text | string | 当前识别到的文本(可能不完整) |
asr_final — 识别最终结果(语音输入模式)
{ "type": "asr_final", "asr_text": "你好,请介绍一下你自己" }
| 字段 | 类型 | 说明 |
|---|---|---|
| asr_text | string | 最终识别文本 |
llm_first_token / llm_token — AI 回复文本(流式)
{ "type": "llm_first_token", "first_token": "你" }
{ "type": "llm_token", "token": "好" }
| 字段 | 类型 | 说明 |
|---|---|---|
| first_token | string | 首个生成字符(llm_first_token 事件) |
| token | string | 后续字符(llm_token 事件) |
按顺序拼接即为完整回复文本。
音频(二进制消息)
MP3 格式音频分片,多帧顺序到达,可边收边播放:
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
playAudioChunk(event.data); // 直接入队播放
}
};
| 属性 | 值 |
|---|---|
| 格式 | MP3 |
| 编码 | 原始二进制(非 Base64) |
done — 本轮对话完成
{
"type": "done",
"reply_text": "你好,我是沁瑶……",
"audio_duration_sec": 5
}
| 字段 | 类型 | 说明 |
|---|---|---|
| reply_text | string | AI 回复完整文本 |
| audio_duration_sec | number | AI 回复音频时长(秒),用于计费 |
error — 错误事件
{ "type": "error", "message": "错误描述" }
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 错误描述 |
三、代码示例
文字输入模式
const ws = new WebSocket(
'wss://api.vuilabs.cn/api/v1/audio/conversation/stream-text?api_key=YOUR_KEY'
);
ws.binaryType = 'arraybuffer';
const audioQueue = [];
let isPlaying = false;
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
meta: {
session_id: '',
voice_id: 'qinyao',
audio_format: 'pcm16',
text_input: '你好,请介绍一下你自己',
},
}));
});
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
audioQueue.push(event.data);
if (!isPlaying) playNext();
return;
}
const msg = JSON.parse(event.data);
if (msg.type === 'done') {
console.log('AI 回复:', msg.reply_text);
ws.close();
} else if (msg.type === 'error') {
console.error(msg.message);
ws.close();
}
});
function playNext() {
if (!audioQueue.length) { isPlaying = false; return; }
isPlaying = true;
const url = URL.createObjectURL(new Blob([audioQueue.shift()], { type: 'audio/mp3' }));
const audio = new Audio(url);
audio.onended = () => { URL.revokeObjectURL(url); playNext(); };
audio.play();
}
语音输入模式
const ws = new WebSocket(
'wss://api.vuilabs.cn/api/v1/audio/conversation/stream-text?api_key=YOUR_KEY'
);
ws.binaryType = 'arraybuffer';
ws.addEventListener('open', async () => {
// text_input 留空 → 语音模式
ws.send(JSON.stringify({
meta: { session_id: '', voice_id: 'qinyao', audio_format: 'pcm16', text_input: '' },
}));
const stream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 16000, channelCount: 1 },
});
const audioCtx = new AudioContext({ sampleRate: 16000 });
const source = audioCtx.createMediaStreamSource(stream);
const processor = audioCtx.createScriptProcessor(2048, 1, 1);
processor.onaudioprocess = (e) => {
if (ws.readyState !== WebSocket.OPEN) return;
const input = e.inputBuffer.getChannelData(0);
const buf = new ArrayBuffer(input.length * 2);
const view = new DataView(buf);
for (let i = 0; i < input.length; i++) {
const s = Math.max(-1, Math.min(1, input[i]));
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
ws.send(buf);
};
source.connect(processor);
processor.connect(audioCtx.destination);
// 说完后停止录音并发结束标记
setTimeout(() => {
processor.disconnect();
stream.getTracks().forEach(t => t.stop());
ws.send(JSON.stringify({ is_final: true }));
}, 5000); // 示例:录制 5 秒
});
四、计费说明
具体计费规则、价格和免费额度请参见 产品计费。
五、角色列表
通过 GET /api/v1/audio/characters 获取完整列表。
| voice_id | 名称 |
|---|---|
qinyao | 沁瑶 |
xiyue | 曦月 |
ruoxi | 若兮 |
bocen | 柏辰 |
daisy | Daisy |
fiona | Fiona |
六、错误码
| 错误码 | 说明 |
|---|---|
| 0 | 成功 |
| 20001 | 未授权,请检查 api_key |
| 20003 | 参数错误 |
| 40011 | 余额不足 |
| 50000 | 服务异常 |