Orden en 74.000 fotos de familia

Ingenieria 04/08/2026

Años de móviles, tarjetas SD y copias de seguridad de WhatsApp acaban en lo mismo: decenas de miles de fotos donde las de verdad (la familia) están enterradas bajo memes, capturas de pantalla y tarjetas de buenos días. Me monté Immich en casa para sacar las fotos de Google, y luego usé IA local para separar el grano de la paja: 74.000 fotos revisadas por máquina, y la máquina no borró ni una. Así lo hice.

Categoría:

Ingenieria

Cliente:

Soft2Help (Internal)

Tecnologías:

Immich, Python, CLIP (open_clip), PyTorch, Docker, CUDA / GPU propia, TrueNAS / ZFS, NFS

Resumen del proyecto

El punto de partida

Como casi todo el mundo, mi fototeca familiar era una suma de restos: el carrete de varios móviles, tarjetas de cámaras viejas, carpetas sueltas en discos duros y, lo peor de todo, los backups de WhatsApp de años. Quien haya abierto alguna vez la carpeta de imágenes de WhatsApp sabe lo que hay dentro: por cada foto real de tus hijos hay diez memes, tarjetas de «buenos días familia», capturas de conversaciones, recibos fotografiados y cadenas reenviadas mil veces. Todo eso, mezclado, sumaba más de 74.000 imágenes.

Y encima estaba donde no quería que estuviera: en Google. Las fotos de mi familia son probablemente lo más privado que tengo en digital, y las tenía en el mismo sitio donde ya no me caben sin pagar y donde las condiciones las pone otro. Ese argumento lo cuento más en serio en cómo usar ChatGPT en la empresa sin regalar tus datos; con las fotos de los niños aplica el doble.

La base: Immich en casa

Elegí Immich tras comparar con PhotoPrism, LibrePhotos y Nextcloud Photos. Gana en lo que importa para sustituir de verdad a Google Fotos: app móvil con subida automática, reconocimiento de caras que funciona, búsqueda en lenguaje natural («tarta de cumpleaños», «playa al atardecer») y mapa por GPS. Es, con diferencia, el proyecto más vivo de los cuatro.

El hierro: un servidor, tres VMs, y la frontera personal/trabajo

Todo esto corre en un único servidor de segunda mano: un Dell PowerEdge R720XD con Proxmox, la caja clásica de homelab (dos procesadores, mucha RAM DDR3 barata y doce bahías de disco). Sobre él, tres máquinas virtuales con papeles muy distintos:

Móviles de la familia (app Immich, subida automática)
                        │
                        ▼
  ┌────────────────────────────────────────────────────────────┐
  │  Dell R720XD · Proxmox                                     │
  │                                                            │
  │  ┌───────────────────────────┐                             │
  │  │ VM de servicios (GPU)     │ ◄── 2× Tesla P4             │
  │  │  Immich + Postgres        │                             │
  │  │  caras · CLIP · vídeo     │                             │
  │  └──────────┬────────────────┘                             │
  │             │ NFS (solo el dataset de fotos)               │
  │             ▼                                              │
  │  ┌───────────────────────────┐  ┌────────────────────────┐ │
  │  │ VM TrueNAS PERSONAL       │  │ VM TrueNAS TRABAJO     │ │
  │  │  pool ZFS propio (3 HDD)  │  │  pool ZFS propio       │ │
  │  │  dataset photos           │  │  (raidz1, 4 HDD)       │ │
  │  │  snapshots 30d + 12sem    │  │  backups + assets      │ │
  │  └───────────────────────────┘  └────────────────────────┘ │
  └────────────────────────────────────────────────────────────┘

La decisión que más me ha compensado es la separación personal/trabajo a nivel de NAS: no son dos carpetas, son dos TrueNAS completas, cada una con su pool ZFS sobre sus propios discos físicos. La controladora del servidor pasa los discos a cada VM y cada TrueNAS gestiona los suyos: tres discos para el pool personal, cuatro en raidz1 para el de trabajo. Nada se comparte: ni usuarios, ni permisos, ni snapshots, ni radio de explosión. Un error gordo administrando el NAS de trabajo (que los hay) no puede tocar ni un byte de las fotos de la familia, y al revés tampoco. Y el día que una de las dos necesite crecer o mudarse de servidor, se lleva sus discos y listo.

Sobre esa base, el reparto de papeles:

  • Los originales viven en la TrueNAS personal, en un dataset ZFS dedicado (photos) con snapshots automáticos: diarios que guardo 30 y semanales que guardo 12. Si mañana Immich se rompe, me equivoco borrando o un ransomware toca lo montado, las fotos están a un snapshot de solo lectura de distancia.
  • La VM de servicios monta ese dataset por NFS, y solo ese dataset: la exportación está limitada al dato que Immich necesita, no al pool. Ahí corren Immich y su Postgres, junto al resto de servicios de casa.
  • La parte de IA (caras, objetos, búsqueda semántica, transcodificado) la aceleran dos Tesla P4 pasadas a esa VM. Es una GPU sin ventilador que se encuentra de segunda mano por el precio de una cena: en CPU el primer indexado de una fototeca así se mide en días; con ella, en horas.
  • Los móviles de la familia suben solos por la app. Desde fuera de casa, por túnel; nada de puertos abiertos.
Con eso ya tienes un Google Fotos privado. Pero el problema gordo no era ese.

El problema gordo: la basura de WhatsApp

Cuando lo volqué todo, la línea de tiempo era inservible: entre las fotos del bautizo aparecían capturas de un instalador de Windows, el meme del lunes y el cartel reenviado de la verbena de 2019. Revisar 74.000 imágenes a mano, a un segundo por foto, son más de veinte horas sin parpadear. No iba a pasar.

