Gorio Tech Blog search

Python 영상·음성 처리 실무 가이드(FFmpeg, MoviePy, OpenCV, PyAV, librosa)

|

목차

음성 추출과 코덱 변환에는 FFmpeg, 장면 연결에는 MoviePy, 프레임별 영상 처리에는 OpenCV를 사용할 수 있다. 각 도구의 예제는 같은 파일을 입력으로 사용하며, 메타데이터 조회, 편집, 프레임 추출, 음성 특징 계산을 다룬다.

문서 확인일은 2026년 9월 6일이다. MoviePy는 2.x API를 기준으로 하며, Python 3.12 환경을 사용한다. 라이브러리마다 FFmpeg를 사용하는 방식이 다르므로 Python 패키지 설치와 시스템 FFmpeg 설치를 구분한다.

1. 작업별 도구 선택

도구 적합한 작업 알아둘 점
FFmpeg / ffprobe 코덱 변환, 음성 추출, 대량 변환, 미디어 정보 조회 CLI 도구다. Python에서는 subprocess로 실행하면 된다.
MoviePy 2.x 장면 자르기·연결, 배경음악, 자막·영상 합성 편집 내용을 Python 객체로 표현하기 쉽다. 픽셀 처리와 재인코딩 비용이 발생한다.
OpenCV 4.x 카메라 입력, 영상의 프레임별 컴퓨터 비전 처리 일반적인 VideoCapture/VideoWriter 작업은 영상 중심이며 오디오 편집·동기화를 별도로 처리해야 한다.
PyAV FFmpeg 스트림·패킷·프레임에 직접 접근, 타임스탬프 기반 처리 시간과 코덱을 세밀하게 제어할 수 있지만 스트림 구조를 이해해야 한다.
imageio v3 API NumPy 배열로 이미지·영상 프레임 읽기, 간단한 파일 입출력 영상 플러그인을 지정해야 동작이 명확하다. 여기서는 PyAV 플러그인을 사용한다.
soundfile WAV·FLAC 등의 파형 읽기·쓰기, 블록 단위 처리 libsndfile 기반이다. 리샘플링이나 동영상 컨테이너 처리는 별도 도구가 필요하다.
librosa 리샘플링, mel spectrogram, MFCC, 음악·음성 분석 파일 편집기보다 신호 분석 도구에 가깝다. 기본 로딩 설정이 원본과 다를 수 있다.
pydub 짧은 음성 자르기·연결·페이드·음량 조절 밀리초 단위 API가 간단하다. 전체 PCM을 메모리에 올리며 Python 버전 호환성을 확인해야 한다.
TorchCodec / TorchAudio PyTorch 모델용 영상·음성 디코딩, 텐서 변환·특징 계산 새 디코딩 코드는 TorchCodec을 검토한다. PyTorch·TorchCodec·FFmpeg 호환 조합이 필요하다.

원본 프레임의 재생 시각을 기준으로 데이터를 추출해야 한다면 PyAV나 TorchCodec의 타임스탬프 API를 사용한다.

2. 설치와 공통 예제 파일

FFmpeg 설치

macOS에서는 Homebrew, Ubuntu/Debian에서는 배포판 패키지를 사용할 수 있다. Windows에서는 FFmpeg 다운로드 페이지에 연결된 빌드를 설치하고 bin 디렉터리를 PATH에 추가한다. pip install ffmpeg는 아래 명령에 필요한 FFmpeg 실행 파일을 설치하는 명령이 아니다.

# macOS
brew install ffmpeg

# Ubuntu / Debian
sudo apt-get update
sudo apt-get install ffmpeg

# 설치 확인
ffmpeg -version
ffprobe -version

Python 패키지는 가상환경에 설치한다. 아래 구성에서 PyTorch 계열은 제외했으며, 해당 절에서 따로 설명한다. 서버에서 창을 띄우지 않으므로 OpenCV는 headless 패키지를 사용한다. opencv-python과 opencv-python-headless는 같은 환경에 함께 설치하지 않는다.

