如何在本地部署ChatTTS?

前言

最近,24-05-27号,github上出现了一个新项目,ChatTTS。该项目提供了一个文本转语音(Text To Speech)的开源方案,同时支持中文和英文。在官网的演示视频中,可以看到合成效果高度接近真人。

到目前(06-04)为止,已经有18.3k的star。

那我们就来看看这个模型的基本部署和使用方法吧。

环境说明

Ubuntu22.04,python版本为默认的3.8.10

安装pip和换清华源

sudo apt install pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

安装mpv用于音频播放

sudo apt install mpv

本地安装

下载modelscope和SDK模型

pip install modelscope
from modelscope import snapshot_download
model_dir = snapshot_download(pzc163/chatTTS')

获取源码并安装依赖

git clone https://github.com/2noise/ChatTTS.git
cd ChatTTS
pip install -r requirement.txt

测试脚本

官网给出的版本会提示torchaudio的相关错误,这里采用材料4的方式,在ChatTTS文件夹下新建py文件并执行:

import ChatTTS
from IPython.display import Audio
from modelscope import snapshot_download


# 注意your-user-path换成你自己的用户目录,这个参数意思是你的本地模型下载目录
model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"



chat = ChatTTS.Chat()

# compile设置为True可以获得更好的表现
# source 和 local_path 是对本地部署的设置
chat.load_models(compile=True, source='local', local_path=model_dir)
rand_spk = chat.sample_random_speaker()


params_infer_code = {
          'spk_emb': rand_spk, # add sampled speaker 
            'temperature': .3, # using custom temperature
              'top_P': 0.7, # top P decode
                'top_K': 20, # top K decode
                }

params_refine_text = {
          'prompt': '[oral_2][laugh_2][break_6]'
          }



# 你要转换的文本
texts = ["幸福是把灵魂安放在最适当的位置。",]

wavs = chat.infer(texts, skip_refine_text=True, params_infer_code=params_infer_code, use_decoder=True)
Audio(wavs[0], rate=24_000, autoplay=True)

#保存Audio
import soundfile as sf
audio_data = wavs[0]
if len(audio_data.shape) > 1:
        audio_data = audio_data.flatten()

        output_file='./output_audio2.wav'
        sf.write(output_file, audio_data, 24000)

注意model_path换成你自己的用户目录。如果提示缺少其他依赖,根据错误提示用pip安装即可。

编译好的音频用mpv进行播放即可。

mpv output_audio2.wav

使用

笑声和停顿的控制

基本形式

chat.infer()函数中,确保use_decoder=True

然后在文本中,[uv_break][lbreak]表示停顿;[laugh]表示笑声。

params_refine_text 中可以进一步配置oral``breaklaugh的类型。

在默认值中,0是关闭的意思。

注意oral指定语气助词,不为零会出现“那个”之类的语气助词。

另外还有两个问题:

  1. 不支持分号、问号和省略号等符号。

  2. 对“地”作为“de”的发音并不支持。

多音调的测试

给出一个测试脚本:

from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import csv
import soundfile as sf
model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()

chat.load_models(compile=True, source='local', local_path=model_dir)

torch.manual_seed(2215)
rand_spk = chat.sample_random_speaker()

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .001, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

params_refine_text = {
        'prompt': '[oral_2][laugh_1][break_1]'
} 

text = "幸福是把[uv_break]灵魂安放在最合适[laugh]的位置。"

test_example = []
oral = 10
laugh = 3
lbreak = 8
for i in range(oral):
    for j in range(laugh):
        for k in range(lbreak):
            row = [i, j, k]
            test_example.append(row)
#print(test_example)

for i in test_example:
  params_refine_text = {
        'prompt': '[oral_' + str(i[0]) + '][laugh_' + str(i[1]) + '][break_' + str(i[2]) + ']'
  } 

  wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

  output_file = '/<your-user-path>/data/tts_test/n/fl-2215-' + str(i[0]) + str(i[1]) + str(i[2]) + '.wav'

  audio_data = wav[0]
  if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()

    sf.write(output_file, audio_data, 24000)
    print(f"Audio saved to {output_file}")

音色固定问题

方法一、保存张量

在材料五中可以看到。本质上是把rand_spk 随机生成的值进行了保存。具体的代码实现这里给出两个:

保存代码:

from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import csv
import soundfile as sf

model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()

chat.load_models(compile=True, source='local', local_path=model_dir)

torch.manual_seed(0)
rand_spk = chat.sample_random_speaker()

def writeToCsv(csv_file_path,data):
  with open(csv_file_path, mode='w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(data.tolist())

writeToCsv(f"saved.csv",rand_spk.detach().numpy())

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .001, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

# use oral_(0-9), laugh_(0-2), break_(0-7) 
# to generate special token in text to synthesize.
params_refine_text = {
        'prompt': '[oral_0][laugh_0][break_7]'
} 


text = "幸福是[uv_break]把灵魂安放在最合适的位置。"


wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

audio_data = wav[0]
if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()

output_file = './output_audio2.wav'
sf.write(output_file, audio_data, 24000)
print(f"Audio saved to {output_file}")

加载代码:

from ChatTTS import Chat
from IPython.display import Audio
from modelscope import snapshot_download
import ChatTTS
import torch
import csv
import pandas as pd

model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"
chat = ChatTTS.Chat()

chat.load_models(compile=True, source='local', local_path=model_dir)

data = pd.read_csv(f"./saved/f-8.csv", header=None)
rand_spk = torch.tensor(data.iloc[0], dtype=torch.float32)

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .1, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

params_refine_text = {
  'prompt': '[oral_1][laugh_1][break_1]'
} 

with open('./test.txt', 'r') as file:
    text = file.readlines()

wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

import soundfile as sf
audio_data = wav[0]
if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()

#记得换成自己的目录
output_file = '/<your-user-path>/data/tts_test/output_audio2.wav'
sf.write(output_file, audio_data, 24000)
print(f"Audio saved to {output_file}")

方法二、设置种子值(推荐)

核心命令在于通过torch.manual_seed来设置种子值:

torch.manual_seed(seed)

其中seed应该是一个整数。

那么我们就可以得到一个根据种子固定音色的通用代码:

import ChatTTS
from IPython.display import Audio
from modelscope import snapshot_download


# 注意your-user-path换成你自己的用户目录,这个参数意思是你的本地模型下载目录
model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"


#seed应该设置为一个整数。
torch.manual_seed(seed)

chat = ChatTTS.Chat()

# compile设置为True可以获得更好的表现
# source 和 local_path 是对本地部署的设置
chat.load_models(compile=True, source='local', local_path=model_dir)
rand_spk = chat.sample_random_speaker()


params_infer_code = {
          'spk_emb': rand_spk, # add sampled speaker 
            'temperature': .3, # using custom temperature
              'top_P': 0.7, # top P decode
                'top_K': 20, # top K decode
                }

params_refine_text = {
          'prompt': '[oral_2][laugh_2][break_6]'
          }



# 你要转换的文本
texts = ["幸福是把灵魂安放在最适当的位置。",]

wavs = chat.infer(texts, skip_refine_text=True, params_infer_code=params_infer_code, use_decoder=True)
Audio(wavs[0], rate=24_000, autoplay=True)

#保存Audio
import soundfile as sf
audio_data = wavs[0]
if len(audio_data.shape) > 1:
        audio_data = audio_data.flatten()

        output_file='./output_audio2.wav'
        sf.write(output_file, audio_data, 24000)

注意把模型目录换成自己的,并且给seed一个初始值。

多种子的测试

给出一个测试1-999种子的代码,如果需要,改变for循环的值即可:

from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import csv
import soundfile as sf
model_dir = "/<your-home-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()

chat.load_models(compile=True, source='local', local_path=model_dir)


params_refine_text = {
        'prompt': '[oral_0][laugh_1][break_7]'
} 

text = "幸福是把[uv_break]灵魂安放在最合适[laugh]的位置。"

for seed in range(1, 1000):
  torch.manual_seed(seed)
  rand_spk = chat.sample_random_speaker()
  params_infer_code = {
    'spk_emb': rand_spk, # add sampled speaker 
    'temperature': .001, # using custom temperature
    'top_P': 0.7, # top P decode
    'top_K': 20, # top K decode

  }
  wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

  output_file = '/<your-home-path>/data/tts_test/seed/' + str(seed) + '.wav'
  #save audio
  audio_data = wav[0]
  if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()
    sf.write(output_file, audio_data, 24000)
    print(f"Audio saved to {output_file}")

文本的切割合成

ChatTTS本身不适合长文本的生成。如果要生成长文本,可以通过切割文本分别生成音频文件再加以连接。

这里给出实现代码。

单句合成代码

版本一:给出种子,文本和目标路径进行合成

### 本文件用于tts的行合成。接受三个参数。第一个是种子值,为int数字。第二个为合成音频的文本。第三个为目标文件路径。

from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import sys
import soundfile as sf

args=sys.argv

seed=int(args[1])
text=args[2]
aim_path=args[3]

model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()
chat.load_models(compile=True, source='local', local_path=model_dir)

torch.manual_seed(seed)
rand_spk = chat.sample_random_speaker()

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .3, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

# use oral_(0-9), laugh_(0-2), break_(0-7) 
# to generate special token in text to synthesize.
params_refine_text = {
        'prompt': '[oral_0][laugh_0][break_7]'
} 

wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

audio_data = wav[0]
if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()

sf.write(aim_path, audio_data, 24000)
print(f"Audio saved to {aim_path}")

版本二:给出文件,源文件路径和目标路径进行音频合成。

### 本文件用于tts的行合成。接受三个参数。第一个是种子值,为int数字。第二个为源文件路径。第三个为目标文件路径。

from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import sys
import soundfile as sf

args=sys.argv

seed=int(args[1])
source_path=args[2]
aim_path=args[3]

model_dir = "/<your-user-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()
chat.load_models(compile=True, source='local', local_path=model_dir)

torch.manual_seed(seed)
rand_spk = chat.sample_random_speaker()

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .3, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

# use oral_(0-9), laugh_(0-2), break_(0-7) 
# to generate special token in text to synthesize.
params_refine_text = {
        'prompt': '[oral_0][laugh_0][break_7]'
}

with open(source_path, 'r') as file:
    text = file.readlines()

wav = chat.infer(text,params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)

audio_data = wav[0]
if len(audio_data.shape) > 1:
    audio_data = audio_data.flatten()

sf.write(aim_path, audio_data, 24000)
print(f"Audio saved to {aim_path}")

切割文本合成

文本切割通过readlines函数进行操作即可。而音频合成则可以通过pydub中的AudioSegment模块来实现。而对于每一行之间的间隔,可以用sox命令生成一段0.5s的空白:

sox -n -r 44100 -c 2 silence-0.5s.wav trim 0 0.5
### 本文件用于tts的行合成。接受三个参数。第一个是种子值,为int数字。第二个为源文件路径。第三个为目标文件夹路径。
from ChatTTS import Chat
from IPython.display import Audio
import ChatTTS
import torch
import sys
import soundfile as sf
from pydub import AudioSegment

args=sys.argv

seed=int(args[1])

source_path=args[2]

tmp_path = "/<your-home-path>/data/tmp"
aim_path=args[3]

model_dir = "/<your-home-path>/.cache/modelscope/hub/pzc163/chatTTS"

chat = ChatTTS.Chat()

chat.load_models(compile=True, source='local', local_path=model_dir)

torch.manual_seed(seed)
rand_spk = chat.sample_random_speaker()

params_infer_code = {
  'spk_emb': rand_spk, # add sampled speaker 
  'temperature': .3, # using custom temperature
  'top_P': 0.7, # top P decode
  'top_K': 20, # top K decode

}

params_refine_text = {
        'prompt': '[oral_0][laugh_0][break_7]'
} 

with open(source_path, 'r') as file:
    text = file.readlines()

i=0
for row in text:
    i+=1
    wav = chat.infer(row.replace("\n",""),params_infer_code=params_infer_code, params_refine_text=params_refine_text, use_decoder=True)
    tmp_file=tmp_path + "/line-" + str(i) + ".wav"
    audio_data = wav[0]
    if len(audio_data.shape) > 1:
        audio_data = audio_data.flatten()
        sf.write(tmp_file, audio_data, 24000)
        print(f"Audio saved to {tmp_file}")


j=0

audio_silence = AudioSegment.from_file("/<your-home-path>/data/tts_other/silence-0.5s.wav", format="wav")

audio = AudioSegment.from_file("/<your-home-path>/data/tts_other/silence-0.5s.wav", format="wav")
for x in range(1, i+1):
    j+=1
    audio_now = AudioSegment.from_file(tmp_path + "/line-" + str(j) + ".wav", format="wav")
    audio = audio + audio_silence + audio_now

audio = audio + audio_silence
audio.export(aim_path, format="wav")

其中为了保存文本生成结果用到了tmp文件夹。并且在生成之后没有删除中间文本。有需要可以增加删除命令。

使用方式还是,第一个参数,种子值;第二个参数,原文本文档;第三个参数,目标路径;

bash和alias的快捷命令

~/src/mybash中增加bash脚本:

#!bin/bash
python3 ~/src/chattts/ChatTTS/text-trans.py $1 $2 $3

~/.bashrc中增加如下行:

alias text-trans='bash ~/src/mybash/text-trans.sh'

应用配置

source ~/.bashrc

以后就有了一个基于ChatTTS进行文本切割合成的命令行工具:

text-trans <seed> <source-file> <aim-file>

添加背景音

通过ffmpeg命令可以对音频进行合成,为合成的语音增加背景音乐。

用法如下:

ffmpeg -i background.mp3 -i foreground.wav -filter_complex amix=inputs=2:duration=shortest output.wav

如果生成的wav文件过大,可以用lame压缩为mp3文件。

lame input.wav output.mp3

结语

本文介绍了ChatTTS的本地化部署方式,并对文本的切割合成和音色的固定,以及背景音的增加进行了相应介绍。对于ChatTTS,未来可以考虑更多的应用场景。

参考

posted @ 2024-06-05 21:40  Laziko  阅读(1159)  评论(0编辑  收藏  举报