CosyVoice:阿里最强语音克隆模型体验并封装API接口
在中文TTS技术领域,最近一年涌现出众多优秀方案,如GPT-SoVITS
、ChatTTS
和Fish TTS
等,它们都以其出色的表现赢得了众多关注。但若论及实力,阿里巴巴无疑最强。其通义千问大模型在全球范围内都是顶尖的存在,而最新推出的CosyVoice,效果也非常棒,个人认为应属中文TTS顶流吧。
亲自体验了下CosyVoice,合成效果确实很不错。官方目前仅提供了Linux的安装指南,我特别为Windows用户撰写了一份详细的安装部署说明,并添加了一个API接口文件——api.py,以便更加便捷地使用CosyVoice。
CosyVoice开源地址 github.com/FunAudioLLM…
Modelscope在线体验 www.modelscope.cn/studios/iic…
支持 中文、英文、日语、韩语、粤语,对应语言代码分别是
zh|en|jp|ko|yue
本文主要内容有
- 安装部署 CosyVoice 官方项目
- CosyVoice的3种语音合成方式
- 在 WebUI 界面中使用
- 使用 API 服务
- API 使用注意问题
- 在pyVideoTrans中使用
安装部署 CosyVoice 官方项目
部署采用 conda,也强烈建议这种方式,否则可能无法成功安装,遇到的问题会非常多,有些依赖无法Wndows上使用pip是无法成功安装的,例如
pynini
1. 下载并安装miniconda
miniconda是一个conda管理软件,在windows上安装很方便,和普通软件一样一路next即可完成。
下载地址 docs.anaconda.com/miniconda/
下载完毕后双击 exe 文件,
需要注意的只有一点,在下图这个界面,需要选中上面2个复选框,否则后边操作会有点麻烦。 第二个框选中的意思是“将conda命令加入系统环境变量”,如果不选中,在使用 conda 命令时会报错 “conda不是内部或外部命令”。
然后点击 “install” 等待完成后close即可
2. 下载 CosyVoice源代码
先创建一个空目录,比如在D盘下建立一个文件夹 D:/py
,后续以此为例说明
打开CosyVoice开源地址 github.com/FunAudioLLM…
下载后解压,将其中CosyVoice-main目录内的所有文件复制到 D:/py中
3. 创建一个虚拟环境并激活
进入 D:/py 文件夹内,地址栏中输入cmd
然后回车,会打开一个cmd黑窗口
在该窗口中输入命令conda create -n cosyvoice python=3.10
然后回车,即创建一个名称为“cosyvoice”、python版本为“3.10”的虚拟环境,
继续输入命令conda activate cosyvoice
回车,即激活了该虚拟环境,只有激活后,才可继续进行安装、启动等操作,否则必然出错。
激活后的标志是命令行开头增加了“(cosyvoice)”字符
4. 安装 pynini
模块
该模块在windows下只有用conda命令才可安装,这也是开头建议windows上使用conda的原因。
继续在上面打开并激活环境的cmd窗口中输入命令 conda install -y -c conda-forge pynini==2.1.5 WeTextProcessing==1.0.3
回车
注意:安装中会出现一个要求输入确认的提示,此时输入y
然后回车,如下图
5. 安装其他一系列依赖,使用阿里镜像
打开
requirements.txt
文件,删掉最后一行的WeTextProcessing==1.0.3
,否则肯定是安装失败的,因为这个模块依赖pynini
,而pynini在windows的pip下无法安装然后在 requirements.txt 里增加一行
Matcha-TTS
如果要使用 api 服务,必须再增加2行内容,分别是
flask
和waitress
继续输入命令
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com
并回车,等待一段漫长的时间后,无意外即安装成功了。
CosyVoice的语音合成方式
CosyVoice主要有3种合成方式
- 单纯的将文字合成为语音,使用指定的预置发音角色(中文男、中文女、英文女、英文男、日语男、韩语女、粤语女),对应的预训练模型是
pretrained_models/CosyVoice-300M-SFT
, - 根据一个参考音频,克隆它的音色,然后使用该音色将文字合成为语音,该文字需要同参考音频发音语言一致,比如都是中文、都是英文等,
pretrained_models/CosyVoice-300M-SFT
- 同第二种方式一样,但此时是跨语言克隆,即参考音频发音语言和要合成的文字发音语言不同,比如参考音频是中文发音,但要合成的文字为英文。
- 根据自然语言指令生成语音(略)
此外还有更高级的使用方式,比如加入语气控制词、情感控制词、根据提示词自动生成相关语音等。这个暂不介绍了,有兴趣的可以去看看demo fun-audio-llm.github.io/
下载模型:第一次使用必须下载预训练模型,下载代码如下
from modelscope import snapshot_download snapshot_download('iic/CosyVoice-300M-SFT', local_dir='pretrained_models/CosyVoice-300M-SFT',cache_dir='pretrained_models/CosyVoice-300M') snapshot_download('iic/CosyVoice-300M', local_dir='pretrained_models/CosyVoice-300M',cache_dir='pretrained_models/CosyVoice-300M')
1. 单纯将文字合成为语音
先来看看第一种方式,也是最简单的,只需要要合成文本
和角色名
,核心代码
# 使用 CosyVoice-300M-SFT 模型
model = CosyVoice('pretrained_models/CosyVoice-300M-SFT')
#查看能使用的预置语音角色
print(model.list_avaliable_spks())
#使用 中文女 内置音色将文字合成为语音
output = model.inference_sft('你好,我是通义生成式语音大模型,请问有什么可以帮您的吗?', '中文女')
torchaudio.save('sft.wav', output['tts_speech'], 22050)
上面代码完成的工作:将文字使用“中文女”角色合成为音频“sft.wav”,执行完毕后,会看到文件夹内新生成了 sft.wav 文件,播放试试吧。
print(cosyvoice.list_avaliable_spks())
这行代码打印出所有可用预置角色
2. 克隆音色进行合成
这比上一种略微复杂一点点,除了合成文本外,还需要指定一个参考音频
和该音频对应的文本内容
,该音频采样最好是16k的,不过无需指定语音角色
。核心代码
# 使用pretrained_models/CosyVoice-300M 模型
model = CosyVoice('pretrained_models/CosyVoice-300M')
# 该参考音频文本内容是 “希望你以后能够做的比我还好呦”
prompt_speech_16k = load_wav('cankao.wav', 16000)
#克隆参考音频音色,将文字合成语音,第二个参数传递参考音频对应文本
output = model.inference_zero_shot('收到好友从远方寄来的生日礼物,那份意外的惊喜与深深的祝福让我心中充满了甜蜜的快乐,笑容如花儿般绽放。', '希望你以后能够做的比我还好呦。', prompt_speech_16k)
torchaudio.save('out.wav', output['tts_speech'], 22050)
3. 跨语言音色克隆合成
同上面一样,也是音色克隆,但不同的是待合成文本与参考音频语言不同,比如参考音频是中文发音,要合成英语文字。使用 inference_cross_lingual
方法,无需传入参考文本,但需要指定待合成文字的语言代码。
# 也是使用pretrained_models/CosyVoice-300M 模型
model = CosyVoice('pretrained_models/CosyVoice-300M')
# 参考音频是中文,内容是 “希望你以后能够做的比我还好呦”
prompt_speech_16k = load_wav('cankao.wav', 16000)
# 需要克隆参考音频音色将英文文字合成为语音,使用 inference_cross_lingual 方法,无需指定参考音频对应文本。 <|en|> 即为待合成文字的语言代码。
output = model.inference_cross_lingual('<|en|>And then later on, fully acquiring that company. So keeping management in line, interest in line with the asset that's coming into the family is a reason why sometimes we don't buy the whole thing.', prompt_speech_16k)
torchaudio.save('cross_lingual.wav', output['tts_speech'], 22050)
在 WebUI 界面中使用
CosyVoice 官方默认自带一个 WebUI 界面,可打开使用
输入命令执行
python webui.py --port 50000 --model_dir pretrained_models/CosyVoice-300M
等待窗口提示 “127.0.0.1:5000” 时,可在浏览器打开地址 http://127.0.0.1:5000
, 显示如下图
界面挺简单,对应上述3种合成方式外,还多了一个“自然语言控制”,本文中未对此介绍,这个就是文生音
,根据指令自动创建相关的语音,而不是严格根据输入的文字合成语音。
使用 API 服务
CosyVoice 没有提供api,笔者根据上文思路创建了一个 api.py
提供API接口服务,下方介绍如何使用 ,需先安装 flask waitress
这2个模块。
如果你没有在
requirements.txt
末尾添加flask
和waitress
,那么需要继续在上面使用的cmd窗口中输入命令安装模块pip install flask waitress
.
api.py
全部代码
import os,time,sys
from flask import Flask, request, render_template, jsonify, send_from_directory,send_file,Response, stream_with_context,make_response
import logging
from logging.handlers import RotatingFileHandler
from waitress import serve
import subprocess
import shutil
import datetime
from modelscope import snapshot_download
from cosyvoice.cli.cosyvoice import CosyVoice
from cosyvoice.utils.file_utils import load_wav
import torchaudio
from pathlib import Path
import base64
root_dir=Path(os.getcwd()).as_posix()
tmp_dir=Path(f'{root_dir}/tmp').as_posix()
logs_dir=Path(f'{root_dir}/logs').as_posix()
print(f'{root_dir=}')
print(f'{tmp_dir=}')
print(f'{logs_dir=}')
os.makedirs(tmp_dir,exist_ok=True)
os.makedirs(logs_dir,exist_ok=True)
os.makedirs(f'{root_dir}/pretrained_models',exist_ok=True)
if not os.path.exists('pretrained_models/CosyVoice-300M/cosyvoice.yaml') or not os.path.exists('pretrained_models/CosyVoice-300M-SFT/cosyvoice.yaml'):
snapshot_download('iic/CosyVoice-300M', cache_dir='pretrained_models/CosyVoice-300M',local_dir='pretrained_models/CosyVoice-300M')
snapshot_download('iic/CosyVoice-300M-SFT', cache_dir='pretrained_models/CosyVoice-300M-SFT',local_dir='pretrained_models/CosyVoice-300M-SFT')
# 预加载SFT模型
tts_model = CosyVoice('pretrained_models/CosyVoice-300M-SFT')
#tts_model = None
# 懒加载clone模型,在第一次克隆时加载
clone_model = None
#clone_model = CosyVoice('pretrained_models/CosyVoice-300M')
'''
app logs
'''
# 配置日志
# 禁用 Werkzeug 默认的日志处理器
log = logging.getLogger('werkzeug')
log.handlers[:] = []
log.setLevel(logging.WARNING)
app = Flask(__name__,
static_folder=root_dir+'/tmp',
static_url_path='/tmp')
root_log = logging.getLogger() # Flask的根日志记录器
root_log.handlers = []
root_log.setLevel(logging.WARNING)
app.logger.setLevel(logging.WARNING)
# 创建 RotatingFileHandler 对象,设置写入的文件路径和大小限制
file_handler = RotatingFileHandler(logs_dir+f'/{datetime.datetime.now().strftime("%Y%m%d")}.log', maxBytes=1024 * 1024, backupCount=5)
# 创建日志的格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 设置文件处理器的级别和格式
file_handler.setLevel(logging.WARNING)
file_handler.setFormatter(formatter)
# 将文件处理器添加到日志记录器中
app.logger.addHandler(file_handler)
def base64_to_wav(encoded_str, output_path):
if not encoded_str:
raise ValueError("Base64 encoded string is empty.")
# 将base64编码的字符串解码为字节
wav_bytes = base64.b64decode(encoded_str)
# 检查输出路径是否存在,如果不存在则创建
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# 将解码后的字节写入文件
with open(output_path, "wb") as wav_file:
wav_file.write(wav_bytes)
print(f"WAV file has been saved to {output_path}")
# 获取请求参数
def get_params(req):
params={
"text":"",
"lang":"",
"role":"中文女",
"reference_audio":None,
"reference_text":""
}
# 原始字符串
params['text'] = req.args.get("text","").strip() or req.form.get("text","").strip()
# 字符串语言代码
params['lang'] = req.args.get("lang","").strip().lower() or req.form.get("lang","").strip().lower()
# 兼容 ja语言代码
if params['lang']=='ja':
params['lang']='jp'
elif params['lang'][:2] == 'zh':
# 兼容 zh-cn zh-tw zh-hk
params['lang']='zh'
# 角色名
role = req.args.get("role","").strip() or req.form.get("role",'')
if role:
params['role']=role
# 要克隆的音色文件
params['reference_audio'] = req.args.get("reference_audio",None) or req.form.get("reference_audio",None)
encode=req.args.get('encode','') or req.form.get('encode','')
if encode=='base64':
tmp_name=f'tmp/{time.time()}-clone-{len(params["reference_audio"])}.wav'
base64_to_wav(params['reference_audio'],root_dir+'/'+tmp_name)
params['reference_audio']=tmp_name
# 音色文件对应文本
params['reference_text'] = req.args.get("reference_text",'').strip() or req.form.get("reference_text",'')
return params
# 实际批量合成完毕后连接为一个文件
def batch(tts_type,outname,params):
text=params['text'].strip().split("\n")
text=[t.replace("。",',') for t in text]
if len(text)>1 and not shutil.which("ffmpeg"):
raise Exception('多行文本合成必须安装 ffmpeg')
# 按行合成
out_list=[]
prompt_speech_16k=None
if tts_type!='tts':
if not params['reference_audio'] or not os.path.exists(f"{root_dir}/{params['reference_audio']}"):
raise Exception(f'参考音频未传入或不存在 {params["reference_audio"]}')
prompt_speech_16k = load_wav(params['reference_audio'], 16000)
for i,t in enumerate(text):
tmp_name=f"{tmp_dir}/{time.time()}-{i}-{tts_type}.wav"
print(f'{t=}\n{tmp_name=},\n{tts_type=}\n{params=}')
if tts_type=='tts':
# 仅文字合成语音
output = tts_model.inference_sft(t, params['role'])
print(output)
elif tts_type=='clone_eq':
# 同语言克隆
output=clone_model.inference_zero_shot(t,params['reference_text'], prompt_speech_16k)
else:
output = clone_model.inference_cross_lingual(f'<|{params["lang"]}|>{t}', prompt_speech_16k)
torchaudio.save(tmp_name, output['tts_speech'], 22050)
out_list.append(tmp_name)
if len(out_list)==1:
return out_list[0]
# 将 多个音频片段连接
txt_tmp="\n".join([f"file '{tmp_dir}/{it}'" for it in out_list])
txt_name=f'{time.time()}.txt'
with open(f'{tmp_dir}/{txt_name}','w',encoding='utf-8') as f:
f.write(txt_tmp)
try:
subprocess.run(["ffmpeg","-hide_banner", "-ignore_unknown","-y","-f","concat","-safe","0","-i",f'{tmp_dir}/{txt_name}',"-c:a","copy",tmp_dir + '/' + outname],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
check=True,
text=True,
creationflags=0 if sys.platform != 'win32' else subprocess.CREATE_NO_WINDOW)
return outname
except Exception as e:
print(e)
raise
# 单纯文字合成语音
@app.route('/tts', methods=['GET', 'POST'])
def tts():
params=get_params(request)
if not params['text']:
return make_response(jsonify({"code":1,"msg":'缺少待合成的文本'}), 500) # 设置状态码为500
try:
# 仅文字合成语音
outname=f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S-')}-tts.wav"
outname=batch(tts_type='tts',outname=outname,params=params)
except Exception as e:
print(e)
return make_response(jsonify({"code":2,"msg":str(e)}), 500) # 设置状态码为500
else:
return send_file(outname, mimetype='audio/x-wav')
# 同一种语言音色克隆
@app.route('/clone_eq', methods=['GET', 'POST'])
def clone_eq():
global clone_model
if not clone_model:
print('第一次克隆加载模型...')
clone_model = CosyVoice('pretrained_models/CosyVoice-300M')
try:
params=get_params(request)
if not params['text']:
return make_response(jsonify({"code":3,"msg":'缺少待合成的文本'}), 500) # 设置状态码为500
if not params['reference_text']:
return make_response(jsonify({"code":4,"msg":'必须设置参考音频对应的参考文本reference_text'}), 500) # 设置状态码为500
outname=f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S-')}-clone_eq.wav"
outname=batch(tts_type='clone_eq',outname=outname,params=params)
except Exception as e:
return make_response(jsonify({"code":5,"msg":str(e)}), 500) # 设置状态码为500
else:
return send_file(outname, mimetype='audio/x-wav')
# 跨语言文字合成语音
@app.route('/clone_mul', methods=['GET', 'POST'])
def clone_mul():
global clone_model
if not clone_model:
print('第一次克隆加载模型...')
clone_model = CosyVoice('pretrained_models/CosyVoice-300M')
try:
params=get_params(request)
if not params['text']:
return make_response(jsonify({"code":6,"msg":'缺少待合成的文本'}), 500) # 设置状态码为500
if not params['lang']:
return make_response(jsonify({"code":7,"msg":'必须设置待合成文本的语言代码'}), 500) # 设置状态码为500
outname=f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S-')}-clone_mul.wav"
outname=batch(tts_type='clone_mul',outname=outname,params=params)
except Exception as e:
return make_response(jsonify({"code":8,"msg":str(e)}), 500) # 设置状态码为500
else:
return send_file(outname, mimetype='audio/x-wav')
if __name__=='__main__':
host='127.0.0.1'
port=9233
print(f'\n启动api:http://{host}:{port}\n')
serve(app,host=host, port=port)
启动 API 服务
api接口地址为:
http://127.0.0.1:9233
输入命令回车执行 python api.py
单纯语音合成接口 /tts
api地址: {api url}/tts
参数:
text:待合成的文本
role:预置语音角色 "中文男|中文女|英文男|英文女|日语男|韩语女|粤语女" 选其一
请求示例
import requests
data={
"text":"你好啊亲爱的朋友们,今天天气不错,暴风骤雨哗哗的。",
"role":"中文女"
}
response=requests.post(f'http://127.0.0.1:9233/tts',data=data,timeout=3600)
if response.status_code!=200:
# 出错了
print(response.json())
else:
# 返回的wav数据流,可直接保存
with open("./tts.wav",'wb') as f:
f.write(response.content)
同语言克隆 /clone_eq
api地址: {api url}/clone_eq
参数:
text:待合成的文字
reference_audio:需要克隆音色的参考音频wav,5-10s最佳,必须是相对于项目根路径的wav音频路径,例如参考音频10.wav在项目根下,则直接传递10.wav即可,如果存在于 wavs 文件夹下,则需传递
wavs/10.wav
.也可将wav进行base64编码后直接传递
reference_text:该参考音频对应的文本内容
请求示例
import requests
data={
"text":"你好啊亲爱的朋友们,今天天气不错,暴风骤雨哗哗的。",
"reference_audio":"10.wav",
"reference_text":"参考音频10.wav对应的文本内容"
}
response=requests.post(f'http://127.0.0.1:9233/tts',data=data,timeout=3600)
if response.status_code!=200:
# 出错了
print(response.json())
else:
# 返回的wav数据流,可直接保存
with open("./clone_eq.wav",'wb') as f:
f.write(response.content)
跨语言克隆 /clone_mul
地址: {api url}/clone_mul
参数:
text:待克隆的文字
lang:text文字对应的语言代码 “zh|en|jp|ko|yue”
reference_audio:要克隆音色的参考音频wav,5-10s最佳,必须是相对于项目根路径的wav音频路径,例如参考音频10.wav在项目根下,则直接传递10.wav即可,如果存在于 wavs 文件夹下,则需传递
wavs/10.wav
.也可将wav进行base64编码后直接传递
请求示例:
import requests
data={
"text":"hello,my friend,I hope you a happy day.",
"lang":"en"
"reference_audio":"10.wav",
}
response=requests.post(f'http://127.0.0.1:9233/tts',data=data,timeout=3600)
if response.status_code!=200:
# 出错了
print(response.json())
else:
# 返回的wav数据流,可直接保存
with open("./clone_eq.wav",'wb') as f:
f.write(response.content)
API 使用注意问题
- 当一行文字中有句号
。
时,有时会发生只合成了句号之前的文字,句号之后的没有合成,为避免该问题,已自动将句号替换为了逗号,
。 - api.py 支持合成多行文字,例如1000行,前提是需要预先安装 ffmpeg,在实际合成中,将按行依次合成,然后再使用ffmpeg将所有合成片段连接为一个文件。
- 一行不要太长,否则合成很慢,效果也不佳。
- 使用api.py 需额外安装
flask
和waitress
这2个模块 - 预先只加载了用于单纯文字合成语音的模型,没有预加载克隆模型,第一次使用时才会加载模型,因此第一次使用克隆音色时可能比较慢。
在开源项目pyVideoTrans中使用
- 首先升级软件到2.08+ pyVideoTrans
- 确保已部署CosyVoice项目,且已将api.py放入,并成功启动了 api.py。
- 打开视频翻译软件,左上角设置--CosyVoice:填写 api 地址,默认是
http://127.0.0.1:9233
- 填写参考音频和音频对应文字
参考音频填写:
每行都由#符号分割为两部分,第一部分是wav音频路径,第二部分是该音频对应的文字内容,可填写多行。
wav音频最佳时长5-15s,如果音频放在了CosyVoice项目的根路径下,即webui.py同目录下,这里直接填写名称即可.
如果放在了根目录下的wavs目录下,那么需要填写 wavs/音频名称.wav
参考音频填写示例:
1.wav#你好啊亲爱的朋友
wavs/2.wav#你好啊朋友们
- 填写完毕后,主界面中配音渠道选择 CosyVoice, 角色选择对应的即可。其中 clone 角色是复制原视频中的音色
api.py 代码已开源到 github.com/jianchang51…
转载自:https://juejin.cn/post/7389946395066368035