Clipify
Encuentra momentos llamativos en un video, recórtalos como clips independientes, opcionalmente reformatea 16:9 → 9:16 (pan de rostro o pantalla dividida), ajusta la velocidad para revelaciones más precisas y añade subtítulos al estilo Opus palabra por palabra.
Modos
Elige uno antes del Paso 1. La elección cambia cómo encuentras momentos y si se aplican subtítulos.
- Modo diálogo (predeterminado para podcasts, entrevistas, cabezas parlantes): Whisper transcribe, tú buscas remates / reacciones / pausas incómodas. → Paso 1A. Subtítulos en el Paso 5.
- Modo acción (cortar leña, deportes, percusión, batería, cualquier cosa con sonidos de impacto repetidos): omite Whisper por completo, ejecuta
detect_transients.pypara encontrar cada golpe, corta en cada uno. → Paso 1B. Omite el Paso 5; los sonidos de golpe son el ritmo. - Híbrido: modo diálogo para secciones habladas, modo acción para el resto, intercala. Usa tanto 1A como 1B.
Si el usuario dice "No estoy hablando en esto", estás en modo acción. Si dice "corta en cada X" (corte, golpe, puñetazo, redoble, balanceo, pisada), estás en modo acción. Por defecto, modo diálogo en caso contrario.
Entradas
- Una ruta de archivo de video (el usuario la proporcionará; de lo contrario, pregunta)
- Opcional: formato solicitado (9:16, 16:9, 1:1) — si no se proporciona, pregunta después de seleccionar los candidatos
- Opcional: preferencia de estilo de subtítulos — si no se proporciona, pregunta antes de subtitular
Herramientas (usa solo la ruta más rápida)
- Whisper:
whisper --model tiny.en --word_timestamps True --output_format json(≈10× más rápido quesmall.en; calidad suficiente para inglés). Para otros idiomas:--model base(omite--language). - ffmpeg: añade
-hwaccel videotoolboxpara decodificación y-preset ultrafastpara renderizados. Usa-c:v libx264 -crf 20para el master final. - Numpy para alineación de audio (correlación cruzada FFT). No se necesita scipy/cv2.
- Scripts:
<skill-dir>/scripts/(donde<skill-dir>es el directorio que contiene este SKILL.md — típicamente~/.claude/skills/clipify/)analyze.py— línea de tiempo del hablante a partir de dos archivos de movimiento ROIbuild_pan.py— expresión x de recorte ffmpeg con cortes durosbuild_ass.py— subtítulos ASS estilo Opus desde JSON de Whisperaudio_align.py— encuentra el desplazamiento de un subclip en una fuente más largadetect_transients.py— encuentra sonidos de impacto agudos (modo acción); consulta--help
Directorio de trabajo: /tmp/clipify/ (crear con mkdir al inicio, dejar artefactos para depuración).
Flujo de trabajo
Paso 1A — Encontrar momentos de diálogo (modo diálogo)
mkdir -p /tmp/clipify
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
whisper /tmp/clipify/audio.wav --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
Lee el JSON resultante (o .txt) y selecciona de 3 a 5 clips candidatos. Señales divertidas a buscar:
- Remates y reacciones: palabras como "qué", "espera", "no puede ser", risas, "jaja", insultos
- Momentos de giro: pregunta preparatoria → respuesta inesperada
- Pausas incómodas: segmento de Whisper con un gran espacio, o muletillas ("eh", "um")
- Autocrítica / frases citables: oraciones declarativas cortas que se sostienen solas
- Picos de audio: detecta mediante
ffmpeg -af volumedetecto busca intercambios rápidos (segmentos cortos alternados de Whisper)
Para cada candidato, propón: [inicio, fin, por qué es divertido, título sugerido]. Busca clips de 10 a 25 segundos. Muestra la lista y deja que el usuario confirme/seleccione.
Paso 1B — Encontrar cada golpe (modo acción)
Reemplaza Whisper con detección de transitorios de audio. El detector encuentra impactos agudos (hacha en madera, puño en almohadilla, parche de batería, pelota en bate, madera cayendo en una pila) calculando el flujo espectral en la banda de 1–6 kHz — el impulso de banda ancha que crea un impacto y que el ambiente/viento no tiene.
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
python3 <skill-dir>/scripts/detect_transients.py /tmp/clipify/audio.wav \
--min-flux 30 --min-gap 0.6 > /tmp/clipify/strikes.json
Ajuste:
--min-flux 30es el valor predeterminado sensato. Los impactos reales registran 50–300; el ambiente/viento se sitúa en 5–15. Demasiados falsos positivos → sube a 40–50. Muy pocos golpes → baja a 20.--min-gap 0.6(segundos) es la distancia mínima entre dos golpes. La batería rápida puede necesitar 0.2; cortar leña funciona con 0.6–1.0.--band 1000:6000cubre impactos de madera/metal/vidrio. Los golpes graves pesados (bombo, golpes al cuerpo) se sitúan más abajo (--band 200:2000); silbidos, tintineos, golpes de caja más arriba (--band 4000:10000).- Para fuentes de múltiples clips (una carpeta de clips de teléfono), ejecuta el detector en cada clip y elige un conjunto variado entre ángulos. Busca 50–65 golpes para un montaje de 45–60 segundos a ~0.9 segundos por toma.
Cada "toma" debe comenzar ~0.4 segundos antes del golpe (mostrando el impulso) y terminar ~0.5 segundos después (golpe + breve seguimiento). Eso es ~0.9 segundos por tiempo.
Paso 1.5 — Verificar que cada candidato esté en cuadro
Haz esto siempre antes de renderizar, en cualquier modo. La detección de audio encuentra el sonido de un evento; la cámara podría no haberlo capturado. Saltarse esto es la forma #1 de recibir comentarios del usuario como "cortaste en golpes donde ni siquiera soy visible".
# Toma un fotograma en cada tiempo de golpe candidato T:
for T in $STRIKE_TIMES; do
ffmpeg -y -ss "$T" -i "$VIDEO" -frames:v 1 -vf scale=320:-1 \
/tmp/clipify/verify_${T}.jpg
done
Lee cada imagen de verificación y descarta el candidato si:
- El sujeto está agachado, fuera de cuadro o detrás de un obstáculo
- Hay un dedo en la lente (sí, esto ocurre — y solo lo detectas visualmente)
- Es claramente una fuente de sonido diferente (portazo, herramienta caída) que activó el detector
Para ~30 candidatos, esto es una llamada rápida a ffmpeg más una lectura de imágenes por lote. Un seguro barato.
Paso 2 — Recortar cada clip elegido
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(Usa -c copy para un recorte instantáneo. Re-codifica solo si los cortes deben ser precisos al fotograma).
Paso 3 — Decidir el formato de salida
Pregunta al usuario (omite si ya lo especificaron): "¿9:16 (TikTok / Reels), 16:9 (YouTube) o 1:1 (feed de Insta)?"
Paso 4 — Si 16:9 → 9:16: paneo entre rostros vs pantalla dividida
Detecta la relación de aspecto de origen con ffprobe. Si la fuente es 16:9 y el destino es 9:16, pregunta:
"Dos opciones: (a) paneo de corte duro que sigue a quien habla (un solo rostro en pantalla a la vez), o (b) pantalla dividida apilada con ambos rostros visibles. ¿Cuál prefieres?"
Omite la pregunta si solo hay un rostro (clip de un solo hablante). Para un solo hablante, simplemente recorta centrado.
Paso 4a — Paneo entre rostros (recomendado para diálogo rápido de cabezas parlantes)
-
Ubica las dos regiones de interés (ROI) de los rostros. Toma un fotograma:
ffmpeg -ss <medio> -i <clip> -frames:v 1 /tmp/clipify/probe.jpg. Léelo. Estima el área de la boca+mentón de cada rostro comox,y,w,hen el espacio de píxeles de la fuente. (No se necesita cv2 — la cámara es estática dentro de un clip; un fotograma es suficiente). Verifica dibujando cuadros:ffmpeg -i probe.jpg -vf "drawbox=x=$LX:y=$LY:w=$LW:h=$LH:[email protected]:t=4,drawbox=x=$RX:y=$RY:w=$RW:h=$RH:[email protected]:t=4" verify.jpgItera como máximo dos veces. Los cuadros deben cubrir boca + mentón y evitar manos/micrófonos. No afines en exceso — la diferencia de fotogramas es tolerante.
-
Extrae la energía de movimiento por fotograma en cada ROI:
ffmpeg -y -i clip.mp4 -filter_complex " [0:v]split=2[a][b]; [a]crop=$LW:$LH:$LX:$LY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/L.txt[la]; [b]crop=$RW:$RH:$RX:$RY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/R.txt[ra] " -map "[la]" -f null - -map "[ra]" -f null - -
Construye la línea de tiempo del hablante (permanencia mínima de 1.0 s — las interjecciones cortas se fusionan con el hablante anterior):
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
Elige las coordenadas x del paneo para una franja vertical 9:16 desde la fuente. Con fuente W=1920 y destino W=1080, ancho de franja recortada = 608.
- LEFT_X =
face_left_center_x - 304(limitar ≥ 0) - RIGHT_X =
face_right_center_x - 304(limitar ≤ source_W - 608)
- LEFT_X =
-
Genera la expresión x de corte duro y renderiza:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $LEFT_X $RIGHT_X) ffmpeg -y -hwaccel videotoolbox -i clip.mp4 -filter_complex \ "[0:v]crop=608:1080:x='$EXPR':y=0,scale=1080:1920:flags=lanczos[v]" \ -map "[v]" -map 0:a -c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p \ -c:a aac -b:a 192k /tmp/clipify/clip_panned.mp4Se asume fuente 1920×1080; para fuente 4K, reduce la escala primero o duplica todas las coordenadas.
Paso 4b — Pantalla dividida (ambos rostros siempre visibles)
Dos mosaicos apilados, 1080×960 cada uno. El mosaico del hablante activo está arriba — la superposición cambia en los cambios de hablante.
[0:v]split=2[a0][a1];
[a0]crop=Wcrop:Hcrop:LX_tile:LY_tile,scale=1080:960,split=2[lt0][lt1];
[a1]crop=Wcrop:Hcrop:RX_tile:RY_tile,scale=1080:960,split=2[rt0][rt1];
[lt0][rt0]vstack[layoutL];
[rt1][lt1]vstack[layoutR];
[layoutL][layoutR]overlay=0:0:enable='<RIGHT_SPEAKER_ENABLE>'[v]
Construye <RIGHT_SPEAKER_ENABLE> desde segments.json como between(t,a,b)+between(t,a,b)+... sobre los segmentos del hablante derecho. Los recortes de los mosaicos deben apuntar a ~720×640 alrededor de cada rostro (1.125:1 para coincidir con 1080×960).
Paso 4c — Revelaciones con ajuste de velocidad (opcional)
Cuando el usuario quiere reducir un plano lento / panorámico / de revelación sin perder el arco, usa aceleración en lugar de recorte. Recortar te obliga a descartar parte del arco; la aceleración preserva todos los tiempos a un tempo mayor.
# Velocidad 2x: arco de 12s → salida de 6s, tono de audio preservado mediante atempo
ffmpeg -y -ss "$START" -i "$CLIP" -t "$ORIG_DUR" \
-vf "setpts=PTS/2,scale=1080:1920:flags=lanczos,fps=30" \
-af "atempo=2.0" \
-c:v libx264 -preset fast -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 128k /tmp/clipify/clip_2x.mp4
Reglas generales:
- 1.5x — movimiento humano que debe sentirse "ligeramente rápido" sin parecer acelerado
- 2x — revelación más contundente; aún se lee como natural
- 3x–4x — ambiente de lapso de tiempo (encadena
atempo=2.0,atempo=2.0para 4x;atemposolo acepta 0.5–2.0 por instancia de filtro) - Omite
-af atempoy silencia el segmento si el audio es solo ambiente/viento y el efecto de tono agudo sería molesto
Útil cuando el resto del corte es rítmico (montaje de golpes, ida y vuelta de charla) y la revelación se sentiría como un punto muerto.
Paso 5 — Añadir subtítulos
Omite este paso en modo acción — los sonidos de golpe son el ritmo y los subtítulos solo saturan lo visual.
Pregunta una vez (solo si el usuario aún no ha especificado un estilo):
"Tres estilos de subtítulos: opus (blanco grande y negrita, resaltado de palabra activa en amarillo), karaoke (grupos de 4 palabras, resaltado en verde), minimal (Helvetica limpio, sin resaltado). O pega un ejemplo que te guste."
Si pegan una imagen/ejemplo de referencia: adapta la fuente, tamaño, peso, color, posición y animación lo más posible — escribe un ASS personalizado a mano o extiende build_ass.py.
De lo contrario, usa el preestablecido:
# Vuelve a ejecutar whisper en el clip recortado para marcas de tiempo precisas relativas al inicio del clip
whisper /tmp/clipify/clip_panned.mp4 --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
python3 <skill-dir>/scripts/build_ass.py /tmp/clipify/clip_panned.json /tmp/clipify/captions.ass opus
Incrusta subtítulos:
ffmpeg -y -i /tmp/clipify/clip_panned.mp4 -vf "subtitles=/tmp/clipify/captions.ass" \
-c:v libx264 -preset fast -crf 20 -c:a copy "$OUTPUT.mp4"
Paso 6 — Entregar
- Guarda cada salida en
<source_dir>/clipify_out/(crea la carpeta si no existe) - Imprime una línea por clip: nombre, duración, qué fue divertido, ruta de salida
- Abre la primera salida con
open <path>para que el usuario pueda verificarla - Ofrece iterar (diferente estilo, diferente ROI, cambiar a pantalla dividida, reajustar tiempos de subtítulos)
Errores comunes (lecciones de ejecuciones anteriores — no los repitas)
- La detección de audio ≠ evento visible. Siempre ejecuta el Paso 1.5 (verificar cada fotograma candidato) antes de renderizar. El detector encuentra el sonido de un golpe, no si el que golpea está en cuadro. Golpes donde el sujeto está agachado, fuera de cámara, o donde hay un dedo en la lente aún activan el detector de audio. Captúralos antes de renderizar.
- La banda de flujo espectral importa. El valor predeterminado
--band 1000:6000cubre impactos de madera/metal/vidrio. Los golpes graves pesados (bombo, golpes al cuerpo) necesitan--band 200:2000. Silbidos, tintineos, golpes de caja necesitan--band 4000:10000. Si--min-flux 30no devuelve nada, prueba bajando a 15 primero; si devuelve miles, prueba una banda diferente antes de subir el umbral. - Aceleración de audio en pares.
atemposolo acepta 0.5–2.0 en un solo filtro; para 4x encadenaatempo=2.0,atempo=2.0. Sinatempo,setptssolo da audio de tono agudo. - No afines demasiado las ROI. Máximo dos iteraciones. La diferencia de movimiento es tolerante — ROI más amplias que cubran boca+mentón funcionan bien incluso si no están perfectamente centradas en la boca.
- Ten cuidado con los cortes de escena dentro de un clip. Ejecuta
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -para contar cortes. Si un clip de 16:9→9:16 tiene muchos cortes, las ROI de rostro fijo solo funcionan para la escena dominante; advierte al usuario y ofrece elegir un clip de una sola toma o aceptar un encuadre descentrado durante los cortes. - La resolución de fuente importa. Si la fuente es 4K, reduce la escala a 1920×1080 primero (más rápido, suficiente para salida 9:16) o multiplica todas las coordenadas de ROI/paneo por 2.
- Subtítulos incrustados en la fuente. Algunos clips "crudos" aún tienen subtítulos. Si es así, encuentra el master sin subtítulos mediante correlación cruzada de audio (
audio_align.py) y recorta desde allí. - No ejecutes whisper en la fuente completa de larga duración si un clip corto es suficiente. Whisper el clip recortado después del Paso 2; solo susurra la fuente completa en el Paso 1 si necesitas una transcripción para encontrar momentos divertidos.
- Expresa el plan en una línea, luego actúa. No narres cada iteración.