import os import re import time import sys import logging from pathlib import Path # --- CONFIGURACIÓN DE PERSISTENCIA Y CACHÉ (CRÍTICO PARA SPACES) --- # Estas líneas permiten que las librerías escriban en el disco persistente /data os.environ["NUMBA_CACHE_DIR"] = "/data/numba_cache" os.environ["MPLCONFIGDIR"] = "/data/matplotlib_config" os.environ["PIP_CACHE_DIR"] = "/data/pip_cache" os.environ["HF_HOME"] = "/data/huggingface" os.environ["XDG_DATA_HOME"] = "/data/xdg_data" try: import rvc_python print("rvc-python ya está instalado.") except ImportError: print("rvc-python no encontrado. Instalando en runtime...") try: from install_rvc import install_rvc install_rvc() import rvc_python # re-importar después de instalación print("rvc-python instalado exitosamente.") except Exception as e: print(f"ERROR instalando rvc-python: {e}") import threading import subprocess import glob # Configuración inicial ANTES de importaciones pesadas os.environ["COQUI_TOS_AGREED"] = "1" os.environ["OMP_NUM_THREADS"] = "1" os.environ["TOKENIZERS_PARALLELISM"] = "false" os.environ["GRADIO_SSR_MODE"] = "false" # FIX: Desactivar SSR para evitar errores con Node # Configurar logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S' ) logger = logging.getLogger(__name__) logger.info("=" * 60) logger.info("🎙️ PEDRO LABATTAGLIA TTS - INICIANDO (MODO PERSISTENTE)") logger.info("=" * 60) # Importaciones try: logger.info("📦 Importando dependencias...") import torch import gradio as gr from TTS.tts.configs.xtts_config import XttsConfig from TTS.tts.models.xtts import Xtts from huggingface_hub import hf_hub_download, snapshot_download import scipy.io.wavfile as wavfile import warnings import shutil import pydantic logger.info(f"Gradio: {gr.__version__}, Pydantic: {pydantic.__version__}") logger.info("✅ Todas las dependencias importadas correctamente") except ImportError as e: logger.error(f"❌ Error importando dependencias: {e}") sys.exit(1) warnings.filterwarnings("ignore") # --- CONFIGURACIÓN RVC PERSISTENTE --- # Ruta cambiada a /data para persistencia RVC_MODELS_DIR = Path("/data/models/rvc_models") def download_rvc_models_from_hf(): """Descarga los modelos RVC desde el dataset privado al disco persistente.""" try: repo_id = "Blakus/Pedro_Lab_RVC_Models" token = os.environ.get("HF_TOKEN") if not token: logger.warning("⚠️ No se encontró HF_TOKEN. No se podrán descargar los modelos RVC privados.") return # Verificar si el modelo principal ya existe en /data if (RVC_MODELS_DIR / "Fresco" / "Fresco.pth").exists(): logger.info("✅ Modelos RVC ya existen en almacenamiento persistente (/data). No se descargarán.") return logger.info(f"📥 [STARTUP] Iniciando descarga de modelos RVC a {RVC_MODELS_DIR}...") # Asegurar que el directorio existe RVC_MODELS_DIR.mkdir(parents=True, exist_ok=True) snapshot_download( repo_id=repo_id, repo_type="model", local_dir=RVC_MODELS_DIR, token=token, ignore_patterns=["*.git*", "README.md", ".gitattributes"] ) logger.info("✅ [STARTUP] Modelos RVC descargados y listos en /data.") except Exception as e: logger.error(f"❌ Error descargando modelos RVC: {e}") def get_rvc_models_list(): """Escanea la carpeta de modelos RVC y devuelve una lista de nombres.""" models = [] if not RVC_MODELS_DIR.exists(): return models for model_dir in RVC_MODELS_DIR.iterdir(): if model_dir.is_dir(): pth_files = list(model_dir.glob('*.pth')) if pth_files: models.append(model_dir.name) return models def run_rvc_pipeline(input_path, model_name): """Ejecuta el pipeline de RVC (Compatible con Linux/Spaces)""" try: model_dir = RVC_MODELS_DIR / model_name pth_files = list(model_dir.glob('*.pth')) index_files = list(model_dir.glob('*.index')) if not pth_files: logger.error(f"❌ No se encontró archivo .pth para el modelo {model_name}") return input_path model_path = str(pth_files[0].absolute()) index_path = str(index_files[0].absolute()) if index_files else "" # Generar nombre de salida output_rvc = input_path.replace(".wav", f"_rvc_{model_name}.wav") # Parámetros RVC pitch = 0 method = "rmvpe" index_rate = 0 filter_radius = 3 resample_sr = 0 rms_mix_rate = 0.25 protect = 0.50 device = "cuda:0" if torch.cuda.is_available() else "cpu" # Comando CLI para RVC en Linux (usando sys.executable) cmd = [ sys.executable, '-m', 'rvc_python', 'cli', '--input', str(input_path), '--model', model_path, '--pitch', str(pitch), '--method', method, '--output', str(output_rvc), '--index_rate', str(index_rate), '--device', device, '--protect', str(protect), '--filter_radius', str(filter_radius), '--resample_sr', str(resample_sr), '--rms_mix_rate', str(rms_mix_rate) ] if index_path: cmd.extend(['--index', index_path]) logger.info(f"🔄 Ejecutando RVC con modelo: {model_name}...") result = subprocess.run(cmd, check=True, capture_output=True, text=True) if os.path.exists(output_rvc): logger.info("✅ RVC aplicado exitosamente") return output_rvc else: logger.error("❌ RVC finalizó pero no generó archivo de salida") if result.stdout: logger.info(f"RVC STDOUT: {result.stdout}") if result.stderr: logger.error(f"RVC STDERR: {result.stderr}") return input_path except subprocess.CalledProcessError as e: logger.error(f"❌ Error al ejecutar comando RVC. Código: {e.returncode}") if e.stderr: logger.error(f"RVC STDERR: {e.stderr}") return input_path except Exception as e: logger.error(f"❌ Error general en pipeline RVC: {e}") return input_path def get_user_data_dir(app_name="tts"): """Directorio para datos de usuario redirigido a /data""" # Usamos /data/xdg_data para que persista data_dir = Path("/data/xdg_data") / app_name data_dir.mkdir(parents=True, exist_ok=True) return str(data_dir) def authenticate_user(username, password): """Autenticación contra variables de entorno""" valid_username = os.environ.get("AUTH_USERNAME") valid_password = os.environ.get("AUTH_PASSWORD") if not valid_username or not valid_password: logger.error("❌ AUTH_USERNAME o AUTH_PASSWORD no configurados") return False if username == valid_username and password == valid_password: logger.info(f"✅ Usuario autenticado: {username}") return True else: logger.warning(f"❌ Autenticación fallida para: {username}") return False class PedroTTSApp: def __init__(self): self.model = None self.config = None self.device = "cuda" if torch.cuda.is_available() else "cpu" self.model_loaded = False self.loading_lock = threading.Lock() logger.info(f"🖥️ Dispositivo: {self.device}") if self.device == "cuda": logger.info(f"🚀 GPU: {torch.cuda.get_device_name(0)}") def setup_resources(self): """Descarga RVC y configura el modelo XTTS en persistencia""" with self.loading_lock: if self.model_loaded: return # 1. Descargar RVC (Prioridad) download_rvc_models_from_hf() # 2. Configurar XTTS try: logger.info("📦 Iniciando configuración del modelo XTTS...") repo_id = "Blakus/Pedro_Lab_XTTS" # MODIFICACIÓN: Ruta persistente directa para XTTS local_dir = Path("/data/models/xtts_v2") local_dir.mkdir(parents=True, exist_ok=True) files_to_download = ["config.json", "model.pth", "vocab.json"] for file_name in files_to_download: file_path = local_dir / file_name if not file_path.exists(): logger.info(f"📥 Descargando {file_name} a /data...") try: hf_hub_download( repo_id=repo_id, filename=file_name, local_dir=str(local_dir), local_dir_use_symlinks=False ) logger.info(f"✅ {file_name} descargado") except Exception as e: logger.warning(f"⚠️ Error en descarga directa de {file_name}: {e}") downloaded_file = hf_hub_download( repo_id=repo_id, filename=file_name ) shutil.copy2(downloaded_file, file_path) logger.info(f"✅ {file_name} copiado") else: logger.info(f"✅ {file_name} ya existe en persistencia") config_path = str(local_dir / "config.json") checkpoint_path = str(local_dir / "model.pth") vocab_path = str(local_dir / "vocab.json") for path, name in [(config_path, "config"), (checkpoint_path, "model"), (vocab_path, "vocab")]: if not os.path.exists(path): raise FileNotFoundError(f"Archivo no encontrado: {name} en {path}") logger.info("⚙️ Cargando configuración...") self.config = XttsConfig() self.config.load_json(config_path) logger.info("🔧 Inicializando modelo...") self.model = Xtts.init_from_config(self.config) logger.info("📂 Cargando checkpoint (esto puede tomar unos minutos)...") self.model.load_checkpoint( self.config, checkpoint_path=checkpoint_path, vocab_path=vocab_path, eval=True, use_deepspeed=False ) if self.device == "cuda": self.model.cuda() logger.info("🚀 Modelo cargado en GPU") else: self.model.cpu() logger.info("🐌 Modelo cargado en CPU") self.model_loaded = True logger.info("✅ Modelo XTTS configurado y listo") except Exception as e: logger.error(f"❌ Error configurando modelo: {e}") self.model_loaded = False import traceback logger.error(traceback.format_exc()) def load_reference_audios(self): """Cargar audios de referencia desde dataset privado a persistencia""" try: logger.info("🎵 Cargando audios de referencia...") dataset_id = os.environ.get("PRIVATE_AUDIO_DATASET", "Blakus/Pedro_Lab_XTTS_Reference_Audios") hf_token = os.environ.get("HF_TOKEN") # MODIFICACIÓN: Ruta persistente para audios local_audio_dir = Path("/data/reference_audios") local_audio_dir.mkdir(parents=True, exist_ok=True) audio_files = ["neutral.wav", "serio.wav", "alegre.wav", "neutral_ingles.wav"] downloaded_audios = [] for audio_file in audio_files: local_path = local_audio_dir / audio_file if not local_path.exists(): logger.info(f"📥 Descargando {audio_file} a /data...") try: downloaded_file = hf_hub_download( repo_id=dataset_id, filename=audio_file, repo_type="dataset", token=hf_token, local_dir_use_symlinks=False ) shutil.copy2(downloaded_file, local_path) logger.info(f"✅ {audio_file} descargado") downloaded_audios.append(str(local_path)) except Exception as e: logger.error(f"❌ Error con {audio_file}: {e}") else: logger.info(f"✅ {audio_file} encontrado en persistencia") downloaded_audios.append(str(local_path)) logger.info(f"🎵 Total audios disponibles: {len(downloaded_audios)}") return downloaded_audios except Exception as e: logger.error(f"❌ Error cargando audios: {e}") return [] def generate_speech(self, text, language, reference_audio, speed, temperature, enable_text_splitting, use_rvc, rvc_model): """Genera audio de voz + RVC""" try: if not self.model_loaded or not self.model: return None, "⏳ Modelo cargando... Intente en unos minutos o contacte al administrador." if not text or len(text.strip()) < 2: return None, "❌ El texto debe tener al menos 2 caracteres" if not reference_audio or reference_audio == "demo": return None, "❌ Seleccione un estilo de voz válido" if not os.path.exists(reference_audio): return None, "❌ Audio de referencia no encontrado" text = text.strip() logger.info(f"🎙️ Generando: '{text[:50]}{'...' if len(text) > 50 else ''}' (RVC: {use_rvc})") try: gpt_cond_latent, speaker_embedding = self.model.get_conditioning_latents( audio_path=reference_audio ) except Exception as e: logger.error(f"Error en conditioning latents: {e}") return None, f"❌ Error procesando audio de referencia: {str(e)}" start_time = time.time() out = self.model.inference( text, language, gpt_cond_latent, speaker_embedding, temperature=float(temperature), length_penalty=1.0, repetition_penalty=5.0, top_k=50, top_p=0.85, speed=float(speed), enable_text_splitting=enable_text_splitting, do_sample=True ) inference_time = time.time() - start_time if "wav" not in out or out["wav"] is None: return None, "❌ No se generó audio" timestamp = int(time.time()) # En Spaces usamos ruta relativa o absoluta segura. output_path = f"output_{timestamp}.wav" sample_rate = self.config.audio.get("output_sample_rate", 22050) wavfile.write(output_path, sample_rate, out["wav"]) audio_length = len(out["wav"]) / sample_rate metrics = f"""✅ Audio generado exitosamente 🎵 Duración: {audio_length:.1f}s ⏱️ Tiempo: {inference_time:.1f}s ⚡ Velocidad: {speed}x 🎨 Creatividad: {temperature} 📖 Segmentación: {'Sí' if enable_text_splitting else 'No'} 🌐 Idioma: {language.upper()}""" # --- RVC PIPELINE --- if use_rvc and rvc_model: logger.info(f"✨ Iniciando mejora de timbre RVC: {rvc_model}") rvc_start_time = time.time() rvc_output_path = run_rvc_pipeline(output_path, rvc_model) if rvc_output_path != output_path: output_path = rvc_output_path rvc_time = time.time() - rvc_start_time metrics += f"\n\n✨ Mejora Aplicada: {rvc_model}\n⏱️ Tiempo RVC: {rvc_time:.1f}s" else: metrics += "\n\n⚠️ Fallo en la mejora del timbre, se retorna audio original." # -------------------- logger.info(f"✅ Proceso completo.") return output_path, metrics except Exception as e: error_msg = f"❌ Error: {str(e)}" logger.error(error_msg) import traceback logger.error(traceback.format_exc()) return None, error_msg # Instancia global app = PedroTTSApp() def create_interface(): """Crear interfaz Gradio""" try: logger.info("🎨 Creando interfaz...") available_audios = app.load_reference_audios() languages = [("Español", "es"), ("English", "en")] ref_mapping = { "neutral.wav": "🎭 Neutral", "serio.wav": "😐 Serio", "alegre.wav": "😊 Alegre", "neutral_ingles.wav": "🎭 Neutral (English)" } if not available_audios: logger.warning("⚠️ No hay audios de referencia") audio_refs = [("❌ No disponible", "demo")] else: audio_refs = [] for audio_file in available_audios: filename = Path(audio_file).name label = ref_mapping.get(filename, filename) audio_refs.append((label, audio_file)) # Modelos RVC (intento inicial) initial_rvc_models = get_rvc_models_list() custom_css = """ .auth-box { max-width: 450px; margin: 40px auto; padding: 40px; border-radius: 20px; background: linear-gradient(145deg, #2d2d2d, #1a1a1a); box-shadow: 0 8px 32px rgba(0,0,0,0.4); } .speaker-info { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 15px; margin-bottom: 20px; text-align: center; } .speaker-image { width: 180px; height: 180px; border-radius: 50%; margin: 0 auto 15px; border: 4px solid rgba(255,255,255,0.3); object-fit: cover; } .social-links { display: flex; justify-content: center; gap: 15px; margin-top: 15px; } .social-link { color: white; text-decoration: none; font-size: 16px; padding: 8px 12px; border-radius: 20px; background: rgba(255,255,255,0.2); transition: all 0.3s ease; } .social-link:hover { background: rgba(255,255,255,0.3); transform: translateY(-2px); } .credits-section { margin-top: 15px; text-align: center; } .credits-text { color: #6c757d; font-size: 12px; margin: 5px 0; } .credits-link { color: #007bff; text-decoration: none; font-size: 11px; transition: color 0.3s ease; } .credits-link:hover { color: #0056b3; } /* CSS para el Info Box en Dark Mode */ .info-box { font-size: 0.9em; color: #e0e0e0; padding: 12px; background-color: #252525; border-left: 5px solid #9b59b6; border-radius: 5px; margin-top: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } """ with gr.Blocks(title="Pedro Labattaglia TTS", theme=gr.themes.Soft(), css=custom_css) as demo: auth_state = gr.State(False) # Pantalla de login with gr.Column(elem_classes="auth-box") as auth_column: gr.Markdown(""" # 🔐 Acceso Restringido ## Pedro Labattaglia - TTS Ingrese sus credenciales para acceder al sistema. """) username_input = gr.Textbox( label="👤 Usuario", placeholder="Usuario" ) password_input = gr.Textbox( label="🔑 Contraseña", placeholder="Contraseña", type="password" ) login_btn = gr.Button("🚪 Iniciar Sesión", variant="primary", size="lg") auth_message = gr.Textbox(label="Estado", value="⏳ Esperando credenciales...", interactive=False) # Interfaz principal with gr.Column(visible=False) as main_column: # Header con info del locutor with gr.Column(elem_classes="speaker-info"): gr.HTML("""
🎙️ Locutor profesional | +20 años dando voz a marcas líderes en Argentina, LATAM y EE.UU. | Español rioplatense / neutro | Voice Over | Source Connect: pedrovotalent | ✉️ pedrolabattaglia@gmail.com
Desarrollado por Ezequiel Casas
LinkedIn