nacho50900/AmITraceable

Herramienta educativa de análisis de exposición de la privacidad: mediante OAuth con tus propias cuentas (Instagram), analiza tu huella digital pública, infiere atributos demográficos y calcula el estrechamiento de anonimato frente a datos de referencia del INE, generando un informe de privacidad con IA.

Python

1

240 commits

updated Oct 7, 2026

See the code

README

Análisis de Exposición de Identidad Digital (TFG)

Quality Gate Status Coverage Release

Base de este proyecto: plantilla de laboratorio ASW (Uniovi) yovi_en1b. Se ha adaptado sustituyendo el dominio (usuarios/juego → análisis de exposición de identidad digital) y sustituyendo el servicio users de Node.js por un backend Python/FastAPI (necesario por las librerías de NLP: spaCy, scikit-learn, DINOv2/FAISS). El servicio gamey (Rust) se ha eliminado por no aplicar a este TFG.

https://github.com/user-attachments/assets/bc91d3e4-bbb0-44b1-be25-00c0aba36d51

TFG: análisis defensivo de la propia huella digital mediante OSINT e IA. El usuario autentica su propia cuenta de Reddit y/o Instagram vía OAuth, y la herramienta genera un informe con lo que es públicamente inferible sobre él — para que decida qué quiere seguir compartiendo, no para vigilar a terceros.

Qué hace la herramienta

  1. Lee tu actividad pública (posts/comentarios de Reddit, publicaciones de Instagram) tras autenticarte vía OAuth.
  2. Analiza tu forma de escribir (huella de estilo: longitud de frase, vocabulario, emojis, patrón horario, idioma, keywords).
  3. Infiere atributos personales de forma explicable (ubicación, ocupación, rutina) a partir de en qué comunidades/hashtags participas.
  4. Detecta declaraciones explícitas sobre ti mismo en el texto ("tengo 24 años", "vivo en León", "estudio Medicina"...) combinando regex (rápido, gratuito, determinista) con un modelo de IA LOCAL (Qwen3.5-4B, opcional) que capta redacciones más libres que la regex no reconoce -- y también una estimación (más débil, marcada aparte) de sexo por convención cultural del nombre público de la cuenta. A partir de ahí, y de distribuciones agregadas del INE, estima cuánta gente en España comparte esa combinación de características (estilo k-anonimato: "solo 17 personas en España cumplen esto"), representado visualmente como un pictograma de monigotes (turquesa = tú, negro = el resto).
  5. Estima la ubicación de tus fotos (solo Instagram) comparándolas por similitud visual (DINOv2 + FAISS) contra un índice de imágenes georreferenciadas de España, mostrando cada foto analizada con su confianza real (no solo las que superan un umbral) en un mapa y una lista. Las fotos cuyo pie indica que son de un viaje o vacaciones se detectan y se excluyen del cálculo de dónde vives habitualmente (pero se siguen mostrando igual en el mapa).
  6. Analiza el contenido visual de tus fotos (solo Instagram, opcional): qué se ve en cada una -- objetos, actividades, aficiones, señales de relación de pareja -- mediante un modelo de visión-lenguaje local (Moondream2). A diferencia del punto anterior, esto no busca dónde se tomó la foto sino qué hay en ella.
  7. Calcula un score de privacidad (0-100) con desglose por componente.
  8. Envía automáticamente el informe ya generado (incluidas las recomendaciones por reglas fijas, como insumo) a un modelo de IA LOCAL (Qwen3.5-4B, sin llamada a ningún tercero) para obtener un veredicto general de una frase y conclusiones específicas y no obvias -- sin necesidad de pulsar ningún botón.
  9. Permite descargar el informe completo en JSON (portabilidad de datos, RGPD Art. 20).

Todo el pipeline corre en memoria durante la petición: no hay base de datos, no se persiste nada del usuario más allá de la sesión de su propio navegador.

Estructura del proyecto

  • webapp/ — frontend React + Vite + TypeScript. Landing (consentimiento
    • login OAuth), Dashboard (informe completo con mapa, tablas y gráficos).
  • backend/ — backend Python + FastAPI (renombrado desde users/, nombre heredado de la plantilla). Aquí vive toda la lógica de OAuth, NLP, k-anonimato, geolocalización por imagen, scoring y el informe (ver backend/app/).
  • docs/ — documentación de arquitectura Arc42.
  • graphify-out/ — grafo de conocimiento del código, ver sección "Graphify" más abajo.

⚠️ Alcance y limitaciones (importante para la memoria del TFG)

  • El usuario solo puede analizar su propia cuenta autenticada. No existe ningún flujo para analizar cuentas de terceros.
  • No hay base de datos. Todo el estado vive en una cookie de sesión firmada (SessionMiddleware) con los tokens de acceso. Cerrar sesión = borrar todo rastro.
  • La estimación de k-anonimato (backend/app/scoring/k_anonymity.py) usa distribuciones agregadas del INE y asume independencia entre atributos (no microdatos reales, no correlaciones cruzadas) — es una aproximación documentada, no un conteo exacto. Ver el docstring del módulo para la justificación de diseño frente a la alternativa descartada (base de datos sintética de ~49M filas).
  • La geolocalización por imagen (backend/app/vision/geolocation.py) es opcional y best-effort: si el índice FAISS no está construido, o si el backend no tiene instaladas sus dependencias pesadas (torch, faiss, transformers -- ver el extra vision de pyproject.toml y el ARG WITH_GEOLOCATION del Dockerfile), esa función del pipeline simplemente no aporta nada, sin romper el resto del análisis. Su precisión realista es a nivel de provincia, no de calle (ver benchmarks de reverse geolocation citados en los docstrings).
  • El análisis de contenido visual (backend/app/vision/scene_analysis.py, Moondream2) es igualmente opcional y best-effort, y depende de las mismas condiciones de activación que la geolocalización (mismo el extra vision de pyproject.toml, mismo ARG WITH_GEOLOCATION del Dockerfile) aunque desde el cambio a llama-cpp-python/GGUF ya NO instala las mismas dependencias de Python que la geolocalización (antes sí, ambas usaban transformers). Es arquitectónicamente distinto: no compara similitud contra un índice, sino que "interpreta" cada foto vía VQA. Se precarga en el arranque del contenedor igual que el modelo de geolocalización (ver lifespan en app/main.py).
  • Exclusión de alcance deliberada: no se hace reconocimiento facial ni se clasifican rasgos físicos/étnicos (color de piel, pelo, ojos) de ninguna persona que aparezca en las fotos, incluida la propia cuenta analizada. Se consideró añadirlo a scene_analysis.py como señal adicional para acotar el k-anonimato (misma lógica que ya se aplica a atributos autodeclarados en texto), pero se descartó por dos motivos, no solo uno:
    • Encaja en el art. 9.1 RGPD (categoría especial de datos: origen racial/étnico, datos biométricos con fin de identificación). El consentimiento OAuth de este proyecto cubre que el usuario analice su propia actividad pública, pero no constituye base legal para que el sistema clasifique rasgos raciales o biométricos de nadie con el fin de perfilar/acotar una búsqueda -- es exactamente el tipo de tratamiento que el RGPD trata con más cautela, no menos, y además el art. 4.2 define "tratamiento" de forma explícita como cualquier operación sobre datos personales se persista o no el resultado: que algo no se guarde no significa que no se haya procesado.
    • Determinar "cuál de las personas de la foto es el usuario" (por comparación con la foto de perfil o por frecuencia de aparición) exige procesar la cara de todas las personas presentes para poder distinguir/descartar, no solo la del usuario -- típicamente detección de rostro + embedding + comparación/clustering entre fotos, es decir, reconocimiento facial de terceros (acompañantes, amigos, desconocidos de fondo) que nunca dieron su consentimiento vía OAuth ni de ninguna otra forma. El consentimiento del titular de la cuenta no se extiende a las demás personas que aparecen en su contenido. A esto se suma, ya como razón secundaria de calidad de la señal (no de legalidad): los VLM son conocidos por ser poco fiables e inconsistentes clasificando tono de piel (varía con iluminación, balance de blancos, ángulo de la cámara), así que alimentar esa señal al k-anonimato daría una falsa sensación de precisión sobre una estimación ya de por sí poco fiable. El prompt de scene_analysis.py se mantiene deliberadamente acotado a objetos/escena/aficiones/indicios de pareja -- nunca a la descripción física de las personas que aparecen.
  • Las heurísticas de inferencia de atributos (backend/app/nlp/attribute_inference.py, backend/app/nlp/demographic_extraction.py) son deliberadamente simples (listas + regex) para mantener el sistema explicable y auditable.
  • El análisis con IA (backend/app/ai_analysis.py, backend/app/nlp/ai_attribute_extraction.py) es totalmente opcional: ambos usan el mismo modelo local (Qwen3.5-4B, ver backend/app/nlp/ai_client.py). Sin QWEN_GGUF_REPO_ID configurado (o sin llama-cpp-python instalado), el veredicto/conclusiones del dashboard indican que no está disponible, y la detección de atributos se queda solo con lo que encuentren las regex -- el resto de la app funciona igual.
  • La correlación entre plataformas (Reddit + Instagram combinados) y el componente identity_consistency_risk del scoring quedan como trabajo futuro, documentado explícitamente en backend/app/scoring/privacy_score.py.

