实时语音 AI 对话系统教程
SEO 信息
- 名称:实时语音AI对话系统教程
- 描述:零基础实时语音AI对话系统教程,涵盖ASR语音识别、LLM对话引擎、TTS语音合成、WebSocket实时通信、流式处理、延迟优化等核心技能,适合AI开发者系统学习。
- 关键词:实时语音AI, 语音对话系统, Whisper, TTS, WebSocket
- 长尾关键词:实时语音AI对话系统开发教程, 语音助手开发实战, Whisper+LLM语音对话系统, WebSocket实时语音AI开发
一、实时语音 AI 技术栈概览
一个完整的实时语音 AI 对话系统,本质上是将语音识别(ASR)、**大语言模型(LLM)和语音合成(TTS)**三个模块通过实时通信管道串联起来。整个链路可以概括为:
用户语音输入 → VAD 语音活动检测 → ASR 语音识别 → LLM 对话生成 → TTS 语音合成 → 播放音频响应
核心技术栈如下:
| 模块 | 开源方案 | 商业方案 |
|---|---|---|
| 语音识别 ASR | Whisper、Paraformer、FunASR | Azure Speech、Google STT |
| 对话引擎 LLM | Qwen、DeepSeek、ChatGLM | GPT-4o、Claude |
| 语音合成 TTS | ChatTTS、CosyVoice、Edge TTS | Azure TTS、ElevenLabs |
| 语音活动检测 | Silero VAD、WebRTC VAD | — |
| 实时通信 | WebSocket、WebRTC | — |
| 音频处理 | FFmpeg、librosa、soundfile | — |
选择开源方案的优势在于:零成本、可本地部署、数据不出域。本教程将以开源方案为主线,构建一套完整的实时语音 AI 对话系统。
二、ASR 语音识别:Whisper 与 Paraformer
2.1 Whisper 简介
Whisper 是 OpenAI 开源的自动语音识别模型,支持多语言识别和翻译。其核心架构是 Encoder-Decoder Transformer,训练数据规模达 68 万小时。
Whisper 有多种规格:
| 模型 | 参数量 | 推理速度 | 精度 |
|---|---|---|---|
| tiny | 39M | 极快 | 一般 |
| base | 74M | 很快 | 较好 |
| small | 244M | 快 | 好 |
| medium | 769M | 中等 | 很好 |
| large-v3 | 1.55B | 较慢 | 最佳 |
对于实时对话场景,推荐使用 small 或 medium 模型,在精度和速度之间取得平衡。
2.2 使用 faster-whisper 进行语音识别
faster-whisper 基于 CTranslate2 实现,推理速度比原版快 4 倍以上:
from faster_whisper import WhisperModel
# 加载模型(首次运行会自动下载)
model = WhisperModel("small", device="cpu", compute_type="int8")
def transcribe(audio_path: str) -> str:
"""将音频文件转为文字"""
segments, info = model.transcribe(audio_path, beam_size=5, language="zh")
text = "".join([seg.text for seg in segments])
return text.strip()
# 使用示例
result = transcribe("recording.wav")
print(f"识别结果: {result}")
2.3 Paraformer:高精度中文识别
Paraformer 是阿里达摩院提出的非自回归语音识别模型,中文识别精度优于同参数量的 Whisper:
# 使用 funasr 库
from funasr import AutoModel
model = AutoModel(model="paraformer-zh")
def transcribe_paraformer(audio_path: str) -> str:
result = model.generate(input=audio_path)
if result and len(result) > 0:
return result[0]["text"]
return ""
text = transcribe_paraformer("recording.wav")
print(f"Paraformer 识别: {text}")
2.4 流式识别策略
实时对话系统不能等用户说完一整段再识别,需要采用流式识别策略:
import numpy as np
from faster_whisper import WhisperModel
model = WhisperModel("small", device="cpu", compute_type="int8")
class StreamingASR:
"""流式语音识别器"""
def __init__(self, sample_rate=16000, chunk_duration=2.0):
self.sample_rate = sample_rate
self.chunk_size = int(sample_rate * chunk_duration)
self.buffer = np.array([], dtype=np.float32)
self.result_text = ""
def feed_audio(self, audio_chunk: np.ndarray) -> str:
"""喂入音频数据,返回当前已识别文本"""
self.buffer = np.concatenate([self.buffer, audio_chunk])
# 缓冲区积累到足够长度时进行识别
if len(self.buffer) >= self.chunk_size:
segments, _ = model.transcribe(
self.buffer,
beam_size=5,
language="zh",
vad_filter=True
)
self.result_text = "".join([seg.text for seg in segments])
# 保留最后 0.5 秒作为重叠
overlap = int(self.sample_rate * 0.5)
self.buffer = self.buffer[-overlap:]
return self.result_text
def finalize(self) -> str:
"""结束时对剩余音频做最终识别"""
if len(self.buffer) > 0:
segments, _ = model.transcribe(self.buffer, beam_size=5, language="zh")
self.result_text = "".join([seg.text for seg in segments])
return self.result_text
三、LLM 对话引擎集成
3.1 对话管理架构
语音对话系统的 LLM 引擎需要处理几个关键问题:上下文管理、流式输出、以及与语音模块的衔接。
from dataclasses import dataclass, field
@dataclass
class ConversationManager:
"""对话管理器"""
system_prompt: str = "你是一个友好的语音助手,回答简洁明了。"
history: list = field(default_factory=list)
max_history: int = 20 # 最多保留的对话轮数
def add_user_message(self, text: str):
self.history.append({"role": "user", "content": text})
self._trim_history()
def add_assistant_message(self, text: str):
self.history.append({"role": "assistant", "content": text})
self._trim_history()
def get_messages(self) -> list:
return [{"role": "system", "content": self.system_prompt}] + self.history
def _trim_history(self):
if len(self.history) > self.max_history:
self.history = self.history[-self.max_history:]
3.2 流式 LLM 调用
对于实时语音场景,LLM 必须采用流式输出,以便尽早将文本送入 TTS:
import httpx
import json
class StreamingLLM:
"""流式 LLM 调用器"""
def __init__(self, api_base: str, model: str, api_key: str = "EMPTY"):
self.api_base = api_base
self.model = model
self.api_key = api_key
async def stream_chat(self, messages: list):
"""流式调用 LLM,yield 每个文本片段"""
url = f"{self.api_base}/chat/completions"
payload = {
"model": self.model,
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 512, # 语音场景控制回复长度
}
async with httpx.AsyncClient() as client:
async with client.stream(
"POST", url,
json=payload,
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=30.0
) as response:
async for line in response.aiter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
chunk = json.loads(line[6:])
delta = chunk["choices"][0].get("delta", {})
if "content" in delta:
yield delta["content"]
# 使用示例
async def demo():
llm = StreamingLLM(
api_base="http://localhost:8000/v1",
model="qwen2.5-7b-instruct"
)
messages = [
{"role": "system", "content": "你是语音助手"},
{"role": "user", "content": "今天天气怎么样?"}
]
async for token in llm.stream_chat(messages):
print(token, end="", flush=True)
3.3 语音优化的 Prompt 策略
语音对话场景与文字对话不同,需要对 LLM 的输出进行约束:
VOICE_SYSTEM_PROMPT = """你是一个实时语音助手。请遵守以下规则:
1. 回答简洁,每次回复控制在 2-3 句话以内
2. 不要使用 markdown 格式、代码块、列表符号
3. 不要使用特殊符号如 **、##、- 等
4. 数字直接用文字表达,如"三十二"而非"32"
5. 避免长段落,口语化表达
6. 如果不确定,直接说不确定,不要编造长篇大论
"""
四、TTS 语音合成
4.1 Edge TTS:零成本高质量
Edge TTS 基于微软 Azure 的语音合成服务,完全免费,音质极佳:
import edge_tts
import asyncio
async def text_to_speech(text: str, output_path: str = "output.mp3"):
"""使用 Edge TTS 将文本转为语音"""
# 中文女声
voice = "zh-CN-XiaoxiaoNeural"
communicate = edge_tts.Communicate(text, voice)
await communicate.save(output_path)
return output_path
# 使用示例
asyncio.run(text_to_speech("你好,我是你的语音助手!"))
# 查看所有可用中文语音
async def list_voices():
voices = await edge_tts.list_voices()
for v in voices:
if "zh-CN" in v["Locale"]:
print(f"{v['ShortName']} - {v['Gender']}")
asyncio.run(list_voices())
4.2 ChatTTS:开源可控语音合成
ChatTTS 是一个开源的对话式语音合成模型,支持情感控制和说话人定制:
import ChatTTS
import torch
import soundfile as sf
# 初始化 ChatTTS
chat = ChatTTS.Chat()
chat.load(compile=False) # 设为 True 可加速但需要编译
def generate_speech(text: str, output_path: str = "chattts_output.wav"):
"""使用 ChatTTS 生成语音"""
# 生成随机说话人嵌入
rand_spk = chat.sample_random_speaker()
params_infer = ChatTTS.Chat.InferCodeParams(
spk_emb=rand_spk,
temperature=0.3, # 越低越稳定
top_P=0.7,
top_K=20,
)
params_refine = ChatTTS.Chat.RefineTextParams(
prompt='[oral_2][laugh_0][break_6]', # 口语化控制
)
wavs = chat.infer(
[text],
params_infer_code=params_infer,
params_refine_text=params_refine,
)
sf.write(output_path, wavs[0], 24000)
return output_path
generate_speech("今天天气真不错,要不要出去走走?")
4.3 CosyVoice:阿里最新语音合成
CosyVoice 支持多语言、情感控制和声音克隆:
# 需要先克隆 CosyVoice 仓库并安装依赖
from cosyvoice.cli.cosyvoice import CosyVoice
model = CosyVoice('pretrained_models/CosyVoice-300M')
# 基础语音合成
output = model.inference_sft('你好,欢迎使用实时语音助手!', '中文女')
# output 是生成的音频 tensor
# 带情感控制的合成
output = model.inference_cross_lingual(
'今天心情真好啊!',
prompt_speech_16k=reference_audio_tensor # 参考音频
)
4.4 流式 TTS 策略
为了降低首字延迟,可以将 LLM 输出的文本分句后逐句合成:
import re
import asyncio
import edge_tts
class StreamingTTS:
"""流式语音合成器:按句子边界切分,逐句合成"""
# 中英文句子结束标志
SENTENCE_END = re.compile(r'[。!?.!?\n;;]')
def __init__(self, voice: str = "zh-CN-XiaoxiaoNeural"):
self.voice = voice
self.buffer = ""
self.audio_queue = asyncio.Queue()
def feed_text(self, text: str) -> list[str]:
"""喂入文本片段,返回可合成的完整句子列表"""
self.buffer += text
sentences = []
while True:
match = self.SENTENCE_END.search(self.buffer)
if match is None:
break
end_pos = match.end()
sentence = self.buffer[:end_pos].strip()
if sentence:
sentences.append(sentence)
self.buffer = self.buffer[end_pos:]
return sentences
async def synthesize(self, text: str) -> bytes:
"""合成单句语音"""
communicate = edge_tts.Communicate(text, self.voice)
audio_data = b""
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio_data += chunk["data"]
return audio_data
五、WebSocket 实时通信架构
5.1 为什么选择 WebSocket
实时语音对话对通信的要求:
- 低延迟:音频数据需要毫秒级传输
- 双向通信:客户端发送音频,服务端推送识别结果和合成音频
- 长连接:一次对话持续数分钟,不适合 HTTP 短连接
- 二进制传输:音频数据用二进制帧传输效率更高
WebSocket 完美满足以上所有需求。
5.2 服务端实现
import asyncio
import json
import numpy as np
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.websockets import WebSocketState
app = FastAPI()
class VoiceSession:
"""单个语音会话"""
def __init__(self, websocket: WebSocket):
self.ws = websocket
self.asr = StreamingASR()
self.conv = ConversationManager()
self.tts = StreamingTTS()
self.is_speaking = False # AI 是否正在说话
async def handle_audio(self, audio_bytes: bytes):
"""处理客户端发来的音频数据"""
# 将 bytes 转为 float32 数组
audio_np = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0
# 送入 ASR
partial_text = self.asr.feed_audio(audio_np)
# 发送中间识别结果
await self.ws.send_json({
"type": "asr_partial",
"text": partial_text
})
async def handle_end_of_speech(self):
"""用户说完了,开始生成回复"""
# 最终识别
final_text = self.asr.finalize()
if not final_text.strip():
return
await self.ws.send_json({
"type": "asr_final",
"text": final_text
})
# 添加到对话历史
self.conv.add_user_message(final_text)
# 流式调用 LLM
full_response = ""
async for token in llm.stream_chat(self.conv.get_messages()):
full_response += token
# 发送文本片段
await self.ws.send_json({
"type": "llm_token",
"text": token
})
# 将文本送入流式 TTS
sentences = self.tts.feed_text(token)
for sentence in sentences:
audio = await self.tts.synthesize(sentence)
await self.ws.send_bytes(audio)
# 保存助手回复
self.conv.add_assistant_message(full_response)
@app.websocket("/ws/voice")
async def voice_websocket(websocket: WebSocket):
await websocket.accept()
session = VoiceSession(websocket)
try:
while True:
message = await websocket.receive()
if message["type"] == "websocket.receive":
if "bytes" in message:
# 二进制帧 = 音频数据
await session.handle_audio(message["bytes"])
elif "text" in message:
# 文本帧 = 控制指令
data = json.loads(message["text"])
if data.get("action") == "end_of_speech":
await session.handle_end_of_speech()
except WebSocketDisconnect:
print("客户端断开连接")
5.3 客户端实现(浏览器端)
class VoiceAssistant {
constructor(wsUrl) {
this.wsUrl = wsUrl;
this.ws = null;
this.mediaRecorder = null;
this.audioContext = null;
this.isRecording = false;
}
async connect() {
this.ws = new WebSocket(this.wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onmessage = (event) => {
if (typeof event.data === 'string') {
const msg = JSON.parse(event.data);
this.handleMessage(msg);
} else {
// 二进制数据 = 音频
this.playAudio(event.data);
}
};
this.ws.onopen = () => console.log('已连接到语音服务');
this.ws.onclose = () => console.log('连接已断开');
}
async startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
this.audioContext = new AudioContext({ sampleRate: 16000 });
const source = this.audioContext.createMediaStreamSource(stream);
const processor = this.audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
if (!this.isRecording) return;
const inputData = e.inputBuffer.getChannelData(0);
// 转为 16-bit PCM
const pcm = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcm[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767;
}
this.ws.send(pcm.buffer);
};
source.connect(processor);
processor.connect(this.audioContext.destination);
this.isRecording = true;
}
stopRecording() {
this.isRecording = false;
// 通知服务端用户说完了
this.ws.send(JSON.stringify({ action: 'end_of_speech' }));
}
handleMessage(msg) {
switch (msg.type) {
case 'asr_partial':
document.getElementById('asr-text').textContent = msg.text;
break;
case 'asr_final':
document.getElementById('user-text').textContent = msg.text;
break;
case 'llm_token':
document.getElementById('ai-text').textContent += msg.text;
break;
}
}
async playAudio(arrayBuffer) {
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
}
}
六、流式音频处理
6.1 音频格式与采样率
实时语音系统中,音频格式的选择直接影响延迟和质量:
import numpy as np
class AudioProcessor:
"""音频处理器:负责格式转换和预处理"""
@staticmethod
def pcm16_to_float32(pcm_bytes: bytes) -> np.ndarray:
"""16-bit PCM 转 float32"""
return np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32) / 32768.0
@staticmethod
def float32_to_pcm16(audio: np.ndarray) -> bytes:
"""float32 转 16-bit PCM"""
clipped = np.clip(audio, -1.0, 1.0)
return (clipped * 32767).astype(np.int16).tobytes()
@staticmethod
def resample(audio: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
"""重采样"""
if orig_sr == target_sr:
return audio
# 简单线性插值(生产环境建议用 librosa 或 soxr)
duration = len(audio) / orig_sr
target_len = int(duration * target_sr)
indices = np.linspace(0, len(audio) - 1, target_len)
return np.interp(indices, np.arange(len(audio)), audio)
@staticmethod
def normalize_volume(audio: np.ndarray, target_db: float = -20.0) -> np.ndarray:
"""音量归一化"""
if len(audio) == 0:
return audio
rms = np.sqrt(np.mean(audio ** 2))
if rms < 1e-8:
return audio
target_rms = 10 ** (target_db / 20)
return audio * (target_rms / rms)
6.2 环形缓冲区
实时音频处理需要高效的缓冲区管理:
import numpy as np
class RingBuffer:
"""环形缓冲区:用于实时音频流"""
def __init__(self, size: int):
self.buffer = np.zeros(size, dtype=np.float32)
self.size = size
self.write_pos = 0
self.read_pos = 0
self.count = 0
def write(self, data: np.ndarray) -> int:
"""写入数据,返回实际写入的样本数"""
available = self.size - self.count
to_write = min(len(data), available)
if to_write <= 0:
return 0
end_pos = self.write_pos + to_write
if end_pos <= self.size:
self.buffer[self.write_pos:end_pos] = data[:to_write]
else:
first_part = self.size - self.write_pos
self.buffer[self.write_pos:] = data[:first_part]
self.buffer[:to_write - first_part] = data[first_part:to_write]
self.write_pos = end_pos % self.size
self.count += to_write
return to_write
def read(self, num_samples: int) -> np.ndarray:
"""读取指定数量的样本"""
to_read = min(num_samples, self.count)
if to_read <= 0:
return np.array([], dtype=np.float32)
end_pos = self.read_pos + to_read
if end_pos <= self.size:
result = self.buffer[self.read_pos:end_pos].copy()
else:
first_part = self.size - self.read_pos
result = np.concatenate([
self.buffer[self.read_pos:],
self.buffer[:to_read - first_part]
])
self.read_pos = end_pos % self.size
self.count -= to_read
return result
七、语音活动检测(VAD)
7.1 VAD 的作用
语音活动检测(Voice Activity Detection)用于判断音频中是否包含人声。在实时语音对话中,VAD 的核心任务是:
- 检测用户何时开始说话(减少无意义的静音传输)
- 检测用户何时说完(触发 LLM 生成回复)
- 过滤背景噪音(避免误触发识别)
7.2 使用 Silero VAD
Silero VAD 是目前最优秀的轻量级 VAD 模型:
import torch
import numpy as np
class VoiceActivityDetector:
"""基于 Silero VAD 的语音活动检测器"""
def __init__(self, threshold: float = 0.5, sample_rate: int = 16000):
self.model, _ = torch.hub.load(
repo_or_dir='snakers4/silero-vad',
model='silero_vad',
force_reload=False
)
self.threshold = threshold
self.sample_rate = sample_rate
self.is_speech = False
self.silence_duration = 0 # 持续静音时长(秒)
self.speech_duration = 0 # 持续说话时长(秒)
# 可调参数
self.silence_timeout = 1.5 # 静音多久算说完(秒)
self.min_speech_duration = 0.3 # 最短语音时长(秒)
def process_chunk(self, audio_chunk: np.ndarray) -> dict:
"""
处理一个音频块,返回状态信息
返回: {"event": "start"|"speaking"|"end"|"silence", "is_speech": bool}
"""
tensor = torch.from_numpy(audio_chunk).float()
prob = self.model(tensor, self.sample_rate).item()
current_speech = prob > self.threshold
chunk_duration = len(audio_chunk) / self.sample_rate
if current_speech:
self.speech_duration += chunk_duration
self.silence_duration = 0
if not self.is_speech:
if self.speech_duration >= 0.1: # 连续说话超过 100ms 才算开始
self.is_speech = True
return {"event": "start", "is_speech": True}
return {"event": "speaking", "is_speech": True}
else:
if self.is_speech:
self.silence_duration += chunk_duration
if self.silence_duration >= self.silence_timeout:
# 确认说完
if self.speech_duration >= self.min_speech_duration:
self.is_speech = False
self.speech_duration = 0
self.silence_duration = 0
return {"event": "end", "is_speech": False}
return {"event": "speaking", "is_speech": True}
self.speech_duration = 0
return {"event": "silence", "is_speech": False}
7.3 VAD 集成到 WebSocket 流
class VoiceWebSocketHandler:
def __init__(self):
self.vad = VoiceActivityDetector(threshold=0.5)
self.asr = StreamingASR()
self.audio_buffer = [] # 用于缓冲待识别的音频
async def on_audio_data(self, audio_bytes: bytes, websocket: WebSocket):
audio = AudioProcessor.pcm16_to_float32(audio_bytes)
# 分块处理(每 512 样本一块,约 32ms @ 16kHz)
chunk_size = 512
for i in range(0, len(audio), chunk_size):
chunk = audio[i:i + chunk_size]
if len(chunk) < chunk_size:
chunk = np.pad(chunk, (0, chunk_size - len(chunk)))
vad_result = self.vad.process_chunk(chunk)
if vad_result["event"] == "start":
# 用户开始说话,清空缓冲区
self.audio_buffer = []
await websocket.send_json({"type": "user_started_speaking"})
elif vad_result["is_speech"]:
# 正在说话,累积音频
self.audio_buffer.append(chunk)
elif vad_result["event"] == "end":
# 用户说完了
full_audio = np.concatenate(self.audio_buffer)
final_text = self.asr.feed_audio(full_audio)
await websocket.send_json({
"type": "speech_end",
"text": final_text
})
# 触发 LLM 回复...
self.audio_buffer = []
八、端到端延迟优化
8.1 延迟来源分析
一个典型的语音 AI 对话的延迟分布:
用户说完 → [网络传输 50ms] → [VAD 检测 100ms] → [ASR 识别 500ms]
→ [LLM 首 token 200ms] → [TTS 合成首句 300ms] → [网络回传 50ms] → 用户听到
总延迟: ~1.2 秒
8.2 优化策略
策略一:流水线并行
import asyncio
async def optimized_pipeline(audio: np.ndarray, session: VoiceSession):
"""优化后的处理流水线"""
# 1. ASR 识别(可以与 VAD 的尾部重叠执行)
asr_task = asyncio.create_task(
asyncio.to_thread(session.asr.finalize)
)
# 2. 等待 ASR 完成
text = await asr_task
session.conv.add_user_message(text)
# 3. LLM 流式生成 + TTS 流式合成并行
tts_buffer = ""
tts_tasks = []
async for token in llm.stream_chat(session.conv.get_messages()):
tts_buffer += token
# 按标点切分,每积累一句就发起 TTS
if token in "。!?.!?\n":
sentence = tts_buffer.strip()
if sentence:
task = asyncio.create_task(session.tts.synthesize(sentence))
tts_tasks.append(task)
tts_buffer = ""
# 4. 收集 TTS 结果并按顺序发送
for task in tts_tasks:
audio_data = await task
await session.ws.send_bytes(audio_data)
策略二:ASR 模型选择优化
# 使用 INT8 量化加速推理
model = WhisperModel("small", device="cuda", compute_type="int8_float16")
# 使用 VAD 预过滤静音段
segments, info = model.transcribe(
audio,
vad_filter=True,
vad_parameters=dict(
min_silence_duration_ms=500,
speech_pad_ms=200
)
)
策略三:首包 TTS 快速响应
class PriorityTTS:
"""优先级 TTS:第一句用快速引擎,后续用高质量引擎"""
def __init__(self):
self.fast_engine = edge_tts # 快速
self.high_quality_engine = None # 高质量(如 CosyVoice)
async def synthesize_first(self, text: str) -> bytes:
"""第一句:快速合成,降低首字延迟"""
communicate = edge_tts.Communicate(text, "zh-CN-YunxiNeural") # 男声,速度快
audio = b""
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio += chunk["data"]
return audio
async def synthesize_normal(self, text: str) -> bytes:
"""后续句子:高质量合成"""
return await self.synthesize_first(text) # 或使用高质量引擎
8.3 延迟监控
import time
class LatencyTracker:
"""端到端延迟追踪器"""
def __init__(self):
self.milestones = {}
def mark(self, name: str):
self.milestones[name] = time.perf_counter()
def report(self) -> dict:
if len(self.milestones) < 2:
return {}
names = list(self.milestones.keys())
report = {}
for i in range(1, len(names)):
delta = (self.milestones[names[i]] - self.milestones[names[i-1]]) * 1000
report[f"{names[i-1]} → {names[i]}"] = f"{delta:.0f}ms"
total = (self.milestones[names[-1]] - self.milestones[names[0]]) * 1000
report["总延迟"] = f"{total:.0f}ms"
return report
# 使用示例
tracker = LatencyTracker()
tracker.mark("vad_end")
tracker.mark("asr_done")
tracker.mark("llm_first_token")
tracker.mark("tts_first_audio")
tracker.mark("audio_sent")
print(tracker.report())
# {'vad_end → asr_done': '480ms', 'asr_done → llm_first_token': '150ms', ...}
九、多语言支持
9.1 多语言 ASR
Whisper 本身支持 99 种语言,可以自动检测语言:
def multilingual_transcribe(audio_path: str) -> tuple[str, str]:
"""多语言识别,返回 (语言, 文字)"""
segments, info = model.transcribe(audio_path, beam_size=5)
language = info.language
text = "".join([seg.text for seg in segments])
return language, text
# 手动指定语言可以提升精度
segments, _ = model.transcribe(audio, language="ja") # 日语
segments, _ = model.transcribe(audio, language="en") # 英语
9.2 多语言 TTS
Edge TTS 支持超过 300 种语音,覆盖 70+ 语言:
MULTILINGUAL_VOICES = {
"zh": "zh-CN-XiaoxiaoNeural",
"en": "en-US-JennyNeural",
"ja": "ja-JP-NanamiNeural",
"ko": "ko-KR-SunHiNeural",
"fr": "fr-FR-DeniseNeural",
"de": "de-DE-KatjaNeural",
"es": "es-ES-ElviraNeural",
}
async def multilingual_tts(text: str, language: str, output_path: str):
voice = MULTILINGUAL_VOICES.get(language, "en-US-JennyNeural")
communicate = edge_tts.Communicate(text, voice)
await communicate.save(output_path)
9.3 自动语言路由
class MultilingualRouter:
"""多语言路由:自动选择 ASR 语言和 TTS 语音"""
def __init__(self):
self.detected_language = "zh"
def on_asr_result(self, language: str):
"""ASR 检测到语言后更新路由"""
self.detected_language = language
def get_tts_voice(self) -> str:
"""根据检测到的语言选择 TTS 语音"""
return MULTILINGUAL_VOICES.get(self.detected_language, "en-US-JennyNeural")
def get_llm_prompt(self) -> str:
"""根据语言切换 LLM 回复语言"""
lang_prompts = {
"zh": "请用中文回答。",
"en": "Please respond in English.",
"ja": "日本語で回答してください。",
}
return lang_prompts.get(self.detected_language, "Please respond in English.")
十、实战:构建实时语音 AI 助手
10.1 项目结构
voice-ai-assistant/
├── server/
│ ├── main.py # FastAPI 主入口
│ ├── asr.py # 语音识别模块
│ ├── llm.py # LLM 对话模块
│ ├── tts.py # 语音合成模块
│ ├── vad.py # 语音活动检测
│ ├── session.py # 会话管理
│ └── config.py # 配置文件
├── web/
│ ├── index.html # 前端页面
│ ├── voice.js # 语音交互逻辑
│ └── style.css # 样式
├── requirements.txt
└── README.md
10.2 完整服务端代码
# server/main.py
import asyncio
import json
import numpy as np
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from dataclasses import dataclass, field
app = FastAPI()
# ==================== 配置 ====================
@dataclass
class Config:
asr_model: str = "small"
llm_api_base: str = "http://localhost:8000/v1"
llm_model: str = "qwen2.5-7b-instruct"
tts_voice: str = "zh-CN-XiaoxiaoNeural"
vad_threshold: float = 0.5
silence_timeout: float = 1.5
sample_rate: int = 16000
config = Config()
# ==================== ASR ====================
from faster_whisper import WhisperModel
asr_model = WhisperModel(config.asr_model, device="cpu", compute_type="int8")
# ==================== TTS ====================
import edge_tts
async def synthesize_speech(text: str, voice: str) -> bytes:
communicate = edge_tts.Communicate(text, voice)
audio = b""
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio += chunk["data"]
return audio
# ==================== LLM ====================
import httpx
async def stream_llm(messages: list):
url = f"{config.llm_api_base}/chat/completions"
payload = {
"model": config.llm_model,
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 300,
}
async with httpx.AsyncClient() as client:
async with client.stream("POST", url, json=payload, timeout=30) as resp:
async for line in resp.aiter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
chunk = json.loads(line[6:])
delta = chunk["choices"][0].get("delta", {})
if "content" in delta:
yield delta["content"]
# ==================== VAD ====================
import torch
vad_model, _ = torch.hub.load('snakers4/silero-vad', 'silero_vad', force_reload=False)
# ==================== 会话 ====================
@dataclass
class Session:
ws: WebSocket
history: list = field(default_factory=list)
audio_buffer: list = field(default_factory=list)
is_speaking: bool = False
silence_counter: int = 0
SYSTEM_PROMPT = """你是一个实时语音助手。回答简洁口语化,每次 1-3 句话。
不用 markdown、不用列表符号、数字用文字表达。"""
# ==================== WebSocket ====================
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
session = Session(ws=ws)
try:
while True:
msg = await ws.receive()
if "bytes" in msg:
audio = np.frombuffer(msg["bytes"], dtype=np.int16).astype(np.float32) / 32768.0
# VAD 检测
chunk_size = 512
for i in range(0, len(audio), chunk_size):
chunk = audio[i:i+chunk_size]
if len(chunk) < chunk_size:
chunk = np.pad(chunk, (0, chunk_size - len(chunk)))
tensor = torch.from_numpy(chunk).float()
prob = vad_model(tensor, config.sample_rate).item()
if prob > config.vad_threshold:
if not session.is_speaking:
session.is_speaking = True
session.audio_buffer = []
await ws.send_json({"type": "status", "text": "listening"})
session.audio_buffer.append(chunk)
session.silence_counter = 0
else:
if session.is_speaking:
session.silence_counter += 1
session.audio_buffer.append(chunk)
# 静音超过阈值 → 用户说完了
if session.silence_counter > int(config.silence_timeout * config.sample_rate / chunk_size):
session.is_speaking = False
await handle_speech_end(session)
elif "text" in msg:
data = json.loads(msg["text"])
if data.get("action") == "stop":
break
except WebSocketDisconnect:
pass
async def handle_speech_end(session: Session):
"""用户说完后处理"""
# ASR 识别
full_audio = np.concatenate(session.audio_buffer)
segments, info = asr_model.transcribe(full_audio, beam_size=5, language="zh")
user_text = "".join([s.text for s in segments]).strip()
if not user_text:
return
await session.ws.send_json({"type": "asr_result", "text": user_text})
# 构建对话
session.history.append({"role": "user", "content": user_text})
messages = [{"role": "system", "content": SYSTEM_PROMPT}] + session.history[-10:]
# 流式生成 + 逐句合成
full_response = ""
sentence_buffer = ""
await session.ws.send_json({"type": "status", "text": "thinking"})
async for token in stream_llm(messages):
full_response += token
sentence_buffer += token
await session.ws.send_json({"type": "llm_token", "text": token})
# 按标点分句合成
if any(p in token for p in "。!?.!?\n"):
sentence = sentence_buffer.strip()
if sentence:
await session.ws.send_json({"type": "status", "text": "speaking"})
audio = await synthesize_speech(sentence, config.tts_voice)
await session.ws.send_bytes(audio)
sentence_buffer = ""
# 处理剩余文本
if sentence_buffer.strip():
audio = await synthesize_speech(sentence_buffer.strip(), config.tts_voice)
await session.ws.send_bytes(audio)
session.history.append({"role": "assistant", "content": full_response})
# 静态文件服务
app.mount("/static", StaticFiles(directory="web"), name="static")
@app.get("/")
async def index():
return FileResponse("web/index.html")
10.3 前端页面
<!-- web/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>实时语音 AI 助手</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #0f0f23;
color: #e0e0e0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 480px;
text-align: center;
}
h1 { font-size: 1.5rem; margin-bottom: 2rem; color: #7eb8da; }
.mic-btn {
width: 120px; height: 120px;
border-radius: 50%;
border: 3px solid #4a9eff;
background: #1a1a3e;
cursor: pointer;
font-size: 3rem;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 2rem;
}
.mic-btn.recording {
border-color: #ff4a4a;
background: #3e1a1a;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255,74,74,0.4); }
50% { box-shadow: 0 0 0 20px rgba(255,74,74,0); }
}
.chat-box {
background: #1a1a2e;
border-radius: 12px;
padding: 1.5rem;
max-height: 400px;
overflow-y: auto;
text-align: left;
}
.msg { margin-bottom: 1rem; line-height: 1.6; }
.msg.user { color: #7eb8da; }
.msg.ai { color: #a8e6a1; }
.msg .label { font-size: 0.8rem; opacity: 0.7; }
.status { color: #aaa; font-size: 0.9rem; margin-top: 1rem; }
</style>
</head>
<body>
<div class="container">
<h1>🎤 实时语音 AI 助手</h1>
<button class="mic-btn" id="micBtn">🎙️</button>
<div class="status" id="status">点击麦克风开始对话</div>
<div class="chat-box" id="chatBox"></div>
</div>
<script>
let ws, audioContext, processor, isRecording = false;
const micBtn = document.getElementById('micBtn');
const chatBox = document.getElementById('chatBox');
const statusEl = document.getElementById('status');
micBtn.addEventListener('click', toggleRecording);
async function toggleRecording() {
if (isRecording) {
stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
ws = new WebSocket(`ws://${location.host}/ws`);
ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
if (typeof e.data === 'string') {
const msg = JSON.parse(e.data);
if (msg.type === 'asr_result') {
addMessage('user', '👤 你: ' + msg.text);
} else if (msg.type === 'llm_token') {
appendAIMessage(msg.text);
} else if (msg.type === 'status') {
const labels = { listening: '🎤 正在听...', thinking: '🤔 思考中...', speaking: '🔊 回复中...' };
statusEl.textContent = labels[msg.text] || '';
}
} else {
playAudio(e.data);
}
};
const stream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true, noiseSuppression: true }
});
audioContext = new AudioContext({ sampleRate: 16000 });
const source = audioContext.createMediaStreamSource(stream);
processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
if (!isRecording) return;
const data = e.inputBuffer.getChannelData(0);
const pcm = new Int16Array(data.length);
for (let i = 0; i < data.length; i++) {
pcm[i] = Math.max(-1, Math.min(1, data[i])) * 32767;
}
ws.send(pcm.buffer);
};
source.connect(processor);
processor.connect(audioContext.destination);
isRecording = true;
micBtn.classList.add('recording');
statusEl.textContent = '🎤 正在听...';
}
function stopRecording() {
isRecording = false;
micBtn.classList.remove('recording');
if (processor) processor.disconnect();
if (audioContext) audioContext.close();
if (ws) ws.close();
statusEl.textContent = '点击麦克风开始对话';
}
let currentAIMsg = null;
function appendAIMessage(text) {
if (!currentAIMsg) {
currentAIMsg = document.createElement('div');
currentAIMsg.className = 'msg ai';
currentAIMsg.innerHTML = '<div class="label">🤖 AI:</div><span></span>';
chatBox.appendChild(currentAIMsg);
}
currentAIMsg.querySelector('span').textContent += text;
chatBox.scrollTop = chatBox.scrollHeight;
}
function addMessage(role, text) {
currentAIMsg = null; // 新一轮对话
const div = document.createElement('div');
div.className = `msg ${role}`;
div.textContent = text;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
async function playAudio(buffer) {
const ctx = new AudioContext();
const audioBuffer = await ctx.decodeAudioData(buffer);
const source = ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(ctx.destination);
source.start();
}
</script>
</body>
</html>
10.4 启动与运行
# 安装依赖
pip install faster-whisper edge-tts fastapi uvicorn httpx torch torchaudio
# 启动 LLM 服务(以 vLLM 为例)
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--port 8000
# 启动语音助手服务
cd voice-ai-assistant
uvicorn server.main:app --host 0.0.0.0 --port 8080
# 浏览器访问 http://localhost:8080
10.5 性能基准
在不同硬件上的延迟表现(端到端,从用户说完到听到回复):
| 硬件配置 | ASR 延迟 | LLM 首 token | TTS 首句 | 总延迟 |
|---|---|---|---|---|
| CPU (i7-12700) | 800ms | 300ms | 400ms | ~1.8s |
| GPU (RTX 3060) | 200ms | 100ms | 400ms | ~1.0s |
| GPU (RTX 4090) | 100ms | 50ms | 400ms | ~0.7s |
优化建议:
- ASR 使用 GPU 加速 + INT8 量化可大幅降低延迟
- LLM 使用 vLLM 或 TensorRT-LLM 推理加速
- TTS 使用 Edge TTS 几乎无额外开销(网络延迟约 200-400ms)
十一、总结与进阶方向
本教程从零开始构建了一个完整的实时语音 AI 对话系统,涵盖了从语音识别到语音合成的全链路技术。以下是进阶方向:
- WebRTC 替代 WebSocket:获得更好的 NAT 穿透和音频回声消除
- 多轮打断:支持用户在 AI 说话时打断
- 情感识别:从语音中识别用户情绪,调整回复策略
- 声纹识别:区分不同说话人,支持多人对话
- 本地化部署:全套模型本地运行,无需外网
语音 AI 是大模型落地的最重要场景之一。掌握这套技术栈,你将能够构建媲美商业产品的语音对话体验。
本教程内容原创,仅供学习交流使用。