GENIE / tts_engine.py
Tom1986's picture
重构:将app.py拆分为模块化架构 + 修复onnxruntime依赖问题
24437ee
"""
Genie TTS 核心引擎模块
包含Genie TTS的主要功能和接口
"""
import os
import tempfile
import logging
import shutil
from installer import setup_genie_import
from config import (
AVAILABLE_CHARACTERS, MODEL_FILES, MODEL_SIZES,
get_cache_dir, get_character_cache_dir, setup_environment
)
logger = logging.getLogger(__name__)
# 设置Genie导入
genie, install_error = setup_genie_import()
class GenieTTSInterface:
"""Genie TTS 接口类"""
def __init__(self):
self.available_characters = AVAILABLE_CHARACTERS
self.current_character = None
self.model_cache_dir = get_cache_dir()
self.is_initialized = False
self.install_error = install_error
def check_model_availability(self, character_name):
"""检查模型是否已缓存"""
character_cache_dir = get_character_cache_dir(self.model_cache_dir, character_name)
if not os.path.exists(character_cache_dir):
return False
for file_name in MODEL_FILES:
if not os.path.exists(os.path.join(character_cache_dir, file_name)):
return False
return True
def initialize_genie(self):
"""初始化Genie TTS环境"""
if self.is_initialized:
return True
try:
setup_environment()
# 设置缓存目录
if hasattr(genie, '_internal'):
logger.info("Genie TTS环境初始化成功")
self.is_initialized = True
return True
except Exception as e:
logger.error(f"初始化Genie TTS失败: {e}")
return False
def load_character(self, character_name):
"""加载角色模型"""
if not genie:
return None, "Genie TTS未正确安装"
if not self.initialize_genie():
return None, "Genie TTS初始化失败"
try:
logger.info(f"正在加载角色: {character_name}")
# 检查模型是否已缓存
if self.check_model_availability(character_name):
logger.info(f"使用缓存的模型: {character_name}")
else:
logger.info(f"首次下载模型: {character_name},请稍候...")
# 加载预定义角色(这会自动处理下载)
genie.load_predefined_character(character_name)
self.current_character = character_name
return f"角色 {character_name} 加载成功!", ""
except Exception as e:
error_msg = str(e)
logger.error(f"加载角色失败: {error_msg}")
# 提供更友好的错误信息
if "network" in error_msg.lower() or "connection" in error_msg.lower():
return None, "网络连接错误,请检查网络连接后重试"
elif "disk space" in error_msg.lower():
return None, "磁盘空间不足,请清理空间后重试"
elif "timeout" in error_msg.lower():
return None, "下载超时,请重试"
else:
return None, f"加载角色失败: {error_msg}"
def estimate_download_size(self, character_name):
"""估算下载大小"""
return MODEL_SIZES.get(character_name, 200)
def cleanup_cache(self):
"""清理缓存"""
try:
if os.path.exists(self.model_cache_dir):
shutil.rmtree(self.model_cache_dir)
self.model_cache_dir = get_cache_dir()
logger.info("缓存清理完成")
return True
except Exception as e:
logger.error(f"清理缓存失败: {e}")
return False
def synthesize_speech(self, text, character_name, play_audio=False):
"""文本转语音 - 增强版"""
if not genie:
if self.install_error:
error_msg = f"Genie TTS 安装失败: {self.install_error}"
if "portaudio" in self.install_error.lower():
error_msg += "\n\n💡 解决方案:\n"
error_msg += "1. 在本地环境运行此应用(支持完整依赖)\n"
error_msg += "2. 或等待我们提供不依赖PyAudio的替代方案\n"
error_msg += "3. 查看项目README了解更多信息"
return None, error_msg
else:
return None, "Genie TTS未正确安装,原因未知"
if not text.strip():
return None, "请输入要合成的文本"
# 文本长度检查
if len(text) > 500:
return None, "文本过长(超过500字符),请缩短文本长度"
if character_name != self.current_character:
status, error = self.load_character(character_name)
if error:
return None, error
try:
# 文本预处理
processed_text = self.preprocess_text(text)
# 创建临时文件保存音频
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
output_path = tmp_file.name
logger.info(f"正在合成语音: {processed_text[:50]}...")
# 设置内存限制环境变量
original_env = os.environ.get('PYTORCH_JIT_USE_NNC_NOT_NVFUSER', None)
os.environ['PYTORCH_JIT_USE_NNC_NOT_NVFUSER'] = '1'
try:
# 执行TTS
genie.tts(
character_name=character_name,
text=processed_text,
play=False, # 在服务器环境不播放
split_sentence=True,
save_path=output_path
)
finally:
# 恢复环境变量
if original_env is None and 'PYTORCH_JIT_USE_NNC_NOT_NVFUSER' in os.environ:
del os.environ['PYTORCH_JIT_USE_NNC_NOT_NVFUSER']
elif original_env is not None:
os.environ['PYTORCH_JIT_USE_NNC_NOT_NVFUSER'] = original_env
# 验证输出文件
if not os.path.exists(output_path):
return None, "语音合成失败:输出文件未生成"
file_size = os.path.getsize(output_path)
if file_size == 0:
return None, "语音合成失败:输出文件为空"
elif file_size < 1000: # 小于1KB可能是错误
return None, "语音合成失败:输出文件异常小"
logger.info(f"语音合成成功,文件大小: {file_size/1024:.1f}KB")
return output_path, ""
except Exception as e:
error_msg = str(e)
logger.error(f"语音合成失败: {error_msg}")
# 提供更详细的错误信息
if "out of memory" in error_msg.lower() or "memory" in error_msg.lower():
return None, "内存不足,请尝试缩短文本或重启应用"
elif "cuda" in error_msg.lower():
return None, "GPU相关错误,正在使用CPU模式重试"
elif "model" in error_msg.lower():
return None, "模型加载错误,请重新选择角色"
elif "timeout" in error_msg.lower():
return None, "处理超时,请尝试缩短文本"
else:
return None, f"语音合成失败: {error_msg}"
def preprocess_text(self, text):
"""文本预处理"""
# 基本清理
text = text.strip()
# 替换常见的问题字符
replacements = {
'"': '"',
'"': '"',
''': "'",
''': "'",
'—': '一',
'–': '-',
}
for old, new in replacements.items():
text = text.replace(old, new)
# 确保句子有适当的标点
if text and not text.endswith(('。', '!', '?', '.', '!', '?')):
text += '。'
return text
def get_system_info(self):
"""获取系统信息用于调试"""
try:
# Try to import psutil, but gracefully handle if it's not available
try:
import psutil
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
return {
'memory_total': f"{memory.total / (1024**3):.1f}GB",
'memory_available': f"{memory.available / (1024**3):.1f}GB",
'memory_percent': f"{memory.percent}%",
'disk_free': f"{disk.free / (1024**3):.1f}GB"
}
except ImportError:
# Fallback to basic system information without psutil
total, used, free = shutil.disk_usage('/')
return {
'disk_free': f"{free / (1024**3):.1f}GB",
'disk_total': f"{total / (1024**3):.1f}GB",
'status': "基础系统信息 (psutil 未安装)"
}
except Exception as e:
return {"status": f"无法获取系统信息: {str(e)}"}
# 创建全局接口实例
tts_interface = GenieTTSInterface()