Clipify
Encuentra los momentos más divertidos de un vídeo, recórtalos en clips independientes, opcionalmente reformatea 16:9 → 9:16 (seguimiento de rostro o pantalla dividida) e incrusta subtítulos palabra por palabra al estilo Opus.
Entradas
- Una ruta de archivo de vídeo (el usuario la proporcionará; si no, pregúntala)
- 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: la decodificación por hardware es opcional y específica de la plataforma — usa
-hwaccel auto, u omítelo (macOS:videotoolbox; Linux:vaapi/cuda/ninguno). Añade-preset ultrafastpara las previsualizaciones. Usa-c:v libx264 -crf 20para la versión final maestra. - NumPy para la 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 — normalmente~/.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 de ffmpeg con cortes durosbuild_ass.py— subtítulos ASS al estilo Opus desde el JSON de Whisperaudio_align.py— encuentra el desplazamiento de un subclip en un origen más largo
Directorio de trabajo: /tmp/clipify/ (crear al inicio, dejar artefactos para depuración).
Flujo de trabajo
Paso 1 — Encuentra las partes más divertidas
mkdir -p /tmp/clipify
ffmpeg -y -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 de humor a escanear:
- 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 por sí solas
- Picos de audio: detecta mediante
ffmpeg -af volumedetecto busca un intercambio rápido (segmentos cortos alternados de Whisper)
Para cada candidato, propón: [inicio, fin, por qué es divertido, título sugerido]. Apunta a clips de 10–25 s. Muestra la lista y deja que el usuario confirme/seleccione.
Paso 2 — Recorta cada clip elegido
ffmpeg -y -ss "$INICIO" -t "$DURACIÓN" -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 por fotograma).
Paso 3 — Decide el formato de salida
Pregunta al usuario (omite si ya lo especificó): "¿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 el origen es 16:9 y el destino es 9:16, pregunta:
"Dos opciones: (a) paneo con 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 centrando.
Paso 4a — Paneo entre rostros (recomendado para diálogos rápidos de dos hablantes)
-
Localiza las dos ROI de los rostros. Muestra un fotograma:
ffmpeg -ss <medio> -i <clip> -frames:v 1 /tmp/clipify/probe.jpg. Léelo. Estima a ojo el área de boca+mentón de cada rostro comox,y,w,hen el espacio de píxeles del origen. (No se necesita cv2 — la cámara es estática dentro de un clip; un fotograma es suficiente). Verifica dibujando cajas: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. Las cajas deben cubrir boca + mentón y evitar manos/micrófonos. No ajustes 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 (duración mínima 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 del origen. Con origen W=1920 y destino W=1080, el ancho de la franja de recorte es 608.
- IZQUIERDA_X =
centro_x_rostro_izquierdo - 304(limitar a ≥ 0) - DERECHA_X =
centro_x_rostro_derecho - 304(limitar a ≤ origen_W - 608)
- IZQUIERDA_X =
-
Genera la expresión x de corte duro y renderiza:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $IZQUIERDA_X $DERECHA_X) ffmpeg -y -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_paneado.mp4Se asume origen 1920×1080; para origen 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='<HABLANTE_DERECHO_HABILITADO>'[v]
Construye <HABLANTE_DERECHO_HABILITADO> 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 5 — Añade subtítulos
Pregunta una vez (solo si el usuario no ha especificado ya un estilo):
"Tres estilos de subtítulos: opus (blanco grande y negrita, resaltado de palabra activa en amarillo), karaoke (grupos de 4 palabras, resaltado verde), minimal (Helvetica limpio, sin resaltado). O pega un ejemplo que te guste."
Si pega una imagen de referencia/ejemplo: iguala la fuente, tamaño, peso, color, posición y animación lo más fielmente posible — escribe un ASS personalizado a mano o amplía build_ass.py.
En caso contrario, usa el preajuste:
# Vuelve a ejecutar whisper en el clip recortado para obtener marcas de tiempo precisas relativas al inicio del clip
whisper /tmp/clipify/clip_paneado.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_paneado.json /tmp/clipify/captions.ass opus
Incrusta los subtítulos:
ffmpeg -y -i /tmp/clipify/clip_paneado.mp4 -vf "subtitles=/tmp/clipify/captions.ass" \
-c:v libx264 -preset fast -crf 20 -c:a copy "$SALIDA.mp4"
Paso 6 — Entrega
- Guarda cada salida en
<directorio_origen>/clipify_out/(crea el directorio si no existe) - Imprime una línea por clip: nombre, duración, qué fue divertido, ruta de salida
- Imprime la primera ruta de salida (o ábrela — Linux
xdg-open <ruta>, macOSopen <ruta>) para que el usuario pueda revisarla - Ofrece iterar (diferente estilo, diferente ROI, cambiar a pantalla dividida, reajustar tiempos de subtítulos)
Errores comunes (lecciones de ejecuciones anteriores — no repetir)
- No ajustes en exceso las ROI. Máximo dos iteraciones. La diferencia de movimiento es tolerante — las ROI más amplias que cubren boca+mentón funcionan bien incluso si no están perfectamente centradas en la boca.
- Cuidado con los cambios de escena dentro de un clip. Ejecuta
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -para contar los cortes. Si un clip 16:9→9:16 tiene muchos cortes, las ROI fijas de rostro solo funcionan para la escena dominante; advierte al usuario y ofrece seleccionar un clip de una sola toma o aceptar un encuadre descentrado durante los cortes. - La resolución de origen importa. Si el origen es 4K, reduce la escala a 1920×1080 primero (más rápido, suficiente para salida 9:16) o multiplica todas las coordenadas ROI/paneo por 2.
- Subtítulos incrustados en el origen. Algunos clips "en bruto" aún tienen subtítulos. Si es así, encuentra el máster sin subtítulos mediante correlación cruzada de audio (
audio_align.py) y recorta desde allí. - No ejecutes whisper en el vídeo completo de larga duración si un clip corto es suficiente. Ejecuta whisper en el clip recortado después del Paso 2; solo ejecuta whisper en el origen completo 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.