import gradio as gr import os import subprocess import requests from huggingface_hub import HfApi, hf_hub_download import spaces api = HfApi() @spaces.GPU def process_full_migration(token, source_dataset, target_dataset, folder_path): if not token or not source_dataset or not target_dataset or not folder_path: yield "❌ Error: Faltan datos obligatorios." return log_output = "" def log(msg): nonlocal log_output log_output += msg + "\n" print(msg, flush=True) yield log_output size_damaged_files = [] # Archivos < 50MB detectados por API JSON repaired_files = [] # Archivos que requirieron re-codificación de emergencia successful_files = [] # Archivos migrados con éxito try: folder_path = folder_path.strip().rstrip('/') yield from log(f"🔍 Consultando API de Hugging Face para obtener metadatos y tamaños en: {folder_path}...") headers = {"Authorization": f"Bearer {token}"} if token else {} api_url = f"https://huggingface.co/api/datasets/{source_dataset}/tree/main/{folder_path}?recursive=true" response = requests.get(api_url, headers=headers) if response.status_code != 200: yield from log(f"❌ Error al consultar la API de HF: {response.status_code} - {response.text}") return items = response.json() if not isinstance(items, list): yield from log(f"❌ Respuesta inesperada de la API: {items}") return # Filtrar solo archivos de video (.mp4 y .mkv) video_items = [ item for item in items if item.get("type") == "file" and (item.get("path", "").endswith('.mp4') or item.get("path", "").endswith('.mkv')) ] total_files = len(video_items) if total_files == 0: yield from log(f"⚠️ No se encontraron archivos de video en la ruta: {folder_path}") return yield from log(f"📁 Se encontraron {total_files} videos en el JSON de la API. Analizando tamaños...\n") for idx, item in enumerate(video_items, 1): file_path = item["path"] file_size_bytes = item.get("size", 0) file_size_mb = round(file_size_bytes / (1024 * 1024), 2) yield from log(f"==================================================") yield from log(f"🎬 Procesando [{idx}/{total_files}]: {file_path}") yield from log(f" 📏 Tamaño detectado en API: {file_size_mb} MB ({file_size_bytes} bytes)") yield from log(f"==================================================") # Validación directa por metadata de API (< 50 Megas) -> Omitir descarga, fix y subida if file_size_bytes < (50 * 1024 * 1024): yield from log(f"⚠️ Archivo DAÑADO O INCOMPLETO POR TAMAÑO: {file_size_mb} MB (< 50 MB). Omitiendo descarga y fix.") size_damaged_files.append({"path": file_path, "size_mb": file_size_mb}) yield from log(f"⏭️ Archivo [{idx}/{total_files}] omitido.\n") continue is_mkv = file_path.endswith('.mkv') output_ext = ".mkv" if is_mkv else ".mp4" output_path = f"video_arreglado{output_ext}" # Descargar archivo válido a la VPS yield from log("📥 Descargando archivo al disco local de la VPS...") try: temp_local_path = hf_hub_download( repo_id=source_dataset, filename=file_path, repo_type="dataset", token=token ) except Exception as e: yield from log(f"❌ Error al descargar el archivo desde HF: {str(e)}") continue # Procesar con FFmpeg if is_mkv: yield from log("⚙️ Verificando y limpiando contenedor MKV con FFmpeg (-c copy)...") command = ["ffmpeg", "-y", "-i", temp_local_path, "-c", "copy", output_path] else: yield from log("⚙️ Aplicando fix moov atom a MP4 con FFmpeg (-c copy -movflags +faststart)...") command = ["ffmpeg", "-y", "-i", temp_local_path, "-c", "copy", "-movflags", "+faststart", output_path] repaired_flag = False try: subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: yield from log("⚠️ ¡Aviso! El copiado directo falló (corrupción interna / streams dañados). Aplicando re-codificación de emergencia...") if is_mkv: command_fallback = ["ffmpeg", "-y", "-i", temp_local_path, output_path] else: command_fallback = ["ffmpeg", "-y", "-i", temp_local_path, "-movflags", "+faststart", output_path] try: subprocess.run(command_fallback, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) repaired_flag = True yield from log("✅ Re-codificación de emergencia completada con éxito.") except Exception as ex: yield from log(f"❌ Error crítico procesando FFmpeg en fallback: {str(ex)}") if os.path.exists(output_path): os.remove(output_path) continue if repaired_flag: repaired_files.append({"path": file_path, "ext": output_ext}) # Subir archivo optimizado al dataset de destino yield from log(f"☁️ Subiendo video optimizado a {target_dataset} ({file_path})...") try: api.upload_file( path_or_fileobj=output_path, path_in_repo=file_path, repo_id=target_dataset, repo_type="dataset", token=token ) successful_files.append(file_path) except Exception as e: yield from log(f"❌ Error al subir a Hugging Face: {str(e)}") # Limpieza local if os.path.exists(output_path): os.remove(output_path) yield from log(f"🎉 ¡Video [{idx}/{total_files}] procesado y subido con éxito!\n") yield from log("🏆 ¡Recorrido masivo total finalizado!") except Exception as e: yield from log(f"❌ Error crítico en el proceso: {str(e)}") finally: # Generar y subir el archivo de log obligatoriamente a la raíz del dataset de destino yield from log("📝 Generando reporte final 'seriestransferencia.log'...") log_content = "=" * 60 + "\n" log_content += " 📊 REPORTE DE TRANSFERENCIA Y MIGRACIÓN DE SERIES\n" log_content += "=" * 60 + "\n\n" log_content += f"1. VIDEOS DAÑADOS POR TAMAÑO (< 50 MB) [OMITIDOS]: {len(size_damaged_files)}\n" for item in size_damaged_files: log_content += f" - Ruta: {item['path']} | Tamaño detectado: {item['size_mb']} MB\n" log_content += f"\n2. VIDEOS REPARADOS POR RE-CODIFICACIÓN (Daños internos / Faltas de start): {len(repaired_files)}\n" for item in repaired_files: log_content += f" - Ruta: {item['path']} | Extensión real: {item['ext']}\n" log_content += f"\n3. VIDEOS MIGRADOS Y SUBIDOS CON ÉXITO: {len(successful_files)}\n" for item in successful_files: log_content += f" - Ruta: {item}\n" log_content += "=" * 60 + "\n" log_filename = "seriestransferencia.log" with open(log_filename, "w", encoding="utf-8") as f: f.write(log_content) try: api.upload_file( path_or_fileobj=log_filename, path_in_repo=log_filename, repo_id=target_dataset, repo_type="dataset", token=token ) yield from log(f"✅ Archivo '{log_filename}' subido con éxito a la raíz del dataset {target_dataset}.") except Exception as log_err: yield from log(f"⚠️ No se pudo subir el log a Hugging Face: {str(log_err)}") if os.path.exists(log_filename): os.remove(log_filename) with gr.Blocks() as demo: gr.Markdown("# 🚀 Migrador Inteligente con Validación de Tamaño por API") gr.Markdown("Lee los tamaños directamente del JSON de Hugging Face para descartar archivos corruptos/incompletos (<50MB) sin descargarlos, repara streams y genera `seriestransferencia.log` al terminar o detenerse.") token_input = gr.Textbox(label="Tu HF_TOKEN (Con permisos de WRITE)", type="password") with gr.Row(): source_input = gr.Textbox(label="Dataset ORIGEN", value="xplhack1/bylatamsrc2") target_input = gr.Textbox(label="Dataset DESTINO", value="orglatamsrc/latamsrcfixed") folder_input = gr.Textbox( label="Carpeta o temporada a escanear", value="SERIES/EN_EMISION/33_Dias_2026/TEMPORADA1" ) with gr.Row(): btn = gr.Button("Iniciar Migración Inteligente 🔥", variant="primary") stop_btn = gr.Button("Detener Proceso ⏹️", variant="stop") output = gr.Textbox(label="Consola de Logs en Tiempo Real", lines=22) watcher = btn.click(fn=process_full_migration, inputs=[token_input, source_input, target_input, folder_input], outputs=output) stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[watcher]) demo.launch(theme=gr.themes.Soft(), server_name="0.0.0.0", server_port=7860)