skills/torculo/SKILL.md · generada al construir
Tórculo
El canon, la pluma y las láminas, las materias WGSL, las tomas y la capa de sentido, para usar el motor sin escribir un pliego.
Tórculo es la prensa del grabador: la plancha se trabaja una vez y se estampa tantas veces como haga falta. Aquí la plancha es una escena, un dibujo o una materia, y las estampas son un vídeo, una web, una app o un juego.
Todo vive en ~/Developer/torculo (repo git propio):
| Pieza | Dónde | Qué es |
|---|---|---|
| Canon | canon/ | La única fuente de verdad: paleta, azar, oficio, foley |
| Motor | manimgx/ (rama seams) | Fork de ManimGX con lo de la casa |
| Pluma | pluma/ | El trazo a mano, igual en Rust, Python y wasm; formato .lamina |
| Materias | materias/ | Papel, acuarela espectral, pincel, risografía, tejido, transiciones (WGSL) |
| Sentido | sentido/ | EmbeddingGemma 2 en local: elegir, comprobar, comparar |
| Turno | tools/pesado | Para no saturar el Mac (obligatorio) |
| Plan del fork | FORK.md | Dónde engancha cada cosa, con fichero y línea |
0. La regla de la carga (antes que nada)
El Mac de José Luis tiene 14 núcleos y no puede volver a saturarse (llegó a una carga de 268 con varios agentes compilando a la vez). Todo trabajo pesado pasa por el turno:
~/Developer/torculo/tools/pesado cargo build --release
~/Developer/torculo/tools/pesado uv run manimgx render escena.py
~/Developer/torculo/tools/pesado uv run pytest -n 4Como mucho dos trabajos pesados a la vez en todo el Mac, espera a carga < 14, nice 15,
CARGO_BUILD_JOBS=4, MESA_JOBS=2. Al lanzar subagentes, decírselo
explícitamente a cada uno. No tocar procesos que no sean nuestros.
1. El canon
canon/canon.toml → python3 canon/genera.py → canon/gen/canon.{py,ts,rs,wgsl,css}.
Nunca se edita gen/ a mano; python3 canon/prueba.py comprueba que el azar da
lo mismo en Python, TypeScript y Rust.
- Papel: la crema de Scholaris,
#F5F0E8(hundido#EDE6D6, hoja#FAF7F0, noche#1A0F0A). Elegida por José Luis el 13-10-2026: no usar#F2EEE4ni#F6F0E2en nada nuevo. - Tinta: café ferrogálico
#2C1810(segunda#5C3D2E, lápiz#9A8E80). - Escritorio (los tres de la Bauhaus): minio
#C1453B, azurita#2B4C7E, amarillo#E8A838, oro#D6A03A. - Tambores risográficos (solo contenido, nunca cromo): ultramar, bermellón, naranja, ámbar, verde, rosa, violeta, humo.
- Azar: semilla = FNV-1a de 32 bits sobre UTF-8; serie = Mulberry32;
ruido sin estado =
hash32. Ojo: elazar.tsantiguo de Scholaris usaba UTF-16, así que un nombre con tilde o eñe tiembla distinto que antes. - Oficio (Lasseter hecho números): 220 palabras/min + 1 s de asiento para leer, 8 fotogramas mínimos por movimiento, 3 piezas moviéndose a la vez como mucho, 0,6 s de pausa al final del beat, anticipación 0,2, hervor a 8 fps con 3 poses, lo dibujado «en dos».
Desde Python: sys.path.insert(0, "~/Developer/torculo/canon/gen"); import canon.
2. El motor (fork de ManimGX)
Venv: ~/Developer/torculo/manimgx/.venv (Python 3.14). La API de ManimGX es la
de Manim CE (import manimgx as m, class X(m.Scene)), pero no sus
tripas: lee docs/content/user-guide/coming-from-manim-ce.md del repo.
La guía de ManimGX para agentes es https://manimgx.academa.ai/llms.txt (en el
repo no hay copia; su cartilla vive en tests/docs/); el bucle es
manimgx inspect escena.py → mirar el storyboard → manimgx render escena.py.
Lo que añade Tórculo (todo apagado por defecto: sin usarlo, el motor dibuja exactamente como el original):
Película y papel: la cadena de postproceso
import manimgx as m
m.config.post = [m.Papel(), m.Bobina("truca")] # el baño de la casa, dentro del motor
m.config.compositing = "linear" # opcional: componer en luz linealm.Bobina("truca"): la bobina con la que se revela Mujeres al borde (grano, halación, vaivén, polvo, parpadeo…), leída como la leefilmlook-metal. Acepta un JSON de ajustes del laboratorio,lut=/lut_in=(.cube) y cualquier ajuste por nombre encima.m.Papel(): la primera etapa del baño (ganancia de punto, luz cálida, placa de papel multiplicada).m.Pass("mi.wgsl", **params): cualquier pasada WGSL con la convención de las materias (abajo). Las dematerias/shaders/entran tal cual.- El grano vive también en las pausas (los fotogramas retenidos se rehacen).
- La bobina se revela sobre su papel (
papel=, por defecto la crema del canon). La ganancia detruca(0,17) levanta un papel crema por encima del blanco: enfilmlook-metalel #F5F0E8 sale (255, 255, 255) y sin grano. Aquí la exposición se mide desde el papel y el hombro lo deja sobre sí mismo: la tinta se levanta igual, el papel queda crema (medido: #F5F0E8 → (246, 242, 233) con la bobina sola, (243, 235, 226) conPapel()delante) y el grano se mueve sobre él.m.Bobina("truca", papel=None)revela exactamente como el laboratorio. - Comparado con
filmlook-metal(sin papel): mismo grano y halación; el original sangra algo más el color en los bordes y da un amarillo un poco más cálido.
Materiales, mezclas, pluma y hervor
circulo = m.Circle(radius=2, fill_opacity=1, color="#2B3BC7")
circulo.set_material("riso", desregistro=1.5) # también "papel", "acuarela"
circulo.set_blend("multiplicar") # dos tintas que se multiplican
# set_material con un objeto Material (3D) sigue funcionando como en ManimGX
t = m.Trazo(puntos, anchos, tiempos=llegadas, grano="lapiz") # grosor variable, puntas afiladas
t = m.Trazo.de_pluma(trazo_de_torculo_pluma) # la pluma de la casa, tal cual
self.play(m.Create(t)) # se dibuja al ritmo real de la mano
viva = m.Hervor(dibujo, poses=3, fps=8, desfase=True) # la línea que vive al reposarm.register_material(nombre, wgsl, parametros): material propio; el WGSL definefn materia(m: Materia) -> vec4<f32>(ver el docstring demanimgx/saorin/registro.py:m.pixel,m.local,m.cobertura,cobertura_en, ruido de la casaazar,ruido,fbm,fibras,diente).- Coste medido: todo encendido (materiales, pluma, multiplicar, hervor) añade unos 1,5 ms por fotograma a 1080p.
gpu=TrueenHervorhace el temblor en el aplanado de la GPU.- La semilla del grano, del desregistro y del hervor de una pieza es la del
canon (FNV-1a sobre UTF-8) de su nombre:
set_material("riso", pieza="c1_09/rosa"); sin nombre, de su clase y su forma. Nunca de un contador: la misma escena rodada dos veces da los mismos bytes.
Voz, foley y errores
m.sidecar("pliego.mesa")(o el.tiempos.json, o la carpeta): la voz real de un pliego como voz deself.say, sincronizada por palabra. Sin voz no se paga: la voz por defecto da error en vez de llamar a un servicio de pago.- Errores estrictos por defecto: LaTeX mal formado (
\frac{a}{,x^{2,\left(sin cerrar) y avisos de Typst fallan en la línea de la escena.--no-strictlos convierte en avisos.
Salida
manimgx render escena.py --codec hevc # HEVC Main10 10 bits por VideoToolbox
manimgx render escena.py --codec prores # ProRes 422 HQ (.mov)
manimgx render escena.py --codec prores4444 # con alfa, desmultiplicada
manimgx render escena.py --take # la toma: para la web y la biblioteca Rusth264 (x264) es el de siempre; los binarios con x264 son GPL, los de
VideoToolbox no. El HEVC entra directo en filmlook-metal si hiciera falta.
Web, apps y juegos
- Web:
<manimgx-film src="Escena.take" autoplay>(paquetebrowser/,film.ts): reproduce una toma grabada sin Pyodide, con WebGPU (Chrome, Edge, Safari 26; Firefox no). - Rust: crate
rust/render(manimgx-render):Replay::read(&bytes),replay.render(i)→ RGBA;Gpu::from_device(...)para dibujar en el dispositivo de otra app (Tauri, el laboratorio de película). - Todavía no: interactividad (clics hacia la escena) y reproductor iOS/Android.
Plan en
FORK.md§6 y §8.
3. La pluma única y las láminas
pluma/ es el puerto fiel de pluma.ts/boceto.ts de la skill
dibujos-a-mano: el mismo dibujo da el mismo SVG carácter a carácter y el mismo
polígono (±1e-14) en Rust, Python (torculo_pluma, venv en pluma/python/.venv)
y wasm.
Una lámina (.lamina, especificación en pluma/LAMINA.md) lleva atlas de
color, atlas de tiempo (cuándo pasó la pluma por cada píxel), 3 poses de
hervor y la geometría vectorial (ancho por punto, longitud de arco, llegada).
Se revela con una línea de shader: clamp((progreso*1.03 - t)*60, 0, 1).
- Exportar desde la skill:
scripts/lamina.tsdedibujos-a-mano. - Ver:
pluma/visor/index.html(WebGL2, sin dependencias). - En el motor:
m.Trazo.de_pluma(...)dibuja la geometría como trazos nativos. - Una lámina grande pesa en GPU:
--escala 1para dibujos de más de 600 de ancho.
4. Las materias (WGSL)
cd ~/Developer/torculo/materias
~/Developer/torculo/tools/pesado cargo build --release
./target/release/materia aplica -c "pincel > difuminado" -e foto.png -s pintura.png
./target/release/materia aplica -c "risografia(tambor1=rosa, tambor2=ultramar)" -e clip.mp4 -s riso.mp4
./target/release/materia transicion -t pasa_pagina -a A.png -b B.png -s paso.mp4 --dura 1.4| Materia | Para qué | ms/fotograma 1080p |
|---|---|---|
papel | Once papeles en unidades físicas (khadi, washi, verjurado, cuaderno…) | 3–15 |
acuarela | Simulación espectral: mezcla sustractiva real, floraciones, bordes, mareas; escribe a pincel con tipografías Hershey | 7–19 |
pincel | Foto o vídeo de archivo convertido en pintura, borde barbado | 5,5 |
difuminado | Ruido anclado (pantalla, temporal, pegado al papel) | 0,5 |
risografia | Separación en 2–3 tambores, trama, desregistro, multiplicar | 3–4 |
tejido | Texto en punto de media que se teje punto a punto | hasta 15 |
| transiciones | Las 14 de .mesa: funde a negro/papel, pasa página, tijera, iris, quemado… | 0,3–2 |
Convención de pasada (la misma que lee m.Pass): entrada rgba16float lineal
premultiplicada, sampler, uniformes {resolucion, fotograma, tiempo, semilla, params[4]} y salida; #import comparte módulos y el canon. El tiempo es el de
la escena, nunca el reloj: el render es determinista.
Límites conocidos: el pincel «nada» en planos con mucho movimiento; el tejido no tiene aún hebras sueltas entre letras; pasa página dobla en cilindro, no en cono; el interior de la acuarela es algo uniforme. La licencia de jelly-painting (origen del pincel) no consta: no publicar el pincel sin confirmarla.
5. La capa de sentido (EmbeddingGemma 2, local)
# no está en el PATH: el ejecutable vive en el venv (README.md tiene las medidas)
SENTIDO=~/Developer/torculo/sentido/.venv/bin/sentido
cd <proyecto> # las rutas, relativas al proyecto
$SENTIDO voz pliego.mesa -o /tmp/voz.json # ¿cada toma dice el texto de su beat?
$SENTIDO qc master.mp4 pliego.mesa # ¿se ve lo que se dice? (informe HTML)
$SENTIDO diff v6.mesa v7.mesa # qué cambió de sentido
$SENTIDO indexa sentido/bestiario # índice de láminas y piezas (README §1)
$SENTIDO elige «la celosía, mirar sin ser mirada» --evita «reja»
$SENTIDO resuelve pliego.mesa # congela pon: lámina ≈ «…» en .mesa.lock
$SENTIDO ritmo master.mp4 # cortes y tira de ritmo- Todo lo de
sentidoes pesado (carga el modelo): portools/pesado. sentido vozantes de cada tirada. Encontró seis tomas de Mujeres que Dropbox había pisado (misma duración, otro corte de la grabación).qcsirve para saber dónde mirar, no para juzgar: lee mal el texto pequeño.eligeacierta a la primera el 93 % con descripciones; con intenciones muy abstractas falla. Las elecciones se congelan en<pliego>.mesa.lock: el render nunca depende de lo que diga el modelo ese día.- Motor MLX bf16. Nunca float16 (da NaN). Los pesos están en
~/.cache/spdf-models, no en la caché de Hugging Face. - Nunca para transcribir: eso es siempre Whisper local (skill
transcribir). - El metraje real (
sentido/metraje.py):sentido metraje indexa <carpeta>(fotogramas a 1 fps, audio en ventanas de 3 s y frases de la transcripción de lo quemesa ingestadejó en.metraje/),sentido metraje busca <carpetas…> --consultas q.json(por planos, centrado, modoplanoofrase) ysentido metraje ancla(qué recorte de un fotograma es «la silla»). Lo llamamesa ingesta|monta; la lengua está en la skillmesa(EL METRAJE REAL) y enSPEC-METRAJE.md.
6. .mesa sobre Tórculo
El pliego no cambia: mesa toca --motor gx y mesa animatica --motor gx
ruedan el mismo pliego, con el mismo sidecar, sobre el motor nuevo (detalles en
la skill mesa). Medido con La terraza no existe entera: 463 s de reloj
frente a unas 3 h 45 min con Cairo.
Lo que el backend toma del motor (manim_saorin/gx/enganches.py):
--bano(obaño trucaen el preámbulo) →config.post = [Papel(), Bobina("truca", papel=canon)]dentro del render; el baño de ffmpeg ya no.--codec hevc→ HEVC Main10 por VideoToolbox (tambiénprores).--sonido→ la voz real la pone el motor conm.sidecaren el instante de cada beat; la mezcla suma foley, clips y suelo a esa pista.- La
Tintahierve en el shader (m.Hervor(gpu=True)) ytrazo.pulsoes unm.Trazonativo. Cada enganche se apaga conMESA_GX_SIN_<NOMBRE>=1.
Medido el 14-10-2026, capítulo de los beats c1 de Mujeres (113,7 s, qh, baño,
HEVC y voz): 81,6 s de render y mezcla; sincronía de la voz medida con Whisper,
mediana 0,01 s. Lo que aún delega o no porta (régimen de canción, materiales
por pieza, algunos verbos) está en manim_saorin/gx/NOPORTA.md.
El 3D y Blender (SPEC-MESA-2 §3.17)
Un pliego con ESCENARIO 3D rueda un decorado en Blender (en segundo
plano, por capas: fondo, lo de delante, contornos) con la misma cámara del
operador, y Tórculo lo compone: la pluma (m.Trazo) dibuja los contornos
con hervor y las fichas son láminas plantadas en el suelo. Ojo: el ancho de
un m.Trazo en 3D no se encoge con la distancia (es del plano del cuadro).
ESCENARIO 3D · en ManimGX usa MeshMobject con Material y la luz física
del motor. Detalles en la skill mesa (EL ESCENARIO 3D).
7. Trampas
- Lanzar trabajo pesado sin
pesado(ver §0). - Renders y tomas dentro de Dropbox: generan copias en conflicto. Escribir fuera y copiar el resultado.
frames=/píxeles del motor vienen premultiplicados: desmultiplicar antes de guardar PNG con alfa.- Las tomas del fork tienen su propio rango de versión (desde
0x8000_0001): el reproductor de ManimGX original no las lee. - El determinismo es por máquina: mismo GPU y mismo motor, mismos bytes; entre GPU distintas, no se garantiza.
- La paleta antigua (
#F2EEE4) sigue en código viejo de manim-saorín y zine-ui: lo nuevo usa siempre el canon.
8. La emulsión física de la película (emulsion/, canon/peliculas/)
El modelo físico de la película, a la altura de la emulsión de Osmo y con la gelatina
Saorín como llave de la casa. Léelo entero en ~/Developer/torculo/emulsion/README.md.
import manimgx as m
m.config.post = [m.Papel(), m.Pelicula("vision3-500t", copia="2383", formato="16")]
m.config.post = [m.Papel(), m.Pelicula("portra-400", copia="escaneo", gelatina="Saorín")]cd ~/Developer/torculo/emulsion
python3 -m torculo_emulsion lista # 50 stocks, copias y looks
~/Developer/torculo/tools/pesado python3 -m torculo_emulsion revela vision3-500t \
-e rodaje.mov -s copia.mov --entrada auto --gelatina Saorín --codec prores
python3 -m torculo_emulsion cube vision3-500t --entrada logc3 -o 5219.cube # Resolve
python3 -m torculo_emulsion dctl vision3-500t --entrada s-log3 -o 5219.dctl # con grano
python3 -m torculo_emulsion web # http://localhost:8765, WebGPU local- Stocks (
canon/peliculas/<id>.toml, cada curva con su hoja y su página): VISION3 500T/250D/200T/50D, VISION2 500T/250D/200T/50D, EXR 50D/200T/500T, ETERNA 250D/500, REALA 500D, EKTACHROME 100D 7294, Double-X 5222, Tri-X 7266; copias 2383, 2393 y ETERNA-CP 3523XD; Portra 160/400/800, Ektar 100, Gold 200, UltraMax 400, Ektachrome E100, Tri-X 400, T-MAX 100/400/P3200, Velvia 50/100, Provia 100F, Superia X-TRA 400, Pro 400H, Fujicolor C200, Acros 100 II, HP5 Plus, FP4 Plus, Delta 100/400/3200, Pan F Plus, XP2 Super; CineStill 800T y 50D (VISION3 sin antihalo). Looks por categoría encanon/peliculas/looks.toml. - La gelatina manda:
gelatina="Saorín"(sobre la película: lo neutro entra en la .cube exactamente como entraba el vídeo; la película pone color de capas, latitud, grano y halación) ogelatina_modo="delante"(filtro delante del objetivo). - Un mismo grafo, tres anfitriones:
grafo.rsdel fork (pasada de clasegrafo),materia reveladel cratemateriasyweb/grafo.js. La ficha (JSON) trae el WGSL, el grafo y los números;materias/shaders/emulsion.wgsles la única fuente del shader. - Medidas (M4 Max, 4K, cadena entera): unos 17 ms por fotograma en la GPU (57-60 fps); en el motor, 4K con ProRes a 44 fps de punta a punta. La GPU cuadra con la referencia de Python a menos de 1 nivel de 8 bits.
- Trampas: en un dibujo,
m.Peliculano recupera altas luces (altas=0: el papel no es una lámpara) y mide la exposición desde el papel del canon (papel=); en metraje SDR sí las recupera (altas, por defecto 2), y por eso brilla la halación de los faros. Los log esperan el código de cámara sin estirar (materia revela --codigos 1, lo pone solo la CLI de Python). El motor necesitaTORCULOapuntando al Tórculo conemulsion/.