# La galería de visuales

Los clips que se ven en el Mezclador y en las capas del mapping viven en
`data/videos`. Además del archivo suelto, hay un **catálogo**
(`data/visuales.json`) con la ficha de cada uno: de qué película sale, quién
aparece, cuánto dura y — lo importante — **si es una silueta de verdad y cómo
de llena está**.

Sin esa ficha el panel solo puede enseñar `p3-davy-jones-tormenta_silueta-negro.mp4`.
Con ella enseña *Davy Jones (en la tormenta)*, agrupado bajo *Piratas del
Caribe: En el fin del mundo*, y marca los que casi no tienen figura.

---

## Cómo se genera

```bash
cd ~/COFRE/CODERS/MUSIKA/VISUALES/FOSFENO

python3 scripts/catalogar-visuales.py            # cataloga y escribe el JSON
python3 scripts/catalogar-visuales.py --limpiar  # + aparta los que no son siluetas
```

Necesita `ffmpeg`/`ffprobe` y `numpy`. Tarda unos segundos por clip: decodifica
2 fotogramas por segundo a 160x90 en gris y saca las cuentas de ahí.

Hay que volver a pasarlo **cada vez que entran clips nuevos** en la galería. El
panel lo recarga solo al pulsar el botón de recargar del Mezclador, o al subir
un archivo; si el JSON no está, el panel sigue funcionando con los nombres de
archivo pelados.

## Qué mide, y por qué así

**¿Es una silueta o es un trozo de película tal cual?** En un clip siluetado
por DETEKTION el fondo es negro **puro** (valor 0), cosa que no pasa nunca en
una imagen de cine, por oscura que sea. Así que basta con medir qué porcentaje
del cuadro está a cero:

| | negro puro |
|---|---|
| Clips siluetados de la galería | 0,63 – 0,90 |
| Cortes sin siluetear | 0,04 – 0,06 |

No hay zona gris, y por eso el umbral está en 0,30. No es un número inventado:
sale de medir la galería entera.

**¿Tiene figura suficiente?** Un clip puede estar perfectamente siluetado y aun
así no servir, porque la escena era muy oscura y el recorte salió casi vacío.
Se mide el porcentaje del cuadro por encima de gris 32, y se mira la **mediana**
(no un fotograma suelto: casi todos los clips tienen tramos vacíos y tramos
llenos, y juzgar por el fotograma central engaña).

| calidad | criterio | qué significa |
|---|---|---|
| `buena` | mediana ≥ 0,03 | se ve bien sobre el fondo |
| `floja` | mediana < 0,03 | figura pequeña o muy oscura |
| `vacia` | mediana < 0,005 o más de la mitad de fotogramas vacíos | apenas hay nada |
| `descartable` | negro puro < 0,30 | no es una silueta: se ve el fondo |

## Qué hace `--limpiar`

Los `descartable` (los que enseñan el fondo entero de la película) se **mueven**
a `data/videos/_descartados/`. No se borran: si un día quieres volver a pasarlos
por DETEKTION, siguen ahí. El servidor no los sirve, porque solo lista archivos
sueltos de `data/videos`, no subcarpetas.

Los `floja` y `vacia` **no se tocan**: son siluetas correctas, solo que oscuras.
Se quedan en la galería marcadas, y ya decides tú si las usas — sobre un fondo
de MilkDrop brillante, una silueta tenue puede funcionar.

## La ficha

```json
{
  "archivo": "p3-davy-jones-tormenta_silueta-negro.mp4",
  "obra": "Piratas del Caribe: En el fin del mundo",
  "anio": 2007,
  "titulo": "Davy Jones (en la tormenta)",
  "tipo": "silueta",
  "modo": "silueta sobre negro",
  "origen": "detektion",
  "personajes": ["Davy Jones"],
  "etiquetas": ["en la tormenta"],
  "ancho": 1152, "alto": 480, "fps": 23.98, "duracion": 30.03,
  "metricas": { "negroPuro": 0.737, "figura": 0.0582,
                "figuraPico": 0.1974, "brillo": 8.49, "vacios": 0.03 },
  "clase": "silueta", "calidad": "buena", "aviso": "",
  "miniatura": "miniaturas/p3-davy-jones-tormenta_silueta-negro.jpg"
}
```