python3.12 -m venv .venv-media
source .venv-media/bin/activate
# Windows PowerShell: .venv-media\Scripts\Activate.ps1

python -m pip install "moviepy==2.2.1" "opencv-python-headless>=4.10,<5" \
  "av>=16,<19" "imageio>=2.37,<3" "librosa==0.11.0" \
  "soundfile==0.13.1" "pydub==0.25.1"

MoviePy의 FFmpeg 바이너리, PyAV wheel에 포함된 FFmpeg 라이브러리, 시스템의 ffmpeg는 서로 다른 버전일 수 있다. 한 도구에서 코덱을 읽었다고 다른 도구에서도 반드시 읽을 수 있는 것은 아니다.

외부 파일 없이 6초짜리 샘플 만들기

아래 명령은 320×240, 30 fps 영상과 48 kHz 사인파 음성을 합친 input.mp4를 만든다. 이후 예제는 이 파일이 있는 디렉터리에서 실행한다. 출력 파일이 이미 있으면 -n 때문에 덮어쓰지 않고 종료한다.

ffmpeg -hide_banner -n \
  -f lavfi -i "testsrc2=size=320x240:rate=30:duration=6" \
  -f lavfi -i "sine=frequency=440:sample_rate=48000:duration=6" \
  -map 0:v:0 -map 1:a:0 -c:v libx264 -pix_fmt yuv420p \
  -c:a aac -shortest input.mp4

MP4는 컨테이너, H.264는 영상 코덱, AAC는 음성 코덱이다. 파일 확장자만 바꿔도 코덱은 그대로 유지된다. 실제 파일에는 영상·음성·자막 스트림이 여러 개 있을 수 있다.

3. ffprobe로 먼저 확인하기

ffprobe -v error -show_format -show_streams -of json input.mp4

streams에서 codec_type, codec_name, width, height, sample_rate, channels, time_base, start_time을 확인한다. format.duration은 컨테이너 길이이며 개별 스트림 길이와 약간 다를 수 있다. avg_frame_rate는 평균 프레임률이므로 프레임별 간격이 일정하다는 보장은 없다. ffprobe 공식 문서

Python에서는 JSON을 파싱할 수 있다. 인자를 리스트로 넘기면 공백이 포함된 파일명도 처리할 수 있으며, 파일명에 포함된 문자를 셸 명령으로 해석하지 않는다.

import json
import subprocess
from pathlib import Path

source = Path("input.mp4").resolve(strict=True)
result = subprocess.run(
    ["ffprobe", "-v", "error", "-show_streams", "-show_format",
     "-of", "json", str(source)],
    check=True, capture_output=True, text=True, timeout=30,
)
metadata = json.loads(result.stdout)
for stream in metadata["streams"]:
    print(stream["index"], stream["codec_type"], stream["codec_name"])
print("duration:", metadata["format"].get("duration", "unknown"))

샘플의 출력은 영상 스트림 0 video h264, 음성 스트림 1 audio aac, 길이 약 6.000000초다. 손상된 파일이나 실시간 입력에서는 길이가 없을 수 있으므로 모든 키가 존재한다고 가정하지 않는다.

4. FFmpeg로 변환·추출·자르기

호환성 높은 MP4 만들기

ffmpeg -hide_banner -n -i input.mp4 \
  -map 0:v:0 -map "0:a:0?" \
  -vf "scale=640:-2" -c:v libx264 -crf 23 -preset medium \
  -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart converted.mp4

이 예제는 너비를 640으로 맞추고 높이를 비율에 맞는 짝수로 정한다. -crf는 H.264 품질 설정이며 낮을수록 대체로 화질과 용량이 높아진다. -preset은 인코딩 속도와 압축 효율에 영향을 준다. -map "0:a:0?"의 물음표는 음성 스트림이 없어도 진행하라는 의미다. +faststart는 MP4 메타데이터를 앞쪽에 배치해 웹 재생 시작에 유리하도록 한다. FFmpeg 옵션 문서, MP4 muxer 문서

