Transformers 文档

文本到语音

Hugging Face's logo
加入 Hugging Face 社区

并获取增强的文档体验

开始使用

文本到语音

文本到语音 (TTS) 是指从文本创建自然发声的语音的任务,其中语音可以用多种语言和为多位说话人生成。🤗 Transformers 中目前有几种文本到语音模型,例如 BarkMMSVITSSpeechT5

您可以使用 "text-to-audio" pipeline(或其别名 "text-to-speech")轻松生成音频。一些模型,如 Bark,也可以被条件化以生成非语言交流,如笑声、叹息和哭泣,甚至添加音乐。以下是如何将 "text-to-speech" pipeline 与 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)

以下是在 notebook 中收听生成的音频的代码片段

>>> 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. 以两种方式之一使用您精炼的模型进行推理:使用 pipeline 或直接使用。

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

pip install datasets soundfile speechbrain accelerate

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

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

20968 个示例应该足以进行微调。SpeechT5 期望音频数据的采样率为 16 kHz,因此请确保数据集中的示例满足此要求

dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))

预处理数据

首先,定义要使用的模型 checkpoint 并加载相应的 processor

>>> from transformers import SpeechT5Processor

>>> checkpoint = "microsoft/speecht5_tts"
>>> processor = SpeechT5Processor.from_pretrained(checkpoint)

SpeechT5 分词器的文本清理

首先清理文本数据。您将需要 processor 的分词器部分来处理文本

>>> tokenizer = processor.tokenizer

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

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

要识别不支持的 token,请使用 SpeechT5Tokenizer 提取数据集中的所有唯一字符,该分词器使用字符作为 token。为此,请编写 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
{' ', 'à', 'ç', 'è', 'ë', 'í', 'ï', 'ö', 'ü'}

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

>>> 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 对象来标记输入文本,并将目标音频加载到 log-mel 频谱图中。它还应添加说话人嵌入作为附加输入。

>>> 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 个 mel bins 的 log-mel 频谱图。

>>> 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 个 token)。从数据集中删除这些示例。这里我们更进一步,为了允许更大的批处理大小,我们删除了任何超过 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)

数据 collator

为了将多个示例组合成一个批次,您需要定义一个自定义数据 collator。此 collator 将使用 padding 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 中,模型解码器部分的输入减少了 2 倍。换句话说,它从目标序列中丢弃了每个其他时间步。然后,解码器预测的序列长度是原来的两倍。由于原始目标序列长度可能是奇数,因此数据 collator 确保将批次的最大长度向下舍入为 2 的倍数。

>>> data_collator = TTSDataCollatorWithPadding(processor=processor)

训练模型

从与加载 processor 相同的 checkpoint 加载预训练模型

>>> 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 对象并将模型、数据集和数据 collator 传递给它。

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

为了能够将您的 checkpoint 与 pipeline 一起使用,请确保将 processor 与 checkpoint 一起保存

>>> processor.save_pretrained("YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")

将最终模型推送到 🤗 Hub

>>> trainer.push_to_hub()

推理

使用 pipeline 进行推理

太棒了,现在您已经微调了一个模型,您可以将其用于推理!首先,让我们看看如何将其与相应的 pipeline 一起使用。让我们使用您的 checkpoint 创建一个 "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!"

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

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

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

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

>>> from IPython.display import Audio

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

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

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

最后,必须考虑伦理因素。尽管 TTS 技术有许多有用的应用,但它也可能被用于恶意目的,例如在未经他人知晓或同意的情况下冒充他人的声音。请明智且负责任地使用 TTS。

< > 在 GitHub 上更新