La miniatura no es un fotograma cualquiera: es **el fotograma con más figura**
de todo el clip, que es el que dice de verdad qué vas a ver.

### De dónde salen los nombres

El catálogo lee las dos convenciones de DETEKTION:

- `siluetear.py` → `<algo>_silueta-<modo>.mp4` → `tipo: silueta`
- `recortar.py` → `<peli>_13m26s.mp4` → `tipo: corte` (crudo, **sin** siluetear)

y del resto del nombre saca la película (`p1`…`p5`, `hackers`, `tron`…), los
personajes conocidos y las palabras de escena. Las tablas están arriba del
todo en `scripts/catalogar-visuales.py`: **para añadir películas o personajes
nuevos se tocan ahí y ya**.

Un clip que no encaje en ninguna tabla no rompe nada: sale con el nombre
prettificado y agrupado en "Sin catalogar".

## Transparencia de verdad (canal alfa)

Hay **dos formas** de que un personaje se vea sobre las visuales, y conviene
no confundirlas:

**1. Recorte por luminancia (lo de siempre).** El clip `_silueta-negro.mp4`
tiene fondo negro opaco, y es el Mezclador el que lo hace transparente al
vuelo: `src(s0).layer(src(s1).luma(umbral, 0.15))`. Funciona y va en H.264,
que es lo que mejor decodifica una Raspberry. Pega: hay un umbral que ajustar,
y las zonas oscuras del propio personaje (pelo, ropa negra) se comen con él.

**2. Canal alfa de verdad (`_silueta-alfa.webm`).** El clip lleva la
transparencia dentro, en un WebM con VP9. No hay umbral, no hay bordes
comidos: los píxeles del fondo simplemente **no existen**, y debajo se ve la
capa que haya. Se genera con:

```bash
.venv/bin/python3 siluetear.py clip.mp4 --modo alfa
```

FOSFENO ya lo compone bien sin tocar nada: el fragment shader del mapper
saca `gl_FragColor = vec4(c.rgb, c.a * uAlpha)` y mezcla con
`SRC_ALPHA / ONE_MINUS_SRC_ALPHA`, así que una capa con alfa deja ver la capa
dibujada antes que ella. **Comprobado**, no supuesto: con una capa de fondo
roja y encima un clip alfa, el 48,6 % de la pantalla sale roja por los huecos.

> Para que se vea a través hace falta que **haya algo debajo**: otra capa del
> mapping dibujada antes (MilkDrop, shader, cámara). Si el clip alfa es la
> única capa, debajo solo está el negro del compositor.

### Dos trampas del alfa en WebM

- **El decodificador VP9 nativo de ffmpeg se come el alfa en silencio**: no
  falla, devuelve el vídeo entero opaco. Para leerlo hay que pedir
  `-c:v libvpx-vp9` a mano. Es exactamente por esto que el catalogador mide
  estos clips con `ffmpeg -c:v libvpx-vp9 ... -vf format=rgba,alphaextract`
  (y el `format=rgba` no es adorno: sin él `alphaextract` no negocia formato
  y ffmpeg aborta).
- **VP9 no tiene decodificación por hardware en la Raspberry Pi 5**, que sí la
  tiene para H.264/HEVC. En el portátil va sobrado; en la Pi, con clips de
  480p cortos debería ir, pero **hay que probarlo antes de un bolo**. Si se
  atraganta, el camino 1 (negro + luma key) sigue ahí.

## En el panel

`server.py` carga el catálogo en `state.meta.visuales` (`{archivo: ficha}`),
filtrado contra lo que hay de verdad en disco para que un catálogo viejo no
enseñe clips borrados. El panel lo usa en `fillClipSelect()`: las listas de
clips salen agrupadas por película con `<optgroup>`, con el título legible, la
duración y la marca de calidad (`· poca figura`, `· casi vacio`), y el aviso
completo en el `title` de cada opción.
