Cosyvoice-api 代码分析
发布日期:2025/5/11 23:47:20 浏览量:
该代码用于 CosyVoice2 的 api 文件,部署好 cosyVoice 项目后,将该 api.py 文件同 webui.py放在一起,然后执行 python api.py。
如果是三方整合包,将 api.py 同 bat 脚本放在一起,然后查找其中python.exe所在的位置,在bat所在当前文件夹地址栏中输入cmd回车,然后执行 目录/python.exe api.py
如果执行时提示module flask not found,请执行 python.exe -m pip install flask 安装
根据内置角色合成文字
-
接口地址: /tts
-
单纯将文字合成语音,不进行音色克隆
-
必须设置的参数:
text:需要合成语音的文字
role: ’中文女’, ’中文男’, ’日语男’, ’粤语女’, ’英文女’, ’英文男’, ’韩语女’ 选择一个
-
成功返回:wav音频数据
-
示例代码
data={
"text":"你好啊亲爱的朋友们",
"reference_audio":"10.wav"
}
response=requests.post(f’http://127.0.0.1:9933/tts’,data=data,timeout=3600)
同语言克隆音色合成
- 地址:/clone_eq
参考音频发音语言和需要合成的文字语言一致,例如参考音频是中文发音,同时需要根据该音频将中文文本合成为语音
- 必须设置参数:
text: 需要合成语音的文字
reference_audio:需要克隆音色的参考音频
reference_text:参考音频对应的文字内容 参考音频相对于 api.py 的路径,例如引用1.wav,该文件和api.py在同一文件夹内,则填写 1.wav
-
成功返回:wav数据
-
示例代码
data={
"text":"你好啊亲爱的朋友们。",
"reference_audio":"10.wav",
"reference_text":"希望你过的比我更好哟。"
}
response=requests.post(f’http://127.0.0.1:9933/tts’,data=data,timeout=3600)
不同语言音色克隆:
- 地址: /cone
参考音频发音语言和需要合成的文字语言不一致,例如需要根据中文发音的参考音频,将一段英文文本合成为语音。
- 必须设置参数:
text: 需要合成语音的文字
reference_audio:需要克隆音色的参考音频 参考音频相对于 api.py 的路径,例如引用1.wav,该文件和api.py在同一文件夹内,则填写 1.wav
-
成功返回:wav数据
-
示例代码
data={
"text":"親友からの誕生日プレゼントを遠くから受け取り、思いがけないサプライズと深い祝福に、私の心は甘い喜びで満たされた!。",
"reference_audio":"10.wav"
}
response=requests.post(f’http://127.0.0.1:9933/tts’,data=data,timeout=3600)
兼容openai tts
- 接口地址 /v1/audio/speech
- 请求方法 POST
- 请求类型 Content-Type: application/json
- 请求参数 input: 要合成的文字 model: 固定 tts-1, 兼容openai参数,实际未使用 speed: 语速,默认1.0 reponse_format:返回格式,固定wav音频数据 voice: 仅用于文字合成时,取其一 ’中文女’, ’中文男’, ’日语男’, ’粤语女’, ’英文女’, ’英文男’, ’韩语女’
用于克隆时,填写引用的参考音频相对于 api.py 的路径,例如引用1.wav,该文件和api.py在同一文件夹内,则填写 1.wav
- 示例代码
from openai import OpenAI
client = OpenAI(api_key=’12314’, base_url=’http://127.0.0.1:9933/v1’)
with client.audio.speech.with_streaming_response.create(
model=’tts-1’,
voice=’中文女’,
input=’你好啊,亲爱的朋友们’,
speed=1.0
) as response:
with open(’./test.wav’, ’wb’) as f:
for chunk in response.iter_bytes():
f.write(chunk)
####################################################
import os,time,sys
from pathlib import Path
root_dir=Path(__file__).parent.as_posix()
# ffmpeg
if sys.platform == ’win32’:
os.environ[’PATH’] = root_dir + f’;{root_dir}\\ffmpeg;’ + os.environ[’PATH’]+f’;{root_dir}/third_party/Matcha-TTS’
else:
os.environ[’PATH’] = root_dir + f’:{root_dir}/ffmpeg:’ + os.environ[’PATH’]
os.environ[’PYTHONPATH’] = os.environ.get(’PYTHONPATH’, ’’) + ’:third_party/Matcha-TTS’
sys.path.append(f’{root_dir}/third_party/Matcha-TTS’)
tmp_dir=Path(f’{root_dir}/tmp’).as_posix()
logs_dir=Path(f’{root_dir}/logs’).as_posix()
os.makedirs(tmp_dir,exist_ok=True)
os.makedirs(logs_dir,exist_ok=True)
from flask import Flask, request, render_template, jsonify, send_from_directory,send_file,Response, stream_with_context,make_response,send_file
import logging
from logging.handlers import RotatingFileHandler
import subprocess
import shutil
import datetime
from cosyvoice.cli.cosyvoice import CosyVoice, CosyVoice2
from cosyvoice.utils.file_utils import load_wav
import torchaudio,torch
from pathlib import Path
import base64
# 下载模型
from modelscope import snapshot_download
snapshot_download(’iic/CosyVoice2-0.5B’, local_dir=’pretrained_models/CosyVoice2-0.5B’)
snapshot_download(’iic/CosyVoice-300M-SFT’, local_dir=’pretrained_models/CosyVoice-300M-SFT’)
’’’
app logs
’’’
# 配置日志
# 禁用 Werkzeug 默认的日志处理器
log = logging.getLogger(’werkzeug’)
log.handlers[:] = []
log.setLevel(logging.WARNING)
root_log = logging.getLogger() # Flask的根日志记录器
root_log.handlers = []
root_log.setLevel(logging.WARNING)
app = Flask(__name__,
static_folder=root_dir+’/tmp’,
static_url_path=’/tmp’)
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)
sft_model = None
tts_model = None
VOICE_LIST=[’中文女’, ’中文男’, ’日语男’, ’粤语女’, ’英文女’, ’英文男’, ’韩语女’]
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":"",
"speed":1.0
}
# 原始字符串
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 del_tmp_files(tmp_files: list):
print(’正在删除缓存文件...’)
for f in tmp_files:
if os.path.exists(f):
print(’删除缓存文件:’, f)
os.remove(f)
# 实际批量合成完毕后连接为一个文件
def batch(tts_type,outname,params):
global sft_model,tts_model
if not shutil.which("ffmpeg"):
raise Exception(’必须安装 ffmpeg’)
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"]}’)
ref_audio=f"{tmp_dir}/-refaudio-{time.time()}.wav"
try:
subprocess.run(["ffmpeg","-hide_banner", "-ignore_unknown","-y","-i",params[’reference_audio’],"-ar","16000",ref_audio],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
check=True,
text=True,
creationflags=0 if sys.platform != ’win32’ else subprocess.CREATE_NO_WINDOW)
except Exception as e:
raise Exception(f’处理参考音频失败:{e}’)
prompt_speech_16k = load_wav(ref_audio, 16000)
text=params[’text’]
audio_list=[]
if tts_type==’tts’:
if sft_model is None:
sft_model = CosyVoice(’pretrained_models/CosyVoice-300M-SFT’, load_jit=True, load_onnx=False)
# 仅文字合成语音
for i, j in enumerate(sft_model.inference_sft(text, params[’role’],stream=False,speed=params[’speed’])):
audio_list.append(j[’tts_speech’])
elif tts_type==’clone_eq’ and params.get(’reference_text’):
if tts_model is None:
tts_model=CosyVoice2(’pretrained_models/CosyVoice2-0.5B’, load_jit=True, load_onnx=False, load_trt=False)
for i, j in enumerate(tts_model.inference_zero_shot(text,params.get(’reference_text’),prompt_speech_16k, stream=False,speed=params[’speed’])):
audio_list.append(j[’tts_speech’])
else:
if tts_model is None:
tts_model=CosyVoice2(’pretrained_models/CosyVoice2-0.5B’, load_jit=True, load_onnx=False, load_trt=False)
for i, j in enumerate(tts_model.inference_cross_lingual(text,prompt_speech_16k, stream=False,speed=params[’speed’])):
audio_list.append(j[’tts_speech’])
audio_data = torch.concat(audio_list, dim=1)
# 根据模型yaml配置设置采样率
if tts_type==’tts’:
torchaudio.save(tmp_dir + ’/’ + outname,audio_data, 22050, format="wav")
elif tts_type==’clone_eq’:
torchaudio.save(tmp_dir + ’/’ + outname,audio_data, 24000, format="wav")
else:
torchaudio.save(tmp_dir + ’/’ + outname,audio_data, 24000, format="wav")
print(f"音频文件生成成功:{tmp_dir}/{outname}")
return tmp_dir + ’/’ + outname
# 单纯文字合成语音
@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"tts-{datetime.datetime.now().strftime(’%Y%m%d-%H%M%S-’)}.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_mul’, methods=[’GET’, ’POST’])
@app.route(’/clone’, methods=[’GET’, ’POST’])
def clone():
try:
params=get_params(request)
if not params[’text’]:
return make_response(jsonify({"code":6,"msg":’缺少待合成的文本’}), 500) # 设置状态码为500
outname=f"clone-{datetime.datetime.now().strftime(’%Y%m%d-%H%M%S-’)}.wav"
outname=batch(tts_type=’clone’,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’)
@app.route(’/clone_eq’, methods=[’GET’, ’POST’])
def clone_eq():
try:
params=get_params(request)
if not params[’text’]:
return make_response(jsonify({"code":6,"msg":’缺少待合成的文本’}), 500) # 设置状态码为500
if not params[’reference_text’]:
return make_response(jsonify({"code":6,"msg":’同语言克隆必须传递引用文本’}), 500) # 设置状态码为500
outname=f"clone-{datetime.datetime.now().strftime(’%Y%m%d-%H%M%S-’)}.wav"
outname=batch(tts_type=’clone_eq’,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’)
@app.route(’/v1/audio/speech’, methods=[’POST’])
def audio_speech():
"""
兼容 OpenAI /v1/audio/speech API 的接口
"""
import random
if not request.is_json:
return jsonify({"error": "请求必须是 JSON 格式"}), 400
data = request.get_json()
# 检查请求中是否包含必要的参数
if ’input’ not in data or ’voice’ not in data:
return jsonify({"error": "请求缺少必要的参数: input, voice"}), 400
text = data.get(’input’)
speed = float(data.get(’speed’,1.0))
voice = data.get(’voice’,’中文女’)
params = {}
params[’text’]=text
params[’speed’]=speed
api_name=’tts’
if voice in VOICE_LIST:
params[’role’]=voice
elif Path(voice).exists() or Path(f’{root_dir}/{voice}’).exists():
api_name=’clone’
params[’reference_audio’]=voice
else:
return jsonify({"error": {"message": f"必须填写配音角色名或参考音频路径", "type": e.__class__.__name__, "param": f’speed={speed},voice={voice},input={text}’, "code": 400}}), 500
filename=f’openai-{len(text)}-{speed}-{time.time()}-{random.randint(1000,99999)}.wav’
try:
outname=batch(tts_type=api_name,outname=filename,params=params)
return send_file(outname, mimetype=’audio/x-wav’)
except Exception as e:
return jsonify({"error": {"message": f"{e}", "type": e.__class__.__name__, "param": f’speed={speed},voice={voice},input={text}’, "code": 400}}), 500
if __name__==’__main__’:
host=’127.0.0.1’
port=50000
print(f’\n启动api: http://{host}:{port}\n’)
try:
from waitress import serve
except Exception:
app.run(host=host, port=port)
else:
serve(app,host=host, port=port)
马上咨询: 如果您有业务方面的问题或者需求,欢迎您咨询!我们带来的不仅仅是技术,还有行业经验积累。
QQ: 39764417/308460098 Phone: 13 9800 1 9844 / 135 6887 9550 联系人:石先生/雷先生