Saltar al contenido
Tórculo1.0 · borrador de trabajo

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):

PiezaDóndeQué es
Canoncanon/La única fuente de verdad: paleta, azar, oficio, foley
Motormanimgx/ (rama seams)Fork de ManimGX con lo de la casa
Plumapluma/El trazo a mano, igual en Rust, Python y wasm; formato .lamina
Materiasmaterias/Papel, acuarela espectral, pincel, risografía, tejido, transiciones (WGSL)
Sentidosentido/EmbeddingGemma 2 en local: elegir, comprobar, comparar
Turnotools/pesadoPara no saturar el Mac (obligatorio)
Plan del forkFORK.mdDó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 4

Como 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 #F2EEE4 ni #F6F0E2 en 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: el azar.ts antiguo 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 lineal
  • m.Bobina("truca"): la bobina con la que se revela Mujeres al borde (grano, halación, vaivén, polvo, parpadeo…), leída como la lee filmlook-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 de materias/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 de truca (0,17) levanta un papel crema por encima del blanco: en filmlook-metal el #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) con Papel() 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 reposar
  • m.register_material(nombre, wgsl, parametros): material propio; el WGSL define fn materia(m: Materia) -> vec4<f32> (ver el docstring de manimgx/saorin/registro.py: m.pixel, m.local, m.cobertura, cobertura_en, ruido de la casa azar, ruido, fbm, fibras, diente).
  • Coste medido: todo encendido (materiales, pluma, multiplicar, hervor) añade unos 1,5 ms por fotograma a 1080p.
  • gpu=True en Hervor hace 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 de self.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-strict los 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 Rust

h264 (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> (paquete browser/, 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.ts de dibujos-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 1 para 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
MateriaPara quéms/fotograma 1080p
papelOnce papeles en unidades físicas (khadi, washi, verjurado, cuaderno…)3–15
acuarelaSimulación espectral: mezcla sustractiva real, floraciones, bordes, mareas; escribe a pincel con tipografías Hershey7–19
pincelFoto o vídeo de archivo convertido en pintura, borde barbado5,5
difuminadoRuido anclado (pantalla, temporal, pegado al papel)0,5
risografiaSeparación en 2–3 tambores, trama, desregistro, multiplicar3–4
tejidoTexto en punto de media que se teje punto a puntohasta 15
transicionesLas 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 sentido es pesado (carga el modelo): por tools/pesado.
  • sentido voz antes de cada tirada. Encontró seis tomas de Mujeres que Dropbox había pisado (misma duración, otro corte de la grabación).
  • qc sirve para saber dónde mirar, no para juzgar: lee mal el texto pequeño.
  • elige acierta 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 que mesa ingesta dejó en .metraje/), sentido metraje busca <carpetas…> --consultas q.json (por planos, centrado, modo plano o frase) y sentido metraje ancla (qué recorte de un fotograma es «la silla»). Lo llama mesa ingesta|monta; la lengua está en la skill mesa (EL METRAJE REAL) y en SPEC-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 (o baño truca en 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én prores).
  • --sonido → la voz real la pone el motor con m.sidecar en el instante de cada beat; la mezcla suma foley, clips y suelo a esa pista.
  • La Tinta hierve en el shader (m.Hervor(gpu=True)) y trazo.pulso es un m.Trazo nativo. Cada enganche se apaga con MESA_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 en canon/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) o gelatina_modo="delante" (filtro delante del objetivo).
  • Un mismo grafo, tres anfitriones: grafo.rs del fork (pasada de clase grafo), materia revela del crate materias y web/grafo.js. La ficha (JSON) trae el WGSL, el grafo y los números; materias/shaders/emulsion.wgsl es 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.Pelicula no 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 necesita TORCULO apuntando al Tórculo con emulsion/.