语音对话系统技术解析 - 基于阿里云 Qwen-Omni 实时语音大模型

发表信息: by

语音对话系统技术解析

基于阿里云 Qwen-Omni 实时语音大模型的 Voice-to-Voice 对话实现

GitHub: SUT-GC/Voice_chat_qwen Python + JavaScript 阿里云 DashScope API

一、系统架构概览

本项目实现了一个实时语音对话系统,用户可以通过浏览器与 AI 进行自然的语音交流。整个系统采用三段式架构,通过 WebSocket 长连接实现低延迟的双向通信。

👤
用户
语音输入/输出
🌐 浏览器
Web Audio API
采集麦克风音频
AudioPlayer
播放 AI 语音
WebSocket
双向实时通信
↑ audio chunks
↓ events + audio
WebSocket
🐍 Python Server
aiohttp
异步 Web 服务
VoiceChatSession
会话管理
🔀 代理转发
1 份音频 → 2 个 API
双路 WebSocket
WSS (加密)
☁️ 阿里云 DashScope
🧠 Qwen-Omni
语音理解
对话生成
语音合成
Server VAD
📝 Qwen-ASR
流式转写
中文优化

🌐 浏览器 (Frontend)

负责音频采集、播放和 UI 交互

  • 使用 ScriptProcessor 采集 PCM 音频
  • AudioPlayer 队列播放 AI 回复
  • 根据事件类型更新 UI 状态

🐍 Python Server

纯代理转发,不处理业务逻辑

  • 接收前端音频,转发给阿里云
  • 同一音频同时发给 Omni 和 ASR
  • 将阿里云事件透传给前端

🧠 Qwen-Omni API

语音对话的核心智能

  • 服务端 VAD 检测语音活动
  • 理解用户意图并生成回复
  • 流式返回语音和文字

📝 Qwen-ASR API

实时语音转写服务

  • 边接收边转写,流式返回
  • 用于在 UI 显示用户说的话
  • 支持中文及多种方言

二、WebSocket 长连接机制

整个系统使用 3 条 WebSocket 长连接保持实时通信。相比 HTTP 短连接,WebSocket 避免了每次请求的握手开销,延迟可低至 10ms 级别,非常适合实时语音场景。

连接 起点 终点 协议 用途
连接 1 浏览器 Python Server ws:// 音频上传、事件下发
连接 2 Python Server Qwen-Omni API wss:// 语音对话
连接 3 Python Server Qwen-ASR API wss:// 语音转写

连接地址

WebSocket URLs
# Qwen-Omni 实时对话
wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model=qwen3-omni-flash-realtime

# Qwen-ASR 实时转写
wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model=qwen3-asr-flash-realtime

生命周期

事件 连接状态变化
用户点击「连接」 浏览器 → Server 建立 WS;Server → 阿里云建立 2 个 WSS
用户说话 数据在已建立的长连接上传输,无需重新建立
用户点击「断开」 3 条连接全部关闭

为什么需要 Python Server 中转?

1. API Key 保护 - 密钥存放在服务端,不暴露给前端
2. 双路转发 - 同一份音频需要发给 Omni 和 ASR 两个服务
3. 跨域限制 - 浏览器直连阿里云可能存在 CORS 问题

三、音频格式与数据流

PCM 音频格式

PCM (Pulse Code Modulation) 是一种无损音频编码格式,直接记录声波的采样值,没有压缩损失。

📥 输入格式 (用户语音)

  • 格式: PCM 16-bit
  • 采样率: 16,000 Hz
  • 声道: 单声道 (Mono)
  • 每样本: 2 字节
  • 取值范围: -32768 ~ +32767

📤 输出格式 (AI 回复)

  • 格式: PCM 24-bit
  • 采样率: 24,000 Hz
  • 声道: 单声道 (Mono)
  • 质量: 更高 (更细腻)

数据分块传输

浏览器持续录音,每采集到一定量的样本就打包发送,实现边录边发的流式传输:

