Capas independientes, projectM en el mapping y catalogo de la galeria

Mapping
- Los clips salian del reves: mapper.js activaba UNPACK_FLIP_Y_WEBGL, pero el
  modelo va con origen arriba-izquierda y texImage2D ya sube la primera fila
  en t=0, asi que el flip la mandaba al final. Comprobado renderizando el
  mapper real en Chromium headless con una fuente mitad roja / mitad azul.
- projectM se puede usar YA en una capa: los .milk de data/presets-projectm se
  traducen a Butterchurn en el navegador al elegirlos (milkdrop-preset-
  converter). La capa MilkDrop gana 'biblioteca' (butter | projectm) sin
  cambiar su firma, para no recrear el contexto WebGL al cambiar de una a otra.
  Medido: 100/100 presets de una muestra convierten, 84/84 de los que llevan
  shaders warp/comp; ~7 ms por preset.
- Cada capa tiene su pestana arriba y su propia vista, y '+ CAPA' crea una y
  entra en ella: con varias capas, ir y volver a MAPPING no era viable.
- Dos capas MilkDrop sin preset ya no salen identicas (cogian el indice 0):
  cada instancia elige uno al azar y lo escribe en el estado.
- Una capa nueva ya no nace en la fuente "motor", que es el lienzo del motor
  activo y hacia que dos capas ensenaran lo mismo.

Galeria de visuales
- scripts/catalogar-visuales.py: ficha de cada clip (pelicula, personajes,
  duracion) y, sobre todo, si es una silueta de verdad y cuanta figura tiene.
  Distingue silueta de corte crudo por el negro puro del fondo: 0,63-0,90
  frente a 0,04-0,06, sin zona gris.
- El panel lista los clips agrupados por pelicula, con nombre legible y aviso
  de los que casi no tienen figura, en vez del nombre del archivo.
- Soporte de siluetas con canal alfa (.webm VP9): transparencia de verdad, sin
  recorte por luminancia. El shader del mapper ya la respeta.

Documentacion
- docs/visuales.md nuevo; pendiente.md al dia con lo hecho y lo que queda.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hacklab 2026-08-02 14:56:01 +02:00
parent 5d5223db9f
commit f017246f0e
30 changed files with 3771 additions and 292 deletions

160
docs/visuales.md Normal file
View file

@ -0,0 +1,160 @@
# 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.