Componentes

  • webapp/ — SPA con Vite + React/TypeScript: pantalla de consentimiento y login OAuth, dashboard con el informe completo (score, progreso en vivo vía SSE, estrechamiento de población, mapa, gráficos, veredicto de IA), tests con Vitest/Testing Library y E2E con Playwright + Cucumber.
  • backend/ — API en Python/FastAPI: OAuth con cada plataforma, extracción y normalización de posts públicos, inferencia de atributos (regex + IA opcional), estrechamiento de k-anonimato contra datos del INE, scoring de privacidad, geolocalización de fotos (DINOv2 + FAISS, opcional) y análisis de escena (Moondream2, opcional), generación del informe final y veredicto de IA.
Desglose fichero por fichero

Webapp (webapp/)

  • src/pages/Landing.tsx — pantalla de consentimiento + login OAuth (Reddit/Instagram).
  • src/pages/Dashboard.tsx — informe completo: score, progreso en vivo vía SSE, tabla de estrechamiento de población (con pictograma visual), mapa + lista de ubicaciones estimadas, gráfico horario, perfil de escritura, veredicto y conclusiones de IA, descarga en JSON.
  • src/components/ — ScoreBar, HourlyActivityChart, PopulationNarrowingTable, PopulationPictogram (representación visual tipo isotype: monigotes turquesa/negro), LocationMap (Leaflet/OpenStreetMap), AiSummaryCard, DownloadReportButton.
  • src/api.ts / src/types.ts — cliente tipado del backend, incluido analyzeStream (SSE vía EventSource).
  • src/utils/reportToJson.ts — exportación del informe a JSON.
  • Tests: Vitest + Testing Library (src/__tests__/), E2E con Playwright + Cucumber (webapp/test/, ver webapp/E2E.md).

Backend Python/FastAPI (backend/)

  • app/auth/reddit_oauth.py, app/auth/instagram_oauth.py — OAuth 2.0 con cada plataforma.
  • app/auth/dynamic_origin.py — deriva el redirect_uri de Instagram y el destino de la redirección final del Host de cada petición cuando no hay un valor fijo en .env (pensado para túneles HTTPS tipo ngrok, cuya URL cambia en cada reinicio; ver nota más abajo sobre por qué no usar Cloudflare Quick Tunnel).
  • app/reddit_client.py, app/instagram_client.py — extracción de posts/comentarios/publicaciones públicas, normalizados a un modelo común (SocialPost).
  • app/nlp/fingerprint.py — huella de escritura (longitud de frase, vocabulario, emojis, patrón horario, keywords TF-IDF, idioma).
  • app/nlp/attribute_inference.py — inferencia explicable de atributos (ubicación, ocupación, rutina) a partir de comunidades/hashtags.
  • app/nlp/demographic_extraction.py — extracción de declaraciones explícitas en texto por regex (edad, sexo, ubicación, estudios, ocupación, universidad, empresa).
  • app/nlp/ai_attribute_extraction.py — la misma extracción, pero vía IA local (Qwen3.5-4B, opcional): capta redacciones libres que la regex no reconoce, y una estimación aparte de sexo por nombre público de la cuenta (marcada con menor fiabilidad). Complementa a la regex, nunca la sustituye.
  • app/nlp/travel_detection.py — detección regex de menciones de "de viaje/vacaciones" en el pie de foto, para excluir esas publicaciones del cálculo de dónde vives habitualmente (ver report/generator.py). Complementa (no sustituye) a una detección equivalente vía IA en ai_attribute_extraction.py.
  • app/data/ine_reference.py — tablas de distribución poblacional (INE) usadas para el estrechamiento de población.
  • app/scoring/k_anonymity.py — motor de estimación de k-anonimato (estrechamiento de población en cascada), expone también la proporción ya calculada para el pictograma del frontend.
  • app/scoring/privacy_score.py — motor de scoring de privacidad (0-100).
  • app/vision/geolocation.py — geolocalización de fotos por similitud visual (DINOv2 + FAISS), opcional. Devuelve todas las estimaciones (con su confianza real) más un flag de si el índice está disponible, para poder distinguir "no hay índice" de "no hay resultados fiables".
  • app/vision/scene_analysis.py — análisis del contenido visual de cada foto (Moondream2, modelo de visión-lenguaje local vía llama-cpp-python/GGUF, ~1.4B parámetros de texto): objetos, actividades, aficiones, señales de relación de pareja, matrícula. Arquitectónicamente distinto de geolocation.py (que compara similitud visual contra un índice sin "entender" la foto). Opcional, mismo flag de activación que la geolocalización (WITH_GEOLOCATION), aunque ya no comparte sus mismas dependencias de Python (ver más abajo).
  • app/ai_analysis.py — veredicto general + conclusiones sobre el informe vía IA local (Qwen3.5-4B), opcional; se dispara automáticamente, sin botón, y usa recommendations como insumo.
  • app/progress.py — callback de progreso compartido, usado por el endpoint de streaming.
  • app/analysis_router.py — endpoints de análisis (/api/analyze/{platform}, /api/analyze/{platform}/stream, /api/analyze/ai-summary).
  • app/report/generator.py — ensamblado del informe final + recomendaciones (estas últimas ya no se muestran como sección propia en el dashboard, ver más arriba).
  • app/main.py — app FastAPI; en el arranque (lifespan) precarga el modelo de geolocalización si está disponible, en vez de esperar al primer análisis; métricas Prometheus en /metrics.
  • tests/ — pytest (unit + endpoints), ~95% cobertura, para Sonar.
  • scripts/ — descarga del dataset OSV-5M, construcción del índice FAISS, y recuperación de metadatos (ver más abajo).
  • monitoring/ — configuración de Prometheus/Grafana.

Running the Project

With Docker

docker compose up --build

Arranca solo backend + webapp (servicios sin perfil):

prometheus/grafana (perfil monitoring) y dinov2-igpu-worker (perfil igpu, ver más abajo) son opt-in por diseño -- no arrancan con el comando de arriba a secas, salvo que actives el perfil correspondiente por defecto (ver justo debajo):

docker compose --profile monitoring up --build   # + Grafana/Prometheus
docker compose --profile igpu up --build         # + worker de iGPU
# combinables:
docker compose --profile monitoring --profile igpu up --build

