欢迎光临
我们一直在努力

手写 AI 文本到语音(TTS)系统:从零实现语音合成引擎

一、引言

文本到语音(Text-to-Speech, TTS)技术是人工智能最贴近我们日常生活的应用之一——手机导航的语音播报、智能音箱的对话回复、短视频的 AI 配音、阅读障碍者的辅助工具……TTS 已渗透到生活的方方面面。

早期 TTS 系统以拼接合成(Concatenative Synthesis)为主:预先录制海量语音片段,在运行时按需拼接。这类方法音质稳定但灵活性极差——你无法让一个只录了中文的语音库说出英文。2016 年 WaveNet 的出现彻底改变了 TTS 的技术路线,端到端神经语音合成(Neural TTS)成为主流。如今,从 Tacotron 到 FastSpeech,从 VITS 到 CosyVoice,TTS 技术在自然度、可控性和推理速度上不断突破。

本文将带你从零构建一个完整的 TTS 系统。我们的目标是:用代码清晰展示 TTS 的核心技术栈,包括文本前端处理(文本规范化、音素转换)、声学模型(从 Mel 频谱预测到波形生成的全流程)、以及完整的训练与推理管线。你将理解端到端 TTS 系统"黑盒"背后的每一个齿轮如何咬合。

注意:本文是教学性质的简化实现。完整的工业级 TTS 需要数万小时标注数据和多 GPU 分布式训练,但核心思想完全一致。

技术路线概览