음성 추출: 복사와 PCM 변환

# 이 샘플의 AAC 오디오를 재인코딩 없이 M4A에 저장한다.
ffmpeg -hide_banner -n -i input.mp4 -map 0:a:0 -c:a copy audio.m4a

# 분석용 16 kHz mono PCM WAV를 만든다.
ffmpeg -hide_banner -n -i input.mp4 -map 0:a:0 \
  -ac 1 -ar 16000 -c:a pcm_s16le audio.wav

첫 명령은 컨테이너와 코덱이 호환될 때 사용할 수 있다. 임의의 코덱을 -c:a copy로 WAV나 M4A에 넣을 수 있는 것은 아니다. 두 번째 명령은 파형을 디코딩하고 채널 수와 샘플레이트를 변환한다. -ar 16000을 지정해도 녹음에 없던 정보가 생기지는 않으며, 모델이 24 kHz나 48 kHz를 요구하면 그 설정에 맞춰야 한다.

1초부터 3초 동안 자르기

# 재인코딩: 지정 지점까지 디코딩한 뒤 결과를 저장한다.
ffmpeg -hide_banner -n -ss 1 -i input.mp4 -t 3 \
  -map 0:v:0 -map "0:a:0?" -c:v libx264 -crf 18 \
  -c:a aac cut.mp4

# 빠른 스트림 복사: 시작점과 길이가 정확히 일치하지 않을 수 있다.
ffmpeg -hide_banner -n -ss 1 -i input.mp4 -t 3 \
  -map 0:v:0 -map "0:a:0?" -c copy cut-copy.mp4

영상은 키프레임을 기준으로 탐색하는 경우가 많다. 입력 앞의 -ss로 찾은 탐색 지점과 요청 시각이 다르면, 기본적인 재인코딩 경로는 그 사이를 디코딩한 뒤 출력에서 제외한다. 스트림 복사에서는 그 구간이 남을 수 있다. 따라서 -c copy는 정확한 구간 라벨을 만드는 방법으로 사용하면 안 된다. 재인코딩도 실제 프레임·음성 샘플 시간 단위의 제약을 받으므로 결과의 시작 타임스탬프와 길이를 확인한다. FFmpeg의 -ss 설명

시간 기준 프레임 샘플링

mkdir -p frames
ffmpeg -hide_banner -n -i input.mp4 \
  -vf "fps=1,scale=320:-2" frames/frame_%04d.jpg

출력은 frame_0001.jpg부터 시작하는 약 1초 간격 이미지다. fps=1 필터는 타임스탬프를 기준으로 고정 프레임률 출력을 만들면서 프레임을 선택하거나 복제한다. 파일 번호는 원본 프레임 번호나 원본 PTS가 아니다. 원본 재생 시각을 같이 저장해야 하는 데이터셋이라면 아래 PyAV 예제를 사용한다. FFmpeg fps 필터

Python에서 위 작업을 자동화할 때도 subprocess.run([...], check=True)를 사용한다. 긴 파일을 변환한다면 적절한 timeout을 정하고, 대량 로그를 capture_output=True로 계속 메모리에 보관하기보다 파일로 기록한다.

5. MoviePy 2.x로 장면 연결과 음량 조절

MoviePy 2.x에서는 from moviepy import VideoFileClip을 사용한다. 과거의 moviepy.editor, subclip, resize, set_audio 예제를 그대로 복사하면 맞지 않을 수 있다. 현재 이름은 각각 subclipped, resized, with_audio다. with_... 메서드는 변경된 클립을 반환하므로 반환값을 사용해야 한다. 2.x 마이그레이션 가이드

다음 코드는 0~2초와 3~5초 장면을 연결하고 음량을 절반으로 낮춰 4초짜리 edited.mp4를 만든다. 이 샘플은 두 장면의 크기와 FPS가 같지만, 크기가 다른 클립은 method="compose"로 연결할 수 있다.