Activar un perfil por defecto, sin pasar --profile cada vez: copia .env.example (el de la raíz del repo, junto a este docker-compose.yml -- distinto de backend/.env.example) a .env y pon ahí COMPOSE_PROFILES=igpu (combinable: COMPOSE_PROFILES=igpu,monitoring). Compose lee ese .env de la raíz automáticamente, así que a partir de ahí docker compose up --build a secas ya arranca también dinov2-igpu-worker -- sin el .env de la raíz (o sin esa variable en él), sigue haciendo falta el flag --profile explícito. Sirve para cualquier perfil, no solo igpu.

Antes de levantarlo, crea backend/.env a partir de backend/.env.example (ver variables de entorno más abajo).

Análisis de imagen en Docker (geolocalización + contenido visual): el docker-compose.yml trae WITH_GEOLOCATION=true por defecto, que instala torch/faiss/transformers (para DINOv2, geolocalización) y llama-cpp-python compilado con soporte CUDA (para Moondream2, análisis de contenido -- ver el extra vision de pyproject.toml y la etapa cuda-builder del Dockerfile) en la imagen del backend, varios cientos de MB extra. Pese al nombre del flag (heredado de cuando solo existía geolocalización), esa misma build es la que necesita también el análisis de contenido visual (app/vision/scene_analysis.py, Moondream2 vía GGUF) -- no hay un flag independiente para activar solo una de las dos, aunque desde el cambio a llama-cpp-python ya NO comparten las mismas dependencias de Python (antes sí, ambas usaban transformers). Sin esto, el backend puede tener el índice FAISS perfectamente construido y montado y aun así reportar la geolocalización como "no disponible" -- esas librerías hacen falta en el análisis, no solo para construir el índice. Si no vas a usar ninguna de las dos funciones, ponlo a false para una imagen más ligera. Los dos modelos (DINOv2 y Moondream2) se precargan en el arranque del contenedor, no en el primer análisis (ver lifespan en app/main.py). La caché de ambos se persiste en backend/data/hf_cache/ para no volver a descargarlos en cada reinicio; Moondream2 además cachea ahí su versión cuantizada (ver MOONDREAM_QUANT_TYPE en app/vision/scene_analysis.py) una vez generada, para no recuantizar en cada reinicio.

Offload de DINOv2 a una iGPU (opcional, --profile igpu): en máquinas con GPU dedicada + iGPU (Intel/AMD, vía DirectML sobre WSL2), dinov2-igpu-worker descarga su propio torch==2.4.1+cu121 + torch-directml -- deliberadamente en una imagen/venv completamente aparte del backend, nunca instalado ahí (ver ADR-28 en docs/src/09_architecture_decisions.adoc para el porqué: mezclarlo con el torch/cu121 que ya usa el backend para DINOv2 corrompe ese entorno). Actívalo con ENABLE_IGPU_OFFLOAD=true en backend/.env y el perfil igpu al arrancar (a mano con --profile igpu, o por defecto con COMPOSE_PROFILES=igpu en el .env de la raíz -- ver más arriba) -- ninguno de los dos por separado hace nada.

Construir backend + webapp + dinov2-igpu-worker los tres a la vez (cada uno con su propia descarga de torch) puede saturar los recursos de Docker Desktop/WSL2 y el build del worker termina con un rpc error: code = Unavailable desc = error reading from server: EOF justo al final (paso exporting layers) aunque los otros dos hayan terminado bien. Si te pasa:

docker images | findstr amitraceable    # confirma si backend/webapp sí terminaron
docker compose build dinov2-igpu-worker # reintenta solo el que falló

Si vuelve a fallar igual, es contención de recursos, no mala suerte puntual -- en orden de probabilidad:

  1. wsl --shutdown y reabrir Docker Desktop (el demonio de BuildKit a veces queda colgado tras un fallo así).
  2. Subir el límite de RAM de WSL2 si lo tienes ajustado bajo -- en %UserProfile%\.wslconfig:
    [wsl2]
    memory=8GB
    
    y wsl --shutdown para aplicarlo.
  3. Construir uno a uno en vez de en paralelo:
    docker compose build backend
    docker compose build webapp
    docker compose --profile igpu build dinov2-igpu-worker
    
  4. Comprobar espacio en disco (docker system df) -- los dos torch+cu121 (backend y worker, cada uno de varios cientos de MB/GB en wheels) más las capas intermedias de build acumulan bastante; docker builder prune libera caché de builds antiguos sin tocar las imágenes en uso.

Without Docker

1. Backend (backend/)

cd backend
uv sync                           # crea .venv con las dependencias, el grupo dev y los modelos de spaCy
cp .env.example .env              # y rellenar credenciales
uv run uvicorn app.main:app --reload --port 3000

(Requiere uv; descarga solo el Python 3.12 que fija backend/pyproject.toml.)

Si quieres geolocalización por imagen o análisis de contenido visual (Moondream2) funcionando en este venv, hace falta un paso más -- lo de arriba, por sí solo, deja ambas funciones en "no disponible" de forma silenciosa (ver más abajo, "Alcance y limitaciones"): no falla con un error, simplemente esa parte del pipeline no aporta nada, así que es fácil no darse cuenta de que falta este paso.

uv sync --extra vision

Con Docker esto ya viene resuelto por defecto (ver WITH_GEOLOCATION más abajo) -- este paso solo es necesario si ejecutas el backend directamente con uvicorn, como aquí arriba.

En arranques posteriores, con el venv ya creado, basta con activar el entorno y lanzar uvicorn directamente.

2. Webapp

cd webapp
npm install
npm run dev

La webapp estará en http://localhost:5173

Variables de entorno

Ver backend/.env.example para la lista completa comentada. Resumen:

