Clipify
Trouvez des moments captivants dans une vidéo, découpez-les en clips autonomes, reformatez éventuellement du 16:9 → 9:16 (panoramique sur le visage ou écran partagé), accélérez les plans larges pour des révélations plus serrées, et ajoutez des sous-titres mot par mot façon Opus.
Modes
Choisissez-en un avant l'étape 1. Le choix change la façon de trouver les moments et si les sous-titres s'appliquent.
- Mode dialogue (par défaut pour les podcasts, interviews, têtes parlantes) : Whisper transcrit, vous cherchez les punchlines / réactions / silences gênants. → Étape 1A. Sous-titres à l'étape 5.
- Mode action (coupe du bois, sports, percussions, batterie, tout ce qui implique des bruits d'impact répétés) : ignorez complètement Whisper, exécutez
detect_transients.pypour trouver chaque coup, découpez sur chacun. → Étape 1B. Sautez l'étape 5 ; les bruits de coup sont le rythme. - Hybride : mode dialogue pour les sections parlées, mode action pour le reste, entrecoupez. Utilisez à la fois 1A et 1B.
Si l'utilisateur dit "Je ne parle pas dans cette vidéo", vous êtes en mode action. S'il dit "coupe sur chaque X" (coup, frappe, punch, battement de batterie, swing, pas), vous êtes en mode action. Par défaut, optez pour le mode dialogue.
Entrées
- Un chemin de fichier vidéo (l'utilisateur le fournira ; sinon, demandez)
- Facultatif : format demandé (9:16, 16:9, 1:1) — si non fourni, demandez après avoir choisi les candidats
- Facultatif : préférence de style de sous-titres — si non fournie, demandez avant le sous-titrage
Outillage (utilisez uniquement le chemin le plus rapide)
- Whisper :
whisper --model tiny.en --word_timestamps True --output_format json(≈10× plus rapide quesmall.en; qualité suffisante pour l'anglais). Pour les langues non anglaises :--model base(supprimez--language). - ffmpeg : ajoutez
-hwaccel videotoolboxpour le décodage et-preset ultrafastpour les rendus. Utilisez-c:v libx264 -crf 20pour le master final. - Numpy pour l'alignement audio (corrélation croisée FFT). Pas besoin de scipy/cv2.
- Scripts :
<skill-dir>/scripts/(<skill-dir>est le répertoire contenant ce SKILL.md — généralement~/.claude/skills/clipify/)analyze.py— chronologie des locuteurs à partir de deux fichiers de mouvement ROIbuild_pan.py— expression x de recadrage ffmpeg avec coupures franchesbuild_ass.py— sous-titres ASS style Opus à partir du JSON de Whisperaudio_align.py— trouver le décalage d'un sous-clip dans une source plus longuedetect_transients.py— trouver les bruits d'impact forts (mode action) ; voir--help
Répertoire de travail : /tmp/clipify/ (mkdir au début, laissez les artefacts pour le débogage).
Workflow
Étape 1A — Trouver les moments de dialogue (mode dialogue)
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
Lisez le JSON (ou .txt) obtenu et choisissez 3 à 5 clips candidats. Signaux amusants à rechercher :
- Punchlines et réactions : mots comme "quoi", "attends", "pas possible", rires, "haha", jurons
- Moments de renversement : question de mise en place → réponse inattendue
- Silences gênants : segment Whisper avec un long écart, ou remplissage ("euh", "hum")
- Auto-dérision / citations détachables : phrases déclaratives courtes qui tiennent toutes seules
- Pics audio : détectez via
ffmpeg -af volumedetectou cherchez des allers-retours rapides (segments Whisper courts alternés)
Pour chaque candidat, proposez : [début, fin, pourquoi-c'est-drôle, titre suggéré]. Visez des clips de 10 à 25 secondes. Affichez la liste et laissez l'utilisateur confirmer/choisir.
Étape 1B — Trouver chaque coup (mode action)
Remplacez Whisper par la détection de transitoires audio. Le détecteur trouve les impacts forts (hache sur bois, poing sur tapis, peau de batterie, balle sur batte, bois tombant sur un tas) en calculant le flux spectral sur la bande 1–6 kHz — l'impulsion large bande que crée un impact et que l'ambiance/vent ne crée pas.
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
Réglage :
--min-flux 30est la valeur par défaut raisonnable. Les vrais impacts enregistrent 50–300 ; l'ambiance/vent se situe à 5–15. Trop de faux positifs → augmentez à 40–50. Trop peu de coups → descendez à 20.--min-gap 0.6(secondes) est l'écart minimal entre deux coups. Une batterie rapide peut nécessiter 0.2 ; la coupe du bois est correcte à 0.6–1.0.--band 1000:6000couvre les impacts bois/métal/verre. Les bruits sourds lourds (grosse caisse, coups au corps) se situent plus bas (--band 200:2000) ; les sifflements/cliquetis/coups de caisse claire plus haut (--band 4000:10000).- Pour les sources multi-clips (un dossier de clips de téléphone), exécutez le détecteur sur chaque clip et choisissez un ensemble varié d'angles. Visez 50 à 65 coups pour un montage de 45 à 60 secondes à ~0,9 s par plan.
Chaque "plan" doit commencer ~0,4 s avant le coup (montrant l'élan) et se terminer ~0,5 s après (coup + bref suivi). Cela fait ~0,9 s par temps.
Étape 1.5 — Vérifier que chaque candidat est dans le cadre
Faites-le toujours avant le rendu, quel que soit le mode. La détection audio trouve le son d'un événement ; la caméra peut ne pas l'avoir capté. Sauter cette étape est la principale façon d'obtenir des retours utilisateur comme "tu as coupé sur des coups où je ne suis même pas visible."
# Échantillonnez une image à chaque temps de coup candidat 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
Lisez chaque image de vérification et éliminez le candidat si :
- Le sujet est penché, hors cadre ou derrière un obstacle
- Un pouce est sur l'objectif (oui, ça arrive — et vous ne le rattrapez que visuellement)
- Il s'agit clairement d'une source sonore différente (claquement de porte, outil tombé) qui a déclenché le détecteur
Pour ~30 candidats, c'est un appel ffmpeg rapide chacun plus une seule lecture d'image par lot. Une assurance bon marché.
Étape 2 — Découper chaque clip choisi
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(Utilisez -c copy pour un découpage instantané. Ne ré-encodez que si les coupes doivent être précises à l'image.)
Étape 3 — Décider du format de sortie
Demandez à l'utilisateur (passez s'il a déjà spécifié) : "9:16 (TikTok / Reels), 16:9 (YouTube), ou 1:1 (fil Insta) ?"
Étape 4 — Si 16:9 → 9:16 : panoramique entre les visages vs écran partagé
Détectez le format source avec ffprobe. Si la source est 16:9 et la cible 9:16, demandez :
"Deux options : (a) panoramique à coupure franche qui suit la personne qui parle (un seul visage à l'écran à la fois), ou (b) écran partagé empilé avec les deux visages visibles. Laquelle voulez-vous ?"
Passez la question s'il n'y a qu'un seul visage (clip à un seul intervenant). Pour un seul intervenant, recadrez simplement au centre.
Étape 4a — Panoramique entre les visages (recommandé pour un dialogue rapide entre têtes parlantes)
-
Localisez les deux ROI de visage. Échantillonnez une image :
ffmpeg -ss <milieu> -i <clip> -frames:v 1 /tmp/clipify/probe.jpg. Lisez-la. Estimez à l'œil la zone bouche+menton de chaque visage commex,y,w,hdans l'espace pixel de la source. (Pas besoin de cv2 — la caméra est statique dans un clip ; une image suffit.) Vérifiez en dessinant des boîtes :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.jpgItérez au maximum deux fois. Les boîtes doivent couvrir la bouche + le menton et éviter les mains/micros. Ne peaufinez pas trop — la différence de trame est tolérante.
-
Extrayez l'énergie de mouvement par image dans chaque 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 - -
Construisez la chronologie des locuteurs (temps d'arrêt minimum 1,0 s — les courtes interjections fusionnent avec le locuteur précédent) :
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
Choisissez les coordonnées x du panoramique pour une bande verticale 9:16 à partir de la source. Avec une source W=1920 et une cible W=1080, largeur de bande de recadrage = 608.
- LEFT_X =
face_left_center_x - 304(limité à ≥ 0) - RIGHT_X =
face_right_center_x - 304(limité à ≤ source_W - 608)
- LEFT_X =
-
Générez l'expression x de coupure franche et effectuez le rendu :
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.mp4Source 1920×1080 supposée ; pour une source 4K, réduisez d'abord l'échelle ou doublez toutes les coordonnées.
Étape 4b — Écran partagé (les deux visages toujours visibles)
Deux tuiles empilées, 1080×960 chacune. Le locuteur actif est en haut — la superposition bascule lors des changements de locuteur.
[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]
Construisez <RIGHT_SPEAKER_ENABLE> à partir de segments.json comme between(t,a,b)+between(t,a,b)+... sur les segments du locuteur de droite. Les recadrages de tuile doivent cibler ~720×640 autour de chaque visage (1.125:1 pour correspondre à 1080×960).
Étape 4c — Révélations avec variation de vitesse (facultatif)
Lorsque l'utilisateur souhaite resserrer un plan lent / panoramique / de révélation ("coupe ça en deux") sans perdre l'arc narratif, accélérez plutôt que de couper. Couper vous oblige à abandonner une partie de l'arc ; l'accélération préserve tous les temps forts à un tempo plus élevé.
# Vitesse 2x : arc de 12 s → sortie de 6 s, hauteur audio préservée via 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
Règles empiriques :
- 1.5x — mouvement humain qui doit paraître "légèrement rapide" sans sembler accéléré
- 2x — révélation plus percutante ; reste naturelle
- 3x–4x — ambiance accélérée (enchaînez
atempo=2.0,atempo=2.0pour 4x ;atempon'accepte que 0.5–2.0 par instance de filtre) - Supprimez
-af atempoet coupez le son du segment si l'audio est simplement ambiant/vent et que l'effet de voix accélérée serait gênant
Utile lorsque le reste de la coupe est rythmique (montage de coups, va-et-vient de conversation) et que la révélation semblerait autrement un temps mort.
Étape 5 — Ajouter des sous-titres
Sautez cette étape en mode action — les bruits de coup sont le rythme et les sous-titres ne font qu'encombrer le visuel.
Demandez une fois (uniquement si l'utilisateur n'a pas déjà spécifié un style) :
"Trois styles de sous-titres : opus (gras blanc, surlignage jaune du mot actif), karaoké (groupes de 4 mots, surlignage vert), minimal (Helvetica propre, pas de surlignage). Ou collez un exemple que vous aimez."
S'ils collent une image/un exemple de référence : adaptez la police, la taille, le poids, la couleur, la position et l'animation aussi proche que possible — écrivez un ASS personnalisé à la main ou étendez build_ass.py.
Sinon, utilisez le préréglage :
# Relancez whisper sur le clip découpé pour des horodatages précis par rapport au début du 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
Gravez les sous-titres :
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"
Étape 6 — Livraison
- Enregistrez chaque sortie dans
<source_dir>/clipify_out/(mkdir si manquant) - Imprimez une ligne par clip : nom, durée, ce qui était drôle, chemin de sortie
- Ouvrez la première sortie avec
open <path>pour que l'utilisateur puisse la vérifier - Proposez d'itérer (style différent, ROI différent, passer à l'écran partagé, réajuster les sous-titres)
Pièges (leçons des exécutions précédentes — ne les répétez pas)
- La détection audio n'est pas un événement visible. Exécutez toujours l'étape 1.5 (vérifier chaque image candidate) avant le rendu. Le détecteur trouve le son d'une coupe, pas si le coupeur est dans le cadre. Les coups où le sujet est penché, hors caméra, ou où un pouce est sur l'objectif déclenchent toujours le détecteur audio. Attrapez-les avant le rendu.
- La bande de flux spectral est importante. Par défaut,
--band 1000:6000couvre les impacts bois/métal/verre. Les bruits sourds lourds (grosse caisse, coups au corps) nécessitent--band 200:2000. Les sifflements, cliquetis, coups de caisse claire nécessitent--band 4000:10000. Si--min-flux 30ne renvoie rien, essayez d'abord de le descendre à 15 ; s'il renvoie des milliers, essayez une bande différente avant d'augmenter le seuil. - Audio de variation de vitesse par paires.
atempon'accepte que 0.5–2.0 dans un seul filtre ; pour 4x, enchaînezatempo=2.0,atempo=2.0. Sansatempo,setptsseul donne un audio de voix accélérée. - Ne peaufinez pas trop les ROI. Deux itérations maximum. La différence de mouvement est tolérante — des ROI plus larges couvrant bouche+menton fonctionnent bien même s'ils ne sont pas parfaitement centrés sur la bouche.
- Attention aux coupures de plan à l'intérieur d'un clip. Exécutez
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -pour compter les coupures. Si un clip 16:9→9:16 a de nombreuses coupures, les ROI de visage fixes ne fonctionnent que pour la scène dominante ; prévenez l'utilisateur et proposez soit de choisir un clip en une seule prise, soit d'accepter un cadrage décentré pendant les coupures. - La résolution source compte. Si la source est 4K, réduisez d'abord l'échelle à 1920×1080 (plus rapide, correct pour une sortie 9:16) ou multipliez toutes les coordonnées ROI/panoramique par 2.
- Sous-titres incrustés dans la source. Certains clips "bruts" ont encore des sous-titres. Si c'est le cas, trouvez le master sans sous-titres via corrélation croisée audio (
audio_align.py) et découpez à partir de là. - N'exécutez pas whisper sur la source complète de long métrage si un clip court suffit. Exécutez whisper sur le clip découpé après l'étape 2 ; n'exécutez whisper sur la source complète à l'étape 1 que si vous avez besoin d'une transcription pour trouver des moments amusants.
- Énoncez le plan en une ligne, puis agissez. Ne commentez pas chaque itération.