from moviepy import VideoFileClip, concatenate_videoclips

with VideoFileClip("input.mp4") as source:
    if source.duration < 5:
        raise ValueError("The input must be at least 5 seconds long")
    first = source.subclipped(0, 2).resized(new_size=(320, 240))
    second = source.subclipped(3, 5).resized(new_size=(320, 240))
    joined = concatenate_videoclips([first, second], method="compose")
    edited = joined.with_volume_scaled(0.5)
    try:
        edited.write_videofile(
            "edited.mp4", fps=30, codec="libx264", audio_codec="aac",
            audio_fps=48000, ffmpeg_params=["-pix_fmt", "yuv420p"],
            logger=None,
        )
    finally:
        edited.close()
        joined.close()
        first.close()
        second.close()

파일 기반 클립은 FFmpeg 프로세스와 파일 핸들을 유지한다. 파생 클립은 원본 reader를 공유할 수 있으므로 저장이 끝날 때까지 원본 클립을 열어 둔다. 여러 입력을 연결할 때도 모든 원본을 같은 범위에서 관리한다. MoviePy 클립 로딩과 자원 관리

배경음악을 교체하려면 AudioFileClip을 열고 source.with_audio(music)으로 지정할 수 있다. 원본 소리와 배경음악을 섞을 때는 CompositeAudioClip을 사용한다. 음악 길이, 반복 여부, 시작 시각, 합산 후 클리핑을 별도로 정해야 한다. write_videofile(fps=30)은 출력을 30 fps로 렌더링하므로 VFR 원본의 프레임별 시간을 그대로 보존하는 작업에는 적합하지 않다.

6. OpenCV로 프레임별 처리하기

OpenCV의 일반적인 컬러 프레임은 (height, width, 3) 모양의 BGR 배열이다. Pillow·MoviePy·대부분의 RGB 모델로 넘길 때는 cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)로 바꾼다. 아래 코드는 각 프레임을 흑백으로 바꾼 뒤 3채널 영상으로 저장한다. 화면을 띄우지 않으므로 서버에서도 실행할 수 있다. OpenCV 영상 입출력 튜토리얼

import math
import cv2

capture = cv2.VideoCapture("input.mp4")
writer = None
try:
    if not capture.isOpened():
        raise RuntimeError("Cannot open input.mp4")
    fps = capture.get(cv2.CAP_PROP_FPS)
    if not math.isfinite(fps) or fps <= 0:
        raise ValueError("Input FPS is unavailable; inspect it with ffprobe")
    ok, frame = capture.read()
    if not ok:
        raise RuntimeError("No decodable frames")
    height, width = frame.shape[:2]
    writer = cv2.VideoWriter(
        "gray.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height)
    )
    if not writer.isOpened():
        raise RuntimeError("Cannot create video with the selected codec")
    count = 0
    while ok:
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        writer.write(cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR))
        count += 1
        ok, frame = capture.read()
    print("frames:", count)
finally:
    if writer is not None:
        writer.release()
    capture.release()

샘플에서는 180프레임을 기록한다. VideoWriter에 지정한 FPS는 출력 재생 속도를 정하며, 원본 오디오는 gray.mp4에 포함되지 않는다. 같은 CFR 원본의 모든 프레임을 그대로 기록했으므로 다음과 같이 원본 음성을 다시 넣을 수 있다.

ffmpeg -hide_banner -n -i gray.mp4 -i input.mp4 \
  -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -shortest gray-audio.mp4

일부 프레임을 버렸거나 FPS를 바꿨다면 음성 동기화가 자동으로 해결되지는 않는다. -shortest는 짧은 스트림에서 출력을 끝낼 뿐, 서로 어긋난 시각을 교정하는 옵션이 아니다. CAP_PROP_POS_MSEC나 임의 프레임 탐색의 동작은 backend와 파일에 따라 다르므로, 정밀한 샘플링에는 다음 방식을 사용한다.

