语音交互正在成为人机交互的核心入口。从智能客服到语音助手,从会议纪要到无障碍阅读,语音识别(ASR)和文字转语音(TTS)是构建语音能力的两大基石。本文将深入探讨ASR/TTS的技术原理、接口设计和工程实践,帮助开发者快速集成语音能力。
一、语音技术全景
1.1 两大核心能力
语音识别(ASR, Automatic Speech Recognition)
音频 → 文字
"你好世界" 的录音 → "你好世界"
文字转语音(TTS, Text-to-Speech)
文字 → 音频
"你好世界" → 一段自然流畅的语音音频
1.2 应用场景
| 场景 | 技术 | 说明 |
|---|---|---|
| 语音助手 | ASR + TTS | 听懂用户指令,语音播报结果 |
| 会议纪要 | ASR | 实时语音转文字,自动生成会议记录 |
| 有声读物 | TTS | 将文章、小说转为语音,解放双眼 |
| 智能客服 | ASR + TTS | 电话场景下的自动应答 |
| 无障碍服务 | TTS | 为视障用户朗读网页、文档内容 |
| 语音输入法 | ASR | 说话即打字,效率提升3倍 |
| 短视频配音 | TTS | 自动生成解说旁白,降低创作门槛 |
| 外语学习 | TTS | 标准发音示范,听力训练素材生成 |
二、语音识别(ASR)技术解析
2.1 技术原理
现代语音识别的主流技术路线已从传统HMM-GMM演进到端到端深度学习模型:
传统方案:
音频 → 特征提取(MFCC) → 声学模型(HMM-GMM) → 语言模型(N-gram) → 文字
现代方案(端到端):
音频 → 特征提取(Mel频谱) → 深度神经网络(Transformer/CTC) → 文字
关键技术指标:
| 指标 | 说明 | 行业水平 |
|---|---|---|
| WER | 词错误率,越低越好 | 中文普通话 < 5% |
| 延迟 | 从说完到出结果的时间 | 实时场景 < 500ms |
| 采样率 | 音频采样频率 | 16kHz(电话8kHz) |
| 支持格式 | 可处理的音频格式 | WAV, MP3, PCM, FLAC |
2.2 音频格式基础
在调用语音识别之前,理解音频格式至关重要:
/**
* 音频参数说明
* - 采样率(Sample Rate):每秒采样次数,16000Hz最常用
* - 位深(Bit Depth):每个采样点的精度,16bit标准
* - 声道(Channel):单声道(Mono)用于语音识别
* - 编码(Encoding):PCM原始数据 / MP3压缩 / WAV无损
*/
public class AudioConfig {
public static final int SAMPLE_RATE = 16000;
public static final int BIT_DEPTH = 16;
public static final int CHANNELS = 1; // Mono
/**
* 计算PCM音频时长(秒)
*/
public static double getDuration(long byteSize) {
// 时长 = 字节数 / (采样率 × 位深/8 × 声道数)
return (double) byteSize / (SAMPLE_RATE * BIT_DEPTH / 8 * CHANNELS);
}
}
2.3 接口设计
@RestController
@RequestMapping("/api")
public class SpeechController {
@Autowired
private SpeechRecognitionService asrService;
/**
* 语音识别:音频文件 → 文字
* 支持格式:WAV, MP3, PCM, FLAC, M4A, OGG
* 文件大小限制:20MB
*/
@PostMapping("/speechRecognition")
public Result recognize(
@RequestParam("file") MultipartFile audioFile,
@RequestParam(value = "language", defaultValue = "zh")
String language) {
// 1. 文件校验
if (audioFile.isEmpty()) {
return Result.error("音频文件不能为空");
}
long maxSize = 20 * 1024 * 1024; // 20MB
if (audioFile.getSize() > maxSize) {
return Result.error("文件大小不能超过20MB");
}
// 2. 格式校验
String filename = audioFile.getOriginalFilename();
if (!isValidAudioFormat(filename)) {
return Result.error("不支持的音频格式,支持WAV/MP3/PCM/FLAC/M4A/OGG");
}
// 3. 执行识别
RecognitionResult result = asrService.recognize(
audioFile, language
);
return Result.success(result);
}
private boolean isValidAudioFormat(String filename) {
if (filename == null) return false;
String ext = filename.substring(filename.lastIndexOf(".") + 1)
.toLowerCase();
return Set.of("wav", "mp3", "pcm", "flac", "m4a", "ogg")
.contains(ext);
}
}
2.4 返回数据结构
{
"code": 200,
"data": {
"text": "今天天气真不错,适合出去走走",
"duration": 3.5,
"language": "zh",
"confidence": 0.96
}
}
三、文字转语音(TTS)技术解析
3.1 技术原理
TTS经历了三代技术演进:
第一代:拼接合成
文字 → 分词 → 从语音库中选取音素片段 → 拼接成语音
特点:机械感强,像"机器人说话"
第二代:参数合成
文字 → 语言学分析 → 声学参数预测 → 声码器合成
特点:流畅度提升,但音质仍有差距
第三代:神经网络合成(当前主流)
文字 → Encoder → Decoder(Tacotron/FastSpeech) → 声码器(HiFi-GAN) → 语音
特点:接近真人发音,支持情感表达
3.2 关键参数
TTS的效果由多个参数共同决定:
| 参数 | 说明 | 取值范围 |
|---|---|---|
| voice | 发音人/音色 | 多种角色可选 |
| speed | 语速 | 0.5~2.0,1.0为正常 |
| volume | 音量 | 0~10,5为正常 |
| pitch | 音调 | -10~10,0为正常 |
| format | 输出格式 | MP3, WAV, PCM |
3.3 接口设计
@RestController
@RequestMapping("/api")
public class TtsController {
@Autowired
private TextToSpeechService ttsService;
/**
* 文字转语音
* 支持多种音色、语速、音量、音调调节
*/
@PostMapping("/textToSpeech")
public ResponseEntity<byte[]> synthesize(
@RequestBody TtsRequest request) {
// 1. 参数校验
if (StringUtils.isBlank(request.getText())) {
throw new IllegalArgumentException("文本内容不能为空");
}
if (request.getText().length() > 5000) {
throw new IllegalArgumentException("文本长度不能超过5000字");
}
// 2. 设置默认值
String voice = Optional.ofNullable(request.getVoice())
.orElse("zh-CN-XiaoxiaoNeural");
float speed = Optional.ofNullable(request.getSpeed())
.orElse(1.0f);
int volume = Optional.ofNullable(request.getVolume())
.orElse(5);
// 3. 合成语音
byte[] audioData = ttsService.synthesize(
request.getText(), voice, speed, volume
);
// 4. 返回音频流
return ResponseEntity.ok()
.header("Content-Type", "audio/mpeg")
.header("Content-Disposition",
"attachment; filename=speech.mp3")
.body(audioData);
}
}
3.4 请求参数
{
"text": "欢迎使用轻语API开放平台,我们为您提供稳定可靠的接口服务。",
"voice": "zh-CN-XiaoxiaoNeural",
"speed": 1.0,
"volume": 5,
"pitch": 0,
"format": "mp3"
}
3.5 常见音色选择
不同场景适合不同的音色:
| 音色 | 风格 | 适用场景 |
|---|---|---|
| 晓晓 (Xiaoxiao) | 温暖亲切的女声 | 通用播报、语音助手 |
| 云扬 (Yunyang) | 稳重大气的男声 | 新闻播报、纪录片 |
| 晓墨 (Xiaomo) | 活泼可爱的女声 | 儿童故事、趣味内容 |
| 云希 (Yunxi) | 年轻阳光的男声 | 有声小说、教育场景 |
四、前端集成实践
4.1 语音识别 - 文件上传方式
/**
* 上传音频文件进行语音识别
*/
async function recognizeSpeech(audioFile) {
const formData = new FormData();
formData.append('file', audioFile);
formData.append('language', 'zh');
const response = await fetch('/api/speechRecognition', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.code === 200) {
console.log('识别结果:', data.data.text);
console.log('音频时长:', data.data.duration, '秒');
console.log('置信度:', data.data.confidence);
}
return data;
}
// 绑定文件选择
document.getElementById('audioInput')
.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) recognizeSpeech(file);
});
4.2 浏览器录音 + 识别
更好的用户体验是直接在浏览器中录音,然后发送识别:
class AudioRecorder {
constructor() {
this.mediaRecorder = null;
this.chunks = [];
}
/**
* 开始录音
*/
async start() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
this.chunks = [];
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
this.chunks.push(e.data);
}
};
this.mediaRecorder.start();
console.log('开始录音...');
}
/**
* 停止录音并返回Blob
*/
stop() {
return new Promise((resolve) => {
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.chunks, {
type: 'audio/webm'
});
// 停止所有音轨
this.mediaRecorder.stream
.getTracks().forEach(t => t.stop());
resolve(blob);
};
this.mediaRecorder.stop();
});
}
}
// 使用示例
const recorder = new AudioRecorder();
document.getElementById('recordBtn').addEventListener('click',
async () => {
await recorder.start();
}
);
document.getElementById('stopBtn').addEventListener('click',
async () => {
const audioBlob = await recorder.stop();
// 将录音发送到识别接口
const file = new File([audioBlob], 'recording.webm', {
type: 'audio/webm'
});
const result = await recognizeSpeech(file);
document.getElementById('resultText').textContent =
result.data.text;
}
);
4.3 文字转语音 - 在线播放
/**
* 文字转语音并播放
*/
async function speakText(text, options = {}) {
const requestBody = {
text: text,
voice: options.voice || 'zh-CN-XiaoxiaoNeural',
speed: options.speed || 1.0,
volume: options.volume || 5,
format: 'mp3'
};
const response = await fetch('/api/textToSpeech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error('语音合成失败');
}
// 将响应转为Blob并播放
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.onended = () => {
URL.revokeObjectURL(audioUrl); // 释放内存
};
await audio.play();
return audio;
}
// 使用示例
document.getElementById('speakBtn').addEventListener('click',
() => {
const text = document.getElementById('textInput').value;
speakText(text, { voice: 'zh-CN-YunxiNeural', speed: 1.1 });
}
);
4.4 语音播放器组件
提供更完善的播放控制体验:
class SpeechPlayer {
constructor(containerEl) {
this.container = containerEl;
this.audio = null;
this.isPlaying = false;
}
/**
* 合成并加载音频(不自动播放)
*/
async load(text, options) {
const response = await fetch('/api/textToSpeech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, ...options })
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
if (this.audio) {
this.audio.pause();
URL.revokeObjectURL(this.audio.src);
}
this.audio = new Audio(url);
this.bindEvents();
}
bindEvents() {
this.audio.addEventListener('timeupdate', () => {
const progress = this.audio.currentTime /
this.audio.duration * 100;
this.updateProgress(progress);
});
this.audio.addEventListener('ended', () => {
this.isPlaying = false;
this.updateUI();
});
}
toggle() {
if (!this.audio) return;
if (this.isPlaying) {
this.audio.pause();
} else {
this.audio.play();
}
this.isPlaying = !this.isPlaying;
this.updateUI();
}
/**
* 调节语速(重新合成)
*/
async changeSpeed(text, speed) {
await this.load(text, { speed });
this.toggle();
}
}
五、工程实践要点
5.1 音频预处理
实际项目中,原始音频质量参差不齐,预处理可以显著提升识别准确率:
public class AudioPreprocessor {
/**
* 音频预处理流程
* 1. 格式转换:统一转为16kHz/16bit/Mono的WAV
* 2. 降噪:去除背景噪声
* 3. 静音检测:去除首尾静音段
* 4. 音量归一化:统一音量水平
*/
public byte[] preprocess(InputStream audioInput, String format) {
// 转为标准WAV格式
AudioFormat targetFormat = new AudioFormat(
16000, // 采样率
16, // 位深
1, // 单声道
true, // signed
false // little-endian
);
// 使用FFmpeg进行格式转换和降噪
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-i", "pipe:0",
"-ar", "16000", // 采样率
"-ac", "1", // 单声道
"-sample_fmt", "s16", // 16bit
"-af", "silenceremove=1:0:-40dB," + // 去首尾静音
"highpass=f=80," + // 高通滤波
"lowpass=f=8000," + // 低通滤波
"volume=1.5", // 音量增益
"-f", "wav", "pipe:1"
);
// 执行转换...
return executeProcess(pb, audioInput);
}
}
5.2 长音频分片处理
对于会议录音等长音频,需要分片处理:
public class LongAudioProcessor {
private static final int CHUNK_DURATION = 60; // 每片60秒
private static final int OVERLAP = 2; // 2秒重叠
/**
* 长音频分片识别
* 策略:将长音频按固定时长切片,相邻片段保留重叠区间,
* 识别后根据重叠部分去重拼接
*/
public String recognizeLong(InputStream audioInput) {
List<byte[]> chunks = splitAudio(audioInput,
CHUNK_DURATION, OVERLAP);
StringBuilder fullText = new StringBuilder();
for (int i = 0; i < chunks.size(); i++) {
String chunkText = asrService.recognize(chunks.get(i));
if (i > 0 && chunkText.length() > 0) {
// 去除重叠部分的重复文字
chunkText = removeOverlap(
fullText.toString(), chunkText
);
}
fullText.append(chunkText);
}
return fullText.toString();
}
/**
* 去除重叠部分
* 思路:找到前一段末尾和当前段开头的最长公共子串
*/
private String removeOverlap(String previous, String current) {
int checkLength = Math.min(20, previous.length());
String tail = previous.substring(previous.length() - checkLength);
for (int len = checkLength; len >= 2; len--) {
String suffix = tail.substring(checkLength - len);
if (current.startsWith(suffix)) {
return current.substring(suffix.length());
}
}
return current;
}
}
5.3 TTS长文本分段合成
TTS接口通常有文本长度限制,长文本需要分段合成后拼接:
public class LongTextSynthesizer {
private static final int MAX_CHARS = 500; // 每段最大字符数
/**
* 长文本分段合成
* 按句号、感叹号、问号等自然断句点切分
*/
public byte[] synthesizeLong(String text, String voice) {
List<String> segments = splitByNaturalBreaks(text, MAX_CHARS);
List<byte[]> audioSegments = new ArrayList<>();
for (String segment : segments) {
byte[] audio = ttsService.synthesize(segment, voice);
audioSegments.add(audio);
}
// 拼接所有音频片段
return concatenateAudio(audioSegments);
}
/**
* 按自然断句点切分文本
*/
private List<String> splitByNaturalBreaks(String text, int maxLen) {
List<String> segments = new ArrayList<>();
StringBuilder current = new StringBuilder();
// 按句子分割
String[] sentences = text.split("(?<=[。!?;\\.!?;])");
for (String sentence : sentences) {
if (current.length() + sentence.length() > maxLen
&& current.length() > 0) {
segments.add(current.toString().trim());
current = new StringBuilder();
}
current.append(sentence);
}
if (current.length() > 0) {
segments.add(current.toString().trim());
}
return segments;
}
}
5.4 缓存策略
TTS合成是计算密集型操作,相同文本+相同参数的结果应当缓存:
@Service
public class TtsCacheService {
private final Cache<String, byte[]> cache = Caffeine.newBuilder()
.maximumWeight(500 * 1024 * 1024) // 最大500MB
.weigher((String k, byte[] v) -> v.length)
.expireAfterAccess(Duration.ofHours(6))
.build();
/**
* 生成缓存Key
* 基于文本内容+语音参数生成唯一标识
*/
private String buildKey(String text, String voice,
float speed, int volume) {
String raw = text + "|" + voice + "|" + speed + "|" + volume;
return DigestUtils.md5Hex(raw);
}
public byte[] getOrSynthesize(String text, String voice,
float speed, int volume) {
String key = buildKey(text, voice, speed, volume);
return cache.get(key, k ->
ttsService.synthesize(text, voice, speed, volume)
);
}
}
六、常见问题与调优
6.1 语音识别准确率提升
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 识别结果有错别字 | 同音字混淆 | 结合语言模型做纠错 |
| 背景噪声干扰 | 录音环境嘈杂 | 音频预处理降噪 |
| 方言识别差 | 模型主要训练普通话 | 指定方言参数或使用专用模型 |
| 专业术语识别错误 | 词表中缺少专业词汇 | 添加自定义热词 |
| 长段落断句混乱 | 连续语音缺少停顿 | 结合标点预测模型 |
6.2 TTS音质优化
1. 文本预处理
- 数字转读法:"2024年" → "二零二四年"
- 英文处理:"API" → 逐字母读 或 整词读
- 特殊符号:"¥100" → "一百元"
2. SSML标记增强(如果支持)
<speak>
欢迎使用
<break time="500ms"/>
轻语API
<emphasis level="strong">开放平台</emphasis>
</speak>
3. 参数微调
- 语速:新闻播报1.0~1.1,故事朗读0.9~1.0
- 音调:根据内容情绪适当调整
- 停顿:段落间自动插入适当停顿
七、在线体验
想快速体验语音识别和文字转语音的效果?轻语API开放平台 提供了开箱即用的语音接口服务:
- 语音识别:上传音频文件即可获取识别文字,支持WAV、MP3等多种格式
- 文字转语音(V2):支持多种音色、语速、音量、音调调节,生成自然流畅的语音
按次计费低至0.001元/次,注册即送免费额度,适合个人开发者和中小企业快速集成语音能力。
八、总结
语音识别和文字转语音是AI语音交互的两大支柱,工程落地时需要关注:
- 音频预处理:格式转换、降噪、静音去除,直接影响识别准确率
- 长内容分片:无论ASR还是TTS,长内容都需要分片处理再拼接
- 缓存复用:TTS结果的缓存可以显著降低计算开销和响应延迟
- 参数调优:音色选择、语速音调调节、热词配置,让效果更贴合业务场景
- 错误处理:网络超时、格式不支持、内容过长等异常情况的兜底
掌握这些要点,就能为你的应用快速接入"能听能说"的语音能力。
本文基于实际项目经验总结,欢迎交流讨论。