mirror of
https://github.com/MartinGia/lora-analyzer.git
synced 2026-08-10 19:03:36 +02:00
Primera versión: LoRA Analyzer completo con CLI, Web App y API
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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
@@ -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)
|
||||||
@@ -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 ""
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user