7. PyAV로 원본 타임스탬프 보존하기

고정 프레임률(CFR) 영상에서는 프레임 번호와 FPS로 시간을 계산할 수 있지만, 가변 프레임률(VFR) 영상에서는 프레임 간 간격이 일정하지 않다. PyAV의 frame.pts는 표시 시각의 정수 값이며 초 단위 시간은 frame.pts * frame.time_base다. frame.time은 이를 초 단위로 제공한다. PyAV 18.1.0 Frame API

다음 코드는 첫 디코딩 프레임을 상대 시각 0초로 삼고, 0~5초의 각 목표 시각 이상에서 처음 나타나는 프레임을 PNG로 저장한다. 목표 시각과 실제 선택한 PTS를 CSV에 함께 남긴다. 길게 유지되는 한 프레임이 여러 목표 시각에 걸치는 경우에도 목표 시각 이후에 시작하는 프레임을 고른다.

import csv
from pathlib import Path
import av

output = Path("pyav-frames")
output.mkdir(exist_ok=True)
targets = iter(range(6))
target = next(targets, None)
origin = None

with av.open("input.mp4") as container, \
        (output / "timestamps.csv").open("w", newline="", encoding="utf-8") as handle:
    if not container.streams.video:
        raise ValueError("No video stream")
    rows = csv.writer(handle)
    rows.writerow(["file", "target_seconds", "pts_seconds", "relative_seconds"])
    for frame in container.decode(video=0):
        if frame.time is None:
            continue
        timestamp = frame.time
        if origin is None:
            origin = timestamp
        relative = timestamp - origin
        while target is not None and relative + 1e-9 >= target:
            name = f"frame_{target:02d}.png"
            frame.to_image().save(output / name)
            rows.writerow([name, target, timestamp, relative])
            target = next(targets, None)
        if target is None:
            break

음성과 정렬할 때는 영상의 첫 PTS를 뺀 값만 저장하지 말고, 원본 스트림의 시작 시각과 시간 기준을 유지한다. 영상과 음성의 시작점이 다를 수 있기 때문이다. 디코딩 순서를 나타내는 DTS와 표시 순서를 나타내는 PTS도 구분해야 한다. 멀리 떨어진 구간으로 seek하면 일반적으로 가까운 키프레임부터 다시 디코딩해 목표 시각을 찾아야 한다.

8. imageio로 프레임 배열 읽기

imageio는 이미지와 영상에 공통 입출력 인터페이스를 제공한다. 여기서 v3는 API 이름이며 설치 패키지 버전은 2.x다. 영상 plugin을 pyav로 명시하면 PyAV를 통해 프레임을 읽는다. imageio 사용 예제

import imageio.v3 as iio

first = iio.imread("input.mp4", index=0, plugin="pyav")
iio.imwrite("first-frame.png", first)
print(first.shape, first.dtype)

with iio.imopen("input.mp4", "r", plugin="pyav") as reader:
    count = 0
    for frame in reader.iter():
        # Process one frame at a time.
        count += 1
    print("frames:", count)

샘플에서는 (240, 320, 3) uint8, frames: 180이 출력된다. 전체 영상을 한 배열로 읽거나 list(reader.iter())로 바꾸면 모든 디코딩 프레임이 메모리에 쌓인다. 반복자에서 한 프레임씩 처리하는 편이 긴 영상에 적합하다.

9. soundfile과 librosa로 파형·특징 다루기

채널과 샘플레이트를 명시적으로 처리하기

앞에서 만든 audio.wav를 읽어 16 kHz mono로 변환하고 log-mel 특징을 저장한다. soundfile.read(..., always_2d=True)는 mono 파일도 (samples, channels)로 읽는다. 반면 librosa의 다채널 파형은 일반적으로 (channels, samples) 배열이므로 두 라이브러리 사이에서 축을 확인한다. soundfile 문서

import numpy as np
import soundfile as sf
import librosa

audio, original_sr = sf.read("audio.wav", dtype="float32", always_2d=True)
if audio.shape[0] == 0:
    raise ValueError("Empty audio")