一个现代神经 TTS 系统通常包含三个核心模块:

  • 文本前端(Text Frontend):将原始文本转换为语言学特征(音素序列、韵律边界等)
  • 声学模型(Acoustic Model):将语言学特征映射为声学特征(通常是 Mel 频谱图)
  • 声码器(Vocoder):将声学特征转换为原始音频波形
  • 本文的代码将实现一条完整但精简的流水线,每个模块都可独立运行和调试。


    二、TTS 系统架构概览

    在深入代码之前,我们先建立 TTS 系统的全局视角。下图展示了从用户输入到音频输出的完整数据流:

    用户输入文本 "Hello World"


    ┌─────────────────────┐
    │ 文本前端处理器 │
    │ ┌─────────────────┐ │
    │ │ 文本规范化 │ │ → "Hello World" → "hello world"
    │ │ 分词 │ │ → ["hello", "world"]
    │ │ 音素转换 │ │ → [HH AH L OW W ER L D]
    │ │ 韵律预测 │ │ → 加入停顿/重音标记
    │ └─────────────────┘ │
    └─────────┬───────────┘
    │ 音素序列(整数ID)

    ┌─────────────────────┐
    │ 编码器(Encoder) │
    │ ┌─────────────────┐ │
    │ │ 嵌入层 │ │
    │ │ 多头自注意力 │ │
    │ │ 位置编码 │ │
    │ └─────────────────┘ │
    └─────────┬───────────┘
    │ 隐层表示(hidden states)

    ┌─────────────────────┐
    │ 持续时间预测器 │
    │ ┌─────────────────┐ │
    │ │ 卷积层 + 线性层 │ │
    │ └─────────────────┘ │
    └─────────┬───────────┘
    │ 每个音素持续多少帧

    ┌─────────────────────┐
    │ 长度调节器 │
    │ → 根据持续时间展开 │
    └─────────┬───────────┘
    │ 对齐后的隐层序列

    ┌─────────────────────┐
    │ 解码器(Decoder) │
    │ ┌─────────────────┐ │
    │ │ 多头注意力 │ │
    │ │ 前馈网络 │ │
    │ │ 线性投影 → Mel │ │
    │ └─────────────────┘ │
    └─────────┬───────────┘
    │ Mel 频谱图(80维,T帧)

    ┌─────────────────────┐
    │ 声码器(Vocoder) │
    │ ┌─────────────────┐ │
    │ │ HiFi-GAN 生成器 │ │
    │ │ (多尺度残差块) │ │
    │ └─────────────────┘ │
    └─────────┬───────────┘
    │ 原始音频波形

    音频输出 (.wav)

    这个架构借鉴了 FastSpeech 系列的设计理念——非自回归、时长可控、适合生产环境。我们分为五个部分展开:

    • 编码器:将音素序列编码为上下文感知的隐层表示
    • 持续时间预测器:预测每个音素对应的 Mel 帧数(解决文本-语音长度不对齐的问题)
    • 长度调节器:根据预测的持续时间将编码展开到帧级别
    • 解码器:从展开的隐层表示生成 Mel 频谱图
    • 声码器:将 Mel 频谱转换为可播放的波形

    三、环境搭建与数据准备

    3.1 依赖安装

    我们使用 PyTorch 作为深度学习框架,librosa 处理音频,以及 HiFi-GAN 风格的声码器相关依赖:

    # 创建虚拟环境
    python -m venv tts_env
    source tts_env/bin/activate

    # 安装核心依赖
    pip install torch torchvision torchaudio –index-url https://download.pytorch.org/whl/cu118
    pip install librosa soundfile numpy scipy matplotlib tqdm

    # 用于文本前端处理
    pip install phonemizer g2p-en

    # 语音Activity Detection(可选)
    pip install webrtcvad

    3.2 数据集选择

    对教学而言,我们使用 LJSpeech 数据集——一个包含 13,100 条英文语音片段(总长约 24 小时)的公共数据集,单说话人(一位女性朗读者),16kHz 采样率,44-bit PCM 格式。

    # dataset.py
    import os
    import json
    import torch
    from torch.utils.data import Dataset
    import torchaudio
    import numpy as np

    class LJSpeechDataset(Dataset):
    """
    LJSpeech 数据集加载器
    数据集结构:
    LJSpeech-1.1/
    ├── metadata.csv # 文件名|文本|规范化文本
    └── wavs/
    ├── LJ001-0001.wav
    └── …
    """
    def __init__(self, root_dir, sample_rate=22050):
    self.root_dir = root_dir
    self.sample_rate = sample_rate
    self.metadata = self._load_metadata()

    def _load_metadata(self):
    """加载 metadata.csv"""
    meta_path = os.path.join(self.root_dir, 'metadata.csv')
    entries = []
    with open(meta_path, 'r', encoding='utf-8') as f:
    for line in f:
    parts = line.strip().split('|')
    if len(parts) >= 3:
    entries.append({
    'file_id': parts[0],
    'text': parts[2], # 使用规范化文本
    'wav_path': os.path.join(self.root_dir, 'wavs', f"{parts[0]}.wav")
    })
    return entries

    def __len__(self):
    return len(self.metadata)

    def __getitem__(self, idx):
    entry = self.metadata[idx]

    # 加载音频
    waveform, sr = torchaudio.load(entry['wav_path'])
    if sr != self.sample_rate:
    resampler = torchaudio.transforms.Resample(sr, self.sample_rate)
    waveform = resampler(waveform)

    return {
    'text': entry['text'],
    'waveform': waveform.squeeze(0), # 移除通道维度
    'file_id': entry['file_id']
    }

    3.3 音频预处理

    音频预处理的目的是从原始波形提取 Mel 频谱图——这是声学模型的"输入-输出目标"。

    # audio_processing.py
    import torch
    import torchaudio
    import torchaudio.functional as F

    class MelSpectrogram:
    """
    Mel 频谱图提取器
    使用 torchaudio 实现,参数配置与 Tacotron2/FastSpeech 兼容
    """
    def __init__(self,
    sample_rate=22050,
    n_fft=1024,
    win_length=1024,
    hop_length=256,
    n_mels=80,
    f_min=0,
    f_max=8000):
    self.mel_transform = torchaudio.transforms.MelSpectrogram(
    sample_rate=sample_rate,
    n_fft=n_fft,
    win_length=win_length,
    hop_length=hop_length,
    n_mels=n_mels,
    f_min=f_min,
    f_max=f_max
    )
    self.hop_length = hop_length
    self.sample_rate = sample_rate

    def extract(self, waveform):
    """
    从波形提取 Mel 频谱
    参数:
    waveform: (T,) 的一维张量
    返回:
    mel: (n_mels, T_mel) 的 Mel 频谱
    """
    mel = self.mel_transform(waveform)
    # 转换为对数刻度(分贝)
    mel = torch.log(torch.clamp(mel, min=1e-5))
    return mel

    def reconstruct_waveform(self, mel, vocoder):
    """
    使用声码器从 Mel 恢复波形
    参数:
    mel: (n_mels, T_mel) 张量
    vocoder: 声码器模型
    """
    return vocoder.infer(mel)

    # 数据增强:SpecAugment(基础版)
    def spec_augment(mel, freq_mask_param=10, time_mask_param=10):
    """
    对 Mel 频谱进行频率掩码和时间掩码
    提高模型对缺失信息的鲁棒性
    """
    mel = mel.clone()
    n_mels, T = mel.shape

    # 频率掩码
    f = int(np.random.uniform(0, freq_mask_param))
    f_start = np.random.randint(0, max(1, n_mels – f))
    if f > 0:
    mel[f_start:f_start+f, :] = mel.min()

    # 时间掩码
    t = int(np.random.uniform(0, time_mask_param))
    t_start = np.random.randint(0, max(1, T – t))
    if t > 0:
    mel[:, t_start:t_start+t] = mel.min()

    return mel


    四、文本前端处理器

    文本前端是 TTS 系统中容易被低估但至关重要的模块。它的任务是把原始人类语言转换成模型能理解的符号序列。

    4.1 文本规范化

    原始文本中包含数字、缩写、符号等,都需要展开为对应的发音形式:

    # text_frontend.py
    import re
    from typing import List, Dict

    class TextNormalizer:
    """
    文本规范化:将数字、缩写、符号转换为自然语言形式
    """
    def __init__(self):
    # 数字映射
    self._digit_map = {
    '0': 'zero', '1': 'one', '2': 'two', '3': 'three', '4': 'four',
    '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine'
    }

    def normalize(self, text: str) -> str:
    """完整规范化流水线"""
    text = text.strip()
    text = self._normalize_abbreviations(text)
    text = self._normalize_numbers(text)
    text = self._normalize_punctuation(text)
    return text.lower()

    def _normalize_abbreviations(self, text: str) -> str:
    """将常见缩写展开"""
    abbreviations = {
    r'\\bMr\\.': 'Mister',
    r'\\bMrs\\.': 'Misses',
    r'\\bDr\\.': 'Doctor',
    r'\\bProf\\.': 'Professor',
    r'\\bSt\\.': 'Saint',
    r'\\bAve\\.': 'Avenue',
    r'\\be\\.g\\.': 'for example',
    r'\\bi\\.e\\.': 'that is',
    }
    for pattern, replacement in abbreviations.items():
    text = re.sub(pattern, replacement, text)
    return text

    def _normalize_numbers(self, text: str) -> str:
    """将数字转为文字"""
    def _replace_num(match):
    num_str = match.group()
    try:
    num = int(num_str)
    return self._number_to_words(num)
    except ValueError:
    return num_str

    # 匹配独立的数字(不是单词的一部分)
    text = re.sub(r'\\b\\d+\\b', _replace_num, text)
    return text

    def _number_to_words(self, num: int) -> str:
    """将整数转为英文单词"""
    if num < 10:
    return self._digit_map[str(num)]
    elif num < 20:
    teens = ['ten', 'eleven', 'twelve', 'thirteen', 'fourteen',
    'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen']
    return teens[num – 10]
    elif num < 100:
    tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty',
    'sixty', 'seventy', 'eighty', 'ninety']
    ten = tens[num // 10]
    unit = self._digit_map[str(num % 10)] if num % 10 else ''
    return f"{ten} {unit}".strip()
    elif num < 1000:
    hundreds = self._digit_map[str(num // 100)] + ' hundred'
    rest = self._number_to_words(num % 100) if num % 100 else ''
    return f"{hundreds} {rest}".strip()
    elif num < 1_000_000:
    thousands = self._number_to_words(num // 1000) + ' thousand'
    rest = self._number_to_words(num % 1000) if num % 1000 else ''
    return f"{thousands} {rest}".strip()
    else:
    # 对超大数字简化为数字字符串
    return str(num)

    def _normalize_punctuation(self, text: str) -> str:
    """去除多余空格,统一标点"""
    text = re.sub(r'[^\\w\\s\\.\\,\\!\\?\\-]', '', text)
    text = re.sub(r'\\s+', ' ', text)
    return text.strip()

    # 演示
    if __name__ == '__main__':
    normalizer = TextNormalizer()
    tests = [
    "Dr. Smith lives at 42nd Street.",
    "I have 1000 dollars.",
    "In 2024, AI has made great progress."
    ]
    for t in tests:
    print(f"输入: {t}")
    print(f"输出: {normalizer.normalize(t)}")
    print("—")

    4.2 音素转换

    规范化后的文本需要转换为音素序列。国际上最广泛使用的是 ARPABET 音标系统。我们使用 phonemizer 库实现:

    # text_frontend.py (续)
    from phonemizer import phonemize
    from phonemizer.backend import EspeakBackend

    class PhonemeConverter:
    """
    将规范化文本转换为音素序列
    使用 eSpeak-ng 后端,支持多语言
    """
    def __init__(self, language='en-us', add_blank=False):
    self.language = language
    self.add_blank = add_blank
    # 音素到 ID 的映射表
    self.phoneme_to_id = {}
    self.id_to_phoneme = {}

    def text_to_phonemes(self, text: str) -> List[str]:
    """
    将规范文本转为音素列表
    参数:
    text: 规范化后的文本
    返回:
    音素字符串列表,例如 ['HH', 'AH', 'L', 'OW']
    """
    # phonemizer 返回用空格分隔的音素字符串
    phonemes_str = phonemize(
    text,
    language=self.language,
    backend='espeak',
    strip=True,
    preserve_punctuation=True
    )
    # 拆分并过滤空白
    phonemes = [p for p in phonemes_str.split() if p.strip()]
    return phonemes

    def build_vocab(self, phoneme_sequences: List[List[str]]):
    """
    从所有音素序列构建词汇表
    为每个音素分配一个唯一整数 ID
    """
    # 收集所有唯一音素
    unique_phonemes = set()
    for seq in phoneme_sequences:
    for p in seq:
    unique_phonemes.add(p)

    # 添加特殊标记
    specials = ['<pad>', '<sos>', '<eos>', '<unk>']
    sorted_phonemes = sorted(list(unique_phonemes))
    all_symbols = specials + sorted_phonemes

    self.phoneme_to_id = {s: i for i, s in enumerate(all_symbols)}
    self.id_to_phoneme = {i: s for s, i in self.phoneme_to_id.items()}

    print(f"音素词汇表大小: {len(self.phoneme_to_id)}")
    return self.phoneme_to_id

    def phonemes_to_ids(self, phonemes: List[str]) -> List[int]:
    """音素列表 → ID 列表"""
    unk_id = self.phoneme_to_id.get('<unk>', 0)
    return [self.phoneme_to_id.get(p, unk_id) for p in phonemes]

    def ids_to_phonemes(self, ids: List[int]) -> List[str]:
    """ID 列表 → 音素列表"""
    return [self.id_to_phoneme.get(i, '<unk>') for i in ids]

    # 演示
    if __name__ == '__main__':
    normalizer = TextNormalizer()
    converter = PhonemeConverter()

    text = "Hello World"
    normalized = normalizer.normalize(text)
    phonemes = converter.text_to_phonemes(normalized)
    print(f"原始文本: {text}")
    print(f"规范化后: {normalized}")
    print(f"音素序列: {phonemes}")
    # 输出: ['HH', 'AH', 'L', 'OW', 'W', 'ER', 'L', 'D']

    4.3 完整文本前端管线

    class TextFrontend:
    """
    完整的文本前端管线
    输入: 原始文本 → 输出: 音素 ID 列表
    """
    def __init__(self, vocab=None):
    self.normalizer = TextNormalizer()
    self.phoneme_converter = PhonemeConverter()
    self.vocab = vocab

    def process(self, text: str) -> List[int]:
    """处理单条文本"""
    # 1. 规范化
    norm_text = self.normalizer.normalize(text)
    # 2. 音素化
    phonemes = self.phoneme_converter.text_to_phonemes(norm_text)
    # 3. 加特殊标记
    phonemes_with_boundary = ['<sos>'] + phonemes + ['<eos>']
    # 4. 转 ID
    ids = self.phoneme_converter.phonemes_to_ids(phonemes_with_boundary)
    return ids

    def batch_process(self, texts: List[str], pad_id=0) -> torch.Tensor:
    """批量处理并 pad 到统一长度"""
    sequences = [self.process(t) for t in texts]
    max_len = max(len(s) for s in sequences)
    padded = torch.full((len(sequences), max_len), pad_id, dtype=torch.long)
    for i, seq in enumerate(sequences):
    padded[i, :len(seq)] = torch.tensor(seq, dtype=torch.long)
    return padded


    五、声学模型:从音素到 Mel 频谱

    这是 TTS 系统的核心——一个将音素序列转换为 Mel 频谱图的深度学习模型。我们以 FastSpeech 架构为基础实现,它采用非自回归的方式并行生成整个 Mel 频谱,速度远快于自回归模型(如 Tacotron2)。

    5.1 位置编码

    Transformer 架构本身不具备位置感知能力,需要显式加入位置信息:

    # model.py
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import math
    from typing import Optional, Tuple

    class PositionalEncoding(nn.Module):
    """
    正弦位置编码(Sinusoidal Positional Encoding)
    使用不同频率的正弦/余弦函数,让模型感知序列位置
    """
    def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1):
    super().__init__()
    self.dropout = nn.Dropout(p=dropout)
    pe = torch.zeros(max_len, d_model)
    position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
    div_term = torch.exp(torch.arange(0, d_model, 2).float() *
    (-math.log(10000.0) / d_model))
    pe[:, 0::2] = torch.sin(position * div_term)
    pe[:, 1::2] = torch.cos(position * div_term)
    pe = pe.unsqueeze(0).transpose(0, 1)
    self.register_buffer('pe', pe)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
    """
    参数:
    x: (T, B, d_model) 张量
    返回:
    (T, B, d_model) 位置编码后的张量
    """
    x = x + self.pe[:x.size(0), :]
    return self.dropout(x)

    5.2 多头自注意力机制

    class MultiHeadAttention(nn.Module):
    """
    多头缩放点积注意力(Multi-Head Scaled Dot-Product Attention)
    这是 Transformer 的核心机制,让模型关注序列中的不同位置
    """
    def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1):
    super().__init__()
    assert d_model % n_heads == 0, "d_model must be divisible by n_heads"
    self.d_model = d_model
    self.n_heads = n_heads
    self.d_k = d_model // n_heads

    self.w_q = nn.Linear(d_model, d_model)
    self.w_k = nn.Linear(d_model, d_model)
    self.w_v = nn.Linear(d_model, d_model)
    self.w_o = nn.Linear(d_model, d_model)
    self.dropout = nn.Dropout(p=dropout)

    def forward(self, query: torch.Tensor, key: torch.Tensor,
    value: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
    """
    参数:
    query, key, value: (B, T, d_model)
    mask: (B, 1, 1, T) 或 (B, 1, T, T)
    返回:
    (B, T, d_model) 注意力输出
    """
    B, T, _ = query.shape

    # 线性投影并拆分为多头
    Q = self.w_q(query).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
    K = self.w_k(key).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
    V = self.w_v(value).view(B, T, self.n_heads, self.d_k).transpose(1, 2)

    # 计算注意力分数
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
    if mask is not None:
    scores = scores.masked_fill(mask == 0, float('-inf'))

    # 注意力权重
    attn_weights = F.softmax(scores, dim=-1)
    attn_weights = self.dropout(attn_weights)

    # 加权求和
    output = torch.matmul(attn_weights, V)
    output = output.transpose(1, 2).contiguous().view(B, T, self.d_model)
    output = self.w_o(output)

    return output

    class FeedForward(nn.Module):
    """前馈神经网络(FFN),每个位置独立的两个线性层"""
    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
    super().__init__()
    self.linear1 = nn.Linear(d_model, d_ff)
    self.linear2 = nn.Linear(d_ff, d_model)
    self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
    return self.linear2(self.dropout(F.relu(self.linear1(x))))

    class TransformerFFTLayer(nn.Module):
    """
    Feed-Forward Transformer 层(FastSpeech 风格)
    包含:多头注意力 → 层归一化 → FFN → 层归一化(带残差连接)
    """
    def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
    super().__init__()
    self.self_attn = MultiHeadAttention(d_model, n_heads, dropout)
    self.feed_forward = FeedForward(d_model, d_ff, dropout)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
    # 自注意力 + 残差
    attn_output = self.self_attn(x, x, x, mask)
    x = self.norm1(x + self.dropout(attn_output))
    # FFN + 残差
    ff_output = self.feed_forward(x)
    x = self.norm2(x + self.dropout(ff_output))
    return x

    5.3 编码器(Encoder)

    编码器将输入音素序列编码为上下文感知的隐层表示:

    class Encoder(nn.Module):
    """
    FastSpeech 编码器
    输入: 音素 ID 序列
    输出: 音素级别的隐层表示
    """
    def __init__(self, vocab_size: int, d_model: int = 384, n_heads: int = 2,
    d_ff: int = 1024, n_layers: int = 4, dropout: float = 0.1,
    max_seq_len: int = 5000):
    super().__init__()
    self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
    self.pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)
    self.layers = nn.ModuleList([
    TransformerFFTLayer(d_model, n_heads, d_ff, dropout)
    for _ in range(n_layers)
    ])

    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
    """
    参数:
    x: (B, T_phoneme) 音素 ID
    mask: (B, 1, 1, T_phoneme) 或 (B, 1, T_phoneme, T_phoneme)
    返回:
    (B, T_phoneme, d_model) 编码后的隐层
    """
    x = self.embedding(x)
    x = x.transpose(0, 1) # (T, B, d_model) for PositionalEncoding
    x = self.pos_encoding(x)
    x = x.transpose(0, 1) # (B, T, d_model)

    for layer in self.layers:
    x = layer(x, mask)

    return x

    5.4 持续时间预测器

    持续时间预测器是 FastSpeech 的核心创新之一。它预测每个音素在 Mel 频谱中占多少帧,解决了文本和语音"长度不对齐"的问题。

    class DurationPredictor(nn.Module):
    """
    持续时间预测器
    预测每个音素对应的 Mel 帧数
    采用 2 层 1D 卷积 + 线性层输出
    """
    def __init__(self, d_model: int = 384, d_conv: int = 256,
    kernel_size: int = 3, dropout: float = 0.1):
    super().__init__()
    padding = (kernel_size – 1) // 2
    self.conv1 = nn.Conv1d(d_model, d_conv, kernel_size, padding=padding)
    self.conv2 = nn.Conv1d(d_conv, d_conv, kernel_size, padding=padding)
    self.linear = nn.Linear(d_conv, 1)
    self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
    """
    参数:
    x: (B, T_phoneme, d_model)
    mask: (B, T_phoneme) 布尔掩码,True 表示有效位置
    返回:
    (B, T_phoneme) 每个音素的预测持续时间(帧数)
    """
    x = x.transpose(1, 2) # (B, d_model, T_phoneme) for Conv1d
    x = self.dropout(F.relu(self.conv1(x)))
    x = self.dropout(F.relu(self.conv2(x)))
    x = x.transpose(1, 2) # (B, T_phoneme, d_conv)
    x = self.linear(x).squeeze(-1) # (B, T_phoneme)

    # 使用 softplus 确保为正(持续时间为正数)
    x = F.softplus(x)

    if mask is not None:
    x = x.masked_fill(~mask, 0.0)

    return x

    def expand_with_duration(encoder_output: torch.Tensor,
    durations: torch.Tensor,
    max_mel_len: Optional[int] = None) -> torch.Tensor:
    """
    长度调节器:根据持续时间展开编码器输出
    将音素级别的表示展开为帧级别的表示

    参数:
    encoder_output: (B, T_phoneme, d_model)
    durations: (B, T_phoneme) 整数持续时间
    max_mel_len: 最大 Mel 长度(用于 padding)
    返回:
    (B, T_mel, d_model) 展开后的帧级表示
    """
    B, T_phoneme, d_model = encoder_output.shape
    durations = durations.round().long().clamp(min=1) # 至少1帧

    # 计算总帧数
    total_frames = durations.sum(dim=1).max().item()
    if max_mel_len is None:
    max_mel_len = total_frames

    output = torch.zeros(B, max_mel_len, d_model, device=encoder_output.device)

    for b in range(B):
    idx = 0
    for t in range(T_phoneme):
    d = durations[b, t].item()
    if d > 0:
    end = min(idx + d, max_mel_len)
    output[b, idx:end, :] = encoder_output[b, t, :].unsqueeze(0).expand(end – idx, -1)
    idx = end
    # 如果展开不够长,重复最后一个音素
    if idx < max_mel_len:
    output[b, idx:, :] = encoder_output[b, -1, :].unsqueeze(0).expand(max_mel_len – idx, -1)

    return output

    5.5 解码器(Decoder)

    解码器将展开后的帧级表示转换为 Mel 频谱图:

    class Decoder(nn.Module):
    """
    FastSpeech 解码器
    输入: 展开后的帧级隐层表示
    输出: Mel 频谱图
    结构和编码器对称,但处理帧级别的序列
    """
    def __init__(self, d_model: int = 384, n_heads: int = 2,
    d_ff: int = 1024, n_layers: int = 4, n_mels: int = 80,
    dropout: float = 0.1, max_seq_len: int = 5000):
    super().__init__()
    self.pos_encoding = PositionalEncoding(d_model, max_seq_len, dropout)
    self.layers = nn.ModuleList([
    TransformerFFTLayer(d_model, n_heads, d_ff, dropout)
    for _ in range(n_layers)
    ])
    self.projection = nn.Linear(d_model, n_mels)

    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
    """
    参数:
    x: (B, T_mel, d_model) 展开后的帧级表示
    mask: (B, 1, 1, T_mel)
    返回:
    (B, n_mels, T_mel) Mel 频谱
    """
    x = x.transpose(0, 1) # (T_mel, B, d_model)
    x = self.pos_encoding(x)
    x = x.transpose(0, 1) # (B, T_mel, d_model)

    for layer in self.layers:
    x = layer(x, mask)

    x = self.projection(x)
    x = x.transpose(1, 2) # (B, n_mels, T_mel)
    return x

    5.6 完整 FastSpeech 模型

    将上述模块组装为端到端的 TTS 声学模型:

    class FastSpeech(nn.Module):
    """
    完整 FastSpeech 模型
    输入: 音素 ID 序列 → 输出: Mel 频谱图 + 持续时间预测
    """
    def __init__(self, vocab_size: int, d_model: int = 384,
    n_heads: int = 2, d_ff: int = 1024,
    encoder_layers: int = 4, decoder_layers: int = 4,
    n_mels: int = 80, dropout: float = 0.1):
    super().__init__()
    self.encoder = Encoder(vocab_size, d_model, n_heads, d_ff,
    encoder_layers, dropout)
    self.duration_predictor = DurationPredictor(d_model, dropout=dropout)
    self.decoder = Decoder(d_model, n_heads, d_ff,
    decoder_layers, n_mels, dropout)

    def forward(self, phoneme_ids: torch.Tensor,
    phoneme_mask: Optional[torch.Tensor] = None,
    durations: Optional[torch.Tensor] = None,
    mel_mask: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, …]:
    """
    训练模式:提供真实持续时间
    推理模式:使用预测的持续时间

    参数:
    phoneme_ids: (B, T_phoneme) 音素 ID
    phoneme_mask: (B, T_phoneme) 音素位置掩码
    durations: (## 六、声码器:从 Mel 频谱到波形

    声学模型输出的 Mel 频谱图需要被转换为可播放的音频波形。这部分工作由**声码器(Vocoder)**完成。我们实现 HiFi-GAN 生成器——它使用多尺度残差块和转置卷积,是目前最流行的高质量神经声码器之一。

    ### 6.1 HiFi-GAN 生成器架构

    ```python
    # vocoder.py
    import torch
    import torch.nn as nn
    import torch.nn.functional as F

    class MRFBlock(nn.Module):
    """
    多感受野残差块(Multi-Receptive Field Fusion Block)
    使用不同大小的膨胀卷积捕捉不同尺度的音频模式
    """
    def __init__(self, channels, dilations, kernel_size=3):
    super().__init__()
    self.convs = nn.ModuleList()
    for d in dilations:
    padding = (kernel_size – 1) * d // 2
    self.convs.append(
    nn.Sequential(
    nn.LeakyReLU(0.1),
    nn.Conv1d(channels, channels, kernel_size, dilation=d, padding=padding),
    nn.LeakyReLU(0.1),
    nn.Conv1d(channels, channels, kernel_size, dilation=1, padding=kernel_size//2),
    )
    )

    def forward(self, x):
    return x + torch.mean(torch.stack([conv(x) for conv in self.convs]), dim=0)

    class HiFiGANGenerator(nn.Module):
    """
    HiFi-GAN V1 生成器
    从 Mel 频谱图生成原始音频波形
    """
    def __init__(self, n_mels=80, upsample_rates=[8, 8, 2, 2],
    upsample_kernel_sizes=[16, 16, 4, 4],
    resblock_kernel_sizes=[3, 7, 11],
    resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]]):
    super().__init__()
    self.num_kernels = len(resblock_kernel_sizes)
    self.num_upsamples = len(upsample_rates)

    self.conv_pre = nn.Conv1d(n_mels, 512, kernel_size=7, padding=3)

    self.ups = nn.ModuleList()
    for i, (u_rate, u_kernel) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
    self.ups.append(nn.ConvTranspose1d(
    512 // (2 ** i), 512 // (2 ** (i + 1)),
    kernel_size=u_kernel, stride=u_rate,
    padding=(u_kernel – u_rate) // 2
    ))

    self.resblocks = nn.ModuleList()
    for i in range(self.num_upsamples):
    current_channels = 512 // (2 ** (i + 1))
    resblock_group = nn.ModuleList()
    for _ in range(self.num_kernels):
    resblock_group.append(MRFBlock(current_channels, resblock_dilation_sizes[i]))
    self.resblocks.append(resblock_group)

    self.conv_post = nn.Conv1d(512 // (2 ** self.num_upsamples), 1, kernel_size=7, padding=3)

    def forward(self, mel):
    x = self.conv_pre(mel)
    for i in range(self.num_upsamples):
    x = F.leaky_relu(x, 0.1)
    x = self.ups[i](x)
    xs = None
    for j in range(self.num_kernels):
    if xs is None:
    xs = self.resblocks[i][j](x)
    else:
    xs += self.resblocks[i][j](x)
    x = xs / self.num_kernels
    x = F.leaky_relu(x, 0.1)
    x = self.conv_post(x)
    x = torch.tanh(x)
    return x

    def infer(self, mel):
    return self.forward(mel)

    保存音频

    def save_audio(waveform, path, sample_rate=22050):
    """将波形张量保存为 .wav 文件"""
    import soundfile as sf
    import numpy as np
    if waveform.dim() == 2:
    waveform = waveform.squeeze(0)
    audio = waveform.detach().cpu().numpy()
    audio = np.clip(audio, -1.0, 1.0)
    sf.write(path, audio, sample_rate)


    七、完整推理管线

    将所有模块串联起来,实现从文本输入到音频输出的完整端到端流程:

    # inference.py
    import torch

    class TTSEngine:
    """
    完整 TTS 推理引擎
    封装文本前端、声学模型和声码器
    """
    def __init__(self, model, vocoder, text_frontend, device='cpu'):
    self.model = model.to(device)
    self.vocoder = vocoder.to(device)
    self.text_frontend = text_frontend
    self.device = device
    self.model.eval()
    self.vocoder.eval()

    def synthesize(self, text, speed_factor=1.0):
    """
    将文本合成为音频
    参数:
    text: 待合成文本
    speed_factor: 语速调节,>1 加快,<1 减慢
    返回:
    (T,) 原始音频波形
    """
    with torch.no_grad():
    # 1. 文本前端处理
    phoneme_ids = self.text_frontend.process(text)
    phoneme_tensor = torch.tensor([phoneme_ids], dtype=torch.long).to(self.device)

    # 2. 编码
    enc_output = self.model.encoder(phoneme_tensor)

    # 3. 持续时间预测 + 语速控制
    dur_pred = self.model.duration_predictor(enc_output)
    dur_pred = dur_pred / speed_factor

    # 4. 长度展开
    mel_hidden = expand_with_duration(enc_output, dur_pred)

    # 5. 解码 → Mel 频谱
    mel = self.model.decoder(mel_hidden)

    # 6. 声码器 → 波形
    waveform = self.vocoder.infer(mel)

    return waveform.squeeze(0)

    def batch_synthesize(self, texts, speed_factor=1.0):
    """批量合成"""
    return [self.synthesize(t, speed_factor) for t in texts]

    语速和音调控制

    FastSpeech 架构的一个显著优势是可以独立控制语速和音调:

    def synthesize_with_control(engine, text, speed_factor=1.0, pitch_shift=0.0):
    """带语速和音调控制的合成"""
    waveform = engine.synthesize(text, speed_factor)
    if pitch_shift != 0.0:
    import librosa
    audio = waveform.cpu().numpy()
    audio_shifted = librosa.effects.pitch_shift(audio, sr=22050, n_steps=pitch_shift)
    waveform = torch.from_numpy(audio_shifted).float()
    return waveform


    八、模型评估与优化

    8.1 客观评估指标

    # evaluation.py
    import numpy as np
    from scipy.spatial.distance import cosine

    class TTSEvaluator:
    """TTS 系统客观评估工具"""

    @staticmethod
    def mel_cepstral_distortion(mel_pred, mel_target):
    """
    MCD(Mel Cepstral Distortion)
    衡量合成 Mel 与真实 Mel 之间的差异
    值越小越好,通常 < 10 表示可接受
    """
    frames = min(mel_pred.shape[1], mel_target.shape[1])
    mel_pred = mel_pred[:, :frames]
    mel_target = mel_target[:, :frames]
    mcd = 0.0
    for t in range(frames):
    mcd += cosine(mel_pred[:, t], mel_target[:, t])
    mcd = (10.0 / np.log(10)) * np.sqrt(2) * (mcd / frames)
    return mcd

    @staticmethod
    def f0_rmse(f0_pred, f0_target, voiced_only=True):
    """基频 RMSE(F0 RMSE),衡量语调准确度"""
    if voiced_only:
    mask = (f0_pred > 0) & (f0_target > 0)
    if mask.sum() == 0:
    return float('inf')
    return np.sqrt(np.mean((f0_pred[mask] – f0_target[mask]) ** 2))
    return np.sqrt(np.mean((f0_pred – f0_target) ** 2))

    8.2 推理优化技巧

    class TTSOptimizer:
    """TTS 模型推理优化器"""

    @staticmethod
    def export_to_onnx(model, sample_input, output_path='tts_model.onnx'):
    """导出为 ONNX 格式,支持 TensorRT/ONNX Runtime 加速"""
    torch.onnx.export(
    model, sample_input, output_path,
    input_names=['phoneme_ids'],
    output_names=['mel', 'duration'],
    dynamic_axes={
    'phoneme_ids': {0: 'batch', 1: 'phoneme_len'},
    'mel': {0: 'batch', 2: 'mel_len'},
    },
    opset_version=14
    )
    print(f"ONNX 模型已导出: {output_path}")

    @staticmethod
    def apply_int8_quantization(model):
    """INT8 动态量化,减少约 75% 模型大小"""
    return torch.quantization.quantize_dynamic(
    model,
    {torch.nn.Linear, torch.nn.Conv1d, torch.nn.ConvTranspose1d},
    dtype=torch.qint8
    )

    @staticmethod
    def optimize_for_cpu(model):
    """CPU 推理优化:JIT trace"""
    model.eval()
    example_input = torch.randint(0, 50, (1, 50))
    with torch.no_grad():
    return torch.jit.trace(model, example_input)


    九、完整项目结构

    9.1 项目文件结构

    tts-from-scratch/
    ├── README.md
    ├── requirements.txt
    ├── config.yaml # 训练配置
    ├── data/
    │ └── LJSpeech-1.1/ # 数据集
    ├── src/
    │ ├── __init__.py
    │ ├── dataset.py # 数据加载
    │ ├── audio_processing.py # Mel 提取
    │ ├── text_frontend.py # 文本前端
    │ ├── model.py # FastSpeech 模型
    │ ├── vocoder.py # HiFi-GAN 声码器
    │ ├── train.py # 训练脚本
    │ ├── inference.py # 推理管道
    │ └── evaluation.py # 评估工具
    ├── checkpoints/ # 模型权重
    ├── logs/ # 训练日志
    └── outputs/ # 合成音频

    9.2 快速使用示例

    # quick_start.py
    from src.text_frontend import TextFrontend
    from src.model import FastSpeech
    from src.vocoder import HiFiGANGenerator
    from src.inference import TTSEngine

    # 1. 加载模型
    vocab_size = 100
    device = 'cuda' if torch.cuda.is_available() else 'cpu'

    model = FastSpeech(vocab_size=vocab_size)
    model.load_state_dict(torch.load('checkpoints/fastspeech_best.pt', map_location=device))

    vocoder = HiFiGANGenerator(n_mels=80)
    vocoder.load_state_dict(torch.load('checkpoints/hifigan_best.pt', map_location=device))

    text_frontend = TextFrontend()

    # 2. 初始化引擎
    engine = TTSEngine(model, vocoder, text_frontend, device=device)

    # 3. 合成音频
    texts = [
    "Text-to-speech technology has revolutionized how we interact with machines.",
    "The quick brown fox jumps over the lazy dog.",
    "In 2024, neural TTS systems achieved near-human naturalness."
    ]

    for i, text in enumerate(texts):
    waveform = engine.synthesize(text, speed_factor=1.0)
    save_audio(waveform, f'outputs/sample_{i}.wav')
    print(f"合成完成: sample_{i}.wav")

    9.3 训练启动脚本

    # run_training.sh
    python src/train.py \\
    –data_dir data/LJSpeech-1.1 \\
    –batch_size 16 \\
    –epochs 100 \\
    –lr 1e-4 \\
    –checkpoint_dir checkpoints \\
    –n_mels 80 \\
    –d_model 384 \\
    –device cuda


    十、常见问题与解决方案

    10.1 合成音频有噪音

    可能原因: – 声码器训练不充分(建议 HiFi-GAN 训练 > 200K steps) – Mel 频谱中有异常帧(检查数据预处理)

    解决方案:

    def denoise(waveform, threshold=0.01):
    """对静音段进行降噪"""
    mask = torch.abs(waveform) > threshold
    waveform = waveform * mask.float()
    return waveform

    10.2 合成语速太慢/太快

    直接调整 speed_factor 参数:

    slow_wave = engine.synthesize("Hello world", speed_factor=0.8) # 慢速
    fast_wave = engine.synthesize("Hello world", speed_factor=1.3) # 快速

    10.3 训练 loss 不下降

    常见排查清单: – 检查数据是否加载正确(可视化若干样本的 Mel 频谱) – 学习率是否合适(建议 warmup + 余弦退火) – 持续时间对齐是否正确(用 torch.sum(durations) == mel_length 验证) – 是否有 NaN(在 loss.backward() 前加 torch.isnan() 检查)

    10.4 生成长文本时出错

    长文本合成需要分句处理:

    def synthesize_long_text(engine, text, max_phonemes=200):
    """分句合成长文本"""
    import re
    sentences = re.split(r'[\\.\\!\\?]', text)
    sentences = [s.strip() for s in sentences if s.strip()]

    all_waveforms = []
    for sent in sentences:
    wav = engine.synthesize(sent)
    silence = torch.zeros(int(0.3 * 22050)) # 句间 300ms 停顿
    all_waveforms.append(wav)
    all_waveforms.append(silence)

    return torch.cat(all_waveforms)

    10.5 多说话人扩展

    本文实现的是单说话人系统。扩展为多说话人只需两处修改:

    class MultiSpeakerEncoder(nn.Module):
    """多说话人编码器,引入可学习的说话人嵌入"""
    def __init__(self, vocab_size, n_speakers=10, d_model=384, **kwargs):
    super().__init__()
    self.speaker_embedding = nn.Embedding(n_speakers, d_model)
    # … 其余同 Encoder …

    def forward(self, x, speaker_id, mask=None):
    speaker_emb = self.speaker_embedding(speaker_id).unsqueeze(1)
    # … 在嵌入层后加到 x 上 …


    总结

    本文从零实现了一个完整的 TTS 系统,覆盖了从文本前端处理到波形生成的全流程。关键要点回顾:

  • 架构设计:基于 FastSpeech 非自回归架构,编码器-持续时间预测器-长度调节器-解码器-声码器五段式结构。相比自回归模型推理速度快 10 倍以上,且支持独立的语速控制。

  • 文本前端:文本规范化 + 音素转换是 TTS 系统的"入口关"——看似简单,但数字、缩写、多音字等边缘情况需要仔细处理。

  • 声学模型:通过多头注意力捕捉文本中的长距离依赖关系,通过持续时间预测器解决文本-语音长度不对齐的难题。这是 FastSpeech 的核心创新——用显式的时长预测取代了注意力对齐。

  • 声码器:HiFi-GAN 用多感受野残差块和转置卷积实现了高质量波形生成。其"多尺度"设计让模型同时捕捉粗粒度和细粒度的音频特征。

  • 工程实践:INT8 量化可将模型缩小 75%,ONNX 导出支持跨平台部署,分句合成应对长文本——这些优化让 TTS 模型从"玩具"走向"可用"。

  • 如果你想深入某个方向,建议的进阶路径: – 性能优化:尝试 FlashAttention 加速自注意力计算 – 多语言支持:扩展音素表为国际音标(IPA),支持中文拼音 – 情感控制:在编码器中加入风格嵌入,支持高兴、悲伤等情感输出 – Zero-shot 合成:研究 VITS、CosyVoice 等最新架构


    延伸阅读

    如果你对 AI 生成技术的实战实现感兴趣,推荐阅读我的其他文章:

    • DeepSeek 实战指南:提示词工程、API 集成与效率提升全攻略
    • 手写 Transformer:从零实现多头注意力机制与完整架构
    • 手写 Flash Attention:从算法原理到高性能实现

    本文是"手写 AI 系统"系列文章之一。该系列从零实现 AI 系统中的关键组件,涵盖 RAG、Agent、Function Calling、MCP 等核心技术,帮助你深入理解底层原理,构建属于自己的 AI 工具。


    十一、深入理解:持续时间对齐的数据准备

    在第五章节,我们提到了模型在训练时需要使用"真实的音素持续时间"。这个数据不是天然存在的——语音数据集中只有文本和音频,没有文本中每个音素在音频中占多少帧的标注。我们需要通过音素对齐(Phoneme Alignment)算法来获取。

    11.1 基于 HMM 的强制对齐

    最经典的对齐工具是 Montreal Forced Aligner (MFA)。它的工作原理是:

  • 训练一个音素级别的声学模型(通常是 GMM-HMM)
  • 给定文本转写的音素序列,在音频中找到每个音素最可能出现的时间边界
  • # alignment_utils.py
    import numpy as np

    class PhonemeAligner:
    """
    音素-音频对齐工具
    使用 MFA 输出的 TextGrid 文件解析持续时间
    """
    @staticmethod
    def parse_textgrid(textgrid_path):
    """
    解析 Praat TextGrid 格式的对齐文件
    返回: {音素: (开始秒, 结束秒)} 列表
    """
    import re
    with open(textgrid_path, 'r') as f:
    content = f.read()

    # 提取音素层的时间间隔
    intervals = re.findall(
    r'xmin = ([\\d.]+)\\s+xmax = ([\\d.]+)\\s+text = "([^"]*)"',
    content
    )
    return [(ph, float(start), float(end))
    for start, end, ph in intervals if ph.strip()]

    @staticmethod
    def durations_to_frames(durations, hop_length=256, sample_rate=22050):
    """将持续时间(秒)转换为 Mel 帧数"""
    return [int(dur * sample_rate / hop_length + 0.5) for _, dur in durations]

    11.2 基于 CTC 的端到端对齐

    一个更现代的方法是训练一个 CTC(Connectionist Temporal Classification)模型来自动对齐:

    class CTCAttentionAligner(nn.Module):
    """
    基于 CTC 的音素对齐器
    训练一个轻量级的声学模型来预测每帧的音素标签
    """
    def __init__(self, vocab_size, n_mels=80, hidden_size=256):
    super().__init__()
    self.conv = nn.Sequential(
    nn.Conv1d(n_mels, hidden_size, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.Conv1d(hidden_size, hidden_size, kernel_size=3, padding=1),
    nn.ReLU(),
    )
    self.lstm = nn.LSTM(hidden_size, hidden_size, bidirectional=True, batch_first=True)
    self.classifier = nn.Linear(hidden_size * 2, vocab_size)

    def forward(self, mel):
    """
    参数:
    mel: (B, n_mels, T)
    返回:
    (B, T, vocab_size) 每帧的音素概率分布
    """
    x = self.conv(mel)
    x = x.transpose(1, 2)
    x, _ = self.lstm(x)
    x = self.classifier(x)
    return F.log_softmax(x, dim=-1)

    def align(self, mel, phoneme_ids, blank_id=0):
    """
    使用 CTC 解码获取对齐
    返回每个音素的起始帧索引
    """
    log_probs = self.forward(mel)
    # 使用贪心解码
    preds = log_probs.argmax(dim=-1).squeeze(0)
    # 去除连续的重复和 blank
    aligned_phonemes = []
    prev = blank_id
    for t, p in enumerate(preds):
    if p != prev:
    aligned_phonemes.append((p.item(), t))
    prev = p
    return aligned_phonemes

    对齐完成后,我们就可以为每对(文本, 音频)生成训练数据了:

    def prepare_training_data(text, audio_path, aligner, mel_extractor, text_frontend):
    """生成完整的训练样本"""
    # 1. 文本到音素
    phoneme_ids = text_frontend.process(text)

    # 2. 音频到 Mel
    waveform, _ = torchaudio.load(audio_path)
    mel = mel_extractor.extract(waveform)

    # 3. 对齐获得持续时间
    alignment = aligner.mel_to_phoneme_alignment(mel, phoneme_ids)

    return {
    'phoneme_ids': torch.tensor(phoneme_ids),
    'mel': mel,
    'durations': torch.tensor([d for _, d in alignment]),
    }


    十二、性能分析与对比实验

    12.1 推理速度对比

    为了直观感受 FastSpeech 非自回归架构的速度优势,我们做一个简单的基准测试:

    # benchmark.py
    import time
    import torch

    def benchmark_model(model, input_tensor, n_runs=100, warmup=10):
    """基准测试模型推理速度"""
    model.eval()
    device = next(model.parameters()).device

    # 预热
    for _ in range(warmup):
    with torch.no_grad():
    model(input_tensor)

    # 正式测试
    torch.cuda.synchronize() if device.type == 'cuda' else None
    start = time.time()

    for _ in range(n_runs):
    with torch.no_grad():
    model(input_tensor)

    torch.cuda.synchronize() if device.type == 'cuda' else None
    end = time.time()

    avg_time = (end – start) / n_runs * 1000 # ms
    return avg_time

    # 对比自回归模型(模拟 Tacotron2 的逐帧生成)
    def simulate_autoregressive_inference(seq_len, dim, n_runs=100):
    """模拟自回归推理的逐帧解码耗时"""
    decoder = nn.LSTM(dim, dim, batch_first=True)
    input_tokens = torch.randn(1, 1, dim)

    torch.cuda.synchronize() if torch.cuda.is_available() else None
    start = time.time()

    for _ in range(n_runs):
    h = None
    for t in range(seq_len):
    output, h = decoder(input_tokens, h)

    torch.cuda.synchronize() if torch.cuda.is_available() else None
    end = time.time()

    return (end – start) / n_runs * 1000 # ms

    12.2 音质主观评估(MOS 评分)

    虽然客观指标(MCD、F0 RMSE)有一定参考价值,但语音合成质量的终极标准是人耳听觉体验。业界采用 Mean Opinion Score (MOS) 来评测:让评测者对合成语音的自然度打分(1-5,5 为最自然)。

    实用建议: – 批量生成测试音频:从测试集中选 20-50 条,保证覆盖不同句式长度 – AB 盲测:将合成音频与真实音频打乱,让评测者无法区分 – 重点关注:自然度、可懂度、韵律自然性、音色一致性

    当前最先进的 TTS 系统(如 VITS、CosyVoice)的 MOS 已接近真人录音(4.5+),而一个训练充分的 FastSpeech + HiFi-GAN 系统通常在 3.8-4.2 之间。

    12.3 下采样与实时因子

    在工业生产中,还有一个关键指标叫实时因子(Real-Time Factor, RTF): – RTF = 合成耗时 / 音频时长 – RTF < 1 表示比真人说话快(可用于实时场景) – 我们的 FastSpeech 架构 RTF 通常在 0.01-0.05 之间(即合成 10 秒音频只需 0.1-0.5 秒)

    def compute_rtf(engine, texts, sample_rate=22050):
    """计算实时因子"""
    total_audio_duration = 0
    total_synthesis_time = 0

    for text in texts:
    phoneme_ids = engine.text_frontend.process(text)
    phoneme_tensor = torch.tensor([phoneme_ids])

    start = time.time()
    with torch.no_grad():
    enc = engine.model.encoder(phoneme_tensor)
    dur = engine.model.duration_predictor(enc)
    mel_hidden = expand_with_duration(enc, dur)
    mel = engine.model.decoder(mel_hidden)
    wav = engine.vocoder.infer(mel)
    elapsed = time.time() – start

    audio_duration = wav.shape[-1] / sample_rate
    total_audio_duration += audio_duration
    total_synthesis_time += elapsed

    return total_synthesis_time / total_audio_duration


    参考文献

  • Ren, Y. et al. "FastSpeech: Fast, Robust and Controllable Text to Speech." NeurIPS 2019.
  • Ren, Y. et al. "FastSpeech 2: Fast and High-Quality End-to-End Text to Speech." ICLR 2021.
  • Kong, J. et al. "HiFi-GAN: Generative Adversarial Networks for Efficient and High Fidelity Speech Synthesis." NeurIPS 2020.
  • Shen, J. et al. "Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions." ICASSP 2018.
  • Wang, Y. et al. "Tacotron: Towards End-to-End Speech Synthesis." Interspeech 2017.
  • Oord, A. et al. "WaveNet: A Generative Model for Raw Audio." arXiv 2016.

  • 十三、TTS 技术演进与选型建议

    13.1 从自回归到非自回归:架构演进

    TTS 技术的迭代有非常清晰的脉络。2017 年 Tacotron 首次将端到端神经合成变成现实,采用的是自回归架构——逐帧预测 Mel 频谱,每预测一帧都依赖前一帧的输出。这种方法音质很好,但有两个致命问题:

    • 推理速度极慢:一段 10 秒的语音需要数千次模型前向传播,GPU 上勉强可用,CPU 上基本不可用
    • 误码传播:如果某帧预测偏差,后续所有帧都会跟着跑偏,导致语音出现奇怪的杂音或跳变

    2019 年 FastSpeech 的突破在于用持续时间预测器 + 长度调节器替代了逐帧自回归解码。模型一次性对所有音素编码,然后根据每个音素应该持续多少帧展开。同时并行生成所有 Mel 帧,速度提升了 270 倍以上(这也正是我们在第五章中实现的核心架构)。

    到了 2021 年,VITS 引入了变分推理和对抗训练,将声学模型和声码器合并为单一模型,在保持非自回归速度的同时,音质达到了自回归模型的水平。而 2024 年的 CosyVoice 更进一步,引入了自然语言prompt来控制合成风格,甚至可以模仿任意说话人的声音。

    13.2 如何选择 TTS 技术方案

    如果你需要在项目中集成 TTS 能力,这里有一个简洁的选型指南:

    场景推荐方案理由
    移动端/边缘设备 FastSpeech + HiFi-GAN 轻量化 INT8 量化后 < 50MB,CPU 实时
    云端批量合成 FastSpeech 2 + HiFi-GAN 速度优先,支持大批量并行
    最高音质要求 VITS / NaturalSpeech 接近真人,MOS 4.5+
    中文语音合成 CosyVoice / ChatTTS 对中文声调韵律优化更好
    多说话人克隆 CosyVoice / YourTTS 支持少样本声音克隆
    实时交互系统 FastSpeech + LPCNet RTF < 0.01,可用在直播场景

    13.3 中文 TTS 的特殊挑战

    英文 TTS 的技术路线相对成熟,但迁移到中文时会遇到几个特有的问题:

    声调建模:中文是有声调的语言,同一个音节点"ma"可以表示"妈"(一声)、"麻"(二声)、"马"(三声)、"骂"(四声)。TTS 系统必须正确预测每个音节的声调。解决方案是在音素表中加入声调标记(如 ma1、ma2、ma3、ma4),或者使用带声调的拼音作为输入。

    多音字消歧:中文有大量多音字——"行"可以读 háng(行业)或 xíng(行动)。TTS 系统需要一个上下文感知的消歧模块。常用的做法是在文本前端中集成 BERT 或类似的语言模型,利用上下文来判断当前多音字的正确读音。

    韵律边界:中文的韵律结构比英文更复杂,包含了韵律词、韵律短语和语调短语三个层次。正确的韵律停顿听起来才自然。可以在训练数据中人工标注韵律边界,或者在模型中加入韵律预测头。

    文本规范化:中文的文本规范化比英文复杂得多——"2024年5月1日"要读成"二零二四年五月一日","99.9%"要读成"百分之九十九点九"。还需要处理日期、时间、电话号码、身份证号等各种格式。

    # 中文文本规范化示例
    class ChineseTextNormalizer:
    """中文文本规范化——将日期、数字等转为口语化表达"""

    def __init__(self):
    self.digit_map = {
    '0': '零', '1': '一', '2': '二', '3': '三', '4': '四',
    '5': '五', '6': '六', '7': '七', '8': '八', '9': '九'
    }

    def normalize_date(self, text):
    """将 YYYY年MM月DD日 转为口语表达"""
    import re
    def replace_date(match):
    year = ''.join(self.digit_map[c] for c in match.group(1))
    month = match.group(2)
    day = match.group(3)
    return f"{year}年{month}月{day}日"
    return re.sub(r'(\\d{4})年(\\d{1,2})月(\\d{1,2})日', replace_date, text)

    def normalize_percent(self, text):
    """将 XX.X% 转为 百分之XX点X"""
    def replace_pct(match):
    nums = match.group(1)
    return f"百分之{nums}"
    return re.sub(r'(\\d+\\.?\\d*)%', replace_pct, text)

    13.4 前沿方向:语音合成 + 大语言模型

    2024-2025 年,大语言模型(LLM)开始被引入 TTS 领域,产生了几个让人兴奋的方向:

    AudioLM / VALL-E:将语音量化为离散 token,直接使用语言模型建模语音 token 序列。这使得 TTS 系统可以像 GPT 生成文本一样生成语音。你可以给模型一段 3 秒的音频作为"提示",它就能用同样的音色说出任何话。

    CosyVoice:阿里的 CosyVoice 更进一步,使用自然语言 prompt 来控制合成风格("用新闻联播的语气读"、"带点悲伤的情绪")。它将语音风格信息编码在 latent space 中,通过 prompt 实现零样本风格迁移。

    文字到语音驱动的数字人:结合 TTS 和数字人技术,可以实现唇形同步的虚拟人对话。核心思路是将声学模型输出的 Mel 频谱同步映射到面部动画参数。

    这些前沿方向都说明了一个趋势:语音合成正在从"能听"走向"能看、能控制、能理解"。但就目前而言,本文实现的 FastSpeech + HiFi-GAN 架构仍然是生产环境中性价比最高的选择——速度快、音质好、部署简单、可控性强。


    十四、完整代码与最佳实践

    14.1 一键训练脚本

    为了帮助你快速上手,这里提供一个完整的训练入口脚本,集成数据预处理、模型初始化和训练循环:

    # run.py
    import os
    import yaml
    import torch
    from torch.utils.data import DataLoader, random_split

    def main(config_path='config.yaml'):
    # 加载配置
    with open(config_path) as f:
    cfg = yaml.safe_load(f)

    device = torch.device(cfg['device'] if torch.cuda.is_available() else 'cpu')

    # 1. 初始化文本前端
    frontend = TextFrontend()
    print("[1/5] 文本前端初始化完成")

    # 2. 加载数据集(假设已预处理了对齐数据)
    dataset = LJSpeechDataset(cfg['data_dir'])
    train_size = int(0.95 * len(dataset))
    val_size = len(dataset) – train_size
    train_set, val_set = random_split(dataset, [train_size, val_size])

    train_loader = DataLoader(train_set, batch_size=cfg['batch_size'],
    shuffle=True, num_workers=4)
    val_loader = DataLoader(val_set, batch_size=cfg['batch_size'],
    shuffle=False, num_workers=4)
    print(f"[2/5] 数据加载完成: {len(train_set)} 训练, {len(val_set)} 验证")

    # 3. 构建词汇表
    all_phonemes = []
    for text in dataset.texts:
    phonemes = frontend.phoneme_converter.text_to_phonemes(
    frontend.normalizer.normalize(text)
    )
    all_phonemes.append(phonemes)
    vocab = frontend.phoneme_converter.build_vocab(all_phonemes)
    print(f"[3/5] 词汇表构建完成,大小: {len(vocab)}")

    # 4. 初始化模型
    model = FastSpeech(vocab_size=len(vocab), **cfg['model'])
    model = model.to(device)
    optimizer = torch.optim.AdamW(model.parameters(), lr=cfg['lr'])
    criterion = FastSpeechLoss(duration_weight=cfg['duration_weight'])
    print(f"[4/5] 模型初始化完成,参数量: {sum(p.numel() for p in model.parameters()):,}")

    # 5. 训练
    print(f"[5/5] 开始训练(设备: {device})")
    best_val_loss = float('inf')
    for epoch in range(1, cfg['epochs'] + 1):
    train_metrics = train_one_epoch(model, train_loader, optimizer,
    criterion, device, epoch)
    val_loss = validate(model, val_loader, criterion, device)

    print(f"Epoch {epoch}: train_loss={train_metrics['loss']:.4f}, "
    f"val_loss={val_loss:.4f}")

    if val_loss < best_val_loss:
    best_val_loss = val_loss
    torch.save(model.state_dict(), 'checkpoints/fastspeech_best.pt')
    print(f" → 模型已保存(val_loss={val_loss:.4f})")

    if __name__ == '__main__':
    main()

    对应的配置文件 config.yaml:

    data_dir: data/LJSpeech-1.1
    batch_size: 16
    epochs: 100
    lr: 0.0001
    device: cuda
    duration_weight: 1.0

    model:
    d_model: 384
    n_heads: 2
    d_ff: 1024
    encoder_layers: 4
    decoder_layers: 4
    n_mels: 80
    dropout: 0.1

    14.2 常见陷阱与调试技巧

    在实现 TTS 系统的过程中,有几个非常容易踩的坑,这里列出最有代表性的三个:

    陷阱一:Mel 频谱归一化

    很多人直接对 Mel 频谱取 log 后送入模型训练,导致 loss 超大、训练发散。原因是 Mel 频谱的能量分布非常不均匀——低频能量远高于高频,如果不对 Mel 频谱做归一化,模型会把所有注意力放在拟合低频部分。

    正确的做法是对每个 Mel 频带分别计算均值和方差,进行 Z-score 归一化:

    class MelNormalizer:
    """Mel 频谱归一化器"""
    def __init__(self):
    self.mean = None
    self.std = None

    def fit(self, mel_dataset):
    """从数据集计算归一化参数"""
    all_mels = torch.cat([m.flatten() for m in mel_dataset])
    self.mean = all_mels.mean()
    self.std = all_mels.std()
    return self

    def normalize(self, mel):
    return (mel – self.mean) / (self.std + 1e-8)

    def denormalize(self, mel_norm):
    return mel_norm * self.std + self.mean

    陷阱二:持续时间预测的整数舍入

    在 expand_with_duration 函数中,我们将预测的持续时间取整为整数。这个离散化过程会引入误差。如果多个音素的持续时间都被向下取整,累计误差会导致合成语音越来越快(或者越来越慢)。

    解决方案是在训练时对持续时间使用 MSE 损失(允许小数输出),推理时才做取整。我们已经在 FastSpeechLoss 中实现了这一点。

    陷阱三:声码器与 Mel 频谱参数不匹配

    Mel 频谱提取使用的 n_fft、hop_length、n_mels 等参数,必须与 HiFi-GAN 预训练模型使用的参数完全一致。如果提取参数与声码器期望的参数不一致,合成出来的音频会充满噪音。甚至不同的 Librosa/TorchAudio 版本,默认的窗函数类型也可能不同。

    解决办法是在推理管线中硬编码参数,并确保声学模型训练和声码器推理使用同一套:

    TTS_CONFIG = {
    'sample_rate': 22050,
    'n_fft': 1024,
    'win_length': 1024,
    'hop_length': 256,
    'n_mels': 80,
    'f_min': 0,
    'f_max': 8000,
    'mel_normalization': True,
    }


    结语

    文本到语音合成是一个跨学科的技术领域——它需要自然语言处理、数字信号处理、深度学习、声学工程等多个领域的知识。本文通过从零实现一个完整的 TTS 系统,帮助你理解了每个模块的工作原理和相互配合的方式。

    代码虽然教学化,但核心架构与工业级方案一致。掌握了 FastSpeech + HiFi-GAN 这条技术路线,你就具备了理解和使用更先进 TTS 系统(VITS、CosyVoice)的基础。

    最后,记住一个技术选择的原则:在语音合成中,系统的"天花板"由声学模型决定,但"地板"由文本前端和声码器决定。花时间优化文本前端处理的边界情况,以及确保声码器预训练权重与你选择的 Mel 参数匹配,往往比调整 Transformer 的层数收获更大。

    赞(0)
    未经允许不得转载:171主机测评 » 手写 AI 文本到语音(TTS)系统:从零实现语音合成引擎
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址