VariableObligatoriaNotas
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_REDIRECT_URI, REDDIT_USER_AGENTSíApp tipo "web app" en https://www.reddit.com/prefs/apps
SESSION_SECRET_KEYSíCadena aleatoria larga, firma la cookie de sesión
INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRETNoSin ellas, Instagram devuelve 503 pero Reddit sigue funcionando. App "API setup with Instagram Login" en Meta for Developers
INSTAGRAM_REDIRECT_URINoDéjala vacía si usas Docker (ver nota de túnel más abajo): se deriva sola del Host de cada petición. Fíjala solo con dominio propio en producción
FRONTEND_ORIGINNoDéjala vacía si usas Docker: se deriva sola del Host. Fíjala (p. ej. http://localhost:5173) solo si sirves frontend y backend en puertos distintos sin Docker
ENABLE_AI_ANALYSISNotrue por defecto. Desactiva del todo el análisis con IA (veredicto/conclusiones + extracción de atributos, ver backend/app/nlp/ai_client.py) sin desinstalar nada
QWEN_GGUF_REPO_ID, QWEN_GGUF_FILENAMENoRepo/fichero de Hugging Face con el GGUF de Qwen3.5-4B a cuantizar localmente (Q4_K_M vía llama-cpp-python) -- SIN VALOR POR DEFECTO A PROPÓSITO, hay que confirmarlo contra lo disponible en Hugging Face antes de desplegar (ver .env.example). Sin rellenar, la sección "Analizar con IA" del dashboard indica que no está disponible, sin afectar al resto
ENABLE_SCENE_ANALYSISNofalse por defecto. Activa el análisis de contenido visual con Moondream2 (descripción de escena por foto). Afecta exclusivamente a esto: la geolocalización por similitud visual (DINOv2) no lee esta variable y sigue funcionando igual, esté esto activado o no -- probado explícitamente en test_geolocation.py. Para que Moondream2 pueda funcionar (con esta variable en true) hace falta además WITH_GEOLOCATION=true en el build de Docker (mismo build-arg, aunque desde ADR-43 instala llama-cpp-python para Moondream2, no las mismas librerías que DINOv2). Ver ADR-43 en docs/ (sustituye a ADR-19, que documentaba el backend transformers anterior): la cifra de RAM/VRAM necesaria depende ahora de si MOONDREAM_QUANT_TYPE está cuantizando el modelo o no, no hay un único número fijo como antes

Nota sobre Instagram y HTTPS en local: la API de Instagram (Business Login) exige que redirect_uri sea HTTPS, incluso en desarrollo. Para probarlo en local sin dominio propio, usa ngrok apuntando a la webapp (puerto 8080), no al backend directamente -- la webapp lleva su propio nginx que hace de proxy hacia el backend bajo el mismo origen (ver webapp/nginx.conf), así que un único túnel sirve para todo:

ngrok http 8080

(requiere cuenta gratuita en https://ngrok.com y ngrok config add-authtoken <token> una vez).

Restringir quién puede entrar (Basic Auth): el plan gratuito de ngrok incluye un dominio propio estable (no cambia en cada reinicio) y permite añadir autenticación sin plan de pago, vía Traffic Policy. Copia backend/policy.example.yaml a backend/policy.yaml (este segundo nombre está en .gitignore, nunca se sube) y pon tu usuario/contraseña real dentro:

on_http_request:
  - actions:
      - type: basic-auth
        config:
          realm: "AmITraceable"
          credentials:
            - "usuario:contraseña-larga-y-unica"

Y arranca el túnel apuntando a ese archivo:

ngrok http 8080 --url=tu-dominio.ngrok-free.app --traffic-policy-file=backend/policy.yaml

Así, quien no tenga esas credenciales ni siquiera llega al backend -- ngrok rechaza la petición en su propio borde, antes de reenviarla. Ver ADR-39 sobre por qué esto (y no dominio propio + Cloudflare) es la solución elegida.

Notas sobre ngrok y por qué no usar Cloudflare Quick Tunnel

⚠️ No usar cloudflared tunnel --url ... (Cloudflare Quick Tunnel): el edge de trycloudflare.com bufferiza por completo las respuestas text/event-stream, así que el streaming SSE de /api/analyze/{platform}/stream (la pantalla de carga con el listado de fases) nunca llega en tiempo real al navegador -- todos los eventos se reciben de golpe justo cuando el análisis termina, y se ve solo "Analizando tu actividad pública en X..." sin el listado de fases mientras carga. Es un problema conocido y documentado del propio Cloudflare (no del backend: las cabeceras text/event-stream y X-Accel-Buffering: no ya se mandan bien). Un named tunnel de Cloudflare (con dominio propio) sí respetaría el streaming, pero para desarrollo local sin dominio, ngrok funciona correctamente sin ese problema.

Con INSTAGRAM_REDIRECT_URI y FRONTEND_ORIGIN vacías en .env (valor por defecto), lo único que hay que hacer con la URL https://xxx.ngrok-free.app que te dé cada vez es darla de alta en la app de Meta Developers (con el path /auth/instagram/callback) -- eso sigue siendo manual, Meta no tiene API para gestionar esa lista. Todo lo demás (.env, reconstruir imágenes, reiniciar uvicorn) se adapta solo en cada reinicio del túnel.

La primera vez que abras la URL de ngrok en el navegador, el plan gratuito muestra una página intermedia de aviso ("You are about to visit...") -- solo hay que darle a "Visit Site", no es un error.

Scripts de geolocalización por imagen (opcional)

El módulo de geolocalización de fotos (app/vision/geolocation.py) es opcional: sin el índice FAISS construido, simplemente no aporta nada al informe, sin errores. Para activarlo:

cd backend
uv sync --extra vision

uv run python scripts/geolocalization/download_osv5m_spain.py --output data/osv5m_spain --max-disk-gb 35
uv run python scripts/geolocalization/build_faiss_index.py --images data/osv5m_spain

(El extra vision apunta al índice de PyTorch con CUDA 12.1 (pytorch-cu121, ver [tool.uv.sources] en backend/pyproject.toml) -- ver también el ARG WITH_GEOLOCATION del Dockerfile si vas a correr esto dentro de Docker.)

El análisis de contenido visual (app/vision/scene_analysis.py, Moondream2) usa el mismo el extra vision de pyproject.toml, pero no necesita nada de lo anterior: no hay índice que construir ni dataset que descargar, se ejecuta directamente sobre cada foto.

  • download_osv5m_spain.py descarga solo las imágenes de España del dataset OpenStreetView-5M (streaming shard a shard, con límite de disco configurable, reanudable tras interrupción).
  • download_osv5m_world.py es la variante sin filtro de país (mucho más pesada en tráfico de red, ~260GB).
  • build_faiss_index.py extrae embeddings con DINOv2 y construye el índice de búsqueda por similitud.
  • recover_metadata.py reconstruye metadata.csv a partir de las imágenes ya descargadas, sin volver a bajarlas -- pensado para el caso (ya corregido, pero por si tienes un metadata.csv de una ejecución anterior al fix) en que se perdía la columna id al guardar, dejando el índice sin poder casar ninguna imagen con su fila de metadatos.

Estos datos/artefactos no se versionan en el repositorio (ver .gitignore): son regenerables ejecutando los scripts.

Available Scripts

Webapp (webapp/package.json)

  • npm run dev — servidor de desarrollo.
  • npm test — tests unitarios (Vitest, ~100 tests).
  • npm run test:coverage — tests con cobertura (para Sonar).
  • npm run test:e2e — tests E2E (levanta webapp + backend sueltos y corre Cucumber; ver webapp/E2E.md). Cubre la pantalla de consentimiento y que el dashboard exige autenticación real contra el backend (sin mockear), pero no el flujo completo de OAuth (necesitaría credenciales reales) ni el montaje con Docker/nginx.
  • npm run start:all — levanta webapp + backend Python a la vez (conveniencia para desarrollo/E2E).
  • npm run lint — ESLint.

Backend (Python)

  • uv run uvicorn app.main:app --reload --port 3000 — arranca el backend en desarrollo.
  • uv run pytest — tests unitarios (~260 tests, 1 se salta si no tienes instalado el extra vision en local).
  • uv run pytest --cov=app --cov-report=xml --cov-report=term — tests con cobertura (genera coverage.xml para Sonar).
Graphify (grafo de conocimiento del código)

El repo tiene un grafo generado con Graphify (graphify-out/) que Claude Code consulta automáticamente en vez de releer archivos sueltos, ahorrando tokens. Solo funciona así de forma automática dentro de Claude Code (usa hooks internos de esa herramienta) — en un chat normal de Claude no hay ahorro automático salvo que se le pida explícitamente consultar el grafo por terminal.

Al clonar en un equipo nuevo, ejecutar una vez (regeneran configuración local, no se versiona):

uv tool install graphifyy          # o: pipx install graphifyy — evitar "pip" a secas
graphify claude install --project  # regenera .claude/settings.json con la ruta de este PC
graphify hook install              # auto-actualiza el grafo tras cada commit

graphify-out/ (grafo, informe, visualización) sí se versiona; .claude/settings.json, graphify-out/cache/, graphify-out/cost.json y las carpetas de backup con fecha (graphify-out/20YY-MM-DD/) no, porque son específicos de cada máquina — ver .gitignore.

Respecto a SonarQube

Debido a restricciones de la cuenta gratuita, el token de Sonar debe renovarse cada dos meses. Configuración en sonar-project.properties (rutas de fuentes/tests, exclusiones, reportes de cobertura).

Plan de evaluación pendiente (para la memoria)

No incluido en el código, pero necesario antes de la defensa:

  1. Dataset de prueba con consentimiento (cuentas propias del equipo, alts conocidos) para validar el módulo de inferencia de atributos, el de geolocalización por imagen y el de análisis de contenido visual (aficiones/actividades/señales de pareja).
  2. Métricas: precisión de los atributos inferidos vs. verdad conocida, tasa de falsos positivos, precisión real del módulo de geolocalización sobre fotos propias geoetiquetadas, y precisión del análisis de contenido visual sobre fotos propias etiquetadas manualmente.
  3. Comparativa de las ponderaciones del scoring (_WEIGHTS en privacy_score.py) y de la asunción de independencia del estrechamiento de población (k_anonymity.py) contra percepción subjetiva de usuarios reales o correlaciones reales del Censo/EPA.

nacho50900/AmITraceable

Herramienta educativa de análisis de exposición de la privacidad: mediante OAuth con tus propias cuentas (Instagram), analiza tu huella digital pública, infiere atributos demográficos y calcula el estrechamiento de anonimato frente a datos de referencia del INE, generando un informe de privacidad con IA.

Python

1

240 commits

updated Oct 7, 2026

See the code

README

Análisis de Exposición de Identidad Digital (TFG)

Quality Gate Status Coverage Release

Base de este proyecto: plantilla de laboratorio ASW (Uniovi) yovi_en1b. Se ha adaptado sustituyendo el dominio (usuarios/juego → análisis de exposición de identidad digital) y sustituyendo el servicio users de Node.js por un backend Python/FastAPI (necesario por las librerías de NLP: spaCy, scikit-learn, DINOv2/FAISS). El servicio gamey (Rust) se ha eliminado por no aplicar a este TFG.

https://github.com/user-attachments/assets/bc91d3e4-bbb0-44b1-be25-00c0aba36d51

TFG: análisis defensivo de la propia huella digital mediante OSINT e IA. El usuario autentica su propia cuenta de Reddit y/o Instagram vía OAuth, y la herramienta genera un informe con lo que es públicamente inferible sobre él — para que decida qué quiere seguir compartiendo, no para vigilar a terceros.

Qué hace la herramienta

  1. Lee tu actividad pública (posts/comentarios de Reddit, publicaciones de Instagram) tras autenticarte vía OAuth.
  2. Analiza tu forma de escribir (huella de estilo: longitud de frase, vocabulario, emojis, patrón horario, idioma, keywords).
  3. Infiere atributos personales de forma explicable (ubicación, ocupación, rutina) a partir de en qué comunidades/hashtags participas.
  4. Detecta declaraciones explícitas sobre ti mismo en el texto ("tengo 24 años", "vivo en León", "estudio Medicina"...) combinando regex (rápido, gratuito, determinista) con un modelo de IA LOCAL (Qwen3.5-4B, opcional) que capta redacciones más libres que la regex no reconoce -- y también una estimación (más débil, marcada aparte) de sexo por convención cultural del nombre público de la cuenta. A partir de ahí, y de distribuciones agregadas del INE, estima cuánta gente en España comparte esa combinación de características (estilo k-anonimato: "solo 17 personas en España cumplen esto"), representado visualmente como un pictograma de monigotes (turquesa = tú, negro = el resto).
  5. Estima la ubicación de tus fotos (solo Instagram) comparándolas por similitud visual (DINOv2 + FAISS) contra un índice de imágenes georreferenciadas de España, mostrando cada foto analizada con su confianza real (no solo las que superan un umbral) en un mapa y una lista. Las fotos cuyo pie indica que son de un viaje o vacaciones se detectan y se excluyen del cálculo de dónde vives habitualmente (pero se siguen mostrando igual en el mapa).
  6. Analiza el contenido visual de tus fotos (solo Instagram, opcional): qué se ve en cada una -- objetos, actividades, aficiones, señales de relación de pareja -- mediante un modelo de visión-lenguaje local (Moondream2). A diferencia del punto anterior, esto no busca dónde se tomó la foto sino qué hay en ella.
  7. Calcula un score de privacidad (0-100) con desglose por componente.
  8. Envía automáticamente el informe ya generado (incluidas las recomendaciones por reglas fijas, como insumo) a un modelo de IA LOCAL (Qwen3.5-4B, sin llamada a ningún tercero) para obtener un veredicto general de una frase y conclusiones específicas y no obvias -- sin necesidad de pulsar ningún botón.
  9. Permite descargar el informe completo en JSON (portabilidad de datos, RGPD Art. 20).

Todo el pipeline corre en memoria durante la petición: no hay base de datos, no se persiste nada del usuario más allá de la sesión de su propio navegador.

Estructura del proyecto

  • webapp/ — frontend React + Vite + TypeScript. Landing (consentimiento
    • login OAuth), Dashboard (informe completo con mapa, tablas y gráficos).
  • backend/ — backend Python + FastAPI (renombrado desde users/, nombre heredado de la plantilla). Aquí vive toda la lógica de OAuth, NLP, k-anonimato, geolocalización por imagen, scoring y el informe (ver backend/app/).
  • docs/ — documentación de arquitectura Arc42.
  • graphify-out/ — grafo de conocimiento del código, ver sección "Graphify" más abajo.

⚠️ Alcance y limitaciones (importante para la memoria del TFG)

  • El usuario solo puede analizar su propia cuenta autenticada. No existe ningún flujo para analizar cuentas de terceros.
  • No hay base de datos. Todo el estado vive en una cookie de sesión firmada (SessionMiddleware) con los tokens de acceso. Cerrar sesión = borrar todo rastro.
  • La estimación de k-anonimato (backend/app/scoring/k_anonymity.py) usa distribuciones agregadas del INE y asume independencia entre atributos (no microdatos reales, no correlaciones cruzadas) — es una aproximación documentada, no un conteo exacto. Ver el docstring del módulo para la justificación de diseño frente a la alternativa descartada (base de datos sintética de ~49M filas).
  • La geolocalización por imagen (backend/app/vision/geolocation.py) es opcional y best-effort: si el índice FAISS no está construido, o si el backend no tiene instaladas sus dependencias pesadas (torch, faiss, transformers -- ver el extra vision de pyproject.toml y el ARG WITH_GEOLOCATION del Dockerfile), esa función del pipeline simplemente no aporta nada, sin romper el resto del análisis. Su precisión realista es a nivel de provincia, no de calle (ver benchmarks de reverse geolocation citados en los docstrings).
  • El análisis de contenido visual (backend/app/vision/scene_analysis.py, Moondream2) es igualmente opcional y best-effort, y depende de las mismas condiciones de activación que la geolocalización (mismo el extra vision de pyproject.toml, mismo ARG WITH_GEOLOCATION del Dockerfile) aunque desde el cambio a llama-cpp-python/GGUF ya NO instala las mismas dependencias de Python que la geolocalización (antes sí, ambas usaban transformers). Es arquitectónicamente distinto: no compara similitud contra un índice, sino que "interpreta" cada foto vía VQA. Se precarga en el arranque del contenedor igual que el modelo de geolocalización (ver lifespan en app/main.py).
  • Exclusión de alcance deliberada: no se hace reconocimiento facial ni se clasifican rasgos físicos/étnicos (color de piel, pelo, ojos) de ninguna persona que aparezca en las fotos, incluida la propia cuenta analizada. Se consideró añadirlo a scene_analysis.py como señal adicional para acotar el k-anonimato (misma lógica que ya se aplica a atributos autodeclarados en texto), pero se descartó por dos motivos, no solo uno:
    • Encaja en el art. 9.1 RGPD (categoría especial de datos: origen racial/étnico, datos biométricos con fin de identificación). El consentimiento OAuth de este proyecto cubre que el usuario analice su propia actividad pública, pero no constituye base legal para que el sistema clasifique rasgos raciales o biométricos de nadie con el fin de perfilar/acotar una búsqueda -- es exactamente el tipo de tratamiento que el RGPD trata con más cautela, no menos, y además el art. 4.2 define "tratamiento" de forma explícita como cualquier operación sobre datos personales se persista o no el resultado: que algo no se guarde no significa que no se haya procesado.
    • Determinar "cuál de las personas de la foto es el usuario" (por comparación con la foto de perfil o por frecuencia de aparición) exige procesar la cara de todas las personas presentes para poder distinguir/descartar, no solo la del usuario -- típicamente detección de rostro + embedding + comparación/clustering entre fotos, es decir, reconocimiento facial de terceros (acompañantes, amigos, desconocidos de fondo) que nunca dieron su consentimiento vía OAuth ni de ninguna otra forma. El consentimiento del titular de la cuenta no se extiende a las demás personas que aparecen en su contenido. A esto se suma, ya como razón secundaria de calidad de la señal (no de legalidad): los VLM son conocidos por ser poco fiables e inconsistentes clasificando tono de piel (varía con iluminación, balance de blancos, ángulo de la cámara), así que alimentar esa señal al k-anonimato daría una falsa sensación de precisión sobre una estimación ya de por sí poco fiable. El prompt de scene_analysis.py se mantiene deliberadamente acotado a objetos/escena/aficiones/indicios de pareja -- nunca a la descripción física de las personas que aparecen.
  • Las heurísticas de inferencia de atributos (backend/app/nlp/attribute_inference.py, backend/app/nlp/demographic_extraction.py) son deliberadamente simples (listas + regex) para mantener el sistema explicable y auditable.
  • El análisis con IA (backend/app/ai_analysis.py, backend/app/nlp/ai_attribute_extraction.py) es totalmente opcional: ambos usan el mismo modelo local (Qwen3.5-4B, ver backend/app/nlp/ai_client.py). Sin QWEN_GGUF_REPO_ID configurado (o sin llama-cpp-python instalado), el veredicto/conclusiones del dashboard indican que no está disponible, y la detección de atributos se queda solo con lo que encuentren las regex -- el resto de la app funciona igual.
  • La correlación entre plataformas (Reddit + Instagram combinados) y el componente identity_consistency_risk del scoring quedan como trabajo futuro, documentado explícitamente en backend/app/scoring/privacy_score.py.

Componentes

  • webapp/ — SPA con Vite + React/TypeScript: pantalla de consentimiento y login OAuth, dashboard con el informe completo (score, progreso en vivo vía SSE, estrechamiento de población, mapa, gráficos, veredicto de IA), tests con Vitest/Testing Library y E2E con Playwright + Cucumber.
  • backend/ — API en Python/FastAPI: OAuth con cada plataforma, extracción y normalización de posts públicos, inferencia de atributos (regex + IA opcional), estrechamiento de k-anonimato contra datos del INE, scoring de privacidad, geolocalización de fotos (DINOv2 + FAISS, opcional) y análisis de escena (Moondream2, opcional), generación del informe final y veredicto de IA.
Desglose fichero por fichero

Webapp (webapp/)

  • src/pages/Landing.tsx — pantalla de consentimiento + login OAuth (Reddit/Instagram).
  • src/pages/Dashboard.tsx — informe completo: score, progreso en vivo vía SSE, tabla de estrechamiento de población (con pictograma visual), mapa + lista de ubicaciones estimadas, gráfico horario, perfil de escritura, veredicto y conclusiones de IA, descarga en JSON.
  • src/components/ — ScoreBar, HourlyActivityChart, PopulationNarrowingTable, PopulationPictogram (representación visual tipo isotype: monigotes turquesa/negro), LocationMap (Leaflet/OpenStreetMap), AiSummaryCard, DownloadReportButton.
  • src/api.ts / src/types.ts — cliente tipado del backend, incluido analyzeStream (SSE vía EventSource).
  • src/utils/reportToJson.ts — exportación del informe a JSON.
  • Tests: Vitest + Testing Library (src/__tests__/), E2E con Playwright + Cucumber (webapp/test/, ver webapp/E2E.md).

Backend Python/FastAPI (backend/)

  • app/auth/reddit_oauth.py, app/auth/instagram_oauth.py — OAuth 2.0 con cada plataforma.
  • app/auth/dynamic_origin.py — deriva el redirect_uri de Instagram y el destino de la redirección final del Host de cada petición cuando no hay un valor fijo en .env (pensado para túneles HTTPS tipo ngrok, cuya URL cambia en cada reinicio; ver nota más abajo sobre por qué no usar Cloudflare Quick Tunnel).
  • app/reddit_client.py, app/instagram_client.py — extracción de posts/comentarios/publicaciones públicas, normalizados a un modelo común (SocialPost).
  • app/nlp/fingerprint.py — huella de escritura (longitud de frase, vocabulario, emojis, patrón horario, keywords TF-IDF, idioma).
  • app/nlp/attribute_inference.py — inferencia explicable de atributos (ubicación, ocupación, rutina) a partir de comunidades/hashtags.
  • app/nlp/demographic_extraction.py — extracción de declaraciones explícitas en texto por regex (edad, sexo, ubicación, estudios, ocupación, universidad, empresa).
  • app/nlp/ai_attribute_extraction.py — la misma extracción, pero vía IA local (Qwen3.5-4B, opcional): capta redacciones libres que la regex no reconoce, y una estimación aparte de sexo por nombre público de la cuenta (marcada con menor fiabilidad). Complementa a la regex, nunca la sustituye.
  • app/nlp/travel_detection.py — detección regex de menciones de "de viaje/vacaciones" en el pie de foto, para excluir esas publicaciones del cálculo de dónde vives habitualmente (ver report/generator.py). Complementa (no sustituye) a una detección equivalente vía IA en ai_attribute_extraction.py.
  • app/data/ine_reference.py — tablas de distribución poblacional (INE) usadas para el estrechamiento de población.
  • app/scoring/k_anonymity.py — motor de estimación de k-anonimato (estrechamiento de población en cascada), expone también la proporción ya calculada para el pictograma del frontend.
  • app/scoring/privacy_score.py — motor de scoring de privacidad (0-100).
  • app/vision/geolocation.py — geolocalización de fotos por similitud visual (DINOv2 + FAISS), opcional. Devuelve todas las estimaciones (con su confianza real) más un flag de si el índice está disponible, para poder distinguir "no hay índice" de "no hay resultados fiables".
  • app/vision/scene_analysis.py — análisis del contenido visual de cada foto (Moondream2, modelo de visión-lenguaje local vía llama-cpp-python/GGUF, ~1.4B parámetros de texto): objetos, actividades, aficiones, señales de relación de pareja, matrícula. Arquitectónicamente distinto de geolocation.py (que compara similitud visual contra un índice sin "entender" la foto). Opcional, mismo flag de activación que la geolocalización (WITH_GEOLOCATION), aunque ya no comparte sus mismas dependencias de Python (ver más abajo).
  • app/ai_analysis.py — veredicto general + conclusiones sobre el informe vía IA local (Qwen3.5-4B), opcional; se dispara automáticamente, sin botón, y usa recommendations como insumo.
  • app/progress.py — callback de progreso compartido, usado por el endpoint de streaming.
  • app/analysis_router.py — endpoints de análisis (/api/analyze/{platform}, /api/analyze/{platform}/stream, /api/analyze/ai-summary).
  • app/report/generator.py — ensamblado del informe final + recomendaciones (estas últimas ya no se muestran como sección propia en el dashboard, ver más arriba).
  • app/main.py — app FastAPI; en el arranque (lifespan) precarga el modelo de geolocalización si está disponible, en vez de esperar al primer análisis; métricas Prometheus en /metrics.
  • tests/ — pytest (unit + endpoints), ~95% cobertura, para Sonar.
  • scripts/ — descarga del dataset OSV-5M, construcción del índice FAISS, y recuperación de metadatos (ver más abajo).
  • monitoring/ — configuración de Prometheus/Grafana.

Running the Project

With Docker

docker compose up --build

Arranca solo backend + webapp (servicios sin perfil):

prometheus/grafana (perfil monitoring) y dinov2-igpu-worker (perfil igpu, ver más abajo) son opt-in por diseño -- no arrancan con el comando de arriba a secas, salvo que actives el perfil correspondiente por defecto (ver justo debajo):

docker compose --profile monitoring up --build   # + Grafana/Prometheus
docker compose --profile igpu up --build         # + worker de iGPU
# combinables:
docker compose --profile monitoring --profile igpu up --build

Activar un perfil por defecto, sin pasar --profile cada vez: copia .env.example (el de la raíz del repo, junto a este docker-compose.yml -- distinto de backend/.env.example) a .env y pon ahí COMPOSE_PROFILES=igpu (combinable: COMPOSE_PROFILES=igpu,monitoring). Compose lee ese .env de la raíz automáticamente, así que a partir de ahí docker compose up --build a secas ya arranca también dinov2-igpu-worker -- sin el .env de la raíz (o sin esa variable en él), sigue haciendo falta el flag --profile explícito. Sirve para cualquier perfil, no solo igpu.

Antes de levantarlo, crea backend/.env a partir de backend/.env.example (ver variables de entorno más abajo).

Análisis de imagen en Docker (geolocalización + contenido visual): el docker-compose.yml trae WITH_GEOLOCATION=true por defecto, que instala torch/faiss/transformers (para DINOv2, geolocalización) y llama-cpp-python compilado con soporte CUDA (para Moondream2, análisis de contenido -- ver el extra vision de pyproject.toml y la etapa cuda-builder del Dockerfile) en la imagen del backend, varios cientos de MB extra. Pese al nombre del flag (heredado de cuando solo existía geolocalización), esa misma build es la que necesita también el análisis de contenido visual (app/vision/scene_analysis.py, Moondream2 vía GGUF) -- no hay un flag independiente para activar solo una de las dos, aunque desde el cambio a llama-cpp-python ya NO comparten las mismas dependencias de Python (antes sí, ambas usaban transformers). Sin esto, el backend puede tener el índice FAISS perfectamente construido y montado y aun así reportar la geolocalización como "no disponible" -- esas librerías hacen falta en el análisis, no solo para construir el índice. Si no vas a usar ninguna de las dos funciones, ponlo a false para una imagen más ligera. Los dos modelos (DINOv2 y Moondream2) se precargan en el arranque del contenedor, no en el primer análisis (ver lifespan en app/main.py). La caché de ambos se persiste en backend/data/hf_cache/ para no volver a descargarlos en cada reinicio; Moondream2 además cachea ahí su versión cuantizada (ver MOONDREAM_QUANT_TYPE en app/vision/scene_analysis.py) una vez generada, para no recuantizar en cada reinicio.

Offload de DINOv2 a una iGPU (opcional, --profile igpu): en máquinas con GPU dedicada + iGPU (Intel/AMD, vía DirectML sobre WSL2), dinov2-igpu-worker descarga su propio torch==2.4.1+cu121 + torch-directml -- deliberadamente en una imagen/venv completamente aparte del backend, nunca instalado ahí (ver ADR-28 en docs/src/09_architecture_decisions.adoc para el porqué: mezclarlo con el torch/cu121 que ya usa el backend para DINOv2 corrompe ese entorno). Actívalo con ENABLE_IGPU_OFFLOAD=true en backend/.env y el perfil igpu al arrancar (a mano con --profile igpu, o por defecto con COMPOSE_PROFILES=igpu en el .env de la raíz -- ver más arriba) -- ninguno de los dos por separado hace nada.

Construir backend + webapp + dinov2-igpu-worker los tres a la vez (cada uno con su propia descarga de torch) puede saturar los recursos de Docker Desktop/WSL2 y el build del worker termina con un rpc error: code = Unavailable desc = error reading from server: EOF justo al final (paso exporting layers) aunque los otros dos hayan terminado bien. Si te pasa:

docker images | findstr amitraceable    # confirma si backend/webapp sí terminaron
docker compose build dinov2-igpu-worker # reintenta solo el que falló

Si vuelve a fallar igual, es contención de recursos, no mala suerte puntual -- en orden de probabilidad:

  1. wsl --shutdown y reabrir Docker Desktop (el demonio de BuildKit a veces queda colgado tras un fallo así).
  2. Subir el límite de RAM de WSL2 si lo tienes ajustado bajo -- en %UserProfile%\.wslconfig:
    [wsl2]
    memory=8GB
    
    y wsl --shutdown para aplicarlo.
  3. Construir uno a uno en vez de en paralelo:
    docker compose build backend
    docker compose build webapp
    docker compose --profile igpu build dinov2-igpu-worker
    
  4. Comprobar espacio en disco (docker system df) -- los dos torch+cu121 (backend y worker, cada uno de varios cientos de MB/GB en wheels) más las capas intermedias de build acumulan bastante; docker builder prune libera caché de builds antiguos sin tocar las imágenes en uso.

Without Docker

1. Backend (backend/)

cd backend
uv sync                           # crea .venv con las dependencias, el grupo dev y los modelos de spaCy
cp .env.example .env              # y rellenar credenciales
uv run uvicorn app.main:app --reload --port 3000

(Requiere uv; descarga solo el Python 3.12 que fija backend/pyproject.toml.)

Si quieres geolocalización por imagen o análisis de contenido visual (Moondream2) funcionando en este venv, hace falta un paso más -- lo de arriba, por sí solo, deja ambas funciones en "no disponible" de forma silenciosa (ver más abajo, "Alcance y limitaciones"): no falla con un error, simplemente esa parte del pipeline no aporta nada, así que es fácil no darse cuenta de que falta este paso.

uv sync --extra vision

Con Docker esto ya viene resuelto por defecto (ver WITH_GEOLOCATION más abajo) -- este paso solo es necesario si ejecutas el backend directamente con uvicorn, como aquí arriba.

En arranques posteriores, con el venv ya creado, basta con activar el entorno y lanzar uvicorn directamente.

2. Webapp

cd webapp
npm install
npm run dev

La webapp estará en http://localhost:5173

Variables de entorno

Ver backend/.env.example para la lista completa comentada. Resumen:

VariableObligatoriaNotas
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_REDIRECT_URI, REDDIT_USER_AGENTSíApp tipo "web app" en https://www.reddit.com/prefs/apps
SESSION_SECRET_KEYSíCadena aleatoria larga, firma la cookie de sesión
INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRETNoSin ellas, Instagram devuelve 503 pero Reddit sigue funcionando. App "API setup with Instagram Login" en Meta for Developers
INSTAGRAM_REDIRECT_URINoDéjala vacía si usas Docker (ver nota de túnel más abajo): se deriva sola del Host de cada petición. Fíjala solo con dominio propio en producción
FRONTEND_ORIGINNoDéjala vacía si usas Docker: se deriva sola del Host. Fíjala (p. ej. http://localhost:5173) solo si sirves frontend y backend en puertos distintos sin Docker
ENABLE_AI_ANALYSISNotrue por defecto. Desactiva del todo el análisis con IA (veredicto/conclusiones + extracción de atributos, ver backend/app/nlp/ai_client.py) sin desinstalar nada
QWEN_GGUF_REPO_ID, QWEN_GGUF_FILENAMENoRepo/fichero de Hugging Face con el GGUF de Qwen3.5-4B a cuantizar localmente (Q4_K_M vía llama-cpp-python) -- SIN VALOR POR DEFECTO A PROPÓSITO, hay que confirmarlo contra lo disponible en Hugging Face antes de desplegar (ver .env.example). Sin rellenar, la sección "Analizar con IA" del dashboard indica que no está disponible, sin afectar al resto
ENABLE_SCENE_ANALYSISNofalse por defecto. Activa el análisis de contenido visual con Moondream2 (descripción de escena por foto). Afecta exclusivamente a esto: la geolocalización por similitud visual (DINOv2) no lee esta variable y sigue funcionando igual, esté esto activado o no -- probado explícitamente en test_geolocation.py. Para que Moondream2 pueda funcionar (con esta variable en true) hace falta además WITH_GEOLOCATION=true en el build de Docker (mismo build-arg, aunque desde ADR-43 instala llama-cpp-python para Moondream2, no las mismas librerías que DINOv2). Ver ADR-43 en docs/ (sustituye a ADR-19, que documentaba el backend transformers anterior): la cifra de RAM/VRAM necesaria depende ahora de si MOONDREAM_QUANT_TYPE está cuantizando el modelo o no, no hay un único número fijo como antes

Nota sobre Instagram y HTTPS en local: la API de Instagram (Business Login) exige que redirect_uri sea HTTPS, incluso en desarrollo. Para probarlo en local sin dominio propio, usa ngrok apuntando a la webapp (puerto 8080), no al backend directamente -- la webapp lleva su propio nginx que hace de proxy hacia el backend bajo el mismo origen (ver webapp/nginx.conf), así que un único túnel sirve para todo:

ngrok http 8080

(requiere cuenta gratuita en https://ngrok.com y ngrok config add-authtoken <token> una vez).

Restringir quién puede entrar (Basic Auth): el plan gratuito de ngrok incluye un dominio propio estable (no cambia en cada reinicio) y permite añadir autenticación sin plan de pago, vía Traffic Policy. Copia backend/policy.example.yaml a backend/policy.yaml (este segundo nombre está en .gitignore, nunca se sube) y pon tu usuario/contraseña real dentro:

on_http_request:
  - actions:
      - type: basic-auth
        config:
          realm: "AmITraceable"
          credentials:
            - "usuario:contraseña-larga-y-unica"

Y arranca el túnel apuntando a ese archivo:

ngrok http 8080 --url=tu-dominio.ngrok-free.app --traffic-policy-file=backend/policy.yaml

Así, quien no tenga esas credenciales ni siquiera llega al backend -- ngrok rechaza la petición en su propio borde, antes de reenviarla. Ver ADR-39 sobre por qué esto (y no dominio propio + Cloudflare) es la solución elegida.

Notas sobre ngrok y por qué no usar Cloudflare Quick Tunnel

⚠️ No usar cloudflared tunnel --url ... (Cloudflare Quick Tunnel): el edge de trycloudflare.com bufferiza por completo las respuestas text/event-stream, así que el streaming SSE de /api/analyze/{platform}/stream (la pantalla de carga con el listado de fases) nunca llega en tiempo real al navegador -- todos los eventos se reciben de golpe justo cuando el análisis termina, y se ve solo "Analizando tu actividad pública en X..." sin el listado de fases mientras carga. Es un problema conocido y documentado del propio Cloudflare (no del backend: las cabeceras text/event-stream y X-Accel-Buffering: no ya se mandan bien). Un named tunnel de Cloudflare (con dominio propio) sí respetaría el streaming, pero para desarrollo local sin dominio, ngrok funciona correctamente sin ese problema.

Con INSTAGRAM_REDIRECT_URI y FRONTEND_ORIGIN vacías en .env (valor por defecto), lo único que hay que hacer con la URL https://xxx.ngrok-free.app que te dé cada vez es darla de alta en la app de Meta Developers (con el path /auth/instagram/callback) -- eso sigue siendo manual, Meta no tiene API para gestionar esa lista. Todo lo demás (.env, reconstruir imágenes, reiniciar uvicorn) se adapta solo en cada reinicio del túnel.

La primera vez que abras la URL de ngrok en el navegador, el plan gratuito muestra una página intermedia de aviso ("You are about to visit...") -- solo hay que darle a "Visit Site", no es un error.

Scripts de geolocalización por imagen (opcional)

El módulo de geolocalización de fotos (app/vision/geolocation.py) es opcional: sin el índice FAISS construido, simplemente no aporta nada al informe, sin errores. Para activarlo:

cd backend
uv sync --extra vision

uv run python scripts/geolocalization/download_osv5m_spain.py --output data/osv5m_spain --max-disk-gb 35
uv run python scripts/geolocalization/build_faiss_index.py --images data/osv5m_spain

(El extra vision apunta al índice de PyTorch con CUDA 12.1 (pytorch-cu121, ver [tool.uv.sources] en backend/pyproject.toml) -- ver también el ARG WITH_GEOLOCATION del Dockerfile si vas a correr esto dentro de Docker.)

El análisis de contenido visual (app/vision/scene_analysis.py, Moondream2) usa el mismo el extra vision de pyproject.toml, pero no necesita nada de lo anterior: no hay índice que construir ni dataset que descargar, se ejecuta directamente sobre cada foto.

  • download_osv5m_spain.py descarga solo las imágenes de España del dataset OpenStreetView-5M (streaming shard a shard, con límite de disco configurable, reanudable tras interrupción).
  • download_osv5m_world.py es la variante sin filtro de país (mucho más pesada en tráfico de red, ~260GB).
  • build_faiss_index.py extrae embeddings con DINOv2 y construye el índice de búsqueda por similitud.
  • recover_metadata.py reconstruye metadata.csv a partir de las imágenes ya descargadas, sin volver a bajarlas -- pensado para el caso (ya corregido, pero por si tienes un metadata.csv de una ejecución anterior al fix) en que se perdía la columna id al guardar, dejando el índice sin poder casar ninguna imagen con su fila de metadatos.

Estos datos/artefactos no se versionan en el repositorio (ver .gitignore): son regenerables ejecutando los scripts.

Available Scripts

Webapp (webapp/package.json)

  • npm run dev — servidor de desarrollo.
  • npm test — tests unitarios (Vitest, ~100 tests).
  • npm run test:coverage — tests con cobertura (para Sonar).
  • npm run test:e2e — tests E2E (levanta webapp + backend sueltos y corre Cucumber; ver webapp/E2E.md). Cubre la pantalla de consentimiento y que el dashboard exige autenticación real contra el backend (sin mockear), pero no el flujo completo de OAuth (necesitaría credenciales reales) ni el montaje con Docker/nginx.
  • npm run start:all — levanta webapp + backend Python a la vez (conveniencia para desarrollo/E2E).
  • npm run lint — ESLint.

Backend (Python)

  • uv run uvicorn app.main:app --reload --port 3000 — arranca el backend en desarrollo.
  • uv run pytest — tests unitarios (~260 tests, 1 se salta si no tienes instalado el extra vision en local).
  • uv run pytest --cov=app --cov-report=xml --cov-report=term — tests con cobertura (genera coverage.xml para Sonar).
Graphify (grafo de conocimiento del código)

El repo tiene un grafo generado con Graphify (graphify-out/) que Claude Code consulta automáticamente en vez de releer archivos sueltos, ahorrando tokens. Solo funciona así de forma automática dentro de Claude Code (usa hooks internos de esa herramienta) — en un chat normal de Claude no hay ahorro automático salvo que se le pida explícitamente consultar el grafo por terminal.

Al clonar en un equipo nuevo, ejecutar una vez (regeneran configuración local, no se versiona):

uv tool install graphifyy          # o: pipx install graphifyy — evitar "pip" a secas
graphify claude install --project  # regenera .claude/settings.json con la ruta de este PC
graphify hook install              # auto-actualiza el grafo tras cada commit

graphify-out/ (grafo, informe, visualización) sí se versiona; .claude/settings.json, graphify-out/cache/, graphify-out/cost.json y las carpetas de backup con fecha (graphify-out/20YY-MM-DD/) no, porque son específicos de cada máquina — ver .gitignore.

Respecto a SonarQube

Debido a restricciones de la cuenta gratuita, el token de Sonar debe renovarse cada dos meses. Configuración en sonar-project.properties (rutas de fuentes/tests, exclusiones, reportes de cobertura).

Plan de evaluación pendiente (para la memoria)

No incluido en el código, pero necesario antes de la defensa:

  1. Dataset de prueba con consentimiento (cuentas propias del equipo, alts conocidos) para validar el módulo de inferencia de atributos, el de geolocalización por imagen y el de análisis de contenido visual (aficiones/actividades/señales de pareja).
  2. Métricas: precisión de los atributos inferidos vs. verdad conocida, tasa de falsos positivos, precisión real del módulo de geolocalización sobre fotos propias geoetiquetadas, y precisión del análisis de contenido visual sobre fotos propias etiquetadas manualmente.
  3. Comparativa de las ponderaciones del scoring (_WEIGHTS en privacy_score.py) y de la asunción de independencia del estrechamiento de población (k_anonymity.py) contra percepción subjetiva de usuarios reales o correlaciones reales del Censo/EPA.