Primera versión: LoRA Analyzer completo con CLI, Web App y API

This commit is contained in:
Martin Gracia
2025-11-09 12:45:20 -05:00
commit 69992eaff6
11 changed files with 2370 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -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.

384
README.md Normal file
View File

@@ -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 ⭐!

Binary file not shown.

246
examples.py Normal file
View File

@@ -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)

158
install_macos.sh Normal file
View File

@@ -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 ""

454
lora_analyzer.py Normal file
View File

@@ -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)

385
lora_api.py Normal file
View File

@@ -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"
)

169
lora_cli.py Normal file
View File

@@ -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()

391
lora_webapp.py Normal file
View File

@@ -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
)

18
requirements.txt Normal file
View File

@@ -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

144
test_installation.py Normal file
View File

@@ -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 <archivo>")
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)