# -*- coding: utf-8 -*- """ rvc RVC 变声蓝图 通过 subprocess 调用 RVC 自带的 runtime\python.exe 实现语音变声 """ import os import json import uuid import glob import struct import subprocess import threading import tempfile from flask import Blueprint, render_template, request, jsonify, send_file, after_this_request bp = Blueprint('rvc', __name__, url_prefix='/rvc') try: from config import BASE_DIR except ImportError: BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CONFIG_PATH = os.path.join(BASE_DIR, 'config', 'rvc_config.json') WORKER_SCRIPT = os.path.join(BASE_DIR, 'utils', 'rvc_worker.py') # 子进程管理 _proc = None _proc_lock = threading.Lock() _proc_stderr = [] _engine_status = {'initialized': False, 'loading': False, 'error': None, 'device': None, 'model_loaded': False} # 变声请求锁 _synth_lock = threading.Lock() # 任务结果缓存 _task_results_local = {} F0_METHODS = ['pm', 'harvest', 'crepe', 'rmvpe'] F0_METHOD_NAMES = { 'pm': 'PM (最快)', 'harvest': 'Harvest (高质量)', 'crepe': 'Crepe (神经网络)', 'rmvpe': 'RMVPE (推荐)', } def _default_config(): return { 'rvc_project_path': r'E:\AI\RVC\RVC1006Nvidia', 'model_dir': 'assets/weights', 'model_dir_mode': 'relative', 'index_dir': 'logs', 'index_dir_mode': 'relative', 'last_model': '', 'last_index': '', 'f0_up_key': 0, 'f0_method': 'rmvpe', 'index_rate': 0.75, 'filter_radius': 3, 'resample_sr': 0, 'rms_mix_rate': 0.25, 'protect': 0.33, } def _load_config(): cfg = _default_config() if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, 'r', encoding='utf-8') as f: saved = json.load(f) cfg.update(saved) # 空值回退到默认值 if not cfg.get('model_dir'): cfg['model_dir'] = 'assets/weights' if not cfg.get('index_dir'): cfg['index_dir'] = 'logs' if not cfg.get('model_dir_mode'): cfg['model_dir_mode'] = 'relative' if not cfg.get('index_dir_mode'): cfg['index_dir_mode'] = 'relative' return cfg 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_pth_models(directory): """扫描 .pth 模型文件""" if not directory or not os.path.isdir(directory): return [] files = glob.glob(os.path.join(directory, '*.pth')) files.sort(key=lambda x: os.path.basename(x).lower()) return [os.path.basename(f) for f in files] def _scan_index_files(directory): """递归扫描 .index 文件,排除 trained""" if not directory or not os.path.isdir(directory): return [] results = [] for root, dirs, files in os.walk(directory): for f in files: if f.endswith('.index') and 'trained' not in f.lower(): results.append(os.path.join(root, f)) results.sort(key=lambda x: os.path.basename(x).lower()) return results def _send_msg(proc, obj): data = json.dumps(obj, ensure_ascii=False).encode('utf-8') proc.stdin.write(struct.pack('') def task_status(task_id): info = _task_results_local.get(task_id) if not info: return jsonify({'success': False, 'error': '任务不存在'}), 404 resp = {'success': True, 'progress': info} if info['status'] == 'done': resp['audio_url'] = f'/rvc/audio/{task_id}' elif info['status'] == 'error': resp['progress'] = {'status': 'error', 'error': info.get('error', '未知错误')} _task_results_local.pop(task_id, None) return jsonify(resp) @bp.route('/audio/') def get_audio(task_id): info = _task_results_local.pop(task_id, None) audio_path = info['file'] if info else os.path.join(tempfile.gettempdir(), f'rvc_{task_id}.wav') if not os.path.exists(audio_path): return jsonify({'error': '音频文件不存在'}), 404 @after_this_request def cleanup(resp): try: os.remove(audio_path) except OSError: pass return resp return send_file(audio_path, mimetype='audio/wav')