sentido/README.md · generada al construir
sentido
EmbeddingGemma 2 en local para elegir láminas por intención, comprobar que un vídeo enseña lo que dice su pliego y buscar en el metraje.
La capa de sentido de Tórculo: EmbeddingGemma 2 en local para elegir láminas por intención, comprobar que un vídeo enseña lo que dice su pliego, comparar pliegos por lo que significan, verificar que cada toma de voz es la de su beat y leer el ritmo de un montaje. Nada sale del Mac.
Todo se probó sobre «La terraza no existe» (Vídeos/Mujeres al borde de un ataque de nervios): 160 beats, 148 tomas de voz real, máster de 28:49. Las salidas de las pruebas viven en <proyecto>/sentido/ y es lo único que se escribe en la carpeta del proyecto.
Instalación
cd ~/Developer/torculo/sentido
uv venv --python 3.13 .venv
uv pip install --python .venv/bin/python -e ".[onnx,pruebas]"
.venv/bin/python -m pytest -q pruebasLos pesos se leen de ~/.cache/spdf-models/<repo> (allí están google/embeddinggemma-2, las tres conversiones de mlx-community y onnx-community/embeddinggemma-2-ONNX); si no, de la caché de Hugging Face. Otra carpeta: SENTIDO_MODELOS. El parser de .mesa se toma de ~/Developer/manim-saorin (otra ruta: SENTIDO_MANIM_SAORIN).
Ojo: las entradas mlx-community/* y onnx-community/* de ~/.cache/huggingface/hub están vacías (solo refs/main); los pesos de verdad están en ~/.cache/spdf-models. MLX necesita mlx-vlm en la revisión 3d87e884 (la de la ficha de la conversión): la 0.7.6 de PyPI no trae embedding_gemma2.
Órdenes
sentido indexa <dir> [--dims 256]
sentido elige «intención» [--evita «literal»] [--k 5]
sentido resuelve <pliego> [--refresca] [-o pliego.resuelto.mesa | -o -]
sentido qc <vídeo> <pliego> [--capitulo N]
sentido diff <pliego_a> <pliego_b> [-o informe.html]
sentido voz <pliego> [--toma etiqueta=ruta.wav:beat]
sentido ritmo <vídeo> [--k 4]
sentido mide <pliego> <vídeo>Todas aceptan --motor mlx|mlx8|torch|torch-cpu|onnx.
El motor: qué backend y por qué
sentido mide corre cada motor en su propio proceso con el trabajo real del proyecto: 160 intenciones de beat (texto, lotes de 32), 64 fotogramas del máster a 448 px (imagen, lotes de 8, 280 fichas por imagen) y 64 ventanas de 3 s de la voz real (audio, lotes de 16). M4 Max, 13-10-2026. La máquina no estaba en reposo (otros agentes compilando Rust y corriendo un banco de Gemma a la vez), así que los absolutos son pesimistas; el orden entre motores sí es estable.
| Motor | Texto (ms/elem. en lote · suelto) | Imagen | Audio 3 s | Carga | Coseno con torch |
|---|---|---|---|---|---|
| MLX bf16 (mlx-vlm, Metal) | 9,0 · 28 | 276 · 265 | 24,9 · 214 | 16 s | 0,99994 / 0,99991 / 0,99995 |
| MLX 8 bits | 7,1 · 39 | 277 · 249 | 20,2 · 185 | 19 s | 0,99982 / 0,99982 / 0,99986 |
| sentence-transformers bf16, MPS | 11,3 · 137 | 310 · 307 | 31,0 · 270 | 19 s | referencia |
| onnxruntime fp32, CPU | 77 · 61 | 4217 · 5451 | 399 · 775 | 17 s | 0,99997 / 0,99992 / 0,99996 |
(torch f32 en CPU tardó más de diez minutos en la misma tanda y su resultado se perdió al cortarse la primera pasada; no lo repetí. El banco de SPDF del 7-10-2026 lo da en 761 ms por imagen y 758 ms por audio de 10 s: no compite.)
Elección: MLX bf16 para las tres modalidades. Es el más rápido en texto y empata con el de 8 bits en imagen (la imagen la domina el codificador de visión, no los pesos); el de 8 bits gana un 20 % en audio pero se aleja más de la referencia, y bf16 es el que el banco de SPDF da por «mismo espacio» que los pesos originales. ONNX en CPU es 15 veces más lento en imagen: solo sirve donde no hay Metal. Nunca float16: el modelo desborda su rango y devuelve NaN sin avisar; todo sale re-normalizado en f32.
Para los fotogramas de vídeo se usa el presupuesto de visión que la ficha del modelo da al vídeo, 140 fichas por fotograma en vez de las 280 de una imagen: mitad de tiempo (medido: 199-260 ms frente a 443-519 ms por imagen en lotes de 4-16, con la máquina cargada) y coseno 0,96-0,98 con la versión de 280. Como todos los fotogramas de un vídeo van con el mismo presupuesto, la comparación entre ellos y con el texto no se tuerce. A 70 fichas el coseno cae a 0,92-0,96 y ya no compensa.
1. Bestiario: indexa
El bestiario de un proyecto son los callables de su válvula (PYTHON piezas.py) más las estampas de su archivo. herramientas/renderiza_bestiario.py (con el Python de Manim) los dibuja uno a uno sobre papel crema y escribe bestiario.json con nombre, descripción (primer párrafo del docstring y la sección de la válvula) y la línea de pliego que lo pone (python: taxi, pon: foto «bayt»):
cd <proyecto>
PYTHONDONTWRITEBYTECODE=1 /opt/homebrew/Cellar/manim/0.20.1/libexec/bin/python \
~/Developer/torculo/sentido/sentido/herramientas/renderiza_bestiario.py piezas.py sentido/bestiario
sentido indexa sentido/bestiarioEn «La terraza no existe»: 79 dibujos y 11 fotos del archivo (12 fotos de Commons no se pudieron descargar) en 3 min de render; el índice, 90 fichas, en 107 s la primera vez (con la carga del modelo) y 0,3 s después (incremental por huella; si no hay nada nuevo, ni carga el modelo). Sin bestiario.json, indexa toma cualquier PNG/JPEG/WebP/SVG de la carpeta con un <nombre>.txt al lado como descripción (los SVG se rasterizan con rsvg-convert).
Se guardan en SQLite los 768 componentes de tres vectores por ficha: la imagen sola, la descripción (title: nombre | text: …) y un mixto (texto e imagen en una sola entrada intercalada). La búsqueda recorta a 256 por defecto (Matryoshka, re-normalizando).
2. Elegir: elige y pon: lámina ≈ «…»
q = normaliza( e(intención) − 0,5 · e(evita) ) y luego MMR (μ = 0,7)Medido con 28 intenciones parafraseadas a mano y sus respuestas aceptables (pruebas/elige_prueba.json, pruebas/evalua_elige.py):
| Vector | 768 · @1 / @3 | 256 · @1 / @3 | 128 · @1 / @3 |
|---|---|---|---|
| imagen sola | 71 % / 71 % | 68 % / 68 % | 50 % / 57 % |
| descripción sola | 89 % / 96 % | 89 % / 100 % | 75 % / 89 % |
| mixto | 93 % / 100 % | 93 % / 100 % | 71 % / 93 % |
Con prefijo de consulta en la intención (sin él, el mixto a 256 baja a 82 %). Por defecto: mixto, 256, prefijo de consulta. 128 no sirve para esto.
Lo honesto: la imagen sola acierta poco con dibujos a mano (68-71 %); lo que hace bueno al índice es la descripción. Un bestiario sin docstrings funcionará peor. Y las intenciones abstractas fallan: «la comunicación que no llega» no trae el teléfono ni el contestador entre los cinco primeros. El literal a evitar funciona: «la espera» da semaforo; «la espera» evitando «un semáforo» da papelito y baja semaforo_calle.
El lock
al decir «espera»: pon: lámina ≈ «la espera» · evita «un semáforo»sentido resuelve x.mesa busca el índice (sentido/bestiario/indice.sqlite en la carpeta del pliego o en sus padres), elige y congela en x.mesa.lock (JSON: consulta, literal, elección, línea de pliego, puntuación, alternativas, modelo, revisión, motor, dims, λ, índice, fecha). Mientras la consulta no cambie, el lock manda: la segunda pasada tarda 0,06 s y no carga el modelo, aunque el índice o el motor hayan cambiado. --refresca vuelve a elegirlo todo; las entradas que ya nadie pide se caen del lock.
sentido.lock es Python puro: lock.aplica(texto, ruta) es el gancho para el compilador de la Mesa (sustituye cada ≈ por su elección congelada o levanta LockIncompleto). No he tocado manim-saorin: hoy se resuelve con sentido resuelve x.mesa -o x.resuelto.mesa y se compila el resuelto. Probado con sentido/prueba_lamina.mesa: el pliego resuelto pasa por mesa ir (el compilador real) y produce las piezas papelito y celosia_ojo. Para que sea «al compilar» de verdad basta con una línea en manim_saorin/formato/cli.py antes de parsea: if "≈" in texto: texto = lock.aplica(texto, archivo).
3. qc: ¿se ve lo que se dice?
- Cronología. El sidecar da la duración de cada voz, no su instante en el máster (lo decide el render y la Mesa no lo guarda). Se recupera escuchando: el wav de cada beat se busca en orden en la banda sonora por correlación cruzada normalizada a 8 kHz. En el máster: 147 de 148 voces encontradas, correlación mediana 0,946 (mínima 0,78), 85 s. Los mudos se reparten el hueco en proporción a su
> dura. - Fotogramas a 1 fps con ffmpeg (448 px), embebidos sin prefijo, 140 fichas.
- Intención de cada beat, del parser real:
di:, rótulos (pon:, letreros, apuntes,> capítulo «…»), dibujos, clips, poses de los títeres de un mudo y comentarios//(el parser los descarta; se recogen aparte y se atan al beat que los contiene). - Parecido centrado: a cada intención se le resta su parecido medio con todo el vídeo (cada frase tiene su suelo; crudo, todo cae entre 0,6 y 0,8).
Medido en el máster (1729 fotogramas, 13 min la primera vez con la máquina cargada: 255 s de extracción, 401 s de embebido a 232 ms/fotograma, 85 s de cronología; 10-25 s después, de caché):
- 78 % de los beats reconocen su intención como la mejor entre ellos y sus ±2 vecinos (azar: 20 %); 89 % la tienen entre las 5 primeras de 160.
- En una parte (
partes-mujeres/cap02.mp4, Granada, 12 beats): 92 % y 100 %, en 58 s.
Marcas: parecido bajo (decil inferior), arrastra (se parece más a un beat anterior) y se adelanta (a uno siguiente). En el máster salieron 33 marcados; todas las confusiones con un vecino apuntan hacia atrás y ninguna hacia delante: es la página que se conserva (lo dibujado se queda hasta que se retira), no un error, y por eso «arrastra» se avisa en tono menor.
Lo flojo, mirado fotograma a fotograma: el modelo lee mal el texto pequeño de la página a 448 px y 140 fichas, así que un beat cuya intención es un rótulo («Pasaron dos años», con el sello bien visible) puede salir con parecido bajo. Entre los de parecido bajo hay aciertos plausibles (c1_35: la intención habla del bastidor y del titiritero y en pantalla solo está el patio vacío; c3_06: «espacios liminales… crear» mientras siguen en pantalla los clips de cabinas del beat anterior) y falsos positivos claros. Es un detector de dónde mirar, no un juez.
4. diff: por sentido
Cada beat se embebe tres veces (voz, puesta, entero; prefijo de similitud) y los dos pliegos se emparejan con el algoritmo húngaro con una casilla vacía de coste 0,30. La voz es la identidad (pesa 0,75 cuando los dos hablan; casar un beat con voz con uno mudo lleva un recargo de 0,20). Clasifica: movidos (fuera de la subsecuencia creciente más larga), reescritos con cambio de sentido (coseno de voz < 0,92), retocados, puesta cambiada (< 0,90), puesta retocada, solo forma, nuevos y quitados. Los ids no se usan para emparejar.
- v6 → v7 (1 s): 158 parejas, 129 idénticas; 2 con otra puesta (c6_14 gana
celosia_paso, c9_06 gana el salto de Candela), 23 con la puesta retocada sin cambio de sentido (letreros que pasan a apuntes con el mismo texto, clips añadidos), 4 de solo forma, 2 nuevos (c11_02b y c11_02c, los créditos partidos de c11_02, que el diff señala como su pariente más cercano). Ningún movido ni reescrito: cuadra con los 29 beats cuyo texto cambió de verdad. - v1 → v7: 151 parejas, 94 con otra puesta, 54 retocadas, 9 nuevos (los mudos de apertura, el respiro de Granada, «de Madrid, 1988, a Nankín, 2022»…), ninguno quitado.
- Prueba sintética sobre v7 (dos beats intercambiados, una voz reescrita, otra retocada, un beat borrado): las cuatro cosas, y solo esas. El reescrito sale con coseno de voz 0,758 y el retoque con 0,988: hay hueco de sobra en torno al umbral.
Un fallo que se corrigió por el camino: emparejando por el beat entero, el «Las dos cosas a la vez.» de v1 se casaba con un mudo de v7 porque la puesta pesaba más que la voz.
5. voz: ¿dice la toma lo que dice el pliego?
Cada wav en ventanas de 3 s (16 kHz mono, sin prefijo), cada di: en tantos trozos como ventanas (prefijo de consulta), DTW grueso sobre 1 − coseno; y, sobre todo, el rango: en qué puesto queda el texto propio entre los 148 del pliego al ordenar por parecido con la toma.
Medido: 152 tomas (las 148 del sidecar y 4 de control), 452 ventanas, 61 s de audio (134 ms/ventana con la máquina cargada), 2 min en total.
- 98 % de las tomas tienen su propio texto en el primer puesto entre 148 (99 % entre los 5 primeros). El DTW separa la pareja verdadera de un texto ajeno con AUC 0,993.
c3_02: 0,40 s de silencio, avisado. Es deliberado (la frase la dice el taxista del clip), pero el aviso es correcto.- La toma antigua
voz/c3_02.original.wavse reconoce como c3_02 (positivo), y la misma toma probada contra el texto de c3_03 da el aviso (su texto queda 64.º). c2_04es un fallo real del máster. Su texto es «Y la terraza no es sino una celosía moderna», y la herramienta dice que la toma se parece más a c1_33 («…no puede llorar por un amor de felonía»), con su texto en el puesto 23. Comprobado con Whisper local (mlx_whisper, large-v3-turbo):audio/beats/c2_04.wavdice «y llora por un amor de felonía por dejar», y en el máster, a las 7:41, suena eso mismo («…y llora por un amor de felonía por dejar la moderna»). La frase de la terraza como celosía moderna no está en el vídeo.
Esto no transcribe ni lo pretende (para eso, Whisper local); comprueba que el fichero que suena es el de su beat.
6. ritmo
Novedad del segundo t = 1 − cos(v_t, v_{t−1}) sobre los mismos vectores de fotograma; cortes = máximos locales por encima de mediana + 4·MAD. En el máster: 125 cortes, 4,34 por minuto, 24 s con los vectores en caché (son los de qc). Frente al detector de escenas de ffmpeg (umbral 0,3; 307 s): el 80 % de sus 45 cambios tienen un corte por sentido a ±1 s, pero solo el 26 % de los cortes por sentido coinciden con un cambio de píxel. Mirando los más fuertes, son cambios reales (la página que se barre, la entrada de un clip, una cortinilla de capítulo): ffmpeg se pierde muchos porque el papel crema se parece a sí mismo. Sin una verdad anotada a mano no puedo dar la precisión real del detector; la coincidencia con ffmpeg no lo es.
7. metraje: el material filmado
mesa ingesta carpeta/ deja en carpeta/.metraje/ el catálogo (proxies,
audio a 16 kHz, transcripción de Whisper local palabra a palabra, hablantes,
planos y lo que ve Vision). sentido metraje indexa carpeta/ embebe los
fotogramas a 1 fps (140 fichas), ventanas de audio de 3 s y cada frase
(title: <fichero> | text: …) en carpeta/.metraje/sentido.sqlite,
incremental por huella. busca puntúa PLANOS (entre cortes: la media de
sus dos mejores segundos), centrado por consulta, con exclusión de lo ya
usado; modo: frase busca en lo que se dice por sentido. ancla dice cuál
de los recortes de un fotograma es «la silla».
Medido el 15-10-2026 (M4 Max, máquina compartida): 13 vídeos de la NASA (30,6 min) en 224 s (2074 fotogramas, 601 ventanas, 203 frases); una entrevista de 5:18 en 30 s. Los planos elegidos por intención («la Tierra desde la ventanilla», «la cápsula con paracaídas cayendo al océano», «una sala de control llena de pantallas», «los astronautas abrazados al volver») salieron todos a la primera, mirados fotograma a fotograma; una intención sin respuesta en el material («una silla vacía») trae fotogramas negros: el parecido centrado no sabe decir «no hay».
Lo que queda flojo
- El QC visual es una señal de atención, no de corrección: lee mal el texto pequeño y confunde persistencia con error (por eso existe «arrastra»).
eligedepende de las descripciones; con imagen sola acierta el 70 %.- El gancho de
≈en el compilador de la Mesa está escrito pero no puesto (no he tocado manim-saorin). - Las cifras de velocidad se tomaron con la máquina compartida; repetir
sentido mideen reposo. - La cronología necesita que el vídeo lleve la voz real dentro; con una animática muda no hay de dónde agarrarse (se puede pasar
--capitulopara una parte suelta, y los mudos se reparten por> dura).