Transformers 文档

文本转语音

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

文本转语音

文本转语音(TTS)是将文本转换为自然发音的语音的任务,其中语音可以以多种语言为多个说话者生成。目前在🤗 Transformers 中有几种文本转语音模型可用,例如 BarkMMSVITSSpeechT5

您可以使用 "text-to-audio" 流水线(或其别名 - "text-to-speech")轻松生成音频。一些模型,如 Bark,还可以通过条件设置生成非语言交流,如笑声、叹息和哭泣,甚至添加音乐。以下是您如何将 "text-to-speech" 流水线与 Bark 结合使用的示例

>>> from transformers import pipeline

>>> pipe = pipeline("text-to-speech", model="suno/bark-small")
>>> text = "[clears throat] This is a test ... and I just took a long pause."
>>> output = pipe(text)

以下是一个代码片段,您可以使用它在笔记本中收听生成的音频

>>> from IPython.display import Audio
>>> Audio(output["audio"], rate=output["sampling_rate"])

有关 Bark 和其他预训练 TTS 模型可以执行的操作的更多示例,请参阅我们的音频课程

如果您正在寻找微调 TTS 模型,目前 🤗 Transformers 中唯一可用的文本转语音模型是 SpeechT5FastSpeech2Conformer,尽管将来会添加更多。SpeechT5 经过语音到文本和文本到语音数据的组合预训练,使其能够学习文本和语音共享的统一隐藏表示空间。这意味着同一个预训练模型可以针对不同的任务进行微调。此外,SpeechT5 通过 x-vector 说话人嵌入支持多个说话人。

本指南的其余部分将说明如何

  1. VoxPopuli 数据集的荷兰语 (nl) 子集上微调最初在英语语音上训练的 SpeechT5
  2. 通过两种方式之一使用您的优化模型进行推理:使用流水线或直接使用。

在开始之前,请确保您已安装所有必要的库

pip install datasets soundfile speechbrain accelerate

从源安装 🤗Transformers,因为并非所有 SpeechT5 功能都已合并到官方版本中

pip install git+https://github.com/huggingface/transformers.git

要遵循本指南,您需要一个 GPU。如果您在笔记本中工作,请运行以下行以检查 GPU 是否可用

!nvidia-smi

或者对于 AMD GPU

!rocm-smi

我们鼓励您登录您的 Hugging Face 帐户,以便与社区上传和共享您的模型。当提示时,输入您的令牌登录

>>> 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("facebook/voxpopuli", "nl", split="train")
>>> len(dataset)
20968

20968 个示例应该足以进行微调。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

数据集示例包含 raw_textnormalized_text 特征。在决定使用哪个特征作为文本输入时,请考虑 SpeechT5 标记器不包含任何数字标记。在 normalized_text 中,数字以文本形式写出。因此,它更适合,我们建议使用 normalized_text 作为输入文本。

由于 SpeechT5 是用英语训练的,它可能无法识别荷兰语数据集中的某些字符。如果保持原样,这些字符将转换为 <unk> 标记。然而,在荷兰语中,某些字符如 à 用于强调音节。为了保留文本的含义,我们可以将此字符替换为常规的 a

要识别不支持的标记,请使用 SpeechT5Tokenizer 提取数据集中的所有唯一字符,该标记器将字符作为标记。为此,编写 extract_all_chars 映射函数,该函数将所有示例的转录连接成一个字符串,并将其转换为一组字符。请务必在 dataset.map() 中设置 batched=Truebatch_size=-1,以便所有转录都可一次性用于映射函数。

