commit 69992eaff6cb96be75cc3cd60d91cd2faaa42aa6 Author: Martin Gracia Date: Sun Nov 9 12:45:20 2025 -0500 Primera versión: LoRA Analyzer completo con CLI, Web App y API diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18f0909 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 LoRA Analyzer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f59f9b --- /dev/null +++ b/README.md @@ -0,0 +1,384 @@ +# 🔍 LoRA Analyzer + +Herramienta completa para analizar archivos LoRA (Low-Rank Adaptation) y descubrir cómo fueron entrenados. + +## 🌟 Características + +- ✅ **Análisis completo** de arquitectura LoRA (rank, alpha, capas) +- ✅ **Extracción de metadatos** de entrenamiento +- ✅ **Estadísticas de pesos** y distribuciones +- ✅ **Comparación** de múltiples LoRAs +- ✅ **Recomendaciones** para optimización +- ✅ **3 interfaces diferentes**: CLI, Web App y API REST + +## 📁 Formatos Soportados + +- `.safetensors` (recomendado) +- `.pt` / `.pth` (PyTorch) +- `.ckpt` (Checkpoints) + +## 🚀 Instalación Rápida + +```bash +# Clonar o descargar los archivos +cd lora-analyzer + +# Instalar dependencias +pip install -r requirements.txt + +# ¡Listo para usar! +``` + +### Instalación opcional de PyTorch + +Si no tienes PyTorch instalado, usa: + +```bash +# CPU only +pip install torch --index-url https://download.pytorch.org/whl/cpu + +# CUDA (GPU) +pip install torch --index-url https://download.pytorch.org/whl/cu118 +``` + +## 💻 Uso + +### 1️⃣ CLI (Línea de Comandos) + +La forma más rápida para análisis desde terminal: + +```bash +# Analizar un archivo +python lora_cli.py mi_lora.safetensors + +# Analizar múltiples archivos +python lora_cli.py lora1.safetensors lora2.pt lora3.ckpt + +# Guardar resultado en JSON +python lora_cli.py mi_lora.safetensors --output resultado.json + +# Comparar múltiples LoRAs +python lora_cli.py lora1.safetensors lora2.safetensors --compare + +# Modo verbose +python lora_cli.py mi_lora.safetensors --verbose + +# Ver ayuda +python lora_cli.py --help +``` + +**Ejemplo de salida:** +``` +🔍 Analizando: my_style_lora.safetensors +====================================================================== +REPORTE DE ANÁLISIS LORA +====================================================================== + +📁 INFORMACIÓN DEL ARCHIVO: + Nombre: my_style_lora.safetensors + Tamaño: 144.2 MB + Formato: .safetensors + +🏗️ ARQUITECTURA: + Total de capas: 192 + Rank más común: 32 + Rango de ranks: 32 - 32 + +⚙️ METADATOS DE ENTRENAMIENTO: + Modelo base: sd_xl_base_1.0.safetensors + Network dim (rank): 32 + Alpha: 32 + Learning rate: 0.0001 + Épocas: 10 + Imágenes de entrenamiento: 45 + Batch size: 1 + Resolución: 1024 + +💡 RECOMENDACIONES: + 1. Rank óptimo (32): Buen balance entre detalle y eficiencia. + 2. Dataset mediano (45 imágenes): Bueno para conceptos específicos. +``` + +### 2️⃣ Web App (Interfaz Gráfica) + +Interfaz visual con Gradio - ideal para uso interactivo: + +```bash +# Iniciar la aplicación web +python lora_webapp.py +``` + +Luego abre tu navegador en: **http://localhost:7860** + +**Funcionalidades:** +- 📤 Arrastrar y soltar archivos +- 📊 Vista de análisis con resumen visual +- 📈 Datos JSON completos +- ⚖️ Comparación lado a lado +- 💾 Exportar resultados + +![Web App Screenshot](https://via.placeholder.com/800x400?text=Web+App+Interface) + +### 3️⃣ API REST + +Servidor FastAPI para integración programática: + +```bash +# Iniciar el servidor API +python lora_api.py +``` + +El servidor estará disponible en: **http://localhost:8000** + +**Documentación interactiva:** http://localhost:8000/docs + +#### Endpoints disponibles: + +##### `POST /analyze` - Analizar un archivo + +```bash +curl -X POST "http://localhost:8000/analyze" \ + -H "Content-Type: multipart/form-data" \ + -F "file=@mi_lora.safetensors" +``` + +```python +import requests + +with open('mi_lora.safetensors', 'rb') as f: + response = requests.post( + 'http://localhost:8000/analyze', + files={'file': f} + ) + result = response.json() + print(result) +``` + +##### `POST /analyze/batch` - Analizar múltiples archivos + +```python +import requests + +files = [ + ('files', open('lora1.safetensors', 'rb')), + ('files', open('lora2.safetensors', 'rb')) +] +response = requests.post( + 'http://localhost:8000/analyze/batch', + files=files +) +print(response.json()) +``` + +##### `POST /compare` - Comparar archivos + +```bash +curl -X POST "http://localhost:8000/compare" \ + -F "files=@lora1.safetensors" \ + -F "files=@lora2.safetensors" +``` + +##### `GET /health` - Estado del servicio + +```bash +curl http://localhost:8000/health +``` + +##### `GET /examples` - Ver más ejemplos + +```bash +curl http://localhost:8000/examples +``` + +## 📊 Información que Puedes Extraer + +### ✅ Lo que SÍ puedes obtener: + +- **Arquitectura completa** + - Dimensión de rank (8, 16, 32, 64, 128, etc.) + - Factor alpha de escalado + - Capas modificadas (attention blocks, MLP, etc.) + - Total de parámetros + +- **Metadatos de entrenamiento** (si están incluidos) + - Modelo base utilizado (SD 1.5, SDXL, etc.) + - Learning rate + - Número de epochs + - Batch size + - Resolución de entrenamiento + - Número de imágenes de entrenamiento + - Herramienta usada (Kohya, EveryDream, etc.) + +- **Estadísticas de pesos** + - Distribución de valores + - Media y desviación estándar + - Intensidad por capa + +### ❌ Lo que NO puedes recuperar: + +- **Los datos de entrenamiento originales** - Técnicamente imposible +- **Imágenes específicas del dataset** - No están almacenadas en el LoRA +- **Prompts exactos usados** - Solo si están en metadatos + +## 🎯 Casos de Uso + +### 1. Ingeniería Inversa +```bash +# Analiza un LoRA público para aprender de él +python lora_cli.py awesome_public_lora.safetensors + +# Compara con tu propio LoRA +python lora_cli.py awesome_public_lora.safetensors my_lora.safetensors --compare +``` + +### 2. Optimización de tus LoRAs +```bash +# Analiza diferentes versiones para encontrar la mejor configuración +python lora_cli.py lora_v1_rank16.safetensors lora_v2_rank32.safetensors \ + lora_v3_rank64.safetensors --compare +``` + +### 3. Debugging +```bash +# Verifica que tu LoRA se entrenó correctamente +python lora_cli.py my_new_lora.safetensors --verbose +``` + +### 4. Investigación +```python +# Analiza múltiples LoRAs programáticamente +import requests +import os + +for filename in os.listdir('loras/'): + if filename.endswith('.safetensors'): + with open(f'loras/{filename}', 'rb') as f: + response = requests.post( + 'http://localhost:8000/analyze', + files={'file': f} + ) + result = response.json() + # Procesar resultados... +``` + +## 🔧 Uso Programático + +### Como módulo Python + +```python +from lora_analyzer import LoRAAnalyzer, format_analysis_report + +# Analizar un archivo +analyzer = LoRAAnalyzer('mi_lora.safetensors') +analysis = analyzer.analyze() + +# Ver reporte formateado +print(format_analysis_report(analysis)) + +# Acceder a datos específicos +rank = analysis['architecture']['rank_info']['most_common_rank'] +print(f"Rank detectado: {rank}") + +# Extraer metadatos +metadata = analysis['metadata'] +learning_rate = metadata.get('ss_learning_rate', 'N/A') +print(f"Learning rate: {learning_rate}") +``` + +## 📦 Estructura del Proyecto + +``` +lora-analyzer/ +├── lora_analyzer.py # Módulo core de análisis +├── lora_cli.py # Aplicación CLI +├── lora_webapp.py # Aplicación Web (Gradio) +├── lora_api.py # API REST (FastAPI) +├── requirements.txt # Dependencias +└── README.md # Esta documentación +``` + +## 🛠️ Requisitos del Sistema + +- Python 3.8 o superior +- 4GB RAM mínimo (8GB recomendado) +- Espacio en disco: ~2GB para dependencias + +## ⚙️ Configuración Avanzada + +### Cambiar puertos + +**Web App:** +```python +# En lora_webapp.py, línea 249 +app.launch(server_port=8080) # Cambiar de 7860 a 8080 +``` + +**API:** +```python +# En lora_api.py, línea 360 +uvicorn.run(app, host="0.0.0.0", port=9000) # Cambiar de 8000 a 9000 +``` + +### Análisis de archivos grandes + +Para LoRAs muy grandes (>1GB): + +```python +# Aumentar límite de análisis de capas +analyzer = LoRAAnalyzer('huge_lora.safetensors') +# Modifica _analyze_weights para analizar más capas +``` + +## 🤝 Contribuciones + +Las contribuciones son bienvenidas! Para reportar bugs o sugerir features, abre un issue. + +## 📝 Notas Importantes + +1. **Privacidad**: Todo el análisis se hace localmente. No se envía información a servidores externos. + +2. **Rendimiento**: El análisis de archivos grandes puede tomar tiempo. La CLI es más rápida que la Web App. + +3. **Compatibilidad**: Diseñado principalmente para LoRAs de Stable Diffusion, pero puede funcionar con otros tipos. + +4. **Metadatos**: La cantidad de información disponible depende de cómo fue guardado el LoRA. LoRAs entrenados con Kohya suelen tener más metadatos. + +## 🔮 Roadmap + +- [ ] Soporte para análisis de LoRAs de LLMs +- [ ] Visualización de distribución de pesos +- [ ] Detección automática de estilo/concepto +- [ ] Base de datos para indexar LoRAs analizados +- [ ] Exportar reportes en PDF +- [ ] Integración con Hugging Face Hub + +## 📄 Licencia + +Este proyecto es de código abierto y está disponible bajo la licencia MIT. + +## ❓ FAQ + +**P: ¿Puedo recuperar las imágenes con las que se entrenó el LoRA?** +R: No, es técnicamente imposible. Los pesos del modelo son el resultado de la optimización, pero no contienen los datos originales. + +**P: ¿Funciona con cualquier tipo de LoRA?** +R: Está optimizado para LoRAs de Stable Diffusion, pero puede analizar cualquier LoRA en formato safetensors o PyTorch. + +**P: ¿Por qué algunos metadatos no aparecen?** +R: Depende de cómo fue guardado el archivo. Usa herramientas como Kohya que incluyen metadatos extensos. + +**P: ¿Puedo usar esto para crear LoRAs mejores?** +R: Sí! Analiza LoRAs exitosos para aprender qué configuraciones funcionan mejor para diferentes casos de uso. + +**P: ¿Es seguro analizar LoRAs de fuentes desconocidas?** +R: El análisis es seguro, pero ten cuidado al cargar pesos en un modelo. Los archivos .pt y .ckpt pueden contener código malicioso. + +## 🙏 Agradecimientos + +Desarrollado para la comunidad de ML y entusiastas de LoRA. + +--- + +**¿Preguntas o problemas?** Abre un issue en el repositorio. + +**¿Te resultó útil?** ¡Dale una estrella ⭐! diff --git a/__pycache__/lora_analyzer.cpython-313.pyc b/__pycache__/lora_analyzer.cpython-313.pyc new file mode 100644 index 0000000..0b6edfe Binary files /dev/null and b/__pycache__/lora_analyzer.cpython-313.pyc differ diff --git a/examples.py b/examples.py new file mode 100644 index 0000000..eb152c2 --- /dev/null +++ b/examples.py @@ -0,0 +1,246 @@ +""" +Ejemplos de uso del LoRA Analyzer +Demuestra diferentes formas de usar la herramienta +""" + +from lora_analyzer import LoRAAnalyzer, format_analysis_report +import json + + +def example_basic_analysis(): + """Ejemplo básico: analizar un archivo""" + print("=" * 70) + print("EJEMPLO 1: Análisis Básico") + print("=" * 70) + + # Reemplaza con tu archivo real + file_path = "mi_lora.safetensors" + + try: + # Crear analizador + analyzer = LoRAAnalyzer(file_path) + + # Realizar análisis + analysis = analyzer.analyze() + + # Mostrar reporte formateado + print(format_analysis_report(analysis)) + + except FileNotFoundError: + print(f"⚠️ Archivo no encontrado: {file_path}") + print(" Reemplaza 'mi_lora.safetensors' con tu archivo real") + + +def example_extract_specific_data(): + """Ejemplo: extraer datos específicos""" + print("\n" + "=" * 70) + print("EJEMPLO 2: Extraer Datos Específicos") + print("=" * 70) + + file_path = "mi_lora.safetensors" + + try: + analyzer = LoRAAnalyzer(file_path) + analysis = analyzer.analyze() + + # Extraer información específica + print("\n📊 Información clave:") + + # Rank + rank_info = analysis.get('architecture', {}).get('rank_info', {}) + if rank_info: + rank = rank_info.get('most_common_rank', 'N/A') + print(f" • Rank: {rank}") + + # Modelo base + metadata = analysis.get('metadata', {}) + if metadata: + base_model = metadata.get('ss_base_model', 'N/A') + print(f" • Modelo base: {base_model}") + + # Learning rate + lr = metadata.get('ss_learning_rate', 'N/A') + print(f" • Learning rate: {lr}") + + # Imágenes de entrenamiento + num_images = metadata.get('ss_num_train_images', 'N/A') + print(f" • Imágenes: {num_images}") + + # Epochs + epochs = metadata.get('ss_num_epochs', 'N/A') + print(f" • Epochs: {epochs}") + + # Tamaño del archivo + file_size = analysis.get('file_info', {}).get('tamaño_mb', 'N/A') + print(f" • Tamaño: {file_size} MB") + + except FileNotFoundError: + print(f"⚠️ Archivo no encontrado: {file_path}") + + +def example_compare_loras(): + """Ejemplo: comparar múltiples LoRAs""" + print("\n" + "=" * 70) + print("EJEMPLO 3: Comparar Múltiples LoRAs") + print("=" * 70) + + # Reemplaza con tus archivos reales + files = [ + "lora_v1.safetensors", + "lora_v2.safetensors", + "lora_v3.safetensors" + ] + + results = [] + + for file_path in files: + try: + analyzer = LoRAAnalyzer(file_path) + analysis = analyzer.analyze() + results.append({ + 'file': file_path, + 'analysis': analysis + }) + except FileNotFoundError: + print(f"⚠️ Archivo no encontrado: {file_path}") + + if results: + print("\n📊 Comparación:") + print(f"{'Archivo':<30} {'Rank':<10} {'Tamaño (MB)':<15} {'Imágenes'}") + print("-" * 70) + + for result in results: + filename = result['file'] + analysis = result['analysis'] + + rank = analysis.get('architecture', {}).get('rank_info', {}).get('most_common_rank', 'N/A') + size = analysis.get('file_info', {}).get('tamaño_mb', 'N/A') + images = analysis.get('metadata', {}).get('ss_num_train_images', 'N/A') + + print(f"{filename:<30} {str(rank):<10} {str(size):<15} {str(images)}") + + +def example_save_to_json(): + """Ejemplo: guardar análisis en JSON""" + print("\n" + "=" * 70) + print("EJEMPLO 4: Guardar en JSON") + print("=" * 70) + + file_path = "mi_lora.safetensors" + output_path = "analysis_result.json" + + try: + analyzer = LoRAAnalyzer(file_path) + analysis = analyzer.analyze() + + # Guardar en JSON + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(analysis, f, indent=2, ensure_ascii=False) + + print(f"✅ Análisis guardado en: {output_path}") + + except FileNotFoundError: + print(f"⚠️ Archivo no encontrado: {file_path}") + + +def example_batch_processing(): + """Ejemplo: procesamiento por lotes""" + print("\n" + "=" * 70) + print("EJEMPLO 5: Procesamiento por Lotes") + print("=" * 70) + + import os + + # Directorio con archivos LoRA + lora_directory = "loras/" + + if not os.path.exists(lora_directory): + print(f"⚠️ Directorio no encontrado: {lora_directory}") + print(" Crea un directorio 'loras/' y coloca tus archivos allí") + return + + # Analizar todos los archivos .safetensors + results = [] + + for filename in os.listdir(lora_directory): + if filename.endswith('.safetensors'): + file_path = os.path.join(lora_directory, filename) + + try: + print(f"\n🔍 Analizando: {filename}") + analyzer = LoRAAnalyzer(file_path) + analysis = analyzer.analyze() + results.append(analysis) + + # Mostrar resumen rápido + rank = analysis.get('architecture', {}).get('rank_info', {}).get('most_common_rank', 'N/A') + print(f" ✓ Rank: {rank}") + + except Exception as e: + print(f" ✗ Error: {str(e)}") + + print(f"\n✅ Análisis completado: {len(results)} archivos procesados") + + +def example_filter_by_rank(): + """Ejemplo: filtrar LoRAs por rank""" + print("\n" + "=" * 70) + print("EJEMPLO 6: Filtrar por Rank") + print("=" * 70) + + import os + + target_rank = 32 + lora_directory = "loras/" + + if not os.path.exists(lora_directory): + print(f"⚠️ Directorio no encontrado: {lora_directory}") + return + + matching_loras = [] + + for filename in os.listdir(lora_directory): + if filename.endswith(('.safetensors', '.pt', '.ckpt')): + file_path = os.path.join(lora_directory, filename) + + try: + analyzer = LoRAAnalyzer(file_path) + analysis = analyzer.analyze() + + rank = analysis.get('architecture', {}).get('rank_info', {}).get('most_common_rank') + + if rank == target_rank: + matching_loras.append(filename) + print(f"✓ {filename} - Rank {rank}") + + except Exception as e: + continue + + print(f"\n📊 Encontrados {len(matching_loras)} LoRAs con rank {target_rank}") + + +if __name__ == "__main__": + print("\n🔍 EJEMPLOS DE USO - LoRA Analyzer\n") + + # Ejecuta los ejemplos + # Nota: Muchos ejemplos fallarán si no tienes archivos reales + # Esto es solo para demostración + + print("💡 Estos ejemplos requieren archivos LoRA reales") + print(" Reemplaza los nombres de archivo con tus propios archivos\n") + + # Descomenta el ejemplo que quieras probar: + + # example_basic_analysis() + # example_extract_specific_data() + # example_compare_loras() + # example_save_to_json() + # example_batch_processing() + # example_filter_by_rank() + + print("\n" + "=" * 70) + print("Para usar estos ejemplos:") + print("1. Reemplaza los nombres de archivo con tus archivos reales") + print("2. Descomenta el ejemplo que quieras probar") + print("3. Ejecuta: python examples.py") + print("=" * 70) diff --git a/install_macos.sh b/install_macos.sh new file mode 100644 index 0000000..34825ce --- /dev/null +++ b/install_macos.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# LoRA Analyzer - Instalación para macOS +# Script automático que detecta y configura todo + +echo "╔══════════════════════════════════════════════════════════════════════╗" +echo "║ 🔍 LoRA Analyzer - Instalación para macOS ║" +echo "╚══════════════════════════════════════════════════════════════════════╝" +echo "" + +# Detectar si estamos en macOS +if [[ "$OSTYPE" != "darwin"* ]]; then + echo "❌ Este script es solo para macOS" + echo " Usa quick_start.sh en su lugar" + exit 1 +fi + +echo "✅ Sistema operativo: macOS detectado" +echo "" + +# Función para verificar comando +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# 1. Verificar Python3 +echo "📋 Paso 1: Verificando Python..." +if command_exists python3; then + PYTHON_VERSION=$(python3 --version) + echo "✅ Python encontrado: $PYTHON_VERSION" + PYTHON_CMD="python3" +else + echo "❌ Python 3 no está instalado" + echo "" + echo "Por favor instala Python de una de estas formas:" + echo "" + echo "Opción A - Con Homebrew (recomendado):" + echo " 1. Instala Homebrew:" + echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + echo " 2. Instala Python:" + echo " brew install python" + echo "" + echo "Opción B - Descarga directa:" + echo " 1. Ve a: https://www.python.org/downloads/" + echo " 2. Descarga e instala Python 3.11 o superior" + echo "" + exit 1 +fi + +# 2. Verificar pip3 +echo "" +echo "📋 Paso 2: Verificando pip..." +if command_exists pip3; then + PIP_VERSION=$(pip3 --version) + echo "✅ pip encontrado: $PIP_VERSION" + PIP_CMD="pip3" +elif command_exists pip; then + PIP_VERSION=$(pip --version) + echo "✅ pip encontrado: $PIP_VERSION" + PIP_CMD="pip" +else + echo "❌ pip no está instalado" + echo "" + echo "Instalando pip..." + $PYTHON_CMD -m ensurepip --upgrade + + if command_exists pip3; then + echo "✅ pip instalado correctamente" + PIP_CMD="pip3" + else + echo "❌ No se pudo instalar pip automáticamente" + echo " Instala pip manualmente: $PYTHON_CMD -m ensurepip" + exit 1 + fi +fi + +# 3. Crear entorno virtual +echo "" +echo "📋 Paso 3: Configurando entorno virtual..." +if [ -d "venv" ]; then + echo "✅ Entorno virtual ya existe" +else + echo "Creando entorno virtual..." + $PYTHON_CMD -m venv venv + if [ $? -eq 0 ]; then + echo "✅ Entorno virtual creado" + else + echo "⚠️ No se pudo crear entorno virtual" + echo " Continuando sin entorno virtual..." + fi +fi + +# 4. Activar entorno virtual +if [ -d "venv" ]; then + echo "" + echo "🔌 Activando entorno virtual..." + source venv/bin/activate + echo "✅ Entorno virtual activado" + + # Actualizar pip en el entorno virtual + pip install --upgrade pip --quiet +fi + +# 5. Instalar dependencias +echo "" +echo "📋 Paso 4: Instalando dependencias..." +echo "Esto puede tomar algunos minutos..." +echo "" + +$PIP_CMD install -r requirements.txt + +if [ $? -eq 0 ]; then + echo "" + echo "✅ Dependencias instaladas correctamente" +else + echo "" + echo "❌ Hubo un error instalando las dependencias" + echo " Intenta manualmente: $PIP_CMD install -r requirements.txt" + exit 1 +fi + +# 6. Verificar instalación +echo "" +echo "📋 Paso 5: Verificando instalación..." +echo "" +$PYTHON_CMD test_installation.py + +# 7. Instrucciones finales +echo "" +echo "╔══════════════════════════════════════════════════════════════════════╗" +echo "║ ✅ ¡INSTALACIÓN COMPLETADA! ║" +echo "╚══════════════════════════════════════════════════════════════════════╝" +echo "" +echo "🚀 Para usar la herramienta:" +echo "" +echo "1. Activa el entorno virtual (si lo creaste):" +echo " source venv/bin/activate" +echo "" +echo "2. Ejecuta la aplicación que necesites:" +echo "" +echo " 📱 Web App (interfaz gráfica):" +echo " $PYTHON_CMD lora_webapp.py" +echo " Luego abre: http://localhost:7860" +echo "" +echo " 💻 CLI (línea de comandos):" +echo " $PYTHON_CMD lora_cli.py mi_lora.safetensors" +echo "" +echo " 🔌 API REST:" +echo " $PYTHON_CMD lora_api.py" +echo " Luego abre: http://localhost:8000/docs" +echo "" +echo "📚 Lee las guías:" +echo " • LEEME_PRIMERO.txt - Introducción" +echo " • GUIA_WEBAPP.txt - Guía de la Web App" +echo " • README.md - Documentación completa" +echo "" +echo "═══════════════════════════════════════════════════════════════════════" +echo "" diff --git a/lora_analyzer.py b/lora_analyzer.py new file mode 100644 index 0000000..391da8c --- /dev/null +++ b/lora_analyzer.py @@ -0,0 +1,454 @@ +""" +LoRA Analyzer - Core Module +Analiza archivos LoRA (.safetensors, .pt, .ckpt) y extrae información técnica +""" + +import os +import json +from pathlib import Path +from typing import Dict, Any, List, Tuple +import numpy as np + +try: + from safetensors import safe_open + SAFETENSORS_AVAILABLE = True +except ImportError: + SAFETENSORS_AVAILABLE = False + +try: + import torch + TORCH_AVAILABLE = True +except ImportError: + TORCH_AVAILABLE = False + + +class LoRAAnalyzer: + """Analizador de archivos LoRA""" + + def __init__(self, file_path: str): + self.file_path = Path(file_path) + self.file_name = self.file_path.name + self.file_size = self.file_path.stat().st_size + self.extension = self.file_path.suffix.lower() + + if not self.file_path.exists(): + raise FileNotFoundError(f"El archivo no existe: {file_path}") + + def analyze(self) -> Dict[str, Any]: + """Análisis completo del archivo LoRA""" + + result = { + "file_info": self._get_file_info(), + "architecture": {}, + "metadata": {}, + "weights_analysis": {}, + "recommendations": [] + } + + # Cargar y analizar según el formato + if self.extension == ".safetensors": + result.update(self._analyze_safetensors()) + elif self.extension in [".pt", ".pth", ".ckpt"]: + result.update(self._analyze_pytorch()) + else: + raise ValueError(f"Formato no soportado: {self.extension}") + + # Generar recomendaciones + result["recommendations"] = self._generate_recommendations(result) + + return result + + def _get_file_info(self) -> Dict[str, Any]: + """Información básica del archivo""" + return { + "nombre": self.file_name, + "ruta": str(self.file_path.absolute()), + "tamaño_mb": round(self.file_size / (1024 * 1024), 2), + "extension": self.extension + } + + def _analyze_safetensors(self) -> Dict[str, Any]: + """Analiza archivos .safetensors""" + if not SAFETENSORS_AVAILABLE: + return {"error": "Instala 'safetensors': pip install safetensors"} + + result = { + "architecture": {}, + "metadata": {}, + "weights_analysis": {} + } + + with safe_open(self.file_path, framework="pt") as f: + # Obtener metadatos + metadata = f.metadata() + if metadata: + result["metadata"] = self._parse_metadata(metadata) + + # Analizar capas y pesos + keys = list(f.keys()) + result["architecture"] = self._analyze_architecture(keys, f) + result["weights_analysis"] = self._analyze_weights(keys, f) + + return result + + def _analyze_pytorch(self) -> Dict[str, Any]: + """Analiza archivos .pt, .pth, .ckpt""" + if not TORCH_AVAILABLE: + return {"error": "Instala 'torch': pip install torch"} + + result = { + "architecture": {}, + "metadata": {}, + "weights_analysis": {} + } + + try: + state_dict = torch.load(self.file_path, map_location="cpu") + + # Extraer metadatos si existen + if isinstance(state_dict, dict): + if "metadata" in state_dict: + result["metadata"] = state_dict["metadata"] + + # Obtener los pesos (pueden estar en diferentes claves) + weights = state_dict.get("state_dict", state_dict) + + keys = list(weights.keys()) + result["architecture"] = self._analyze_architecture_dict(keys, weights) + result["weights_analysis"] = self._analyze_weights_dict(keys, weights) + + except Exception as e: + result["error"] = f"Error al cargar archivo: {str(e)}" + + return result + + def _parse_metadata(self, metadata: Dict) -> Dict[str, Any]: + """Parsea y organiza los metadatos""" + parsed = {} + + # Buscar información común + common_keys = [ + "ss_network_module", "ss_network_dim", "ss_network_alpha", + "ss_learning_rate", "ss_text_encoder_lr", "ss_unet_lr", + "ss_num_epochs", "ss_num_train_images", "ss_num_batches_per_epoch", + "ss_batch_size", "ss_base_model", "ss_sd_model_name", + "ss_resolution", "ss_clip_skip", "ss_max_train_steps", + "ss_dataset_dirs", "ss_training_comment" + ] + + for key in common_keys: + if key in metadata: + value = metadata[key] + # Intentar parsear JSON si es string + if isinstance(value, str): + try: + value = json.loads(value) + except: + pass + parsed[key] = value + + # Agregar otros metadatos + for key, value in metadata.items(): + if key not in parsed: + parsed[key] = value + + return parsed + + def _analyze_architecture(self, keys: List[str], tensor_file) -> Dict[str, Any]: + """Analiza la arquitectura del LoRA desde safetensors""" + arch = { + "total_layers": len(keys), + "layer_details": [], + "rank_info": {}, + "alpha_info": {}, + "module_types": set() + } + + # Analizar cada capa + for key in keys: + tensor = tensor_file.get_tensor(key) + shape = tensor.shape + + layer_info = { + "name": key, + "shape": list(shape), + "num_params": np.prod(shape) + } + + # Detectar rank (típicamente la dimensión más pequeña en matrices LoRA) + if len(shape) >= 2: + potential_rank = min(shape) + layer_info["potential_rank"] = int(potential_rank) + + # Identificar tipo de módulo + if "lora_up" in key or "lora_down" in key: + module_type = "lora" + elif "alpha" in key: + module_type = "alpha" + else: + module_type = "other" + + layer_info["type"] = module_type + arch["module_types"].add(module_type) + arch["layer_details"].append(layer_info) + + arch["module_types"] = list(arch["module_types"]) + + # Calcular estadísticas de rank + ranks = [l["potential_rank"] for l in arch["layer_details"] + if "potential_rank" in l] + if ranks: + arch["rank_info"] = { + "detected_ranks": list(set(ranks)), + "most_common_rank": max(set(ranks), key=ranks.count), + "min_rank": min(ranks), + "max_rank": max(ranks) + } + + return arch + + def _analyze_architecture_dict(self, keys: List[str], weights: Dict) -> Dict[str, Any]: + """Analiza la arquitectura del LoRA desde diccionario PyTorch""" + arch = { + "total_layers": len(keys), + "layer_details": [], + "rank_info": {}, + "module_types": set() + } + + for key in keys: + tensor = weights[key] + if hasattr(tensor, 'shape'): + shape = tensor.shape + else: + continue + + layer_info = { + "name": key, + "shape": list(shape), + "num_params": np.prod(shape) + } + + if len(shape) >= 2: + potential_rank = min(shape) + layer_info["potential_rank"] = int(potential_rank) + + # Identificar tipo de módulo + if "lora" in key.lower(): + module_type = "lora" + elif "alpha" in key.lower(): + module_type = "alpha" + else: + module_type = "other" + + layer_info["type"] = module_type + arch["module_types"].add(module_type) + arch["layer_details"].append(layer_info) + + arch["module_types"] = list(arch["module_types"]) + + ranks = [l["potential_rank"] for l in arch["layer_details"] + if "potential_rank" in l] + if ranks: + arch["rank_info"] = { + "detected_ranks": list(set(ranks)), + "most_common_rank": max(set(ranks), key=ranks.count), + "min_rank": min(ranks), + "max_rank": max(ranks) + } + + return arch + + def _analyze_weights(self, keys: List[str], tensor_file) -> Dict[str, Any]: + """Analiza los pesos del modelo""" + analysis = { + "total_parameters": 0, + "weight_statistics": {}, + "layer_analysis": [] + } + + for key in keys[:10]: # Limitar para rendimiento + try: + tensor = tensor_file.get_tensor(key) + + # Convertir a numpy para análisis + if hasattr(tensor, 'numpy'): + weights = tensor.numpy() + else: + weights = np.array(tensor) + + stats = { + "layer": key, + "mean": float(np.mean(weights)), + "std": float(np.std(weights)), + "min": float(np.min(weights)), + "max": float(np.max(weights)), + "non_zero_ratio": float(np.count_nonzero(weights) / weights.size) + } + + analysis["layer_analysis"].append(stats) + analysis["total_parameters"] += weights.size + except: + continue + + return analysis + + def _analyze_weights_dict(self, keys: List[str], weights: Dict) -> Dict[str, Any]: + """Analiza los pesos desde diccionario""" + analysis = { + "total_parameters": 0, + "layer_analysis": [] + } + + for key in keys[:10]: + try: + tensor = weights[key] + if hasattr(tensor, 'numpy'): + w = tensor.numpy() + else: + w = np.array(tensor) + + stats = { + "layer": key, + "mean": float(np.mean(w)), + "std": float(np.std(w)), + "min": float(np.min(w)), + "max": float(np.max(w)), + "non_zero_ratio": float(np.count_nonzero(w) / w.size) + } + + analysis["layer_analysis"].append(stats) + analysis["total_parameters"] += w.size + except: + continue + + return analysis + + def _generate_recommendations(self, analysis: Dict) -> List[str]: + """Genera recomendaciones basadas en el análisis""" + recommendations = [] + + # Recomendaciones basadas en rank + if "architecture" in analysis and "rank_info" in analysis["architecture"]: + rank_info = analysis["architecture"]["rank_info"] + if rank_info: + common_rank = rank_info.get("most_common_rank", 0) + + if common_rank < 16: + recommendations.append( + f"Rank bajo ({common_rank}): Bueno para eficiencia. " + "Para más detalles, prueba rank 32-64." + ) + elif common_rank > 128: + recommendations.append( + f"Rank alto ({common_rank}): Mucho detalle pero más pesado. " + "Considera reducir a 64-128 para mejor balance." + ) + else: + recommendations.append( + f"Rank óptimo ({common_rank}): Buen balance entre detalle y eficiencia." + ) + + # Recomendaciones basadas en metadatos + if "metadata" in analysis and analysis["metadata"]: + metadata = analysis["metadata"] + + if "ss_num_train_images" in metadata: + num_images = metadata["ss_num_train_images"] + try: + num_images = int(num_images) + if num_images < 20: + recommendations.append( + f"Dataset pequeño ({num_images} imágenes): " + "Considera aumentar a 30-50 imágenes para mejor generalización." + ) + elif num_images > 100: + recommendations.append( + f"Dataset grande ({num_images} imágenes): " + "Excelente para capturar variaciones." + ) + except: + pass + + if "ss_learning_rate" in metadata: + lr = metadata["ss_learning_rate"] + recommendations.append(f"Learning rate usado: {lr}") + + if not recommendations: + recommendations.append( + "Sube el archivo LoRA para obtener recomendaciones específicas." + ) + + return recommendations + + +def format_analysis_report(analysis: Dict) -> str: + """Formatea el análisis como texto legible""" + report = [] + report.append("=" * 70) + report.append("REPORTE DE ANÁLISIS LORA") + report.append("=" * 70) + report.append("") + + # Información del archivo + if "file_info" in analysis: + report.append("📁 INFORMACIÓN DEL ARCHIVO:") + info = analysis["file_info"] + report.append(f" Nombre: {info.get('nombre', 'N/A')}") + report.append(f" Tamaño: {info.get('tamaño_mb', 0)} MB") + report.append(f" Formato: {info.get('extension', 'N/A')}") + report.append("") + + # Arquitectura + if "architecture" in analysis and analysis["architecture"]: + report.append("🏗️ ARQUITECTURA:") + arch = analysis["architecture"] + report.append(f" Total de capas: {arch.get('total_layers', 0)}") + + if "rank_info" in arch and arch["rank_info"]: + rank = arch["rank_info"] + report.append(f" Rank más común: {rank.get('most_common_rank', 'N/A')}") + report.append(f" Rango de ranks: {rank.get('min_rank', 'N/A')} - {rank.get('max_rank', 'N/A')}") + + report.append("") + + # Metadatos + if "metadata" in analysis and analysis["metadata"]: + report.append("⚙️ METADATOS DE ENTRENAMIENTO:") + meta = analysis["metadata"] + + key_info = { + "ss_base_model": "Modelo base", + "ss_network_dim": "Network dim (rank)", + "ss_network_alpha": "Alpha", + "ss_learning_rate": "Learning rate", + "ss_num_epochs": "Épocas", + "ss_num_train_images": "Imágenes de entrenamiento", + "ss_batch_size": "Batch size", + "ss_resolution": "Resolución" + } + + for key, label in key_info.items(): + if key in meta: + report.append(f" {label}: {meta[key]}") + + report.append("") + + # Análisis de pesos + if "weights_analysis" in analysis and analysis["weights_analysis"]: + weights = analysis["weights_analysis"] + if "total_parameters" in weights: + report.append("📊 ANÁLISIS DE PESOS:") + report.append(f" Total parámetros: {weights['total_parameters']:,}") + report.append("") + + # Recomendaciones + if "recommendations" in analysis and analysis["recommendations"]: + report.append("💡 RECOMENDACIONES:") + for i, rec in enumerate(analysis["recommendations"], 1): + report.append(f" {i}. {rec}") + report.append("") + + report.append("=" * 70) + + return "\n".join(report) diff --git a/lora_api.py b/lora_api.py new file mode 100644 index 0000000..e50f2a3 --- /dev/null +++ b/lora_api.py @@ -0,0 +1,385 @@ +""" +LoRA Analyzer API REST +API con FastAPI para analizar archivos LoRA programáticamente +""" + +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from typing import List +import tempfile +import shutil +from pathlib import Path +import numpy as np +from lora_analyzer import LoRAAnalyzer + + +def convert_numpy_types(obj): + """Convierte tipos numpy a tipos nativos de Python para serialización JSON""" + if isinstance(obj, dict): + return {key: convert_numpy_types(value) for key, value in obj.items()} + elif isinstance(obj, list): + return [convert_numpy_types(item) for item in obj] + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, set): + return list(obj) + else: + return obj + + +# Crear la aplicación FastAPI +app = FastAPI( + title="LoRA Analyzer API", + description="API REST para analizar archivos LoRA (.safetensors, .pt, .ckpt)", + version="1.0.0" +) + +# Configurar CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + """Endpoint raíz con información de la API""" + return { + "name": "LoRA Analyzer API", + "version": "1.0.0", + "endpoints": { + "GET /": "Esta información", + "POST /analyze": "Analiza un archivo LoRA", + "POST /analyze/batch": "Analiza múltiples archivos LoRA", + "POST /compare": "Compara múltiples archivos LoRA", + "GET /health": "Estado de salud del servicio", + "GET /docs": "Documentación interactiva (Swagger)" + }, + "supported_formats": [".safetensors", ".pt", ".pth", ".ckpt"], + "documentation": "/docs" + } + + +@app.get("/health") +async def health_check(): + """Verifica el estado del servicio""" + return { + "status": "healthy", + "service": "lora-analyzer-api", + "version": "1.0.0" + } + + +@app.post("/analyze") +async def analyze_lora(file: UploadFile = File(...)): + """ + Analiza un único archivo LoRA + + Args: + file: Archivo LoRA (.safetensors, .pt, .pth, .ckpt) + + Returns: + JSON con análisis completo del LoRA + """ + # Validar extensión + file_ext = Path(file.filename).suffix.lower() + if file_ext not in [".safetensors", ".pt", ".pth", ".ckpt"]: + raise HTTPException( + status_code=400, + detail=f"Formato no soportado: {file_ext}. Use .safetensors, .pt, .pth o .ckpt" + ) + + # Guardar archivo temporal + temp_file = None + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as temp_file: + shutil.copyfileobj(file.file, temp_file) + temp_path = temp_file.name + + # Analizar el archivo + analyzer = LoRAAnalyzer(temp_path) + analysis = analyzer.analyze() + + # Convertir tipos numpy a tipos nativos de Python + analysis = convert_numpy_types(analysis) + + return JSONResponse(content={ + "success": True, + "filename": file.filename, + "analysis": analysis + }) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error al analizar el archivo: {str(e)}" + ) + + finally: + # Limpiar archivo temporal + if temp_file: + try: + Path(temp_path).unlink() + except: + pass + + +@app.post("/analyze/batch") +async def analyze_batch(files: List[UploadFile] = File(...)): + """ + Analiza múltiples archivos LoRA + + Args: + files: Lista de archivos LoRA + + Returns: + JSON con análisis de todos los archivos + """ + if not files: + raise HTTPException(status_code=400, detail="No se proporcionaron archivos") + + results = [] + errors = [] + + for file in files: + file_ext = Path(file.filename).suffix.lower() + if file_ext not in [".safetensors", ".pt", ".pth", ".ckpt"]: + errors.append({ + "filename": file.filename, + "error": f"Formato no soportado: {file_ext}" + }) + continue + + temp_file = None + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as temp_file: + shutil.copyfileobj(file.file, temp_file) + temp_path = temp_file.name + + analyzer = LoRAAnalyzer(temp_path) + analysis = analyzer.analyze() + + # Convertir tipos numpy + analysis = convert_numpy_types(analysis) + + results.append({ + "filename": file.filename, + "analysis": analysis + }) + + except Exception as e: + errors.append({ + "filename": file.filename, + "error": str(e) + }) + + finally: + if temp_file: + try: + Path(temp_path).unlink() + except: + pass + + return JSONResponse(content={ + "success": True, + "total_files": len(files), + "analyzed": len(results), + "failed": len(errors), + "results": results, + "errors": errors + }) + + +@app.post("/compare") +async def compare_loras(files: List[UploadFile] = File(...)): + """ + Compara múltiples archivos LoRA + + Args: + files: Lista de al menos 2 archivos LoRA + + Returns: + JSON con comparación detallada + """ + if len(files) < 2: + raise HTTPException( + status_code=400, + detail="Se requieren al menos 2 archivos para comparar" + ) + + analyses = [] + temp_files = [] + + try: + # Analizar todos los archivos + for file in files: + file_ext = Path(file.filename).suffix.lower() + if file_ext not in [".safetensors", ".pt", ".pth", ".ckpt"]: + raise HTTPException( + status_code=400, + detail=f"Formato no soportado: {file_ext}" + ) + + with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as temp_file: + shutil.copyfileobj(file.file, temp_file) + temp_path = temp_file.name + temp_files.append(temp_path) + + analyzer = LoRAAnalyzer(temp_path) + analysis = analyzer.analyze() + + # Convertir tipos numpy + analysis = convert_numpy_types(analysis) + + analyses.append({ + "filename": file.filename, + "analysis": analysis + }) + + # Crear comparación + comparison = create_comparison(analyses) + + return JSONResponse(content={ + "success": True, + "total_files": len(files), + "comparison": comparison + }) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error al comparar archivos: {str(e)}" + ) + + finally: + # Limpiar archivos temporales + for temp_path in temp_files: + try: + Path(temp_path).unlink() + except: + pass + + +def create_comparison(analyses): + """Crea un objeto de comparación estructurado""" + comparison = { + "files": [], + "comparison_table": {} + } + + # Información de cada archivo + for item in analyses: + analysis = item["analysis"] + comparison["files"].append({ + "filename": item["filename"], + "size_mb": analysis["file_info"].get("tamaño_mb", 0), + "format": analysis["file_info"].get("extension", "unknown") + }) + + # Tabla de comparación + features = [ + ("size_mb", "Tamaño (MB)", lambda a: a["file_info"].get("tamaño_mb", "N/A")), + ("total_layers", "Total capas", lambda a: a.get("architecture", {}).get("total_layers", "N/A")), + ("rank", "Rank", lambda a: a.get("architecture", {}).get("rank_info", {}).get("most_common_rank", "N/A")), + ("base_model", "Modelo base", lambda a: a.get("metadata", {}).get("ss_base_model", "N/A")), + ("train_images", "Imágenes entreno", lambda a: a.get("metadata", {}).get("ss_num_train_images", "N/A")), + ("learning_rate", "Learning rate", lambda a: a.get("metadata", {}).get("ss_learning_rate", "N/A")), + ("epochs", "Épocas", lambda a: a.get("metadata", {}).get("ss_num_epochs", "N/A")), + ] + + for key, label, getter in features: + comparison["comparison_table"][key] = { + "label": label, + "values": [getter(item["analysis"]) for item in analyses] + } + + return comparison + + +# Agregar documentación personalizada +@app.get("/examples") +async def api_examples(): + """Ejemplos de uso de la API""" + return { + "curl_examples": { + "analyze_single": """ +curl -X POST "http://localhost:8000/analyze" \\ + -H "accept: application/json" \\ + -H "Content-Type: multipart/form-data" \\ + -F "file=@mi_lora.safetensors" + """, + "analyze_batch": """ +curl -X POST "http://localhost:8000/analyze/batch" \\ + -H "accept: application/json" \\ + -H "Content-Type: multipart/form-data" \\ + -F "files=@lora1.safetensors" \\ + -F "files=@lora2.pt" + """, + "compare": """ +curl -X POST "http://localhost:8000/compare" \\ + -H "accept: application/json" \\ + -H "Content-Type: multipart/form-data" \\ + -F "files=@lora1.safetensors" \\ + -F "files=@lora2.safetensors" + """ + }, + "python_example": """ +import requests + +# Analizar un archivo +with open('mi_lora.safetensors', 'rb') as f: + response = requests.post( + 'http://localhost:8000/analyze', + files={'file': f} + ) + print(response.json()) + +# Comparar múltiples archivos +files = [ + ('files', open('lora1.safetensors', 'rb')), + ('files', open('lora2.safetensors', 'rb')) +] +response = requests.post( + 'http://localhost:8000/compare', + files=files +) +print(response.json()) + """, + "javascript_example": """ +// Usando fetch API +const formData = new FormData(); +formData.append('file', fileInput.files[0]); + +fetch('http://localhost:8000/analyze', { + method: 'POST', + body: formData +}) +.then(response => response.json()) +.then(data => console.log(data)); + """ + } + + +if __name__ == "__main__": + import uvicorn + + print("🚀 Iniciando LoRA Analyzer API...") + print("📡 API disponible en: http://localhost:8000") + print("📚 Documentación: http://localhost:8000/docs") + print("🔍 Ejemplos: http://localhost:8000/examples") + print("🛑 Presiona Ctrl+C para detener el servidor") + + uvicorn.run( + app, + host="0.0.0.0", + port=8000, + log_level="info" + ) diff --git a/lora_cli.py b/lora_cli.py new file mode 100644 index 0000000..984e698 --- /dev/null +++ b/lora_cli.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +LoRA Analyzer CLI +Herramienta de línea de comandos para analizar archivos LoRA +""" + +import argparse +import sys +import json +from pathlib import Path +from lora_analyzer import LoRAAnalyzer, format_analysis_report + + +def main(): + parser = argparse.ArgumentParser( + description="Analiza archivos LoRA y extrae información técnica", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Ejemplos de uso: + # Analizar un archivo + python lora_cli.py mi_lora.safetensors + + # Guardar resultado en JSON + python lora_cli.py mi_lora.safetensors --output resultado.json + + # Analizar múltiples archivos + python lora_cli.py lora1.safetensors lora2.pt lora3.ckpt + + # Modo verbose con más detalles + python lora_cli.py mi_lora.safetensors --verbose + """ + ) + + parser.add_argument( + "files", + nargs="+", + help="Archivo(s) LoRA a analizar (.safetensors, .pt, .ckpt)" + ) + + parser.add_argument( + "-o", "--output", + help="Guardar resultado en archivo JSON", + type=str + ) + + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Mostrar información detallada" + ) + + parser.add_argument( + "--json", + action="store_true", + help="Salida en formato JSON" + ) + + parser.add_argument( + "--compare", + action="store_true", + help="Comparar múltiples LoRAs (requiere 2+ archivos)" + ) + + args = parser.parse_args() + + # Validar archivos + files = [Path(f) for f in args.files] + for f in files: + if not f.exists(): + print(f"❌ Error: El archivo no existe: {f}", file=sys.stderr) + sys.exit(1) + + results = [] + + # Analizar cada archivo + for file_path in files: + print(f"\n🔍 Analizando: {file_path.name}") + print("-" * 70) + + try: + analyzer = LoRAAnalyzer(str(file_path)) + analysis = analyzer.analyze() + results.append(analysis) + + if args.json: + print(json.dumps(analysis, indent=2, ensure_ascii=False)) + else: + print(format_analysis_report(analysis)) + + except Exception as e: + print(f"❌ Error al analizar {file_path.name}: {str(e)}", file=sys.stderr) + if args.verbose: + import traceback + traceback.print_exc() + + # Modo comparación + if args.compare and len(results) > 1: + print("\n" + "=" * 70) + print("📊 COMPARACIÓN DE LORAS") + print("=" * 70) + compare_loras(results) + + # Guardar en JSON si se especificó + if args.output and results: + output_path = Path(args.output) + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(results, f, indent=2, ensure_ascii=False) + print(f"\n✅ Resultados guardados en: {output_path}") + + +def compare_loras(results): + """Compara múltiples LoRAs""" + print("\n📋 Comparación de características:\n") + + # Tabla comparativa + headers = ["Característica"] + [r["file_info"]["nombre"] for r in results] + + comparisons = [] + + # Tamaño + row = ["Tamaño (MB)"] + for r in results: + row.append(f"{r['file_info'].get('tamaño_mb', 'N/A')}") + comparisons.append(row) + + # Rank + row = ["Rank"] + for r in results: + rank_info = r.get("architecture", {}).get("rank_info", {}) + rank = rank_info.get("most_common_rank", "N/A") + row.append(str(rank)) + comparisons.append(row) + + # Capas + row = ["Total capas"] + for r in results: + layers = r.get("architecture", {}).get("total_layers", "N/A") + row.append(str(layers)) + comparisons.append(row) + + # Imágenes de entrenamiento + row = ["Imágenes entreno"] + for r in results: + num_images = r.get("metadata", {}).get("ss_num_train_images", "N/A") + row.append(str(num_images)) + comparisons.append(row) + + # Learning rate + row = ["Learning rate"] + for r in results: + lr = r.get("metadata", {}).get("ss_learning_rate", "N/A") + row.append(str(lr)) + comparisons.append(row) + + # Imprimir tabla + col_widths = [max(len(str(row[i])) for row in [headers] + comparisons) + 2 + for i in range(len(headers))] + + # Header + print(" ".join(h.ljust(w) for h, w in zip(headers, col_widths))) + print("-" * sum(col_widths)) + + # Filas + for row in comparisons: + print(" ".join(str(cell).ljust(w) for cell, w in zip(row, col_widths))) + + +if __name__ == "__main__": + main() diff --git a/lora_webapp.py b/lora_webapp.py new file mode 100644 index 0000000..25d1594 --- /dev/null +++ b/lora_webapp.py @@ -0,0 +1,391 @@ +""" +LoRA Analyzer Web App +Interfaz web con Gradio para analizar archivos LoRA +Versión mejorada con interfaz en español +""" + +import gradio as gr +import json +from pathlib import Path +import tempfile +import numpy as np +from lora_analyzer import LoRAAnalyzer, format_analysis_report + + +def convert_numpy_types(obj): + """Convierte tipos numpy a tipos nativos de Python para serialización JSON""" + if isinstance(obj, dict): + return {key: convert_numpy_types(value) for key, value in obj.items()} + elif isinstance(obj, list): + return [convert_numpy_types(item) for item in obj] + elif isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, set): + return list(obj) + else: + return obj + + +def analyze_lora_file(file): + """Analiza un archivo LoRA subido por el usuario""" + if file is None: + return "❌ Por favor sube un archivo LoRA primero", "{}", "⚠️ Sin archivo para analizar" + + try: + # Analizar el archivo + analyzer = LoRAAnalyzer(file.name) + analysis = analyzer.analyze() + + # Convertir tipos numpy a tipos nativos de Python + analysis = convert_numpy_types(analysis) + + # Formato de reporte legible + report = format_analysis_report(analysis) + + # JSON formateado + json_output = json.dumps(analysis, indent=2, ensure_ascii=False) + + # Información clave para la UI + summary = generate_summary(analysis) + + return report, json_output, summary + + except Exception as e: + error_msg = f"❌ Error al analizar el archivo: {str(e)}\n\n💡 Verifica que sea un archivo LoRA válido (.safetensors, .pt, .ckpt)" + return error_msg, "{}", error_msg + + +def generate_summary(analysis): + """Genera un resumen visual de la información clave""" + summary_parts = [] + + # Información del archivo + if "file_info" in analysis: + info = analysis["file_info"] + summary_parts.append(f"📁 **{info.get('nombre', 'N/A')}**") + summary_parts.append(f"💾 Tamaño: {info.get('tamaño_mb', 0)} MB") + + # Arquitectura + if "architecture" in analysis: + arch = analysis["architecture"] + summary_parts.append(f"🏗️ Capas: {arch.get('total_layers', 0)}") + + if "rank_info" in arch and arch["rank_info"]: + rank = arch["rank_info"].get("most_common_rank", "N/A") + summary_parts.append(f"📊 Rank: {rank}") + + # Metadatos clave + if "metadata" in analysis: + meta = analysis["metadata"] + + if "ss_base_model" in meta: + model = str(meta["ss_base_model"]) + if len(model) > 50: + model = model[:47] + "..." + summary_parts.append(f"🤖 Modelo base: {model}") + + if "ss_num_train_images" in meta: + summary_parts.append(f"🖼️ Imágenes entrenamiento: {meta['ss_num_train_images']}") + + if "ss_learning_rate" in meta: + summary_parts.append(f"📈 Learning rate: {meta['ss_learning_rate']}") + + # Recomendaciones + if "recommendations" in analysis and analysis["recommendations"]: + summary_parts.append("\n**💡 Recomendaciones principales:**") + for i, rec in enumerate(analysis["recommendations"][:3], 1): + summary_parts.append(f"{i}. {rec}") + + return "\n\n".join(summary_parts) if summary_parts else "No hay información disponible" + + +def compare_loras(files): + """Compara múltiples archivos LoRA""" + if not files or len(files) < 2: + return "❌ Por favor sube al menos 2 archivos LoRA para comparar" + + try: + results = [] + for file in files: + analyzer = LoRAAnalyzer(file.name) + analysis = analyzer.analyze() + # Convertir tipos numpy + analysis = convert_numpy_types(analysis) + results.append(analysis) + + # Crear tabla comparativa + comparison = create_comparison_table(results) + return comparison + + except Exception as e: + return f"❌ Error al comparar archivos: {str(e)}" + + +def create_comparison_table(results): + """Crea una tabla comparativa de múltiples LoRAs""" + if not results: + return "No hay resultados para comparar" + + table = "# 📊 Comparación de LoRAs\n\n" + + # Nombres de archivos + table += "| Característica | " + " | ".join(r["file_info"]["nombre"] for r in results) + " |\n" + table += "|" + "---|" * (len(results) + 1) + "\n" + + # Filas comparativas + rows = [ + ("Tamaño (MB)", lambda r: f"{r['file_info'].get('tamaño_mb', 'N/A')}"), + ("Formato", lambda r: r['file_info'].get('extension', 'N/A')), + ("Total capas", lambda r: r.get('architecture', {}).get('total_layers', 'N/A')), + ("Rank", lambda r: r.get('architecture', {}).get('rank_info', {}).get('most_common_rank', 'N/A')), + ("Modelo base", lambda r: str(r.get('metadata', {}).get('ss_base_model', 'N/A'))[:30]), + ("Imágenes entreno", lambda r: r.get('metadata', {}).get('ss_num_train_images', 'N/A')), + ("Learning rate", lambda r: r.get('metadata', {}).get('ss_learning_rate', 'N/A')), + ("Epochs", lambda r: r.get('metadata', {}).get('ss_num_epochs', 'N/A')), + ] + + for label, getter in rows: + row = f"| {label} | " + row += " | ".join(str(getter(r)) for r in results) + row += " |\n" + table += row + + return table + + +# Crear la interfaz Gradio +with gr.Blocks(title="LoRA Analyzer", theme=gr.themes.Soft()) as app: + gr.Markdown(""" + # 🔍 LoRA Analyzer - Analizador de Archivos LoRA + ### Descubre la arquitectura, metadatos y configuración de entrenamiento de tus LoRAs + + **📁 Formatos soportados:** `.safetensors` (recomendado), `.pt`, `.pth`, `.ckpt` + + **💡 Tip rápido:** Arrastra tu archivo LoRA en la zona de carga y haz clic en "🔍 Analizar" + """) + + with gr.Tabs(): + # Tab 1: Análisis individual + with gr.Tab("📄 Analizar LoRA"): + gr.Markdown("### Sube un archivo LoRA para análisis completo") + + with gr.Row(): + with gr.Column(scale=1): + file_input = gr.File( + label="Arrastra tu archivo LoRA aquí", + file_types=[".safetensors", ".pt", ".pth", ".ckpt"] + ) + analyze_btn = gr.Button("🔍 Analizar", variant="primary", size="lg") + + with gr.Column(scale=1): + summary_output = gr.Markdown(label="Resumen") + + with gr.Row(): + with gr.Column(): + report_output = gr.Textbox( + label="Reporte Completo", + lines=20, + max_lines=30 + ) + + with gr.Column(): + json_output = gr.Code( + label="Datos JSON", + language="json", + lines=20 + ) + + analyze_btn.click( + fn=analyze_lora_file, + inputs=[file_input], + outputs=[report_output, json_output, summary_output] + ) + + # Tab 2: Comparación + with gr.Tab("📊 Comparar LoRAs"): + gr.Markdown("### Sube 2 o más archivos LoRA para comparar") + + files_input = gr.File( + label="Arrastra múltiples archivos LoRA aquí", + file_count="multiple", + file_types=[".safetensors", ".pt", ".pth", ".ckpt"] + ) + compare_btn = gr.Button("⚖️ Comparar", variant="primary", size="lg") + + comparison_output = gr.Markdown(label="Comparación") + + compare_btn.click( + fn=compare_loras, + inputs=[files_input], + outputs=[comparison_output] + ) + + # Tab 3: Información y ayuda + with gr.Tab("📖 Cómo Usar"): + gr.Markdown(""" + ## 🚀 Guía Rápida de Uso + + ### Paso 1: Consigue un archivo LoRA + - Descarga LoRAs de [CivitAI](https://civitai.com) o [Hugging Face](https://huggingface.co) + - O usa tus propios LoRAs entrenados + - Formatos aceptados: `.safetensors`, `.pt`, `.pth`, `.ckpt` + + ### Paso 2: Analiza un LoRA + 1. Ve a la pestaña **"📄 Analizar LoRA"** + 2. Arrastra tu archivo a la zona de carga (o haz clic para seleccionar) + 3. Haz clic en el botón **"🔍 Analizar"** + 4. ¡Listo! Verás el análisis completo + + ### Paso 3: Lee los resultados + - **Panel izquierdo (Resumen)**: Información clave visual + - **Panel central (Reporte)**: Análisis completo en texto + - **Panel derecho (JSON)**: Datos técnicos en formato JSON + + ### Paso 4: Compara LoRAs (opcional) + 1. Ve a la pestaña **"📊 Comparar LoRAs"** + 2. Sube 2 o más archivos LoRA + 3. Haz clic en **"⚖️ Comparar"** + 4. Verás una tabla comparativa + + --- + + ## 📊 ¿Qué información obtienes? + + ### ✅ Arquitectura del LoRA + - **Rank**: Dimensión de las matrices (8, 16, 32, 64, 128...) + - **Alpha**: Factor de escalado + - **Capas**: Número total de capas modificadas + - **Parámetros**: Total de parámetros entrenables + + ### ✅ Metadatos de Entrenamiento + - **Modelo base**: SD 1.5, SDXL, etc. + - **Learning rate**: Tasa de aprendizaje + - **Epochs**: Número de épocas + - **Batch size**: Tamaño del lote + - **Resolución**: Resolución de entrenamiento + - **Dataset**: Número de imágenes usadas + + ### ✅ Recomendaciones + - Sugerencias para optimizar tus LoRAs + - Comparación con mejores prácticas + - Ideas para mejorar resultados + + --- + + ## 💡 Casos de Uso + + ### 🔍 Ingeniería Inversa + Analiza LoRAs públicos exitosos para aprender: + - ¿Qué rank utilizaron? + - ¿Cuántas imágenes necesitaron? + - ¿Qué learning rate funcionó? + + ### 🎯 Optimización + Compara diferentes versiones de tu LoRA: + - Encuentra la configuración óptima + - Identifica qué cambios mejoraron resultados + - Ahorra tiempo en experimentación + + ### 🐛 Debugging + Detecta problemas en tus LoRAs: + - Verifica que se entrenó correctamente + - Confirma los parámetros usados + - Identifica configuraciones incorrectas + + ### 📚 Investigación + Estudia diferentes enfoques: + - Compara técnicas de entrenamiento + - Analiza múltiples LoRAs + - Documenta mejores prácticas + + --- + + ## ⚠️ Limitaciones Importantes + + ### ❌ NO puedes recuperar: + - **Imágenes originales del dataset** (técnicamente imposible) + - **Prompts exactos usados** (a menos que estén en metadatos) + - **Detalles del preprocesamiento** (cómo se limpiaron las imágenes) + + ### ⚠️ Dependencias: + - La cantidad de información depende de cómo fue guardado el LoRA + - LoRAs de Kohya suelen tener más metadatos + - Algunos LoRAs antiguos pueden tener información limitada + + --- + + ## 🛠️ Otras Herramientas Disponibles + + Además de esta Web App, también puedes usar: + + ### 📟 CLI (Línea de Comandos) + ```bash + python lora_cli.py mi_lora.safetensors + python lora_cli.py lora1.safetensors lora2.safetensors --compare + ``` + Ideal para: análisis rápido, automatización, scripts + + ### 🔌 API REST + ```bash + python lora_api.py + # Visita http://localhost:8000/docs + ``` + Ideal para: integración con otras apps, procesamiento masivo + + --- + + ## 📞 Necesitas Ayuda? + + 1. **Verifica la instalación**: `python test_installation.py` + 2. **Lee el README.md**: Documentación completa + 3. **Revisa examples.py**: Ejemplos de código + 4. **Archivos de prueba**: Descarga LoRAs de ejemplo de CivitAI + + --- + + ## 🎓 Tips y Mejores Prácticas + + ### Para Analizar: + - Usa archivos `.safetensors` cuando sea posible (más seguros) + - Verifica que el archivo no esté corrupto + - Ten paciencia con archivos grandes (>500MB) + + ### Para Comparar: + - Compara LoRAs del mismo tipo (mismo modelo base) + - Enfócate en diferencias de rank y dataset + - Usa la comparación para A/B testing + + ### Para Aprender: + - Analiza varios LoRAs populares de tu estilo favorito + - Documenta qué configuraciones funcionan mejor + - Experimenta con los parámetros que descubras + + --- + + **📚 Desarrollado para la comunidad de ML y creadores con IA** + + *Versión Web App - LoRA Analyzer v1.0* + """) + + + gr.Markdown(""" + --- + 💡 **Tip**: Para obtener mejores resultados, asegúrate de que tus LoRAs incluyan metadatos de entrenamiento. + """) + + +if __name__ == "__main__": + print("🚀 Iniciando LoRA Analyzer Web App...") + print("📱 Abre tu navegador en: http://localhost:7860") + print("🛑 Presiona Ctrl+C para detener el servidor") + + app.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + show_error=True + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d35fdc9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# LoRA Analyzer - Dependencias +# Instalar con: pip install -r requirements.txt + +# Core dependencies +numpy>=1.24.0 +safetensors>=0.4.0 +torch>=2.0.0 + +# Web App (Gradio) +gradio>=4.0.0 + +# API REST (FastAPI) +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +python-multipart>=0.0.6 + +# Utilities +pathlib2>=2.3.7 diff --git a/test_installation.py b/test_installation.py new file mode 100644 index 0000000..165e820 --- /dev/null +++ b/test_installation.py @@ -0,0 +1,144 @@ +""" +Test de instalación - LoRA Analyzer +Verifica que todas las dependencias estén instaladas correctamente +""" + +import sys + + +def test_imports(): + """Verifica que todos los módulos necesarios estén disponibles""" + print("🔍 Verificando instalación...\n") + + results = [] + + # Core dependencies + tests = [ + ("numpy", "NumPy", "pip install numpy"), + ("safetensors", "SafeTensors", "pip install safetensors"), + ("torch", "PyTorch", "pip install torch"), + ("gradio", "Gradio (Web App)", "pip install gradio"), + ("fastapi", "FastAPI (API)", "pip install fastapi"), + ("uvicorn", "Uvicorn (API)", "pip install uvicorn[standard]"), + ] + + for module_name, display_name, install_cmd in tests: + try: + __import__(module_name) + print(f"✅ {display_name:<25} - Instalado") + results.append(True) + except ImportError: + print(f"❌ {display_name:<25} - NO instalado") + print(f" Instalar con: {install_cmd}") + results.append(False) + + print("\n" + "=" * 70) + + if all(results): + print("✅ ¡Todas las dependencias están instaladas!") + print("\n🚀 Puedes usar:") + print(" • CLI: python lora_cli.py ") + print(" • Web App: python lora_webapp.py") + print(" • API: python lora_api.py") + return True + else: + print("⚠️ Faltan algunas dependencias") + print("\n📦 Para instalar todo:") + print(" pip install -r requirements.txt") + return False + + +def test_analyzer_module(): + """Verifica que el módulo principal funcione""" + print("\n" + "=" * 70) + print("🧪 Probando módulo de análisis...\n") + + try: + from lora_analyzer import LoRAAnalyzer, format_analysis_report + print("✅ Módulo 'lora_analyzer' cargado correctamente") + + # Verificar que las clases existen + assert hasattr(LoRAAnalyzer, 'analyze') + assert callable(format_analysis_report) + print("✅ Todas las funciones principales disponibles") + + return True + except Exception as e: + print(f"❌ Error al cargar el módulo: {str(e)}") + return False + + +def display_system_info(): + """Muestra información del sistema""" + print("\n" + "=" * 70) + print("💻 Información del sistema:\n") + + print(f"Python: {sys.version}") + print(f"Plataforma: {sys.platform}") + + try: + import torch + print(f"PyTorch: {torch.__version__}") + print(f"CUDA disponible: {torch.cuda.is_available()}") + if torch.cuda.is_available(): + print(f"CUDA version: {torch.version.cuda}") + except: + pass + + try: + import numpy + print(f"NumPy: {numpy.__version__}") + except: + pass + + try: + import gradio + print(f"Gradio: {gradio.__version__}") + except: + pass + + try: + import fastapi + print(f"FastAPI: {fastapi.__version__}") + except: + pass + + +def show_next_steps(): + """Muestra los próximos pasos""" + print("\n" + "=" * 70) + print("📚 Próximos pasos:\n") + + print("1. Consigue un archivo LoRA (.safetensors, .pt, .ckpt)") + print("2. Prueba la CLI:") + print(" python lora_cli.py tu_archivo.safetensors") + print("\n3. O inicia la Web App:") + print(" python lora_webapp.py") + print("\n4. O inicia la API:") + print(" python lora_api.py") + print("\n5. Lee el README.md para más ejemplos y documentación") + print("\n💡 Tip: Puedes descargar LoRAs de ejemplo de:") + print(" • https://civitai.com") + print(" • https://huggingface.co") + print("=" * 70) + + +if __name__ == "__main__": + print("\n" + "=" * 70) + print(" 🔍 LoRA Analyzer - Test de Instalación") + print("=" * 70 + "\n") + + # Ejecutar tests + deps_ok = test_imports() + module_ok = test_analyzer_module() + display_system_info() + + if deps_ok and module_ok: + print("\n" + "=" * 70) + print("✅ ¡TODO LISTO! La instalación es correcta") + show_next_steps() + else: + print("\n" + "=" * 70) + print("⚠️ Por favor instala las dependencias faltantes") + print("\nEjecuta: pip install -r requirements.txt") + print("=" * 70)