Un personaje generado por IA no es la misma persona de una imagen a otra. A eso se le llama deriva de identidad. Este repo la mide con un número, muestra qué la provoca, y la reduce con un LoRA entrenado en casa.
Todo corre en local sobre una AMD Strix Halo (Radeon 8060S, gfx1151), sin CUDA, con ComfyUI y musubi-tuner.
Las mismas diez escenas con las mismas semillas. Arriba sin LoRA, abajo con el LoRA de identidad. El número es la similitud media de esa cara contra las otras nueve.
| Cohesión facial par a par | |
|---|---|
| Solo prompt, por detallado que sea | 0,536 |
| LoRA de identidad, mejor checkpoint | 0,625 |
Para calibrar, los métodos publicados miden sobre sus propios conjuntos: IPAdapter 0,619, InstantID 0,725, PuLID 0,733.
Las diez escenas mejoraron sin excepción, y las que más ganaron son las que peor estaban: las de ángulo difícil.
Verificado en dos experimentos independientes, con personajes y datasets distintos: con 48 imágenes, rank 32 y lr 1e-4, el pico cae en la época 10 de 16, y el último checkpoint queda unas tres centésimas por debajo.
Quedarse con el último, que es lo que uno haría por defecto, da un modelo peor.
Por eso evaluar_checkpoints.py los mide todos y elige por número.
export TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1| Configuración | s/paso |
|---|---|
| Por defecto | 41,3 |
| Con esa variable | 9,95 |
Además sin --gradient_checkpointing |
7,7 |
Multiplica por 4,2, es gratis y no consume memoria extra. Entrenar 16 épocas sobre 48 imágenes a 1024² pasa de 8 h 49 min a 1 h 40. De paso silencia el aviso que PyTorch emite en cada llamada de atención y que hace ilegible el log.
Medido con dos corridas de dos épocas, cambiando una variable por vez.
La pregunta correcta para un personaje que no existe no es «¿se parece al original?», porque no hay original. Es ¿qué tan cohesionado es el conjunto?
medir_identidad.py calcula la matriz de similitud coseno par a par entre
embeddings faciales (ArcFace vía insightface) y resume media, dispersión y
outliers.
python medir_identidad.py salidas/ --manos
python medir_identidad.py salidas/ --ref referencia.png --cohesivo 8 --json m.jsonCuidado con los umbrales que circulan. deepface configura ArcFace en 0,68, pero eso es distancia coseno, no similitud: equivale a 0,32 de similitud para declarar «misma persona». Es un listón bajo.
Una imagen con la mano fusionada con el cuello puntuó 0,671, entre las mejores del lote, porque el reconocimiento facial mira la cara y la cara estaba perfecta.
manos.py (o --manos) compara muñecas visibles según el detector de pose
contra manos reconocibles según el detector de manos. Si hay más muñecas que
manos, hay carne de mano que no es interpretable como mano.
Límite medido: no detecta el dedo fantasma, un pulgar de más flotando junto a una mano por lo demás correcta. Ahí el detector ajusta sus puntos a la mano buena e ignora el añadido. El patrón del fallo es claro: las manos se rompen cuando tocan algo y hay oclusión; gesticulando en el aire salen bien.
- El ángulo es el factor dominante. Frontal 0,60; perfil estricto 0,457.
- La expresión. Riendo con los ojos cerrados: 0,586 contra 0,797 de la misma persona frontal.
- El tamaño de la cara en el encuadre. Un plano entero cae a 0,564.
Las tres abundan en un feed real. Seis situaciones difíciles de verdad (casco, micrófono, plano entero, comida tapando la boca) midieron 0,505, por debajo del 0,62 del conjunto de evaluación: un conjunto de prueba educado sobreestima lo que se logra en uso real.
Consejo de diseño que salió de medir: un marcador de identidad con forma sobrevive, uno puntual no. Una cicatriz en la ceja se mantiene entre imágenes; un lunar aparece, desaparece y cambia de lado.
Sigue el método iterativo de The Chosen One: la consistencia no se pide, se destila por selección. Se genera mucho, se mide, se conserva el subconjunto que ya es coherente por azar y se amplifica.
# 1. generar candidatas en 16 situaciones distintas
python personaje.py dataset --por-escena 6
# 2. destilar el dataset: 3 por escena, las más cercanas al centro de identidad
python preparar_dataset.py salidas/dataset --por-escena 3 --nombre mi-personaje
# 3. cachear y entrenar (16 checkpoints, uno por época)
entrenamiento/cachear.sh datasets/mi-personaje/dataset.toml
entrenamiento/entrenar.sh datasets/mi-personaje/dataset.toml mi-personaje
# 4. evaluar TODOS los checkpoints sobre las mismas escenas y semillas
python evaluar_checkpoints.py --patron "mi-personaje*" --base 0.536Aplicar el bucle una segunda vez, generando las candidatas con el LoRA de la primera vuelta, mejora el material de partida: el lote en bruto pasó de 0,511 a 0,579 y el dataset destilado de 0,547 a 0,612.
Las tres parecen razonables y las tres arruinan el experimento.
Seleccionar por cohesión selecciona por POSE. El primer dataset salió siendo 24 veces el mismo retrato frontal, con 0,725 de cohesión interna que parecía excelente. Era cohesión de pose disfrazada de identidad, y habría producido un LoRA que solo sabe hacer frontales. La corrección es elegir las más cercanas al centro de identidad dentro de cada escena: el dataset bueno mide 0,547, más bajo y mucho mejor.
Entrenar y evaluar sobre las mismas escenas mide memorización. 16 situaciones para entrenar, 10 distintas para medir.
Comparar contra el centro del dataset es circular. El dataset se generó con el mismo modelo y el mismo prompt, así que su centro es el promedio del modelo base: sin LoRA gana siempre (0,798 contra 0,778).
El token disparador no hizo nada. Entrenamos con el token en todos los captions y evaluamos sin usarlo nunca: 0,620. Repitiendo la evaluación con el token: 0,622, ruido puro.
Con 48 imágenes de la misma persona y captions idénticos el LoRA no tiene nada que desambiguar, así que desplaza el modelo entero en vez de guardar la identidad bajo una etiqueta. Consecuencia práctica: este LoRA no se puede apagar. Para varios personajes en un mismo modelo hay que rediseñar los captions.
entrenamiento/convertir_vae.py pasa el VAE de formato diffusers al formato
original de Black Forest Labs, que es el que espera musubi-tuner. Hace falta
porque los repos abiertos de klein solo publican el primero y el bueno vive en un
repo con aprobación manual.
Dos trampas que fallan en silencio, produciendo imágenes basura en vez de una excepción: los bloques del decoder van en orden inverso entre formatos, y la atención pasa de Linear a Conv2d 1x1. Por eso el script termina con un round-trip que mide PSNR. Dio 40,1 dB, y al conseguir después el archivo oficial la diferencia máxima resultó ser 7,7e-3, o sea solo redondeo de bf16.
- ComfyUI con FLUX.2 klein (4B) y, para entrenar, el modelo base: los destilados son para inferencia.
- musubi-tuner con los wheels de PyTorch ROCm para gfx1151.
- Para medir, un venv aparte:
insightface,onnxruntime,mediapipe,numpy<2. Corre en CPU y no compite con la GPU.
Las imágenes de este repo son de una persona que no existe. Si algo así se publica, hay que declarar que es contenido generado: lo exige el artículo 50 del reglamento europeo de IA desde el 2 de agosto de 2026 y lo piden las plataformas para personajes virtuales.
Un detalle a tener presente: los PNG de ComfyUI llevan incrustado el prompt completo y los nombres de los modelos. Cualquiera que abra el archivo los lee.
MIT.



