叠帧 · 번호 이미지 → 커버 비디오
개요
폴더를 스캔하여 이름이 순수한 숫자로만 구성된 파일(01.jpg, 200.jpg)만 유지하고, 숫자 순서대로 재생하여 H.264 mp4로 인코딩합니다. 이미지 간 전환은 하드 컷(--transition none) 또는 크로스페이드(--transition fade, 기본값)를 사용합니다. 번호가 없는 이미지(logo, discord 등)와 기존 비디오는 무시됩니다.
절차
- 먼저 폴더를 검사합니다. 파일을 나열하고 ffprobe로 한 이미지의 크기를 확인합니다. 번호가 매겨진 스틸 이미지의 개수와 화면 비율이 혼합되어 있는지 확인합니다.
- 기존 출력 파일을 백업합니다.
<폴더>/cover.mp4가 이미 있으면 렌더링하기 전에cover.prev-YYYYMMDD.mp4로 이름을 바꿉니다. 사용자의 이전 렌더링을 절대 자동으로 덮어쓰지 않습니다. - 번들 스크립트로 렌더링합니다:
python "<skill-dir>/scripts/imgseq2mp4.py" --dir "<폴더>" --out cover.mp4
기본값: 이미지당 0.8초, 0.2초 fade 크로스페이드, 30fps, 1920x1080, 레터박스 패딩, CRF 18, 오디오 없음, +faststart.
- 추측하지 말고 검증합니다. 지속 시간/프레임을 확인하고, 검은 프레임이 없는지 확인하며, 마지막 프레임을 추출하여 원본 마지막 이미지와 비교합니다:
ffprobe -v error -show_entries format=duration -show_entries stream=nb_frames -of default=nw=1 cover.mp4
ffmpeg -i cover.mp4 -vf blackdetect=d=0.05:pix_th=0.10 -an -f null -
ffmpeg -y -sseof -0.05 -i cover.mp4 -frames:v 1 frame_last.png
전환이 켜져 있으면 첫 번째 전환 구간 내의 프레임도 가져와서 혼합(블렌드) 상태인지 확인합니다. 하드 컷보다는 이미지 2에 가깝지만 여전히 두 소스와 명확히 다른 값을 가져야 합니다.
- 보고: 이미지 개수, 무시된 이미지, 총 지속 시간, 출력 경로 및 크기를 보고합니다.
템포
총 길이 = n × 초 − (n−1) × 전환_초. 각 이미지의 화면 표시 단계는 초 − 전환_초이며, 시청자가 느끼는 것은 --seconds가 아니라 이 단계입니다.
| 이미지당 단계 | 명령어 | 20개 이미지 | 44개 이미지 | 200개 이미지 |
|---|---|---|---|---|
| 0.35 초 | --seconds 0.5 --transition-seconds 0.15 | ~8 초 | ~18 초 | ~75 초 |
| 0.45 초 | --seconds 0.65 --transition-seconds 0.2 | ~9 초 | ~22 초 | ~95 초 |
| 0.60 초 (기본값) | --seconds 0.8 --transition-seconds 0.2 | ~12 초 | ~27 초 | ~120 초 |
| 0.80 초 | --seconds 1.0 --transition-seconds 0.2 | ~16 초 | ~34 초 | ~150 초 |
하드 컷은 --transition none --seconds <단계>를 사용합니다.
사용자가 빠르게 또는 느리게 요청할 때 한 번에 한 행씩 이동합니다. --transition-seconds는 0.15–0.2로 유지하십시오. 더 길어지면 크로스페이드보다는 슬라이드쇼처럼 보이기 시작합니다.
규칙
- 크로스페이드만 사용합니다. 슬라이드, 와이프 또는 기타 푸시 전환은 사용하지 마십시오. 해당 전환은 방향성에서 제외되었습니다.
- 정규식
^\d+\.(jpe?g|png|webp)$로 선택합니다. 파일 이름은 숫자만 있어야 합니다. 이것이logo.jpg,discord.jpg,cover.jpg를 제외하는 이유입니다. - 사전순이 아닌 정수 값으로 정렬합니다(
2.jpg가10.jpg보다 앞,99.jpg가100.jpg보다 앞). 번호 매기기에 공백이 있어도 괜찮습니다. 누락된 숫자는 오류가 아닙니다. - 전체 프레임이 보이도록
--fit pad(기본값)를 사용합니다. 사용자가 명시적으로 가장자리까지 자르기를 원할 때만--fit fill을 사용하십시오. - 출력은 무음입니다. 사용자가 트랙을 제공하지 않는 한 음악을 추가하지 마십시오.
- 색상을 그대로 유지합니다. JPEG는 풀 레인지(full-range)입니다. 태그되지 않은 풀 레인지 H.264로 인코딩하면 대부분의 플레이어가 리미티드(limited)로 디코딩하여 결과물이 더 밝고 세탁된 것처럼 보입니다(+10 레벨). 스크립트는 리미티드 레인지 bt709로 변환하고 태그를 지정합니다. 이 체인을 "단순화"하지 마십시오.
- 스크립트는
--force없이--out을 덮어쓰지 않습니다. 해결 방법은 맹목적으로 강제하는 것이 아니라 백업하는 것입니다.
시스템 참고 사항
- ffmpeg가 PATH에 없습니다. 스크립트는
.cmd심(shim)보다 실제ffmpeg.exe(~/.dsh/tools/ffmpeg/**/ffmpeg.exe, 그 다음C:/또는D:/ffmpeg아래의 복사본)를 선호합니다. 심은 cmd.exe를 통해 라우팅되어 명령줄을 약 8k자로 제한하기 때문입니다.FFMPEG=/path/to/ffmpeg로 재정의할 수 있습니다. - 이 빌드에는
-filter_complex_script가 없습니다. xfade 그래프를-filter_complex를 통해 인라인으로 전달하십시오(스크립트가 이미 수행함). - 전환은
--chunk(기본값 6) 그룹으로 렌더링된 후 병합되므로 한 번에 최대 6개의 디코더만 열립니다. 단일 44입력 xfade 패스는 16GB 시스템에서 RAM을 소진하고(~2.4GB 여유)Cannot allocate memory오류로 실패했습니다. 여기서--chunk를 약 8 이상으로 올리지 마십시오. 그룹화는 재귀적으로 수행되므로 200개의 이미지도 OOM 없이 세 단계로 병합됩니다. - 중간 단계 파일은 CRF 14 / veryfast로 인코딩되어 폐기됩니다. 최종 패스만
--crf/ medium을 사용합니다. - 중간 프레임에는 쓰기 가능한 디렉토리가 필요합니다.
tempfile.mkdtemp()결과는 제한된 샌드박스에서 접근할 수 없을 수 있습니다(Windows + DSH에서 관찰됨: ffmpeg가Permission denied를 반환하고os.listdir이WinError 5를 발생시킴). 따라서 스크립트는 일반 디렉토리를 생성하고, 쓰기 가능함을 증명한 후$IMGSEQ_WORKDIR→ 시스템 임시 폴더 →<출력-디렉토리>/.imgseq-work순으로 대체합니다.
리소스
scripts/imgseq2mp4.py— 선택, 숫자 정렬, 하드 컷 또는 크로스페이드 렌더링, 검증에 적합한 요약.