mono = audio.mean(axis=1)
target_sr = 16000
waveform = librosa.resample(mono, orig_sr=original_sr, target_sr=target_sr)
sf.write("mono-16k.wav", waveform, target_sr, subtype="PCM_16")

mel_power = librosa.feature.melspectrogram(
    y=waveform, sr=target_sr, n_fft=400, hop_length=160,
    n_mels=80, fmin=0, fmax=8000, power=2.0, center=False,
)
log_mel = librosa.power_to_db(mel_power, ref=1.0, top_db=80)
np.save("log-mel.npy", log_mel)
print("sample rate:", target_sr)
print("waveform:", waveform.shape, "log-mel:", log_mel.shape)

이 설정은 25 ms 창과 10 ms 간격으로 80개 mel 대역을 계산한다. 6초에 정확히 96,000개 샘플이 있다면 출력은 (80, 598)이다. AAC를 WAV로 디코딩한 실제 길이는 패딩 처리에 따라 조금 길어질 수 있다. center=False이므로 특징 프레임 k의 시작 시각은 k * 160 / 16000초다. 음성 모델에 입력할 때는 해당 모델의 학습 전처리에 맞춰 창, mel 정의, 정규화, 패딩 설정을 변경해야 한다. librosa mel spectrogram API

librosa.load("audio.wav")는 기본적으로 22,050 Hz mono로 로딩한다. 원본 샘플레이트와 채널을 유지하려면 librosa.load("audio.wav", sr=None, mono=False)를 사용한다. 파일 헤더의 샘플레이트 숫자만 바꾸는 것은 리샘플링이 아니며 재생 속도와 음높이를 바꾼다. librosa 0.11.0 load API

여러 채널의 단순 평균은 반대 위상 신호를 상쇄할 수 있다. 마이크 배열·음원 분리·공간 음향 작업에서는 mono 변환을 무조건 적용하지 않는다. 실수 파형을 PCM_16으로 쓸 때는 범위를 넘는 값이 클리핑될 수 있으므로, 증폭·합성 후 peak를 확인하거나 부동소수점 WAV를 사용한다.

긴 음성은 블록 단위로 처리하기

import numpy as np
import soundfile as sf

sum_squares = 0.0
count = 0
with sf.SoundFile("audio.wav") as source:
    for block in source.blocks(blocksize=65536, dtype="float32", always_2d=True):
        sum_squares += np.square(block.astype(np.float64)).sum()
        count += block.size
if count == 0:
    raise ValueError("Empty audio")
print("RMS across all channels:", np.sqrt(sum_squares / count))

이 코드는 파일 전체를 메모리에 올리지 않고 모든 채널의 RMS를 계산한다. 단, 블록별 STFT나 리샘플링은 이처럼 독립적으로 계산해 붙이면 경계가 달라질 수 있다. STFT에는 창 길이에 따른 겹침이 필요하고, 연속 리샘플링에는 필터 상태를 유지해야 한다. soundfile 블록 처리

10. pydub으로 짧은 음성 편집하기

pydub은 음성 구간을 Python 슬라이스로 편집하며, 시간 단위로 밀리초를 사용한다. 다음 코드는 첫 2초의 음량을 6 dB 낮추고 앞뒤에 페이드를 적용한다. pydub 공식 사용법

from pydub import AudioSegment

audio = AudioSegment.from_file("audio.wav")
edited = (audio[:2000] - 6).fade_in(100).fade_out(200)
exported = edited.export("pydub-edited.wav", format="wav")
exported.close()
print("duration ms:", len(edited))

MP3·AAC 등의 읽기·쓰기는 대체로 외부 FFmpeg에 의존한다. AudioSegment는 디코딩한 음성을 메모리에 보관하므로 수 시간짜리 파일의 단순 변환은 FFmpeg로 처리하는 편이 적절하다.

