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 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.