文本转语音
文本转语音(TTS)是指从文本生成自然语音的任务,生成的语音可以支持多种语言和多位说话人。目前,🤗 Transformers 中提供了多种文本转语音模型,例如 Dia、CSM、Bark、MMS、VITS 和 SpeechT5。
您可以轻松使用 "text-to-audio" 流水线(或其别名 - "text-to-speech")生成音频。
以下是使用 "text-to-speech" 流水线配合 CSM 的示例:
>>> from transformers import pipeline
>>> pipe = pipeline("text-to-audio", model="sesame/csm-1b")
>>> output = pipe("Hello from Sesame.")以下是一个代码片段,您可以在笔记本中用于播放生成的音频:
>>> from IPython.display import Audio
>>> Audio(output["audio"], rate=output["sampling_rate"])默认情况下,CSM 使用随机语音。您可以通过在聊天模板字典中提供参考音频来进行语音克隆:
>>> import soundfile as sf
>>> import torch
>>> from datasets import Audio, load_dataset
>>> from transformers import pipeline
>>> pipe = pipeline("text-to-audio", model="sesame/csm-1b")
>>> ds = load_dataset("hf-internal-testing/dailytalk-dummy", split="train")
>>> ds = ds.cast_column("audio", Audio(sampling_rate=24000))
>>> conversation = [
... {
... "role": "0",
... "content": [
... {"type": "text", "text": "What are you working on?"},
... {"type": "audio", "path": ds[0]["audio"]["array"]},
... ],
... },
... {"role": "0", "content": [{"type": "text", "text": "How much money can you spend?"}]},
... ]
>>> output = pipe(conversation)一些模型,例如 Dia,还可以被条件化以生成非语言交流,如大笑、叹息和哭泣,甚至添加音乐。下面是一个示例:
>>> from transformers import pipeline
>>> pipe = pipeline("text-to-speech", model="nari-labs/Dia-1.6B-0626")
>>> text = "[S1] (clears throat) Hello! How are you? [S2] I'm good, thanks! How about you?"
>>> output = pipe(text)请注意,Dia 还支持 [S1] 和 [S2] 等说话人标签,以生成不同声音之间的对话。
如需了解 CSM 及其他预训练 TTS 模型能实现的功能的更多示例,请参阅我们的 音频课程。
如果您希望微调 TTS 模型,目前 🤗 Transformers 中可用的文本转语音模型仅有 SpeechT5、FastSpeech2Conformer、Dia 和 CSM,未来还将添加更多模型。SpeechT5 在语音转文本和文本转语音数据的组合上进行预训练,使其能够学习一个由文本和语音共享的隐藏表示统一空间。这意味着同一个预训练模型可以被微调用于不同的任务。此外,SpeechT5 通过 x-vector 说话人嵌入支持多说话人。
本指南的其余部分将介绍如何:
在开始之前,请确保已安装所有必要的库:
pip install transformers datasets soundfile librosa torchcodec speechbrain accelerate
由于并非所有 SpeechT5 功能都已合并到官方发布版本中,请从源代码安装 🤗Transformers:
pip install git+https://github.com/huggingface/transformers.git
要跟随本指南,您需要一个 GPU。如果您在 notebook 中工作,请运行以下代码行以检查是否有可用的 GPU:
!nvidia-smi或者,对于 AMD GPU:
!rocm-smi
我们建议您登录 Hugging Face 账户,以将您的模型上传并与社区共享。当系统提示时,请输入您的 token 以完成登录:
>>> from huggingface_hub import notebook_login
>>> notebook_login()加载数据集
VoxPopuli 是一个大规模多语言语音语料库,其数据来源于 2009 年至 2020 年欧洲议会活动的录音。它包含 15 种欧洲语言的带标签音频-转录数据。在本指南中,我们使用的是荷兰语子集,您也可以自由选择其他子集。
请注意,VoxPopuli 或其他任何自动语音识别(ASR)数据集可能并非训练 TTS 模型的最佳选择。使其适用于 ASR 的特性(例如过多的背景噪声)在 TTS 中通常是不受欢迎的。然而,寻找高质量、多语言且多说话人的 TTS 数据集可能颇具挑战性。
让我们加载数据:
>>> from datasets import load_dataset, Audio
>>> dataset = load_dataset("qmeeus/voxpopuli", "nl", split="train")
>>> len(dataset)
2096820968 个样本对于微调来说应该足够了。SpeechT5 要求音频数据的采样率为 16 kHz,因此请确保数据集中的样本满足此要求:
dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))预处理数据
让我们从定义要使用的模型检查点并加载相应的处理器开始:
>>> from transformers import SpeechT5Processor
>>> checkpoint = "microsoft/speecht5_tts"
>>> processor = SpeechT5Processor.from_pretrained(checkpoint)SpeechT5 分词中的文本清理
首先清理文本数据。你需要使用处理器中的分词器部分来处理文本:
>>> tokenizer = processor.tokenizer数据集示例包含一个带有转录文本的 text 特征。请注意,SpeechT5 分词器没有用于数字的标记,因此文本中的任何数字都会被转换为 <unk> 标记。如果您的数据集包含数字,请考虑将其规范化为书面形式。
由于 SpeechT5 是在英语语料上训练的,它可能无法识别荷兰语数据集中的某些字符。如果保持原样,这些字符将被转换为 <unk> 标记。然而,在荷兰语中,某些字符(如 à)用于表示音节重音。为了保留文本的含义,我们可以将该字符替换为普通的 a。
要识别不受支持的 token,请使用以字符作为 token 的 SpeechT5Tokenizer 提取数据集中所有唯一的字符。为此,编写 extract_all_chars 映射函数,将所有示例的转录文本拼接成一个字符串,并将其转换为字符集合。
请确保在 dataset.map() 中设置 batched=True 和 batch_size=-1,以便映射函数可以一次性获取所有转录文本。
>>> def extract_all_chars(batch):
... all_text = " ".join(batch["text"])
... vocab = list(set(all_text))
... return {"vocab": [vocab], "all_text": [all_text]}
>>> vocabs = dataset.map(
... extract_all_chars,
... batched=True,
... batch_size=-1,
... keep_in_memory=True,
... remove_columns=dataset.column_names,
... )
>>> dataset_vocab = set(vocabs["vocab"][0])
>>> tokenizer_vocab = {k for k, _ in tokenizer.get_vocab().items()}现在你拥有两组字符:一组来自数据集的词表,另一组来自分词器的词表。 要识别数据集中任何不受支持的字符,你可以计算这两个集合的差集。得到的 集合将包含存在于数据集中但不存在于分词器中的字符。
>>> dataset_vocab - tokenizer_vocab
{' ', 'à', 'ç', 'è', 'ë', 'í', 'ï', 'ö', 'ü'}为了处理上一步中识别出的不支持的字符,请定义一个函数,将这些字符映射到有效的 token。请注意,空格在分词器中已经被替换为 ▁,因此无需单独处理。
>>> replacements = [
... ("à", "a"),
... ("ç", "c"),
... ("è", "e"),
... ("ë", "e"),
... ("í", "i"),
... ("ï", "i"),
... ("ö", "o"),
... ("ü", "u"),
... ]
>>> def cleanup_text(inputs):
... for src, dst in replacements:
... inputs["text"] = inputs["text"].replace(src, dst)
... return inputs
>>> dataset = dataset.map(cleanup_text)现在你已经处理了文本中的特殊字符,是时候将注意力转移到音频数据上了。
演讲者
VoxPopuli 数据集包含来自多位说话者的语音,但数据集中究竟有多少位说话者?为了确定这一点,我们可以统计唯一说话者的数量,以及每位说话者为数据集贡献的样本数量。该数据集共有 20,968 个样本,这些信息将帮助我们更好地理解数据中说话者和样本的分布情况。
>>> from collections import defaultdict
>>> speaker_counts = defaultdict(int)
>>> for speaker_id in dataset["speaker_id"]:
... speaker_counts[speaker_id] += 1通过绘制直方图,您可以大致了解每位说话人的数据量。
>>> import matplotlib.pyplot as plt
>>> plt.figure()
>>> plt.hist(speaker_counts.values(), bins=20)
>>> plt.ylabel("Speakers")
>>> plt.xlabel("Examples")
>>> plt.show()
直方图显示,数据集中约三分之一的说话人样本数少于 100 个,而大约有 10 位说话人的样本数超过 500 个。为了提高训练效率并平衡数据集,我们可以将数据限制在样本数介于 100 到 400 个之间的说话人。
>>> def select_speaker(speaker_id):
... return 100 <= speaker_counts[speaker_id] <= 400
>>> dataset = dataset.filter(select_speaker, input_columns=["speaker_id"])让我们检查一下还剩多少位说话人:
>>> len(set(dataset["speaker_id"]))
42让我们看看还剩多少示例:
>>> len(dataset)
9973最终,你只剩下大约 40 位不同说话者的不到 10,000 个样本,这应该已经足够了。
请注意,某些示例较少的说话人实际上可能拥有更多的音频,如果这些示例的时长较长。然而,要确定每位说话人的总音频量,需要扫描整个数据集,这是一个耗时过程,涉及加载和解码每个音频文件。因此,我们选择在此处跳过这一步骤。
说话人嵌入
为了让 TTS 模型能够区分多个说话人,您需要为每个示例创建一个说话人嵌入(speaker embedding)。 说话人嵌入是输入到模型中的额外信息,用于捕捉特定说话人的声音特征。 要生成这些说话人嵌入,请使用 SpeechBrain 中预训练的 spkrec-xvect-voxceleb 模型。
创建一个函数 create_speaker_embedding(),该函数接收输入音频波形,并输出一个包含 512 个元素的向量,其中包含对应的说话人嵌入。
>>> import os
>>> import torch
>>> from speechbrain.inference.classifiers import EncoderClassifier
>>> from accelerate import Accelerator
>>> spk_model_name = "speechbrain/spkrec-xvect-voxceleb"
>>> device = Accelerator().device
>>> speaker_model = EncoderClassifier.from_hparams(
... source=spk_model_name,
... run_opts={"device": device},
... savedir=os.path.join("/tmp", spk_model_name),
... )
>>> def create_speaker_embedding(waveform):
... with torch.no_grad():
... speaker_embeddings = speaker_model.encode_batch(torch.tensor(waveform))
... speaker_embeddings = torch.nn.functional.normalize(speaker_embeddings, dim=2)
... speaker_embeddings = speaker_embeddings.squeeze().cpu().numpy()
... return speaker_embeddings需要注意的是,speechbrain/spkrec-xvect-voxceleb 模型是在 VoxCeleb 数据集的英语语音上训练的,而本指南中的训练示例是荷兰语。虽然我们预计该模型仍能为我们的荷兰语数据集生成合理的说话人嵌入,但这一假设在所有情况下可能并不成立。
为了获得最佳效果,我们建议先在目标语音上训练 X-vector 模型。这将确保模型能够更好地捕捉荷兰语中独特的语音特征。
处理数据集
最后,让我们将数据处理为模型所期望的格式。创建一个 prepare_dataset 函数,该函数接收单个示例,并使用 SpeechT5Processor 对象对输入文本进行分词,并将目标音频加载为对数梅尔频谱图。它还应将说话人嵌入作为额外输入添加。
>>> def prepare_dataset(example):
... audio = example["audio"]
... example = processor(
... text=example["text"],
... audio_target=audio["array"],
... sampling_rate=audio["sampling_rate"],
... return_attention_mask=False,
... )
... # strip off the batch dimension
... example["labels"] = example["labels"][0]
... # use SpeechBrain to obtain x-vector
... example["speaker_embeddings"] = create_speaker_embedding(audio["array"])
... return example通过查看单个示例来验证处理是否正确:
>>> processed_example = prepare_dataset(dataset[0])
>>> list(processed_example.keys())
['input_ids', 'labels', 'stop_labels', 'speaker_embeddings']说话人嵌入应为一个 512 维的向量:
>>> processed_example["speaker_embeddings"].shape
(512,)标签应为具有 80 个 mel 频带的对数 mel 频谱图。
>>> import matplotlib.pyplot as plt
>>> plt.figure()
>>> plt.imshow(processed_example["labels"].T)
>>> plt.show()
附注:如果您觉得这张频谱图令人困惑,可能是因为您习惯了将低频置于图表底部、高频置于顶部的惯例。然而,当使用 matplotlib 库将频谱图作为图像绘制时,y 轴是翻转的,因此频谱图看起来是上下颠倒的。
现在将处理函数应用于整个数据集。这将花费 5 到 10 分钟。
>>> dataset = dataset.map(prepare_dataset, remove_columns=dataset.column_names)您会看到一条警告,指出数据集中部分示例的长度超过了模型可处理的最大输入长度(600 个 token)。 请从数据集中移除这些示例。这里我们更进一步,为了支持更大的 batch size,我们将移除所有超过 200 个 token 的示例。
>>> def is_not_too_long(input_ids):
... input_length = len(input_ids)
... return input_length < 200
>>> dataset = dataset.filter(is_not_too_long, input_columns=["input_ids"])
>>> len(dataset)
8259接下来,创建一个基本的训练/测试集划分:
>>> dataset = dataset.train_test_split(test_size=0.1)数据整理器
为了将多个样本组合成一个批次,你需要定义一个自定义的数据整理器(data collator)。该整理器会用填充 token 对较短的序列进行填充,以确保所有样本具有相同的长度。对于频谱图标签,填充部分会被替换为特殊值 -100。该特殊值指示模型在计算频谱图损失时忽略频谱图的该部分。
>>> from dataclasses import dataclass
>>> from typing import Any, Dict, List, Union
>>> @dataclass
... class TTSDataCollatorWithPadding:
... processor: Any
... def __call__(self, features: list[dict[str, Union[list[int], torch.Tensor]]]) -> dict[str, torch.Tensor]:
... input_ids = [{"input_ids": feature["input_ids"]} for feature in features]
... label_features = [{"input_values": feature["labels"]} for feature in features]
... speaker_features = [feature["speaker_embeddings"] for feature in features]
... # collate the inputs and targets into a batch
... batch = processor.pad(input_ids=input_ids, labels=label_features, return_tensors="pt")
... # replace padding with -100 to ignore loss correctly
... batch["labels"] = batch["labels"].masked_fill(batch.decoder_attention_mask.unsqueeze(-1).ne(1), -100)
... # not used during fine-tuning
... del batch["decoder_attention_mask"]
... # round down target lengths to multiple of reduction factor
... if model.config.reduction_factor > 1:
... target_lengths = torch.tensor([len(feature["input_values"]) for feature in label_features])
... target_lengths = target_lengths.new(
... [length - length % model.config.reduction_factor for length in target_lengths]
... )
... max_length = max(target_lengths)
... batch["labels"] = batch["labels"][:, :max_length]
... # also add in the speaker embeddings
... batch["speaker_embeddings"] = torch.tensor(speaker_features)
... return batch在 SpeechT5 中,模型解码器部分的输入会缩小为原来的 1/2。换句话说,它会丢弃目标序列中每隔一个的时间步。随后,解码器会预测出一个长度翻倍的目标序列。由于原始目标序列的长度可能为奇数,数据整理器(data collator)会确保将批次的最大长度向下取整为 2 的倍数。
>>> data_collator = TTSDataCollatorWithPadding(processor=processor)训练模型
从与加载处理器时使用的相同检查点加载预训练模型:
>>> from transformers import SpeechT5ForTextToSpeech
>>> model = SpeechT5ForTextToSpeech.from_pretrained(checkpoint)use_cache=True 选项与梯度检查点不兼容。在训练时请将其禁用。
>>> model.config.use_cache = False定义训练参数。这里我们在训练过程中不计算任何评估指标。相反,我们只关注损失:
>>> from transformers import Seq2SeqTrainingArguments
>>> training_args = Seq2SeqTrainingArguments(
... output_dir="speecht5_finetuned_voxpopuli_nl", # change to a repo name of your choice
... per_device_train_batch_size=4,
... gradient_accumulation_steps=8,
... learning_rate=1e-5,
... warmup_steps=500,
... max_steps=4000,
... gradient_checkpointing=True,
... fp16=True,
... eval_strategy="steps",
... per_device_eval_batch_size=2,
... save_steps=1000,
... eval_steps=1000,
... logging_steps=25,
... report_to="trackio",
... load_best_model_at_end=True,
... greater_is_better=False,
... label_names=["labels"],
... push_to_hub=True,
... )实例化 Trainer 对象,并将模型、数据集和数据整理器传入其中。
>>> from transformers import Seq2SeqTrainer
>>> trainer = Seq2SeqTrainer(
... args=training_args,
... model=model,
... train_dataset=dataset["train"],
... eval_dataset=dataset["test"],
... data_collator=data_collator,
... processing_class=processor,
... )至此,您已准备好开始训练!训练过程将耗时数小时。根据您的 GPU 配置,在开始训练时可能会遇到 CUDA “out-of-memory” 错误。在这种情况下,您可以将 per_device_train_batch_size 按 2 的倍数逐步减小,并将 gradient_accumulation_steps 增加 2 倍以作补偿。
>>> trainer.train()要在流水线中使用您的检查点,请确保将处理器与检查点一起保存:
>>> processor.save_pretrained("YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")将最终模型推送到 🤗 Hub:
>>> trainer.push_to_hub()推理
使用 pipeline 进行推理
很好,现在你已经微调了一个模型,就可以用它进行推理了!
首先,让我们看看如何使用对应的 pipeline。让我们使用你的检查点创建一个 "text-to-speech" pipeline:
>>> from transformers import pipeline
>>> pipe = pipeline("text-to-speech", model="YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")选择一段您希望进行旁白的荷兰语文本,例如:
>>> text = "hallo allemaal, ik praat nederlands. groetjes aan iedereen!"要在 pipeline 中使用 SpeechT5,你需要一个说话人嵌入。让我们从测试数据集中的示例中获取它:
>>> example = dataset["test"][304]
>>> speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)现在,您可以将文本和说话人嵌入传递给 pipeline,它会自动处理其余部分:
>>> forward_params = {"speaker_embeddings": speaker_embeddings}
>>> output = pipe(text, forward_params=forward_params)
>>> output
{'audio': array([-6.82714235e-05, -4.26525949e-04, 1.06134125e-04, ...,
-1.22392643e-03, -7.76011671e-04, 3.29112721e-04], dtype=float32),
'sampling_rate': 16000}然后,您可以试听结果:
>>> from IPython.display import Audio
>>> Audio(output['audio'], rate=output['sampling_rate'])手动运行推理
不过,你也可以在不使用 pipeline 的情况下获得相同的推理结果,但需要更多步骤。
从 🤗 Hub 加载模型:
>>> model = SpeechT5ForTextToSpeech.from_pretrained("YOUR_ACCOUNT/speecht5_finetuned_voxpopuli_nl")从测试数据集中选取一个示例,并获取说话人嵌入。
>>> example = dataset["test"][304]
>>> speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)定义输入文本并将其分词。
>>> text = "hallo allemaal, ik praat nederlands. groetjes aan iedereen!"
>>> inputs = processor(text=text, return_tensors="pt")使用您的模型创建频谱图:
>>> spectrogram = model.generate_speech(inputs["input_ids"], speaker_embeddings)如果您愿意,可以可视化频谱图:
>>> plt.figure()
>>> plt.imshow(spectrogram.T)
>>> plt.show()
最后,使用声码器将频谱图转换为声音。
>>> with torch.no_grad():
... speech = vocoder(spectrogram)
>>> from IPython.display import Audio
>>> Audio(speech.numpy(), rate=16000)根据我们的经验,要从该模型中获得令人满意的结果可能颇具挑战性。说话人嵌入的质量似乎是一个重要因素。由于 SpeechT5 是使用英语 x-vectors 进行预训练的,因此在使用英语说话人嵌入时表现最佳。如果合成的语音质量不佳,请尝试使用不同的说话人嵌入。
延长训练时长也有望提升结果的质量。即便如此,生成的语音明显是荷兰语而非英语,但它确实捕捉到了说话者的声音特征(可与示例中的原始音频进行对比)。
另一个可以尝试的方向是调整模型的配置。例如,尝试使用 config.reduction_factor = 1,看看这是否能改善结果。
最后,必须考虑伦理问题。尽管 TTS 技术有许多有用的应用场景,但它也可能被用于恶意目的,例如在未经某人知情或同意的情况下模仿其声音。请谨慎且负责任地使用 TTS。
Update on GitHub