feat: complete local version to overwrite remote

This commit is contained in:
DelLevin-Home
2026-06-16 03:30:57 +08:00
parent 1735c19f48
commit 3c78293f4d
129 changed files with 22814 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
# -*- coding: utf-8 -*-
"""
sovits-tts GPT-SoVITS v2 配音蓝图
代理转发请求到 GPT-SoVITS v2 API 服务
"""
import os
import json
import glob
import platform
import subprocess
import requests
from flask import Blueprint, render_template, request, jsonify, Response
bp = Blueprint('sovits_tts', __name__, url_prefix='/sovits-tts')
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'sovits_config.json')
try:
from config import BASE_DIR
CONFIG_PATH = os.path.join(BASE_DIR, 'config', 'sovits_config.json')
except ImportError:
pass
_sovits_proc = None
@bp.route('/')
def page():
return render_template('sovits_tts.html')
def _load_config():
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
return json.load(f)
return {
'api_url': 'http://127.0.0.1:9880',
'gpt_model_dir': '', 'sovits_model_dir': '',
'last_gpt_model': '', 'last_sovits_model': '',
'sovits_path': '', 'start_cmd': 'start api_v2.bat',
'refer_audio_history': [],
'refer_audio_folder': '',
'top_k': 5, 'top_p': 1.0, 'temperature': 1.0,
'batch_size': 32, 'speed': 1.0, 'text_split_method': 'cut5'
}
def _save_config(cfg):
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
def _scan_models(model_dir, ext):
if not model_dir or not os.path.isdir(model_dir):
return []
files = glob.glob(os.path.join(model_dir, '**', f'*{ext}'), recursive=True)
files.sort(key=lambda f: os.path.getmtime(f), reverse=True)
return files
@bp.route('/config', methods=['GET'])
def get_config():
cfg = _load_config()
cfg['gpt_models'] = _scan_models(cfg.get('gpt_model_dir', ''), '.ckpt')
cfg['sovits_models'] = _scan_models(cfg.get('sovits_model_dir', ''), '.pth')
return jsonify(cfg)
@bp.route('/config', methods=['POST'])
def save_config():
data = request.get_json()
cfg = _load_config()
for key in ('api_url', 'gpt_model_dir', 'sovits_model_dir', 'last_gpt_model', 'last_sovits_model', 'sovits_path', 'start_cmd',
'top_k', 'top_p', 'temperature', 'batch_size', 'speed', 'text_split_method', 'refer_audio_folder'):
if key in data:
cfg[key] = data[key]
_save_config(cfg)
return jsonify({'success': True})
@bp.route('/refer-audio-history', methods=['POST'])
def save_refer_audio():
data = request.get_json()
path = (data.get('path') or '').strip()
if not path:
return jsonify({'success': False, 'error': '路径不能为空'})
cfg = _load_config()
history = cfg.get('refer_audio_history', [])
if path in history:
history.remove(path)
history.insert(0, path)
cfg['refer_audio_history'] = history
_save_config(cfg)
return jsonify({'success': True, 'history': history})
@bp.route('/refer-audio-history', methods=['DELETE'])
def delete_refer_audio():
data = request.get_json()
path = (data.get('path') or '').strip()
cfg = _load_config()
history = cfg.get('refer_audio_history', [])
if path in history:
history.remove(path)
cfg['refer_audio_history'] = history
_save_config(cfg)
return jsonify({'success': True, 'history': history})
@bp.route('/refer-audio-history', methods=['PUT'])
def update_refer_audio():
data = request.get_json()
old_path = (data.get('old_path') or '').strip()
new_path = (data.get('new_path') or '').strip()
if not old_path or not new_path:
return jsonify({'success': False, 'error': '路径不能为空'})
cfg = _load_config()
history = cfg.get('refer_audio_history', [])
if old_path not in history:
return jsonify({'success': False, 'error': '原路径不存在'})
if new_path in history and new_path != old_path:
return jsonify({'success': False, 'error': '新路径已存在'})
idx = history.index(old_path)
history[idx] = new_path
cfg['refer_audio_history'] = history
_save_config(cfg)
return jsonify({'success': True, 'history': history})
@bp.route('/scan-audio-folder', methods=['POST'])
def scan_audio_folder():
"""扫描文件夹下的音频文件"""
data = request.get_json()
folder = (data.get('folder') or '').strip()
if not folder:
return jsonify({'success': False, 'error': '请输入文件夹路径'})
if not os.path.isdir(folder):
return jsonify({'success': False, 'error': '文件夹不存在'})
AUDIO_EXTS = ('.wav', '.mp3', '.flac', '.ogg', '.aac', '.m4a', '.wma')
files = []
for f in os.listdir(folder):
full = os.path.join(folder, f)
if os.path.isfile(full) and f.lower().endswith(AUDIO_EXTS):
files.append(full)
files.sort(key=lambda x: os.path.basename(x).lower())
# 保存最后使用的文件夹
cfg = _load_config()
cfg['refer_audio_folder'] = folder
_save_config(cfg)
return jsonify({'success': True, 'files': files, 'count': len(files)})
@bp.route('/test-connection', methods=['POST'])
def test_connection():
data = request.get_json()
api_url = (data.get('api_url') or '').strip().rstrip('/')
if not api_url:
return jsonify({'success': False, 'error': '请填写 GPT-SoVITS 服务地址'})
try:
r = requests.get(api_url + '/test', timeout=10)
if r.status_code == 200:
data = r.json()
return jsonify({'success': True, 'message': data.get('message', '连接成功')})
return jsonify({'success': False, 'error': f'服务返回状态码 {r.status_code}'})
except requests.exceptions.ConnectionError:
return jsonify({'success': False, 'error': f'无法连接到: {api_url}'})
except requests.exceptions.Timeout:
return jsonify({'success': False, 'error': '连接超时'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@bp.route('/run', methods=['POST'])
def run_api():
global _sovits_proc
if platform.system() != 'Windows':
return jsonify({'success': False, 'error': '仅支持 Windows 系统'})
cfg = _load_config()
sovits_path = (cfg.get('sovits_path') or '').strip()
start_cmd = (cfg.get('start_cmd') or '').strip()
if not sovits_path:
return jsonify({'success': False, 'error': '请先配置 GPT-SoVITS 路径'})
if not os.path.isdir(sovits_path):
return jsonify({'success': False, 'error': f'路径不存在: {sovits_path}'})
if not start_cmd:
return jsonify({'success': False, 'error': '请先配置启动命令'})
try:
_sovits_proc = subprocess.Popen(start_cmd, shell=True, cwd=sovits_path)
return jsonify({'success': True, 'message': f'已启动: {start_cmd}'})
except Exception as e:
return jsonify({'success': False, 'error': f'启动失败: {str(e)}'})
@bp.route('/status', methods=['GET'])
def check_status():
cfg = _load_config()
api_url = (cfg.get('api_url') or '').strip().rstrip('/')
if not api_url:
return jsonify({'running': False})
try:
r = requests.get(api_url + '/test', timeout=3)
if r.status_code == 200:
data = r.json()
return jsonify({'running': True, 'message': data.get('message', '')})
except Exception:
pass
return jsonify({'running': False})
@bp.route('/kill', methods=['POST'])
def kill_api():
global _sovits_proc
if _sovits_proc and _sovits_proc.poll() is None:
try:
subprocess.call(['taskkill', '/F', '/T', '/PID', str(_sovits_proc.pid)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass
_sovits_proc = None
else:
cfg = _load_config()
api_url = (cfg.get('api_url') or '').strip().rstrip('/')
if api_url:
try:
r = requests.get(api_url + '/control?command=exit', timeout=5)
except Exception:
pass
return jsonify({'success': True, 'message': '已发送停止信号'})
@bp.route('/synthesize', methods=['POST'])
def synthesize():
data = request.get_json()
api_url = (data.get('api_url') or '').strip().rstrip('/')
text = (data.get('text') or '').strip()
text_lang = (data.get('text_lang') or 'zh').strip()
if not api_url:
return jsonify({'success': False, 'error': '请填写 GPT-SoVITS 服务地址'})
if not text:
return jsonify({'success': False, 'error': '请输入要合成的文本'})
ref_audio_path = (data.get('ref_audio_path') or '').strip()
prompt_text = (data.get('prompt_text') or '').strip()
prompt_lang = (data.get('prompt_lang') or 'zh').strip()
if not ref_audio_path:
return jsonify({'success': False, 'error': '请填写参考音频路径'})
payload = {
'text': text,
'text_lang': text_lang,
'ref_audio_path': ref_audio_path,
'prompt_text': prompt_text,
'prompt_lang': prompt_lang,
'text_split_method': (data.get('text_split_method') or 'cut5').strip(),
'batch_size': data.get('batch_size', 1),
'top_k': data.get('top_k', 5),
'top_p': data.get('top_p', 1.0),
'temperature': data.get('temperature', 1.0),
'speed_factor': data.get('speed_factor', 1.0),
'seed': data.get('seed', -1),
'media_type': 'wav',
'streaming_mode': False,
}
try:
r = requests.post(api_url + '/tts', json=payload, timeout=300)
content_type = r.headers.get('Content-Type', '')
if r.status_code == 200 and 'audio' in content_type:
return Response(r.content, mimetype='audio/wav',
headers={'Content-Disposition': 'inline'})
else:
try:
err = r.json()
msg = err.get('message', r.text[:200])
except Exception:
msg = r.text[:200]
return jsonify({'success': False, 'error': f'合成失败: {msg}'})
except requests.exceptions.ConnectionError:
return jsonify({'success': False, 'error': f'无法连接到: {api_url}'})
except requests.exceptions.Timeout:
return jsonify({'success': False, 'error': '合成超时300秒文本可能过长'})
except Exception as e:
return jsonify({'success': False, 'error': f'请求失败: {str(e)}'})
@bp.route('/set-gpt-model', methods=['POST'])
def set_gpt_model():
data = request.get_json()
api_url = (data.get('api_url') or '').strip().rstrip('/')
weights_path = (data.get('weights_path') or '').strip()
if not api_url:
return jsonify({'success': False, 'error': '请填写 GPT-SoVITS 服务地址'})
if not weights_path:
return jsonify({'success': False, 'error': '请选择 GPT 模型'})
try:
r = requests.get(api_url + '/set_gpt_weights', params={'weights_path': weights_path}, timeout=60)
if r.status_code == 200:
return jsonify({'success': True, 'message': 'GPT 模型切换成功'})
try:
err = r.json()
msg = err.get('message', r.text[:200])
except Exception:
msg = r.text[:200]
return jsonify({'success': False, 'error': f'切换失败: {msg}'})
except requests.exceptions.ConnectionError:
return jsonify({'success': False, 'error': f'无法连接到: {api_url}'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@bp.route('/set-sovits-model', methods=['POST'])
def set_sovits_model():
data = request.get_json()
api_url = (data.get('api_url') or '').strip().rstrip('/')
weights_path = (data.get('weights_path') or '').strip()
if not api_url:
return jsonify({'success': False, 'error': '请填写 GPT-SoVITS 服务地址'})
if not weights_path:
return jsonify({'success': False, 'error': '请选择 SoVITS 模型'})
try:
r = requests.get(api_url + '/set_sovits_weights', params={'weights_path': weights_path}, timeout=60)
if r.status_code == 200:
return jsonify({'success': True, 'message': 'SoVITS 模型切换成功'})
try:
err = r.json()
msg = err.get('message', r.text[:200])
except Exception:
msg = r.text[:200]
return jsonify({'success': False, 'error': f'切换失败: {msg}'})
except requests.exceptions.ConnectionError:
return jsonify({'success': False, 'error': f'无法连接到: {api_url}'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})