参数 说明
缓冲区大小 4096 samples ScriptProcessor 每次处理的样本数
采样率 16000 Hz 每秒采集 16000 个样本
发送间隔 256 ms 4096 ÷ 16000 = 0.256 秒
原始大小 8 KB 4096 × 2 bytes = 8192 bytes
Base64 后 ~11 KB Base64 编码增加约 33% 体积

数据流量估算

说话时长 发送次数 上传数据量
1 秒 ~4 次 ~44 KB
3 秒 ~12 次 ~130 KB
1 分钟 ~234 次 ~2.5 MB

JSON 消息格式

前端 → Server

JSON
{
  "type": "audio",
  "data": "AAD//wAA//8AAP//..."  // Base64 编码的 PCM16 音频
}

Server → 阿里云

JSON
{
  "event_id": "audio_140234567890",
  "type": "input_audio_buffer.append",  // 阿里云 API 要求的事件类型
  "audio": "AAD//wAA//8AAP//..."
}

Server 只做格式转换 - 不解码、不处理音频内容,只是把 data 字段改成 audio,加上 event_id,然后透传。

四、VAD 语音活动检测

VAD (Voice Activity Detection) 是实现自然对话的关键技术,用于检测用户什么时候开始说话、什么时候说完。本项目使用服务端 VAD,所有检测逻辑都在阿里云完成。

工作原理

用户音频流 ─────────────────────────────────────────────────►
│ │ │
▼ ▼ ▼
[说话中] [静音 800ms] [又开始说话]
│ │ │
▼ ▼ ▼
speech_started speech_stopped speech_started
│ │
▼ ▼
AI 开始回复 打断 AI,重新听

配置参数

Python - VAD 配置
# server.py 中的 VAD 配置
"turn_detection": {
    "type": "server_vad",      # 使用服务端 VAD
    "threshold": 0.5,          # 语音检测阈值
    "silence_duration_ms": 800 # 静音多久算说完 (毫秒)
}
参数 默认值 说明
type server_vad 服务端检测,前端不参与判断
threshold 0.5 语音能量阈值,越低越灵敏
silence_duration_ms 800 静音超过此时长则判定说完

打断机制

当用户在 AI 回复过程中再次说话,系统会自动打断 AI:

JavaScript - 打断处理
// index.html - 收到 speech_started 事件
case 'speech_started':
    updateStatus('speaking', '正在聆听...');
    audioPlayer.clear();  // 🔇 清空 AI 音频队列,实现打断
    createUserMessage();
    break;

打断的本质 - 不是真的"打断"阿里云的生成,而是前端停止播放已收到的音频。阿里云检测到新的语音后会自动停止之前的回复生成。

五、核心事件详解

WebSocket 通信基于事件驱动,不同的事件类型代表不同的状态变化。

客户端 → 服务端

事件类型 说明 触发时机
session.update 更新会话配置 连接建立后立即发送
input_audio_buffer.append 追加音频数据 每 256ms 发送一次

服务端 → 客户端 (Qwen-Omni)

事件类型 说明 前端处理
input_audio_buffer.speech_started 检测到用户开始说话 清空音频队列、创建用户消息气泡
input_audio_buffer.speech_stopped 检测到用户停止说话 状态改为"AI 思考中"
response.audio.delta AI 回复音频增量 加入播放队列播放
response.audio_transcript.delta AI 回复文字增量 追加到 AI 消息气泡
response.done AI 回复结束 状态改为"已连接"

服务端 → 客户端 (Qwen-ASR)

事件类型 说明 前端处理
conversation.item.input_audio_transcription.delta 用户语音转写增量 实时更新用户消息气泡
conversation.item.input_audio_transcription.completed 用户语音转写完成 用户消息气泡定稿

事件转发映射

Server 收到阿里云的事件后,简化字段名转发给前端:

阿里云事件 转发给前端
input_audio_buffer.speech_started speech_started
input_audio_buffer.speech_stopped speech_stopped
response.audio.delta audio
response.audio_transcript.delta transcript
response.done response_done

六、交互时序流程

