Video Color Grade (evaluar → corregir → looks → elegir → LUT + aplicar)
Escribe la gradación desde cero como código, genera varios looks con nombre distintos sobre el metraje real, selecciona uno mediante elección humana explícita o juicio delegado del agente, luego hornea un LUT .cube portátil y lo aplica al clip completo. Sin paquetes preestablecidos.
Idea central: corregir, luego estilizar. Cada look es base correctiva + capa creativa:
- base correctiva hace que el metraje sea honesto — para LOG/HLG plano es la conversión log→Rec.709; para metraje ya en Rec.709 es neutralización de WB + exposición + contraste suave. Cada look comparte esta base, por lo que "neutral" sigue siendo significativo y los looks difieren por intención.
- capa creativa es el look apilado encima (cálido / frío / teal-orange / intenso / desvaído…).
La piel es el rasero de calidad. En personas —especialmente piel oscura— teal_orange y cool_desat suelen arrojar un matiz verde/gris. Juzga cada look primero en los rostros; mantén la calidez de los tonos medios, empuja el tinte hacia sombras/altas luces/fondo, prefiere vibrance sobre saturation crudo. No fuerces el balance de blancos hasta el neutral o la piel se vuelve sin vida.
Requisitos: ffmpeg/ffprobe en PATH, Python con numpy + Pillow.
Contrato
- Cada look =
base + capa creativa; la base es compartida por todos los looks. - Todos los looks se renderizan en el mismo fotograma representativo (una comparación controlada y justa), etiquetados, a resolución completa.
- La aplicación completa usa
-c:a copy(el audio nunca se recodifica → sincronización A/V idéntica) y mantiene la duración, fps, dimensiones de la fuente. - Selección completa del look antes de la entrega. Usa el modo
humancuando el usuario pidió elegir; usa el modoagentsin pausas cuando el usuario delegó la elección o solicitó una ejecución autónoma. En ambos modos, conserva la justificación en el plan duradero.
Flujo de trabajo del protocolo del proyecto
Almacena la decisión duradera en work/color-grade/grade-plan.json:
{
"schema_version": 1,
"target": "base-video",
"base": "eq=contrast=1.05",
"looks": [{"name": "clean", "chain": "null"}],
"selected_look": "clean",
"selection_mode": "agent",
"selection_rationale": "La corrección neutral preserva la piel natural y la iluminación de la fuente.",
"selected_lut": "../../final/selected-color-look.cube",
"evidence_refs": ["media:source"]
}
Usa base-video por defecto para que las tarjetas de contenido y los subtítulos no se vuelvan a colorear. composite solo es válido cuando se gradan intencionalmente píxeles ya compuestos. Lee el work/understand/media.json compartido y la evidencia visual. Genera review/02-color-grade/choose-color-look.jpg y review/02-color-grade/skin-tone-check.jpg; opcionalmente genera el breve selected-look-preview.mp4. Registra la decisión y la evidencia en review/02-color-grade/selected-look.md.
Establece selection_mode en human después de una elección explícita del usuario. Establécelo en agent cuando el usuario delegue la decisión o solicite un flujo de trabajo ininterrumpido, y elige de forma conservadora priorizando el tono de piel, la retención de altas luces y el balance neutral. Siempre escribe un selection_rationale no vacío; no hagas pausa en el modo agente.
Hornea el LUT elegido en final/selected-color-look.cube. Cuando ese LUT se entregue, establece selected_lut en el plan de gradación para que el renderizador compartido consuma ese archivo exacto. Un LUT horneado de look completo ya contiene la base correctiva; el renderizador debe aplicar solo lut3d, no base + lut3d.
Registra las revisiones exactas de entrada de la operación en based_on, incrementa su revision entera cuando el plan o la entrada seleccionada cambie, establece la verificación de la operación en pass solo después de que los archivos de revisión estén completos, y contribuye con los campos completos del protocolo:
{
"target": {"sequence": "main", "scope": "base-video"},
"effects": {
"changes_timeline": false,
"changes_geometry": false,
"changes_video_pixels": true,
"changes_audio": false,
"adds_track": null
},
"check": {
"status": "pass",
"report": "../review/02-color-grade/selected-look.md"
},
"render": {
"kind": "video-filter",
"target": "base-video",
"plan": "color-grade/grade-plan.json"
}
}
El renderizador compartido valida que selected_look exista, aplica la gradación después de las transformaciones de la línea de tiempo y antes de las superposiciones, y codifica el video final una vez:
python skills/video-understand/scripts/build_render_plan.py .
python skills/video-understand/scripts/render_project.py work/render/render-plan.json
Entradas de compatibilidad independiente
- El video fuente.
- Un
looks.json(base+lookscon nombre); comienza desdelooks.example.jsony reajusta labasea partir de lo que informeassess.py.
Todos los scripts existentes continúan aceptando el looks.json heredado. gradelib.load_spec() también acepta el grade-plan.json canónico, por lo que los comandos de hoja de contactos, clip, LUT y aplicación independiente permanecen compatibles durante la migración.
Esquema de looks.json
{
"base": "<base correctiva: conversión log->Rec.709, O WB+exposición+contraste para metraje normal>",
"looks": [
{ "name": "clean_neutral", "label": "1 · CLEAN NEUTRAL", "desc": "solo corregido", "chain": "eq=contrast=1.02" },
{ "name": "teal_orange", "label": "4 · TEAL & ORANGE", "desc": "cinematográfico", "chain": "colorbalance=...,eq=..." }
// el filtro ffmpeg completo de un look = base + "," + chain. Establece "prepend_base": false para usar chain independiente.
]
}
Flujo de trabajo
-
Evaluar — nunca gradar a ciegas.
python scripts/assess.py FUENTE.mp4 --out work/Informa log-vs-Rec.709, tinte de balance de blancos (signalstats U/V vs 128), exposición (Y), elige un fotograma representativo (
work/assess/frame.png) y sugiere ajustes de base. Reajusta labasedelooks.jsonen consecuencia (ej. tinte cálido → añadir un poco de azul; log → poner la conversión log→709 enbase).- El fotograma representativo se elige por luma mediana, no por rostros. En un talking-head de un solo plano eso suele dar con un buen fotograma de rostro, pero en metraje con muchos B-roll / múltiples planos puede elegir un fotograma sin rostro — y entonces
face_skin_check.pnges inútil (la piel es el rasero de calidad). Fuerza un momento conocido de rostro con--frame-time SEGUNDOS. - Las estadísticas de WB/exposición provienen de una ventana
signalstatsde ~10s al 10% de la duración — bien para luz constante; para metraje cuya luz cambia, muévela/alárgala con--win-start SEGUNDOS/--win-dur SEGUNDOS.
- El fotograma representativo se elige por luma mediana, no por rostros. En un talking-head de un solo plano eso suele dar con un buen fotograma de rostro, pero en metraje con muchos B-roll / múltiples planos puede elegir un fotograma sin rostro — y entonces
-
Renderizar los looks para elegir.
python scripts/render_looks.py work/assess/frame.png looks.json --out work/looksProduce
work/looks/looks_compare.png(original + cada look, etiquetado, resolución completa) yface_skin_check.png(tira de piel ampliada). MIRA AMBOS. La piel es el rasero — reajusta o descarta cualquier look que vuelva gris/verde la piel, luego vuelve a renderizar. ¿Sujeto descentrado? añade--face-crop W:H:X:Y. Trata esos PNG como intermediarios de trabajo; publica las copias de revisión aprobadas comoreview/02-color-grade/choose-color-look.jpgyreview/02-color-grade/skin-tone-check.jpg. -
(opcional) Previsualizaciones en movimiento de los 2–3 mejores para verlos en movimiento (audio conservado):
python scripts/make_clips.py FUENTE.mp4 looks.json --out work/clips \ --pick clean_neutral,warm_filmic,teal_orange --with-original --ss 148 --t 8 -
Seleccionar un look. Presenta la hoja de contactos y espera solo cuando el usuario pidió elegir. De lo contrario, selecciónalo en modo agente y continúa sin una pausa de decisión. Escribe
selected_look,selection_modeyselection_rationaleantes de la entrega. -
Después de la elección — hornea el LUT y aplícalo al clip completo.
python scripts/bake_lut.py looks.json --name ELEGIDO \ --out final/selected-color-look.cube --verify-frame work/assess/frame.png python scripts/apply_grade.py FUENTE.mp4 --looks looks.json --name ELEGIDO --out out/graded.mp4 # o aplica el LUT entregado: --lut final/selected-color-look.cubeapply_grade.pyverifica que duración/audio se conserven, imprime el desplazamiento de WB (U/V hacia 128) y suelta un fotograma de verificación para inspeccionar.- La aplicación recodifica el video una vez (con pérdida); el audio es
-c:a copy. Calidad/tiempo se establecen con--crf(por defecto 18) y--preset(por defectoslow).slowestá bien para clips pequeños/cortos, pero en metraje de 1080p/4K de una hora es un gran coste de tiempo silencioso — baja a--preset mediumpara entrega (overyfastpara una vista previa rápida),slowsolo para final. - Haz coincidir el video entregado con el LUT entregado: la cadena (
--name) y el.cubehorneado de 33³ (--lut) pueden diferir hasta ~5/255 en curvas pronunciadas (bake_lut.pyinforma estemax|d|). Invisible para la mayoría del trabajo, pero si estás entregando el.cubey quieres que el video renderizado coincida exactamente con él, aplica con--lut final/selected-color-look.cube, no con--name.
- La aplicación recodifica el video una vez (con pérdida); el audio es
Notas de diseño / problemas conocidos
- Grada el corte LIMPIO, luego compón las superposiciones encima del metraje gradado. Si debes gradar un video que ya tiene subtítulos/tarjetas quemadas, NO confíes en
assess.py: sus estadísticas de WB/exposición y elección de fotograma están sesgadas por los gráficos (las cortinas oscuras superior/inferior arrastran Y hacia abajo, el texto brillante de subtítulos / los acentos teal sesgan U/V, y la elección de fotograma puede caer en uno con muchos subtítulos) — estarías corrigiendo las superposiciones, no el metraje. Reutiliza unabaseajustada en el metraje limpio, y espera que la gradación tiña las superposiciones, así que usa solo un look sutil/correctivo (clean_neutral);teal_orange/vintage_fadedrecolorearán visiblemente el texto/los acentos. - Un look/LUT cubre todos los clips relacionados. Una vez que hayas elegido un look, reutiliza el mismo
looks.json/.cubeen cada clip relacionado (ej. corte limpio + la versión con superposiciones) — NO vuelvas a ejecutar la elección en dos fases por clip. - Corregir, luego estilizar. La base es la generalización del paso S-Log3→Rec.709 del material de origen. Para metraje normal la base es WB+exposición+contraste; para metraje log coloca allí la conversión.
- La piel primero.
teal_orange/cool_desatson los habituales agresores de la piel en sujetos más oscuros — mantén los tonos medios cálidos, tiñe sombras/altas luces/fondo, usavibrance. No neutralices U/V por completo. - Las etiquetas son PNG de PIL superpuestos por ffmpeg — nunca
drawtext. En Windows la rutafontfilededrawtext(dos puntos de unidad) no es fiable. Las fuentes se establecen al inicio degradelib.py. - Las rutas con dos puntos de unidad rompen las opciones del filtergraph de ffmpeg que toman una ruta (
lut3d,metadata=print:file=). Los scripts ejecutan ffmpeg concwd= la carpeta del archivo y lo referencian por nombre base. Mantén este patrón para cualquier nueva opción que tome rutas. - Las salidas caen en los directorios
--outque pases (duraderos), no en un directorio temporal del sistema. - La tira de rostro/piel usa un recorte fijo centrado en la parte superior media, no detección de rostros. Si el sujeto no está aproximadamente centrado, pasa
--face-crop W:H:X:Yarender_looks.pyo la tira de piel mostrará la región incorrecta. - El fotograma de verificación de
apply_grade.pyestá al 20% de la duración — un fotograma diferente de aquel en el que se eligieron los looks. Si ese punto del 20% no tiene rostro, inspecciona la piel en otro fotograma (o pasa--frame-timeaassess.pypara que el fotograma elegido sea un momento conocido de rostro). - El horneado del LUT es exacto, porque cada filtro aquí es una operación punto a punto por píxel:
bake_lut.pyempuja una cuadrícula identidad de 33³ a través de la cadena real y se autoverifica contra ella (mean|d|debería estar muy por debajo de 1/255; unmax|d|de unos pocos niveles es solo interpolación). - Selección antes de la entrega, siempre. Tanto la elección humana como la elección delegada al agente son válidas, pero el plan debe registrar cuál ocurrió y por qué.