Clipify
동영상에서 가장 재미있는 순간을 찾아 독립된 클립으로 자르고, 선택적으로 16:9 → 9:16 형식(얼굴 패닝 또는 분할 화면)으로 변환한 후, 오푸스(opus) 스타일의 단어별 자막을 입힙니다.
입력
- 동영상 파일 경로 (사용자가 제공하지 않으면 요청할 것)
- 선택 사항: 요청 형식 (9:16, 16:9, 1:1) — 제공되지 않은 경우, 후보 선정 후 질문
- 선택 사항: 자막 스타일 선호도 — 제공되지 않은 경우, 자막 입히기 전 질문
도구 (가장 빠른 경로만 사용)
- Whisper:
whisper --model tiny.en --word_timestamps True --output_format json(small.en대비 약 10배 빠름; 영어에 대해 품질 양호). 비영어의 경우:--model base사용 (--language생략). - ffmpeg: 하드웨어 디코딩은 선택 사항이며 플랫폼에 따라 다릅니다 —
-hwaccel auto를 사용하거나 생략하십시오 (macOS:videotoolbox; Linux:vaapi/cuda/없음). 렌더링 시-preset ultrafast를 추가하십시오. 최종 마스터에는-c:v libx264 -crf 20을 사용하십시오. - Numpy: 오디오 정렬용 (FFT 상호 상관). scipy/cv2는 필요하지 않습니다.
- 스크립트:
<skill-dir>/scripts/(여기서<skill-dir>은 이 SKILL.md 파일이 있는 디렉토리입니다 — 일반적으로~/.claude/skills/clipify/)analyze.py— 두 개의 ROI 모션 파일에서 화자 타임라인 생성build_pan.py— 하드 컷이 포함된 ffmpeg 자르기 x-표현식 생성build_ass.py— Whisper JSON에서 오푸스 스타일 ASS 자막 생성audio_align.py— 긴 원본에서 하위 클립의 오프셋 찾기
작업 디렉토리: /tmp/clipify/ (시작 시 생성, 디버깅을 위해 아티팩트 유지).
워크플로우
1단계 — 가장 재미있는 부분 찾기
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
결과 JSON (또는 .txt)을 읽고 3~5개의 후보 클립을 고르십시오. 찾아야 할 재미 신호:
- 펀치라인과 반응: "what", "wait", "no way" 같은 단어, 웃음, "haha", 욕설
- 반전 순간: 설정 질문 → 예상치 못한 대답
- 어색한 침묵: 긴 간격이 있는 Whisper 세그먼트 또는 필러 단어 ("uh", "um")
- 자조적 발언 / 인용구: 독립적으로 사용할 수 있는 짧은 평서문
- 오디오 피크:
ffmpeg -af volumedetect를 사용하거나 빠른 대화 교대 (짧은 Whisper 세그먼트 교대)를 찾아 감지
각 후보에 대해 [시작, 끝, 재미있는 이유, 제안 제목]을 제안하십시오. 10~25초 클립을 목표로 하십시오. 목록을 보여주고 사용자가 확인/선택하게 하십시오.
2단계 — 선택된 각 클립 자르기
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(즉시 자르기를 위해 -c copy를 사용하십시오. 컷이 프레임 정확해야 하는 경우에만 다시 인코딩하십시오.)
3단계 — 출력 형식 결정
사용자에게 질문하십시오 (이미 지정한 경우 건너뜁니다): "9:16 (TikTok / Reels), 16:9 (YouTube), 아니면 1:1 (Insta 피드)?"
4단계 — 16:9 → 9:16인 경우: 얼굴 간 패닝 vs 분할 화면
ffprobe로 소스 종횡비를 감지하십시오. 소스가 16:9이고 대상이 9:16인 경우 질문:
"두 가지 옵션: (a) 하드 컷 팬 — 말하는 사람을 따라다니며 (한 번에 한 명의 얼굴만 화면에 표시), 또는 (b) 분할 화면 — 두 얼굴을 모두 볼 수 있도록 수직으로 쌓습니다. 어느 것을 원하십니까?"
클립에 얼굴이 하나만 있는 경우 (단일 화자 클립) 질문을 건너뜁니다. 단일 화자의 경우 중앙 자르기만 수행합니다.
4a단계 — 얼굴 간 패닝 (빠른 컷의 대화하는 머리 권장)
-
두 얼굴 ROI 위치 찾기. 한 프레임 샘플 추출:
ffmpeg -ss <중간> -i <클립> -frames:v 1 /tmp/clipify/probe.jpg. 읽습니다. 소스 픽셀 공간에서 각 얼굴의 입+턱 영역을x,y,w,h로 대략적으로 파악합니다. (cv2 필요 없음 — 클립 내 카메라는 정적이며, 한 프레임이면 충분합니다.) 박스를 그려 확인: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.jpg최대 두 번 반복합니다. 박스는 입 + 턱을 덮고 손/마이크는 피해야 합니다. 과도하게 조정하지 마십시오 — 프레임 차이 분석은 관대합니다.
-
각 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 - -
화자 타임라인 구축 (최소 지속 시간 1.0초 — 짧은 감탄사는 이전 화자에 병합):
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
소스로부터 9:16 세로 스트립을 위한 팬 x좌표 선택. 소스 W=1920이고 대상 W=1080인 경우, 자르기 스트립 너비 = 608입니다.
- LEFT_X =
face_left_center_x - 304(0 이상으로 고정) - RIGHT_X =
face_right_center_x - 304(source_W - 608 이하로 고정)
- LEFT_X =
-
하드 컷 x 표현식 생성 및 렌더링:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $LEFT_X $RIGHT_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_panned.mp4소스 1920×1080을 가정합니다. 4K 소스의 경우 먼저 다운스케일하거나 모든 좌표를 두 배로 늘리십시오.
4b단계 — 분할 화면 (두 얼굴 항상 표시)
두 개의 쌓인 타일, 각각 1080×960. 활성 화자의 타일이 위에 있습니다 — 화자가 변경될 때 오버레이가 전환됩니다.
[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]
오른쪽 화자 세그먼트에 대해 <RIGHT_SPEAKER_ENABLE>을 between(t,a,b)+between(t,a,b)+...로 segments.json에서 빌드합니다. 타일 자르기는 각 얼굴 주변 약 720×640 (1.125:1 비율로 1080×960 일치)을 대상으로 해야 합니다.
5단계 — 자막 추가
한 번만 질문하십시오 (사용자가 이미 스타일을 지정하지 않은 경우에만):
"세 가지 자막 스타일: 오푸스 (크고 굵은 흰색, 노란색 활성 단어 강조), 노래방 (4단어 청크, 녹색 강조), 미니멀 (깔끔한 헬베티카, 강조 없음). 또는 마음에 드는 예시를 붙여넣으십시오."
사용자가 참조 이미지/예시를 붙여넣는 경우: 글꼴, 크기, 두께, 색상, 위치 및 애니메이션을 최대한 가깝게 일치시키십시오 — 수동으로 사용자 정의 ASS를 작성하거나 build_ass.py를 확장하십시오.
그렇지 않으면 프리셋을 사용하십시오:
# 자른 클립의 정확한 타임스탬프를 얻기 위해 클립 시작을 기준으로 Whisper 재실행
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
자막 입히기:
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"
6단계 — 전달
- 각 출력을
<source_dir>/clipify_out/에 저장합니다 (없으면 생성) - 클립당 한 줄씩 출력: 이름, 길이, 재미있었던 이유, 출력 경로
- 첫 번째 출력 경로를 출력하거나 엽니다 (Linux
xdg-open <경로>, macOSopen <경로>) — 사용자가 확인할 수 있도록 - 반복 작업 제안 (다른 스타일, 다른 ROI, 분할 화면으로 전환, 자막 타이밍 재조정)
함정 (이전 실행에서 얻은 교훈 — 반복하지 마십시오)
- ROI를 과도하게 조정하지 마십시오. 최대 두 번 반복. 모션 차이는 관대합니다 — 입+턱을 덮는 더 넓은 ROI가 입 중앙에 완벽하게 맞지 않아도 잘 작동합니다.
- 클립 내 장면 전환을 주의하십시오.
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -를 실행하여 컷 수를 세십시오. 16:9→9:16 클립에 컷이 많은 경우, 고정된 얼굴 ROI는 주요 장면에 대해서만 작동합니다. 사용자에게 경고하고, 단일 테이크 클립을 선택하거나 전환 중에 중앙이 벗어난 프레이밍을 수락하도록 제안하십시오. - 소스 해상도가 중요합니다. 소스가 4K인 경우, 먼저 1920×1080으로 다운스케일하거나 (더 빠르며 9:16 출력에 적합) 모든 ROI/팬 좌표에 2를 곱하십시오.
- 소스에 포함된 자막. 일부 "원본" 클립에는 여전히 자막이 있습니다. 그렇다면 오디오 상호 상관(
audio_align.py)을 통해 자막이 없는 마스터를 찾아 거기서부터 자르십시오. - 짧은 클립으로 충분하다면 전체 길이의 소스에 대해 Whisper를 실행하지 마십시오. 2단계 후 자른 클립에 대해 Whisper를 실행하십시오. 1단계에서 재미있는 순간을 찾기 위해 대본이 필요한 경우에만 전체 소스에 대해 Whisper를 실행하십시오.
- 한 줄로 계획을 말한 다음 행동하십시오. 모든 반복을 설명하지 마십시오.