준비 사항
- FFmpeg: 동영상, 오디오, 메타데이터를 처리하는 강력한 멀티미디어 프레임워크입니다. 최신 버전은 공식 FFmpeg 웹사이트에서 다운로드할 수 있습니다. Windows, macOS, Linux에 맞는 빌드를 선택하세요.
환경 변수 설정: FFmpeg를 시스템 PATH에 추가하여 ffmpeg와 ffprobe 명령어를 전체 경로 없이 실행할 수 있도록 설정합니다. Windows에서는 FFmpeg의 bin 폴더(예: C:\ffmpeg\bin)를 시스템 PATH에 추가하면 됩니다.- Python: 챕터 메타데이터 파일을 생성하는 데 필요합니다. python.org에서 Python을 설치하세요.
- AutoHotkey (AHK): Windows에서 FFmpeg 작업을 자동화하는 데 사용됩니다. autohotkey.com에서 다운로드하세요.
작업 1: MP4 파일에서 챕터 제거하기
이 스크립트는 MP4 파일에서 챕터를 제거하고, 출력 파일을 _noch 접미사를 붙여 저장합니다.
[SHANA] 또는 _noch가 포함된 파일은 제외됩니다.
0 Remove_Chapters_Excluded.ahk
#NoEnv
SetBatchLines, -1
SetWorkingDir, %A_ScriptDir%
; FFmpeg 경로 설정
ffmpegPath := "C:\ffmpeg\bin\ffmpeg.exe"
Loop, *.mp4
{
originalName := A_LoopFileName
; 제외 조건 확인 ([SHANA] 또는 _noch 포함 여부)
if (InStr(originalName, "[SHANA]") || InStr(originalName, "_noch"))
{
;~ MsgBox, [제외] 건너뛰는 파일: %originalName%
continue
}
; 출력 파일명 생성 (원본명 + _noch)
outputName := SubStr(originalName, 1, -4) . "_noch.mp4"
; FFmpeg 명령 실행
RunWait, %ffmpegPath% -y -i "%originalName%" -map_chapters -1 -c copy "%outputName%", , Hide
; 실행 결과 확인
if ErrorLevel
MsgBox, [오류] 처리 실패: %originalName%
;~ else
;~ MsgBox, [성공] 생성 완료: %outputName%
}
MsgBox, 작업이 완료되었습니다!
ExitApp
주요 고려 사항
- 제외 조건: [SHANA] 또는 _noch가 포함된 파일은 이미 처리되었거나 특정 조건에 부합하지 않으므로 건너뜁니다. 이는 불필요한 재처리를 방지합니다.
- 에러 처리: ErrorLevel을 확인하여 FFmpeg 명령이 실패하면 오류 메시지를 표시합니다.
- -c copy: 비디오와 오디오를 재인코딩 없이 복사하여 처리 속도를 높이고 품질 손실을 방지합니다.
작업 2: 챕터 메타데이터 생성 및 MP4 파일에 챕터 추가하기
이 작업은 두 단계로 나뉩니다.
(1) Python 스크립트로 챕터 메타데이터 파일 생성, (2) AHK 스크립트로 메타데이터를 MP4 파일에 적용.
1 generate_chapters.py
import os
import subprocess
# ffprobe 경로 고정
FFPROBE_PATH = r"C:\ffmpeg\bin\ffprobe.exe" # ★★ 너 ffprobe.exe 실제 경로 맞게 수정 ★★
def get_video_duration(filepath):
"""ffprobe로 영상 길이(초 단위) 가져오기"""
result = subprocess.run(
[FFPROBE_PATH, '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', filepath],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
return float(result.stdout.strip())
def time_to_milliseconds(raw_input):
"""숫자로만 hhmmss 받아서 밀리초 변환"""
raw_input = raw_input.strip()
if not raw_input.isdigit():
raise ValueError("시간은 숫자만 입력하세요. (예: 013015 → 1시간 30분 15초)")
raw_input = raw_input.zfill(6) # 부족하면 0으로 채움 (ex: 3015 → 003015)
h = int(raw_input[:2])
m = int(raw_input[2:4])
s = int(raw_input[4:6])
return (h * 3600 + m * 60 + s) * 1000
def milliseconds_to_time(ms):
"""밀리초 -> hh:mm:ss 변환 (디버깅용)"""
seconds = ms // 1000
h = seconds // 3600
m = (seconds % 3600) // 60
s = (seconds % 60)
return f"{h:02}:{m:02}:{s:02}"
def main():
print("현재 폴더의 모든 MP4 파일을 검색합니다...\n")
mp4_files = [f for f in os.listdir('.') if f.lower().endswith('.mp4')]
if not mp4_files:
print("MP4 파일이 없습니다.")
return
for mp4 in mp4_files:
# 제외 조건 체크
if '[SHANA]' in mp4:
print(f"\n[{mp4}] 파일은 제외 대상입니다. 스킵합니다.")
continue
print(f"\n[{mp4}] 처리 시작")
# 영상 길이 가져오기
duration_seconds = get_video_duration(mp4)
duration_ms = int(duration_seconds * 1000)
# 타임코드 입력받기
print(f" - 영상 길이: {milliseconds_to_time(duration_ms)}")
print(" - 챕터용 타임코드를 입력하세요 (끝내려면 빈 줄)")
print(" 예시: 013015 입력 → 01:30:15")
times = []
titles = []
while True:
time_str = input("시작 시간 (hmmss 또는 hhmmss): ")
if not time_str.strip():
break
title = input("제목: ")
try:
times.append(time_to_milliseconds(time_str))
titles.append(title)
except ValueError as e:
print(f"잘못된 입력: {e}")
if not times:
print(" - 타임코드가 없어 건너뜁니다.")
continue
# 파일 저장
base_name = os.path.splitext(mp4)[0]
output_txt = f"{base_name}_SE.txt"
with open(output_txt, "w", encoding="utf-8") as f:
f.write(";FFMETADATA1\n")
for i in range(len(times)):
f.write("\n[CHAPTER]\n")
f.write("TIMEBASE=1/1000\n")
f.write(f"START={times[i]}\n")
if i < len(times) - 1:
f.write(f"END={times[i+1]}\n")
else:
f.write(f"END={duration_ms}\n") # 마지막 END는 영상 길이
f.write(f"title={titles[i]}\n")
print(f" - 챕터 메타데이터 파일 생성 완료: {output_txt}")
print("\n모든 작업이 완료되었습니다!")
if __name__ == "__main__":
main()
2 Chapters_Included.ahk
#NoEnv
SetBatchLines, -1
SetWorkingDir, %A_ScriptDir%
; FFmpeg 경로 설정
ffmpegPath := "C:\ffmpeg\bin\ffmpeg.exe"
Loop, *_SE.txt
{
chapterFile := A_LoopFileName
baseName := SubStr(chapterFile, 1, -7) ; "_SE.txt" 제거
; 해당하는 MP4 파일 찾기
videoFile := baseName . ".mp4"
if !FileExist(videoFile)
{
MsgBox, [경고] 동영상 파일 없음: %videoFile%
continue
}
; 출력 파일명 생성
if (SubStr(baseName, -4) = "_noch") ; 파일명이 _noch로 끝나면
{
trimmedName := SubStr(baseName, 1, -5) ; _noch 제거
outputName := trimmedName . "_ch.mp4"
}
else
{
outputName := baseName . "_ch.mp4"
}
; FFmpeg 명령 실행 (챕터 추가)
RunWait, %ffmpegPath% -y -i "%videoFile%" -i "%chapterFile%" -map_metadata 1 -map_chapters 1 -c copy "%outputName%", , Hide
; 실행 결과 확인
if ErrorLevel
MsgBox, [오류] 처리 실패: %videoFile%
else
FileMove, %chapterFile%, %baseName%_SE_processed.txt ; 처리 완료된 챕터 파일 이동
}
MsgBox, 작업이 완료되었습니다!
ExitApp
주요 고려 사항
- 입력 유효성 검사: Python 스크립트는 타임코드 입력이 숫자로만 이루어졌는지 확인하고, 잘못된 입력에 대해 에러 메시지를 출력합니다. 이는 사용자 실수를 줄입니다.
- 자동화 및 피드백: AHK 스크립트는 처리된 챕터 파일을 _SE_processed.txt로 이동하여 중복 처리를 방지하고, 작업 완료 여부를 메시지로 알립니다.
- 메타데이터 형식: 생성된 _SE.txt 파일은 FFmpeg의 FFMETADATA1 형식을 따르며, TIMEBASE=1/1000을 사용하여 밀리초 단위로 정확한 챕터 타임코드를 지정합니다.
- 효율성: -c copy 옵션을 사용하여 비디오와 오디오를 재인코딩 없이 복사하므로 처리 속도가 빠르고 품질 손실이 없습니다.
요약
이 포스트에서는 FFmpeg를 사용하여 MP4 파일의 챕터를 제거하고 추가하는 방법을 설명했습니다.
챕터 추가 할 mp4 파일과 같은 위치에서 위 코드를 복사해서 만든 아래 파일 3개를 순서대로 실행
(기존 영상에서 챕터 삭제 → 초단위 챕터 메타데이터 생성 → 챕터 추가한 mp4 파일 생성)
0 Remove_Chapters_Excluded.ahk
1 generate_chapters.py
2 Chapters_Included.ahk
Hitomi Downloader로 받은 유튜브 영상에 팟플레이어로 책갈피를 추가하면 챕터 마크가 숨겨지고 책갈피만 남는 부분이 신경 쓰여 여러 AI 도구에 도움을 요청했고, 오랜 시행착오 끝에 해결 방법을 찾게 되어 정리해봅니다.
파이썬만으로도 가능할 것 같았지만, AI를 통해 원하는 결과를 얻기는 쉽지 않아 결국 Python + 오토핫키(AutoHotkey 1버전) 조합으로 성공하여 이 포스트를 작성합니다.