확인일 기준 PyPI의 최신 pydub 배포판은 0.25.1(2021-03-10)이다. 이 버전은 Python의 audioop에 의존하며 audioop는 Python 3.13에서 제거되었다. 따라서 이 예제는 Python 3.12를 사용한다. 새 Python 환경에서는 pydub과 대체 의존성의 호환성을 검증하거나 FFmpeg·soundfile 중심으로 구성한다. pydub 배포 이력, Python 3.13 변경 사항

11. PyTorch에서는 TorchCodec으로 디코딩하기

TorchAudio 2.9부터 torchaudio.load()는 내부적으로 TorchCodec을 사용한다. normalize, buffer_size, backend 인자는 이 경로에서 무시되므로, 이전 버전의 backend 선택 코드를 그대로 사용하지 않는다. TorchAudio는 유지보수 단계로 전환했으며, 공식 문서는 새 I/O 코드에 TorchCodec API를 사용하는 방식을 안내한다. 특징 변환 등 사용 중인 TorchAudio API도 해당 버전 문서에서 확인한다. TorchAudio 2.9 load 문서, TorchAudio 유지보수 안내

TorchCodec은 PyTorch 텐서로 직접 디코딩하며 FFmpeg 공유 라이브러리를 사용한다. ffmpeg CLI가 실행된다는 사실만으로 필요한 공유 라이브러리까지 로딩된다는 보장은 없다. 설치할 때는 CPU/CUDA 환경과 운영체제를 정한 뒤 공식 호환표에서 PyTorch와 TorchCodec 버전을 맞춘다. 이 글에서 확인한 TorchCodec 0.16은 PyTorch 2.11 이상을 지원한다. macOS에서는 가상환경에서 python -m pip install "torch>=2.11" "torchcodec==0.16.0"으로 설치했다. Linux에서 CPU 전용 wheel이 필요하면 공식 설치 안내의 CPU index를 사용한다.

호환되는 환경에서 다음 코드는 MP4의 첫 2초 음성과 두 시점의 영상 프레임을 읽는다. 전체 파일을 읽는 get_all_samples() 대신 구간을 지정해 필요한 데이터만 가져온다.

from torchcodec.decoders import AudioDecoder, VideoDecoder

audio_decoder = AudioDecoder("input.mp4", sample_rate=16000, num_channels=1)
audio = audio_decoder.get_samples_played_in_range(start_seconds=0, stop_seconds=2)
print(audio.data.shape, audio.sample_rate)
print(audio.pts_seconds, audio.duration_seconds)

video_decoder = VideoDecoder("input.mp4", device="cpu")
frames = video_decoder.get_frames_played_at(seconds=[0.5, 1.5])
print(frames.data.shape)
print(frames.pts_seconds)

음성은 (channels, samples)의 float32 텐서, 영상 batch는 기본 설정에서 (frames, channels, height, width)의 uint8 텐서다. 이 샘플에서는 (1, 32000)과 (2, 3, 240, 320)이 출력된다. 시간으로 조회한 영상의 pts_seconds는 요청 시각 자체가 아니라 그때 재생 중인 프레임의 시작 시각이다. 모델에 넘기기 전 채널 순서·dtype·정규화를 맞추고 batch 크기를 제한한다. AudioDecoder API, VideoDecoder API

macOS에서 Library not loaded: @rpath/libavutil... 오류가 발생한다면 실제 FFmpeg 공유 라이브러리의 위치를 확인한다. 이 글의 Apple Silicon Homebrew 환경에서는 위 코드를 decode_media.py로 저장한 뒤 DYLD_LIBRARY_PATH=/opt/homebrew/opt/ffmpeg/lib python decode_media.py처럼 해당 프로세스에 검색 경로를 지정해 실행했다. 경로는 설치 환경에 맞춰야 하며, 이 설정은 PyTorch와 TorchCodec 자체의 버전 불일치를 해결하지는 않는다. TorchCodec 저장소의 macOS 라이브러리 경로 논의

12. 결과를 검증할 때 확인할 것

