likes
comments
collection
share

CosyVoice:阿里最强语音克隆模型体验并封装API接口

作者站长头像
站长
· 阅读数 35

在中文TTS技术领域,最近一年涌现出众多优秀方案,如GPT-SoVITSChatTTSFish 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

本文主要内容有

  1. 安装部署 CosyVoice 官方项目
  2. CosyVoice的3种语音合成方式
  3. 在 WebUI 界面中使用
  4. 使用 API 服务
  5. API 使用注意问题
  6. 在pyVideoTrans中使用

安装部署 CosyVoice 官方项目

部署采用 conda,也强烈建议这种方式,否则可能无法成功安装,遇到的问题会非常多,有些依赖无法Wndows上使用pip是无法成功安装的,例如 pynini

1. 下载并安装miniconda

miniconda是一个conda管理软件,在windows上安装很方便,和普通软件一样一路next即可完成。

下载地址 docs.anaconda.com/miniconda/

CosyVoice:阿里最强语音克隆模型体验并封装API接口

下载完毕后双击 exe 文件,

CosyVoice:阿里最强语音克隆模型体验并封装API接口

CosyVoice:阿里最强语音克隆模型体验并封装API接口

需要注意的只有一点,在下图这个界面,需要选中上面2个复选框,否则后边操作会有点麻烦。 第二个框选中的意思是“将conda命令加入系统环境变量”,如果不选中,在使用 conda 命令时会报错 “conda不是内部或外部命令”。

CosyVoice:阿里最强语音克隆模型体验并封装API接口

然后点击 “install” 等待完成后close即可

2. 下载 CosyVoice源代码

先创建一个空目录,比如在D盘下建立一个文件夹 D:/py,后续以此为例说明

打开CosyVoice开源地址 github.com/FunAudioLLM…

CosyVoice:阿里最强语音克隆模型体验并封装API接口

下载后解压,将其中CosyVoice-main目录内的所有文件复制到 D:/py中

CosyVoice:阿里最强语音克隆模型体验并封装API接口

CosyVoice:阿里最强语音克隆模型体验并封装API接口

3. 创建一个虚拟环境并激活

进入 D:/py 文件夹内,地址栏中输入cmd然后回车,会打开一个cmd黑窗口

CosyVoice:阿里最强语音克隆模型体验并封装API接口

在该窗口中输入命令conda create -n cosyvoice python=3.10 然后回车,即创建一个名称为“cosyvoice”、python版本为“3.10”的虚拟环境,

CosyVoice:阿里最强语音克隆模型体验并封装API接口

继续输入命令conda activate cosyvoice回车,即激活了该虚拟环境,只有激活后,才可继续进行安装、启动等操作,否则必然出错。

CosyVoice:阿里最强语音克隆模型体验并封装API接口

激活后的标志是命令行开头增加了“(cosyvoice)”字符

CosyVoice:阿里最强语音克隆模型体验并封装API接口

4. 安装 pynini 模块

该模块在windows下只有用conda命令才可安装,这也是开头建议windows上使用conda的原因。

继续在上面打开并激活环境的cmd窗口中输入命令 conda install -y -c conda-forge pynini==2.1.5 WeTextProcessing==1.0.3 回车

CosyVoice:阿里最强语音克隆模型体验并封装API接口

注意:安装中会出现一个要求输入确认的提示,此时输入y然后回车,如下图

CosyVoice:阿里最强语音克隆模型体验并封装API接口

5. 安装其他一系列依赖,使用阿里镜像

打开requirements.txt文件,删掉最后一行的WeTextProcessing==1.0.3,否则肯定是安装失败的,因为这个模块依赖 pynini,而pynini在windows的pip下无法安装

然后在 requirements.txt 里增加一行 Matcha-TTS

如果要使用 api 服务,必须再增加2行内容,分别是 flaskwaitress

CosyVoice:阿里最强语音克隆模型体验并封装API接口

继续输入命令

pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com

并回车,等待一段漫长的时间后,无意外即安装成功了。

CosyVoice的语音合成方式

CosyVoice主要有3种合成方式

  1. 单纯的将文字合成为语音,使用指定的预置发音角色(中文男、中文女、英文女、英文男、日语男、韩语女、粤语女),对应的预训练模型是pretrained_models/CosyVoice-300M-SFT,
  2. 根据一个参考音频,克隆它的音色,然后使用该音色将文字合成为语音,该文字需要同参考音频发音语言一致,比如都是中文、都是英文等,pretrained_models/CosyVoice-300M-SFT
  3. 同第二种方式一样,但此时是跨语言克隆,即参考音频发音语言和要合成的文字发音语言不同,比如参考音频是中文发音,但要合成的文字为英文。
  4. 根据自然语言指令生成语音(略)

此外还有更高级的使用方式,比如加入语气控制词、情感控制词、根据提示词自动生成相关语音等。这个暂不介绍了,有兴趣的可以去看看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, 显示如下图

CosyVoice:阿里最强语音克隆模型体验并封装API接口

界面挺简单,对应上述3种合成方式外,还多了一个“自然语言控制”,本文中未对此介绍,这个就是文生音,根据指令自动创建相关的语音,而不是严格根据输入的文字合成语音。

使用 API 服务

CosyVoice 没有提供api,笔者根据上文思路创建了一个 api.py提供API接口服务,下方介绍如何使用 ,需先安装 flask waitress 这2个模块。

如果你没有在 requirements.txt 末尾添加flaskwaitress,那么需要继续在上面使用的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

CosyVoice:阿里最强语音克隆模型体验并封装API接口

单纯语音合成接口 /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 使用注意问题

  1. 当一行文字中有句号时,有时会发生只合成了句号之前的文字,句号之后的没有合成,为避免该问题,已自动将句号替换为了逗号
  2. api.py 支持合成多行文字,例如1000行,前提是需要预先安装 ffmpeg,在实际合成中,将按行依次合成,然后再使用ffmpeg将所有合成片段连接为一个文件。
  3. 一行不要太长,否则合成很慢,效果也不佳。
  4. 使用api.py 需额外安装 flaskwaitress这2个模块
  5. 预先只加载了用于单纯文字合成语音的模型,没有预加载克隆模型,第一次使用时才会加载模型,因此第一次使用克隆音色时可能比较慢。

在开源项目pyVideoTrans中使用

  1. 首先升级软件到2.08+ pyVideoTrans
  2. 确保已部署CosyVoice项目,且已将api.py放入,并成功启动了 api.py。
  3. 打开视频翻译软件,左上角设置--CosyVoice:填写 api 地址,默认是 http://127.0.0.1:9233
  4. 填写参考音频和音频对应文字
参考音频填写:

每行都由#符号分割为两部分,第一部分是wav音频路径,第二部分是该音频对应的文字内容,可填写多行。

wav音频最佳时长5-15s,如果音频放在了CosyVoice项目的根路径下,即webui.py同目录下,这里直接填写名称即可.
如果放在了根目录下的wavs目录下,那么需要填写 wavs/音频名称.wav

参考音频填写示例:

1.wav#你好啊亲爱的朋友
wavs/2.wav#你好啊朋友们

  1. 填写完毕后,主界面中配音渠道选择 CosyVoice, 角色选择对应的即可。其中 clone 角色是复制原视频中的音色

CosyVoice:阿里最强语音克隆模型体验并封装API接口

api.py 代码已开源到 github.com/jianchang51…

转载自:https://juejin.cn/post/7389946395066368035
评论
请登录