以下是用户说话 3 秒的完整交互时序,展示了各个组件之间的协作过程。点击控制按钮可以逐步查看或自动播放。

0s
开始说话
~0.3s
检测到语音
1-3s
持续说话
3s
停止说话
~3.8s
静音800ms
4s+
AI 回复
🎤 浏览器
0-3saudio ×12
3-3.8s静音中继续发
···
🔀 Server
转发→ Omni + ASR
~0.3sspeech_started
~3.8sspeech_stopped
4s+AI 回复流
🧠 Omni
接收音频流
VADspeech_started
VADspeech_stopped
生成audio.delta ×N
~6sresponse.done
📝 ASR
接收音频流
转写delta ×N
~3.8scompleted
💻 UI
0s 点击麦克风
正在录音
~0.3s speech_started
正在聆听
...
1-3s ASR 流式转写
你好,我想问...
~3.8s speech_stopped
AI 思考中
4s+ AI 回复中
🔊 播放语音
好的,今天...
~6s response.done
已连接
0 / 8 点击开始
1.0s
📍 准备就绪
点击「下一步」手动查看每一步,或点击「自动播放」自动演示。支持键盘 ← → 和空格键。

七、关键代码解析

前端录音与发送

JavaScript - 音频采集
// index.html - 创建音频处理器
state.audioContext = new AudioContext({ sampleRate: 16000 });
state.processor = state.audioContext.createScriptProcessor(4096, 1, 1);

// 每 256ms 触发一次
state.processor.onaudioprocess = (e) => {
    const inputData = e.inputBuffer.getChannelData(0);

    // Float32 → Int16 (PCM16)
    const pcmData = new Int16Array(inputData.length);
    for (let i = 0; i < inputData.length; i++) {
        const s = Math.max(-1, Math.min(1, inputData[i]));
        pcmData[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
    }

    // Base64 编码并发送
    const base64 = btoa(String.fromCharCode(...new Uint8Array(pcmData.buffer)));
    state.ws.send(JSON.stringify({ type: 'audio', data: base64 }));
};

服务端会话配置

Python - 会话配置
# server.py - 配置 Qwen-Omni 会话
session_config = {
    "type": "session.update",
    "session": {
        "modalities": ["text", "audio"],
        "voice": "Cherry",                 # AI 声音:Cherry/Ethan/Serena
        "input_audio_format": "pcm16",
        "output_audio_format": "pcm24",
        "instructions": "你是一个友好的AI助手...",
        "turn_detection": {
            "type": "server_vad",
            "threshold": 0.5,
            "silence_duration_ms": 800
        }
    }
}
await self.qwen_ws.send(json.dumps(session_config))

服务端转发逻辑

Python - 转发逻辑
# server.py - 收到前端音频,同时转发给两个 API
async def forward_to_qwen(self, audio_b64):
    event = {
        "event_id": f"audio_{id(audio_b64)}",
        "type": "input_audio_buffer.append",
        "audio": audio_b64  # 直接透传,不处理
    }

    # 发给 Qwen-Omni
    await self.qwen_ws.send(json.dumps(event))

    # 同时发给 ASR
    if self.asr_ws:
        await self.asr_ws.send(json.dumps(asr_event))

异步监听响应

Python - 异步监听
# server.py - 发送和接收是两个独立的协程

# 启动监听任务 (不阻塞发送)
listen_task = asyncio.create_task(session.listen_qwen_responses())
asr_task = asyncio.create_task(session.listen_asr_responses())

# 监听 Qwen 响应
async def listen_qwen_responses(self):
    async for message in self.qwen_ws:  # 持续等待消息
        event = json.loads(message)
        event_type = event.get("type")

        if event_type == "response.audio.delta":
            # 透传给前端
            await self.client_ws.send_json({
                "type": "audio",
                "data": event.get("delta")
            })

八、参考文档

📦 项目源码

GitHub 仓库

🧠 Qwen-Omni 实时模型

语音对话 API 官方文档

📝 Qwen-ASR 实时转写

语音识别 API 官方文档

📖 相关技术

扩展阅读