# -*- coding: utf-8 -*- """ ai-dubbing AI 配音蓝图 通过 subprocess 调用 GPT-SoVITS 自带的 runtime\python.exe 实现文本转语音 """ import os import sys 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('ai_dubbing', __name__, url_prefix='/ai-dubbing') 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', 'ai_dubbing_config.json') WORKER_SCRIPT = os.path.join(BASE_DIR, 'utils', 'sovits_worker.py') # 子进程管理 _proc = None _proc_lock = threading.Lock() _proc_stderr = [] _engine_status = {'initialized': False, 'loading': False, 'error': None, 'version': None} # 合成请求锁 _synth_lock = threading.Lock() # 任务结果缓存 _task_results_local = {} TEXT_LANGUAGES = { 'zh': '中文', 'en': '英文', 'ja': '日文', 'ko': '韩文', 'yue': '粤语', 'all_zh': '全部中文', 'all_ja': '全部日文', 'all_yue': '全部粤语', 'all_ko': '全部韩文', 'auto': '多语种混合', 'auto_yue': '多语种混合(粤语)', } CUT_METHODS = ['cut0', 'cut1', 'cut2', 'cut3', 'cut4', 'cut5'] CUT_METHOD_NAMES = { 'cut0': '不切分', 'cut1': '凑四句一切', 'cut2': '凑50字一切', 'cut3': '按中文句号。切', 'cut4': '按英文句号.切', 'cut5': '按标点符号切', } def _default_config(): return { 'sovits_project_path': r'E:\AI\GPT-SoVITS-v4', 'sovits_config_yaml': r'GPT_SoVITS\configs\tts_infer.yaml', 'last_prompt_text': '', 'last_prompt_lang': 'zh', 'last_text_lang': 'zh', 'last_text_split_method': 'cut5', 'top_k': 5, 'top_p': 1.0, 'temperature': 1.0, 'batch_size': 1, 'speed_factor': 1.0, 'seed': -1, 'gpt_model_dir': '', 'sovits_model_dir': '', 'last_gpt_model': '', 'last_sovits_model': '', 'refer_audio_folder': '', } 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) 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_models(directory, pattern): """扫描目录下指定格式的模型文件,返回文件名列表""" if not directory or not os.path.isdir(directory): return [] files = glob.glob(os.path.join(directory, pattern)) files.sort(key=lambda x: os.path.basename(x).lower()) return [os.path.basename(f) for f in files] 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'/ai-dubbing/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'ai_dub_{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')