La solución fue un script de menos de 200 líneas con CLIP en zero-shot: sin entrenar nada, sin etiquetar nada. CLIP puntúa cuánto se parece una imagen a una frase, así que escribí dos listas de frases: lo que es basura y lo que hay que conservar.

JUNK = [
    "a screenshot of a smartphone user interface",
    "a screenshot of a chat conversation",
    "a scanned document or printed page of text",
    "a photo of a receipt or ticket",
    "an internet meme with caption text",
    "a greeting card image with large text like good morning",
    "a screenshot of computer software or an application window",
    "a screenshot of a website in a web browser",
]
KEEP = [
    "a candid photo of people", "a family photograph",
    "a selfie of a person", "a photo of a child",
    "people at a party or celebration",
    "a landscape or nature photograph",
    "a photo of food", "a photo of a pet or animal",
]

Cada foto pasa por el modelo, se calcula la probabilidad de que sea basura frente a foto de verdad, y por encima de un umbral se marca. En la Tesla P4 va a unas 44 imágenes por segundo: la librería entera, unos 30 minutos.

La regla de oro: la IA no borra nada

Aquí está la decisión de diseño más importante, y es la misma que aplico en los proyectos de OCR para clientes: la máquina propone y la persona dispone. El script no borra ni una foto. Lo único que hace es meter las sospechosas en un álbum de Immich llamado «WhatsApp basura (revisar)». Luego yo abro ese álbum con un café, repaso en vista de miniaturas (ahí sí, a golpe de vista van rapidísimo, porque el álbum ya es 95% basura de verdad) y borro yo.

Y como red de seguridad extra, otro script recorre el álbum de revisión y rescata cualquier foto donde Immich haya reconocido una cara con nombre: si sale alguien de la familia etiquetado, esa foto sale del álbum de basura automáticamente. Prefiero un meme superviviente que una foto del abuelo borrada.

Borrosas y contenido delicado, en pasadas aparte

Dos limpiezas más, cada una con su herramienta:

  • Fotos borrosas: aquí no hace falta IA. La varianza del Laplaciano sobre la propia miniatura (PIL y numpy, en CPU) separa nítido de movido. Para elegir el umbral sin inventármelo, el script imprime los percentiles de una muestra de 1.500 fotos y decides con datos. Van a su propio álbum «borrosas», porque una foto movida del cumpleaños a veces se queda igualmente.
  • Contenido explícito: un clasificador NSFW abierto (un ViT de Hugging Face) hace otra pasada. En una fototeca donde entran backups de grupos de WhatsApp de años, mejor comprobarlo que suponerlo. Mismo destino: álbum de revisión, decisión humana.

Los números

Primera pasada real, sobre 60.900 imágenes:

  • 5.691 marcadas como basura (capturas, memes, documentos, tarjetas)
  • 838 marcadas por el filtro de contenido explícito
  • 5.266 borrosas a su álbum de revisión
Casi 12.000 imágenes (una de cada cinco) que no eran fotos de familia, localizadas en menos de una hora de GPU y despachadas en un par de sesiones de revisión con café. A mano no lo habría hecho nunca, y lo digo literalmente: llevaba años sin hacerlo.

Detalles que agradeces cuando lo relanzas:

  • Incremental: reutiliza el álbum si existe e Immich ignora duplicados, así que relanzar sobre toda la librería es seguro.
  • Ajustable con datos: cada pasada deja un informe JSON con la probabilidad por foto, para afinar el umbral mirando casos reales en vez de a ojo.
  • Modo ensayo: --dry-run --limit 500 para probar prompts y umbrales sin tocar ningún álbum.

El resultado

Hoy la línea de tiempo de Immich son fotos de familia, de la primera a la última. Buscas «nieve» y sale el día de la nevada, no una captura de la app del tiempo. Las caras están agrupadas y con nombre, los originales duermen en ZFS con snapshots, los móviles suben solos y Google no tiene nada.

Y la lección transferible, que es la misma de mi banco de pruebas de OCR: la IA local, bien acotada, convierte tareas imposibles por volumen en tareas de una tarde. Sin subir un solo dato a nadie, con hardware que cuesta lo que una cena, y con la última palabra siempre en manos de una persona. Eso mismo, aplicado a los documentos de una empresa en vez de a las fotos de una familia, es a lo que me dedico.

La versión de esta historia sin jerga técnica (por qué saqué las fotos de Google y qué gana una familia con esto) la cuento en el blog: Tus fotos fuera de Google: mi experiencia con Immich.

Preguntas frecuentes

¿Cómo separo las fotos de familia de los memes y capturas automáticamente?

Con un modelo de IA local (CLIP) al que le das dos listas de descripciones: qué aspecto tiene el ruido (memes, capturas de pantalla, tarjetas de buenos días, documentos fotografiados) y qué aspecto tiene una foto de verdad (gente, celebraciones, paisajes). Con eso la máquina revisó 74.000 imágenes en menos de una hora, sin enviar ni una foto fuera de casa.

¿Qué hace falta para montar Immich con IA en casa?

Un servidor de segunda mano da de sobra: en este caso un Dell R720XD con Proxmox, almacenamiento ZFS sobre TrueNAS para los originales y una GPU modesta para la parte de IA. La app del móvil sube las fotos sola, como con Google Fotos, y el reconocimiento de caras y la búsqueda por texto corren en local.

¿La IA borra fotos al clasificarlas?

No: la máquina solo clasifica y separa para revisar, y de 74.000 fotos no borró ni una. La decisión de borrar sigue siendo humana; la IA se limita a convertir veinte horas de criba manual en una lista ya ordenada que se repasa en un rato.