변환 후에는 결과 스트림의 코덱·길이·채널 수를 확인하고, 파일을 끝까지 디코딩해 오류를 검사한다.

ffprobe -v error -show_streams -show_format -of json edited.mp4
ffmpeg -v error -i edited.mp4 -f null -
증상 먼저 확인할 항목 처리 방법
Unknown encoder 또는 파일 열기 실패 설치한 FFmpeg와 각 라이브러리 backend의 코덱 지원 ffmpeg -encoders와 ffprobe로 입력·출력을 확인한다.
RGB 모델 입력의 색이 뒤바뀜 OpenCV의 BGR 순서 모델 입력 전에 RGB로 변환한다.
소리가 빠짐 OpenCV 출력과 -map 설정 원본 오디오를 명시적으로 연결하고 시간 일치를 확인한다.
영상과 소리가 점점 어긋남 VFR, FPS 변경, 누락 프레임, 시작 PTS 평균 FPS로 시간을 재구성하지 말고 타임스탬프 기준으로 처리한다.
음성이 빨라지거나 느려짐 실제 샘플레이트와 저장 시 설정 헤더만 바꾸지 말고 리샘플러로 샘플을 변환한다.
메모리가 부족함 전체 프레임·파형 로딩과 batch 크기 반복자, 구간 디코딩, 블록 처리로 바꾼다.
처리 후 입력 파일을 이동할 수 없음 열린 reader·writer·하위 프로세스 with, close(), release()로 자원을 해제한다.

1920×1080 RGB uint8 프레임은 약 6.2 MB다. 30 fps로 1분을 디코딩하면 픽셀만 약 11.2 GB이고, float32로 바꾸면 약 네 배가 된다. 디코딩에 필요한 메모리는 해상도, 프레임 수, dtype을 기준으로 계산한다. 서버에서 병렬 변환할 때는 작업 수와 FFmpeg 스레드 수를 함께 제한하고, 같은 출력 파일이나 임시 오디오 경로를 여러 작업이 공유하지 않도록 한다.

예제 실행 확인

macOS arm64·Python 3.12.12에서 본문의 처리 예제를 합성 파일로 실행했다. FFmpeg 9.0.1, MoviePy 2.2.1, OpenCV 4.14.0.94, PyAV 18.1.0, imageio 2.37.4, librosa 0.11.0, soundfile 0.13.1, pydub 0.25.1을 사용했다. TorchCodec 예제는 PyTorch 2.14.0·TorchCodec 0.16.0에서 위 공유 라이브러리 경로를 지정해 확인했다.

장면 연결 결과는 영상·음성이 포함된 4초 MP4였으며, OpenCV 출력은 180프레임이었다. FFmpeg와 PyAV가 각각 이미지 6개를 생성했고 PyAV가 기록한 PTS는 0~5초였다. WAV는 16 kHz mono 96,000샘플, log-mel 배열은 (80, 598), pydub 편집 결과는 2,000 ms였다. TorchCodec 텐서 크기도 위 설명과 일치했다. 검증에는 합성 CFR 파일과 CPU 디코딩을 사용했다.

References와 문서 버전

아래 문서를 2026-09-06에 확인했다. 온라인 stable·main 문서는 이후 내용이 달라질 수 있으므로 재현 환경에는 설치 버전도 기록한다.

문서 이 글에서 확인한 기준
FFmpeg CLI, ffprobe, 필터 확인일의 온라인 문서와 로컬 FFmpeg 9.0.1
MoviePy, v2 변경 사항 2.x API, 예제 검증 대상 2.2.1
OpenCV 영상 입출력 4.13.0 문서
PyAV Frame API 18.1.0 stable 문서
imageio 예제 2.37.4 문서, v3 API
librosa 0.11.0
soundfile 0.13.1
pydub, PyPI 0.25.1
TorchAudio load, TorchCodec AudioDecoder, 호환표 TorchAudio 2.9.0의 전환 규약, TorchCodec 0.16 문서