diff --git a/README.md b/README.md index 4b1cb5c..8f349e2 100644 --- a/README.md +++ b/README.md @@ -23,18 +23,84 @@ Some other features include: - The Chinese speaker supports `mixed Chinese and English`. - Fast enough for `CPU real-time inference`. -## Install on Linux +## Install on Linux or macOS + ```bash -git clone git@github.com:myshell-ai/MeloTTS.git +git clone git+https://github.com/myshell-ai/MeloTTS.git cd MeloTTS pip install -e . python -m unidic download ``` -We welcome the open-source community to make this repo `Mac` and `Windows` compatible. If you find this repo useful, please consider contributing to the repo. + +We welcome the open-source community to make this repo `Windows` compatible. If you find this repo useful, please consider contributing to the repo. ## Usage -### English with Multi Accents +An unofficial [live demo](https://huggingface.co/spaces/mrfakename/MeloTTS) is hosted on Hugging Face Spaces. + +### WebUI + +The WebUI supports muliple languages and voices. First, follow the installation steps. Then, simply run: + +```bash +melo-ui +# Or: python melo/app.py +``` + +### CLI + +You may use the MeloTTS CLI to interact with MeloTTS. The CLI may be invoked using either `melotts` or `melo`. Here are some examples: + +**Read English text:** + +```bash +melo "Text to read" output.wav +``` + +**Specify a language:** + +```bash +melo "Text to read" output.wav --language EN +``` + +**Specify a speaker:** + +```bash +melo "Text to read" output.wav --language EN --speaker EN-US +melo "Text to read" output.wav --language EN --speaker EN-AU +``` + +The available speakers are: `EN-Default`, `EN-US`, `EN-BR`, `EN-INDIA` `EN-AU`. + +**Specify a speed:** + +```bash +melo "Text to read" output.wav --language EN --speaker EN-US --speed 1.5 +melo "Text to read" output.wav --speed 1.5 +``` + +**Use a different language:** + +```bash +melo "语音合成领域近年来发展迅速" zh.wav -l ZH +``` + +**Load from a file:** + +```bash +melo file.txt out.wav --file +``` + +The full API documentation may be found using: + +```bash +melo --help +``` + +### Python API + +#### English with Multiple Accents + ```python from melo.api import TTS @@ -42,8 +108,8 @@ from melo.api import TTS speed = 1.0 # CPU is sufficient for real-time inference. -# You can also change to cuda:0 -device = 'cpu' +# You can set it manually to 'cpu' or 'cuda' or 'cuda:0' or 'mps' +device = 'auto' # Will automatically use GPU if available # English text = "Did you ever hear a folk tale about a giant turtle?" @@ -91,7 +157,8 @@ output_path = 'es.wav' model.tts_to_file(text, speaker_ids['ES'], output_path, speed=speed) ``` -### French +#### French + ```python from melo.api import TTS @@ -107,7 +174,8 @@ output_path = 'fr.wav' model.tts_to_file(text, speaker_ids['FR'], output_path, speed=speed) ``` -### Chinese +#### Chinese + ```python from melo.api import TTS @@ -123,7 +191,8 @@ output_path = 'zh.wav' model.tts_to_file(text, speaker_ids['ZH'], output_path, speed=speed) ``` -### Japanese +#### Japanese + ```python from melo.api import TTS @@ -139,7 +208,8 @@ output_path = 'jp.wav' model.tts_to_file(text, speaker_ids['JP'], output_path, speed=speed) ``` -### Korean +#### Korean + ```python from melo.api import TTS @@ -156,7 +226,9 @@ model.tts_to_file(text, speaker_ids['KR'], output_path, speed=speed) ``` ## License -This library is under MIT License. Free for both commercial and non-commercial use. -## Acknowledgement +This library is under MIT License, which means it is free for both commercial and non-commercial use. + +## Acknowledgements + This implementation is based on several excellent projects, [TTS](https://github.com/coqui-ai/TTS), [VITS](https://github.com/jaywalnut310/vits), [VITS2](https://github.com/daniilrobnikov/vits2) and [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2). We appreciate their awesome work! diff --git a/melo/api.py b/melo/api.py index 852b24e..3727ae2 100644 --- a/melo/api.py +++ b/melo/api.py @@ -7,6 +7,8 @@ import soundfile import torchaudio import numpy as np import torch.nn as nn +from tqdm import tqdm +import torch from . import utils from . import commons @@ -18,8 +20,12 @@ from .download_utils import load_or_download_config, load_or_download_model class TTS(nn.Module): def __init__(self, language, - device='cuda:0'): + device='auto'): super().__init__() + if device == 'auto': + device = 'cpu' + if torch.cuda.is_available(): device = 'cuda' + if torch.backends.mps.is_available(): device = 'mps' if 'cuda' in device: assert torch.cuda.is_available() @@ -63,18 +69,28 @@ class TTS(nn.Module): return audio_segments @staticmethod - def split_sentences_into_pieces(text, language): + def split_sentences_into_pieces(text, language, quiet=False): texts = split_sentence(text, language_str=language) - print(" > Text splitted to sentences.") - print('\n'.join(texts)) - print(" > ===========================") + if not quiet: + print(" > Text split to sentences.") + print('\n'.join(texts)) + print(" > ===========================") return texts - def tts_to_file(self, text, speaker_id, output_path=None, sdp_ratio=0.2, noise_scale=0.6, noise_scale_w=0.8, speed=1.0): + def tts_to_file(self, text, speaker_id, output_path=None, sdp_ratio=0.2, noise_scale=0.6, noise_scale_w=0.8, speed=1.0, pbar=None, format=None, position=None, quiet=False,): language = self.language - texts = self.split_sentences_into_pieces(text, language) + texts = self.split_sentences_into_pieces(text, language, quiet) audio_list = [] - for t in texts: + if pbar: + tx = pbar(texts) + else: + if position: + tx = tqdm(texts, position=position) + elif quiet: + tx = texts + else: + tx = tqdm(texts) + for t in tx: if language in ['EN', 'ZH_MIX_EN']: t = re.sub(r'([a-z])([A-Z])', r'\1 \2', t) device = self.device @@ -110,4 +126,7 @@ class TTS(nn.Module): if output_path is None: return audio else: - soundfile.write(output_path, audio, self.hps.data.sampling_rate) \ No newline at end of file + if format: + soundfile.write(output_path, audio, self.hps.data.sampling_rate, format=format) + else: + soundfile.write(output_path, audio, self.hps.data.sampling_rate) diff --git a/melo/app.py b/melo/app.py new file mode 100644 index 0000000..2fe949b --- /dev/null +++ b/melo/app.py @@ -0,0 +1,47 @@ +# WebUI by mrfakename +# Demo also available on HF Spaces: https://huggingface.co/spaces/mrfakename/MeloTTS +import gradio as gr +import os, torch, io +# os.system('python -m unidic download') +print("Make sure you've downloaded unidic (python -m unidic download) for this WebUI to work.") +from melo.api import TTS +speed = 1.0 +import tempfile +import click +device = 'auto' +models = { + 'EN': TTS(language='EN', device=device), + 'ES': TTS(language='ES', device=device), + 'FR': TTS(language='FR', device=device), + 'ZH': TTS(language='ZH', device=device), + 'JP': TTS(language='JP', device=device), + 'KR': TTS(language='KR', device=device), +} +speaker_ids = models['EN'].hps.data.spk2id +def synthesize(speaker, text, speed, language, progress=gr.Progress()): + bio = io.BytesIO() + models[language].tts_to_file(text, models[language].hps.data.spk2id[speaker], bio, speed=speed, pbar=progress.tqdm, format='wav') + return bio.getvalue() +def load_speakers(language): + return gr.update(value=list(models[language].hps.data.spk2id.keys())[0], choices=list(models[language].hps.data.spk2id.keys())) +with gr.Blocks() as demo: + gr.Markdown('# MeloTTS WebUI\n\nA WebUI for MeloTTS.') + with gr.Group(): + speaker = gr.Dropdown(speaker_ids.keys(), interactive=True, value='EN-Default', label='Speaker') + language = gr.Radio(['EN', 'ES', 'FR', 'ZH', 'JP', 'KR'], label='Language', value='EN') + language.input(load_speakers, inputs=language, outputs=speaker) + speed = gr.Slider(label='Speed', minimum=0.1, maximum=10.0, value=1.0, interactive=True, step=0.1) + text = gr.Textbox(label="Text to speak", value='The field of text to speech has seen rapid development recently') + btn = gr.Button('Synthesize', variant='primary') + aud = gr.Audio(interactive=False) + btn.click(synthesize, inputs=[speaker, text, speed, language], outputs=[aud]) + gr.Markdown('WebUI by [mrfakename](https://twitter.com/realmrfakename).') +@click.command() +@click.option('--share', '-s', is_flag=True, show_default=True, default=False, help="Expose a publicly-accessible shared Gradio link usable by anyone with the link. Only share the link with people you trust.") +@click.option('--host', '-h', default=None) +@click.option('--port', '-p', default=None) +def main(share, host, port): + demo.queue(api_open=False, default_concurrency_limit=10).launch(show_api=False, share=share, server_name=host, server_port=port) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/melo/download_utils.py b/melo/download_utils.py index e7f4afe..5d538ef 100644 --- a/melo/download_utils.py +++ b/melo/download_utils.py @@ -1,7 +1,7 @@ import torch import os from . import utils - +from cached_path import cached_path DOWNLOAD_CKPT_URLS = { 'EN': 'https://myshell-public-repo-hosting.s3.amazonaws.com/openvoice/basespeakers/EN/checkpoint.pth', 'EN_V2': 'https://myshell-public-repo-hosting.s3.amazonaws.com/openvoice/basespeakers/EN_V2/checkpoint.pth', @@ -25,23 +25,11 @@ DOWNLOAD_CONFIG_URLS = { def load_or_download_config(locale): language = locale.split('-')[0].upper() assert language in DOWNLOAD_CONFIG_URLS - config_path = os.path.expanduser(f'~/.local/share/openvoice/basespeakers/{language}/config.json') - try: - return utils.get_hparams_from_file(config_path) - except: - # download - os.makedirs(os.path.dirname(config_path), exist_ok=True) - os.system(f'wget {DOWNLOAD_CONFIG_URLS[language]} -O {config_path}') + config_path = cached_path(DOWNLOAD_CONFIG_URLS[language]) return utils.get_hparams_from_file(config_path) def load_or_download_model(locale, device): language = locale.split('-')[0].upper() assert language in DOWNLOAD_CKPT_URLS - ckpt_path = os.path.expanduser(f'~/.local/share/openvoice/basespeakers/{language}/checkpoint.pth') - try: - return torch.load(ckpt_path, map_location=device) - except: - # download - os.makedirs(os.path.dirname(ckpt_path), exist_ok=True) - os.system(f'wget {DOWNLOAD_CKPT_URLS[language]} -O {ckpt_path}') - return torch.load(ckpt_path, map_location=device) \ No newline at end of file + ckpt_path = cached_path(DOWNLOAD_CKPT_URLS[language]) + return torch.load(ckpt_path, map_location=device) diff --git a/melo/main.py b/melo/main.py new file mode 100644 index 0000000..58064f1 --- /dev/null +++ b/melo/main.py @@ -0,0 +1,38 @@ +import click +import warnings +import os + +@click.command +@click.argument('text') +@click.argument('output_path') +@click.option("--file", '-f', is_flag=True, show_default=True, default=False, help="Text is a file") +@click.option('--language', '-l', default='EN', help='Language, defaults to English', type=click.Choice(['EN', 'ES', 'FR', 'ZH', 'JP', 'KR'], case_sensitive=False)) +@click.option('--speaker', '-spk', default='EN-Default', help='Speaker ID, only for English, leave empty for default, ignored if not English. If English, defaults to "EN-Default"', type=click.Choice(['EN-Default', 'EN-US', 'EN-BR', 'EN-INDIA', 'EN-AU'])) +@click.option('--speed', '-s', default=1.0, help='Speed, defaults to 1.0', type=float) +@click.option('--device', '-d', default='auto', help='Device, defaults to auto') +def main(text, file, output_path, language, speaker, speed, device): + if file: + if not os.path.exists(text): + raise FileNotFoundError(f'Trying to load text from file due to --file/-f flag, but file not found. Remove the --file/-f flag to pass a string.') + else: + with open(text) as f: + text = f.read().strip() + if text == '': + raise ValueError('You entered empty text or the file you passed was empty.') + language = language.upper() + if language == '': language = 'EN' + if speaker == '': speaker = None + if (not language == 'EN') and speaker: + warnings.warn('You specified a speaker but the language is English.') + from melo.api import TTS + model = TTS(language=language, device=device) + speaker_ids = model.hps.data.spk2id + if language == 'EN': + if not speaker: speaker = 'EN-Default' + spkr = speaker_ids[speaker] + else: + spkr = speaker_ids[list(speaker_ids.keys())[0]] + model.tts_to_file(text, spkr, output_path, speed=speed) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/melo/split_utils.py b/melo/split_utils.py index 4bba978..9158e2a 100644 --- a/melo/split_utils.py +++ b/melo/split_utils.py @@ -4,7 +4,7 @@ import glob import numpy as np import soundfile as sf import torchaudio - +from txtsplit import txtsplit def split_sentence(text, min_len=10, language_str='EN'): if language_str in ['EN', 'FR', 'ES', 'SP', 'DE', 'RU']: sentences = split_sentences_latin(text, min_len=min_len) @@ -18,26 +18,27 @@ def split_sentences_latin(text, min_len=10): text = re.sub('[“”]', '"', text) text = re.sub('[‘’]', "'", text) text = re.sub(r"[\<\>\(\)\[\]\"\«\»]+", "", text) + return [item.strip() for item in txtsplit(text, 512, 512) if item.strip()] # 将文本中的换行符、空格和制表符替换为空格 - text = re.sub('[\n\t ]+', ' ', text) - # 在标点符号后添加一个空格 - text = re.sub('([,.!?;])', r'\1 $#!', text) - # 分隔句子并去除前后空格 - sentences = [s.strip() for s in text.split('$#!')] - if len(sentences[-1]) == 0: del sentences[-1] + # text = re.sub('[\n\t ]+', ' ', text) + # # 在标点符号后添加一个空格 + # text = re.sub('([,.!?;])', r'\1 $#!', text) + # # 分隔句子并去除前后空格 + # sentences = [s.strip() for s in text.split('$#!')] + # if len(sentences[-1]) == 0: del sentences[-1] - new_sentences = [] - new_sent = [] - count_len = 0 - for ind, sent in enumerate(sentences): - # print(sent) - new_sent.append(sent) - count_len += len(sent.split(" ")) - if count_len > min_len or ind == len(sentences) - 1: - count_len = 0 - new_sentences.append(' '.join(new_sent)) - new_sent = [] - return merge_short_sentences_en(new_sentences) + # new_sentences = [] + # new_sent = [] + # count_len = 0 + # for ind, sent in enumerate(sentences): + # # print(sent) + # new_sent.append(sent) + # count_len += len(sent.split(" ")) + # if count_len > min_len or ind == len(sentences) - 1: + # count_len = 0 + # new_sentences.append(' '.join(new_sent)) + # new_sent = [] + # return merge_short_sentences_en(new_sentences) def split_sentences_zh(text, min_len=10): text = re.sub('[。!?;]', '.', text) @@ -127,4 +128,4 @@ if __name__ == '__main__': print(split_sentence(sp_text, language_str='SP')) print(split_sentence(fr_text, language_str='FR')) print(split_sentence(de_text, language_str='DE')) - print(split_sentence(ru_text, language_str='RU')) \ No newline at end of file + print(split_sentence(ru_text, language_str='RU')) diff --git a/requirements.txt b/requirements.txt index d661d8f..1ec5e6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ +txtsplit torch<2.0 torchaudio +cached_path transformers==4.27.4 mecab-python3==1.0.5 num2words==0.5.12 @@ -21,5 +23,6 @@ unidecode==1.3.7 pypinyin==0.50.0 cn2an==0.5.22 jieba==0.42.1 -gradio==3.48.0 -langid==1.1.6 \ No newline at end of file +gradio +langid==1.1.6 +tqdm \ No newline at end of file diff --git a/setup.py b/setup.py index c500bd3..5f0990e 100644 --- a/setup.py +++ b/setup.py @@ -21,11 +21,18 @@ class PostDevelopCommand(develop): setup( name='melo', - version='0.1.0', + version='0.1.1', packages=find_packages(), include_package_data=True, install_requires=requirements, package_data={ '': ['*.txt', 'cmudict_*'], }, + entry_points={ + "console_scripts": [ + "melotts = melo.main:main", + "melo = melo.main:main", + "melo-ui = melo.app:main", + ], + }, )