>>> def extract_all_chars(batch):
...     all_text = " ".join(batch["normalized_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
{' ', 'à', 'ç', 'è', 'ë', 'í', 'ï', 'ö', 'ü'}

为了处理上一步中识别出的不支持的字符,定义一个函数将这些字符映射到有效标记。请注意,空格已在标记器中被 替换,无需单独处理。

>>> replacements = [
...     ("à", "a"),
...     ("ç", "c"),
...     ("è", "e"),
...     ("ë", "e"),
...     ("í", "i"),
...     ("ï", "i"),
...     ("ö", "o"),
...     ("ü", "u"),
... ]


>>> def cleanup_text(inputs):
...     for src, dst in replacements:
...         inputs["normalized_text"] = inputs["normalized_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()
Speakers histogram

直方图显示,数据集中大约三分之一的说话者拥有少于 100 个示例,而大约十位说话者拥有超过 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

您剩下不到 10,000 个来自大约 40 位独特说话者的示例,这应该足够了。

请注意,一些示例较少的说话者如果示例较长,实际上可能拥有更多的可用音频。然而,确定每个说话者的总音频量需要扫描整个数据集,这是一个耗时的过程,涉及加载和解码每个音频文件。因此,我们选择在此处跳过此步骤。

说话人嵌入

为了使 TTS 模型能够区分多个说话者,您需要为每个示例创建说话者嵌入。说话者嵌入是模型的一个额外输入,它捕获特定说话者的声音特征。为了生成这些说话者嵌入,请使用 SpeechBrain 预训练的 spkrec-xvect-voxceleb 模型。

创建一个函数 create_speaker_embedding(),该函数接受输入音频波形并输出一个包含相应说话者嵌入的 512 元素向量。

>>> import os
>>> import torch
>>> from speechbrain.inference.classifiers import EncoderClassifier
>>> from accelerate.test_utils.testing import get_backend

>>> spk_model_name = "speechbrain/spkrec-xvect-voxceleb"
>>> device, _, _ = get_backend() # automatically detects the underlying device type (CUDA, CPU, XPU, MPS, etc.)
>>> 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["normalized_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 个梅尔频带的对数梅尔谱图。

>>> import matplotlib.pyplot as plt

>>> plt.figure()
>>> plt.imshow(processed_example["labels"].T)
>>> plt.show()
Log-mel spectrogram with 80 mel bins

旁注:如果您觉得这个频谱图令人困惑,可能是因为您习惯于将低频放在图表的底部,高频放在图表的顶部。然而,使用 matplotlib 库将频谱图绘制为图像时,y 轴会翻转,频谱图会显示为倒置。

现在将处理函数应用于整个数据集。这将需要 5 到 10 分钟。

>>> dataset = dataset.map(prepare_dataset, remove_columns=dataset.column_names)

您会看到一条警告,指出数据集中的某些示例长于模型可以处理的最大输入长度(600 个标记)。从数据集中删除这些示例。在这里,我们甚至更进一步,为了允许更大的批次大小,我们删除了任何超过 200 个标记的示例。

>>> 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)

数据整理器

为了将多个示例组合成一个批次,您需要定义一个自定义数据整理器。该整理器将用填充标记填充较短的序列,确保所有示例具有相同的长度。对于频谱图标签,填充部分将替换为特殊值 -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 中,模型解码器部分的输入会缩小 2 倍。换句话说,它会丢弃目标序列中每隔一个时间步。然后解码器会预测一个长度为两倍的序列。由于原始目标序列长度可能为奇数,因此数据整理器会确保将批次的最大长度向下舍入为 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=["tensorboard"],
...     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 “内存不足”错误。在这种情况下,您可以将 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()

推理

使用流水线进行推理

太棒了,现在您已经微调了一个模型,您可以将其用于推理!首先,让我们看看如何将其与相应的流水线一起使用。让我们使用您的检查点创建一个 "text-to-speech" 流水线

>>> 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!"

要将 SpeechT5 与流水线一起使用,您需要一个说话人嵌入。让我们从测试数据集中的一个示例获取它

>>> example = dataset["test"][304]
>>> speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)

现在您可以将文本和说话人嵌入传递给流水线,它将处理其余部分

>>> 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'])

手动运行推理

您无需使用流水线即可获得相同的推理结果,但需要更多步骤。

从 🤗 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()
Generated log-mel spectrogram

最后,使用声码器将频谱图转换为声音。

>>> with torch.no_grad():
...     speech = vocoder(spectrogram)

>>> from IPython.display import Audio

>>> Audio(speech.numpy(), rate=16000)

根据我们的经验,从该模型获得令人满意的结果可能具有挑战性。说话人嵌入的质量似乎是一个重要因素。由于 SpeechT5 使用英语 x 向量进行了预训练,因此在使用英语说话人嵌入时表现最佳。如果合成语音听起来很差,请尝试使用不同的说话人嵌入。

增加训练时长也可能会提高结果质量。即便如此,语音明显是荷兰语而不是英语,并且确实捕捉到了说话者的语音特征(与示例中的原始音频进行比较)。另一个可以尝试的是模型的配置。例如,尝试使用 config.reduction_factor = 1 来查看这是否能改善结果。

最后,考虑伦理问题至关重要。尽管 TTS 技术有许多有用的应用,但它也可能被用于恶意目的,例如未经他人知情或同意冒充他人的声音。请谨慎并负责任地使用 TTS。

< > 在 GitHub 上更新