mirror of
https://github.com/MartinGia/lora-analyzer.git
synced 2026-03-28 17:43:01 +01:00
Primera versión: LoRA Analyzer completo con CLI, Web App y API
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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
384
README.md
Normal 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
|
||||
|
||||

|
||||
|
||||
### 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 ⭐!
|
||||
BIN
__pycache__/lora_analyzer.cpython-313.pyc
Normal file
BIN
__pycache__/lora_analyzer.cpython-313.pyc
Normal file
Binary file not shown.
246
examples.py
Normal file
246
examples.py
Normal 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
158
install_macos.sh
Normal 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
454
lora_analyzer.py
Normal 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
385
lora_api.py
Normal 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
169
lora_cli.py
Normal 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
391
lora_webapp.py
Normal 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
18
requirements.txt
Normal 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
144
test_installation.py
Normal 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)
|
||||
Reference in New Issue
Block a user