본문으로 바로가기

파일의 IT 블로그

  1. Home
  2. 프로그래밍/Python
  3. [Python] 동영상 파일의 책갈피(Chapter, 챕터) 데이터 읽어오기 - 자막 싱크 조절

[Python] 동영상 파일의 책갈피(Chapter, 챕터) 데이터 읽어오기 - 자막 싱크 조절

· 댓글개 · KRFile

요새 새벽에 할 짓이 없어서 도박묵시록 카이지를 보고 있습니다. 2기 먼저 다보고 1기 보고 있는데 오랜만에 봐도 정말 재미있더라구요. 하지만 가장 큰 문제점 싱크가 안맞는다는 점입니다 ㅠㅠ

 

 

싱크가 전체적으로 몇 초씩 뒤틀려 있으면 그냥 파일 전체 몇 초씩 교정하면 끝나는 일인데 골때리게도 오프닝, 메인 , 중간 쉬어가는 부분, 엔딩 이렇게 4 파트가 모두 싱크가 + 되거나 - 해야 제대로 맞습니다.

 

도박묵시록 카이지 1기 (역경무뢰 카이지) 의 경우 2007년도에 나온 애니메이션 무려 18년이나 된 애니메이션이라 그 당시에 방영하던 파일로 자막 작업이 되어있어서 그런 거 같습니다. 이 자막이 이글루스에서 구한 건데 최근 이글루스 블로그가 서비스를 종료했습니다 ㅎㄷㄷ

 

뭐 영상 받아서 보시는 분들은 대부분 아시겠지만 TV 방영본을 기준으로 작업된 자막은 나중에 나온 BD판 등엔 호환이 되지 않는 경우가 많습니다. 왜냐면 BD 판에서는 TV 방영본에서 있던 쓸 데 없던 광고나 크레딧 영상들을 편집해서 제공하기 때문입니다. 저의 경우도 같은 상황입니다.

 

제가 받은 파일은 [DB] 라는 이름? 릴제작자? 가 제공하는 영상인데 어쨌던 자막 싱크가 안맞습니다.

 

그래도 그나마 다행인건 제가 받아 놓은 카이지 영상의 릴 제작자가 오프닝, 엔딩, 그리고 중간 쉬어가는 타이밍에 맞춰 전부 분리를 해뒀다는 점 입니다.

 

다음팟 플레이어 기준으로 저렇게 아래 재생바에 영상이 부분 부분 은색 삼각형 같은것으로 분리되어 있는걸 볼 수 있는데요. 다음팟 플레이어에선 이를 책갈피 라고 표현하고 있습니다. 다만 책갈피라는 용어는 다음팟플레이어 제작자가 쓰는 용어인 듯 하고 영어권에서 좀 더 범용적으로 사용되는 용어는 챕터(Chapter) 인 듯 싶습니다.

 

저 아래 저렇게 영상에 분리해놓은 일종의 메타데이터 부분을 챕터라고 하는 거 같더라구요.

 

음지 사이트인 Nyaa에서 영상을 구해보면 릴 제작자들이 대부분 저렇게 친절하게 영상의 각 구간을 챕터로 분리해놔서 빨리 오프닝을 스킵할 수 있게 도와주고 있습니다.

 

어쨌던 저는 영상에서 저 챕터 부분을 읽어와서, 각 챕터 부분별로 싱크를 조절하면 될 거라고 생각했습니다.

당연하지만 프로그래밍이 필요합니다. 파이썬을 이용해서 문제를 해결해보겠습니다.

 

챕터를 가져오는 방법

이미지 출처 : https://blogx.ch/blog/technology/ffmpeg-rearrange-audio-streams-in-video-files-950/

인터넷 검색을 해도 파이썬으로 영상을 읽어서 챕터를 가져오는 방법이 잘 나오지 않아서 좀 고역을 겪었습니다. 영상의 챕터나 다양한 정보를 가져오기 위해선 ffmpeg 라는 프로그램을 이용해야 합니다.

 

https://github.com/FFmpeg/FFmpeg

 

GitHub - FFmpeg/FFmpeg: Mirror of https://git.ffmpeg.org/ffmpeg.git

Mirror of https://git.ffmpeg.org/ffmpeg.git. Contribute to FFmpeg/FFmpeg development by creating an account on GitHub.

github.com

ffmpeg 란 모든 동영상, 음악, 사진 포맷들의 디코딩과 인코딩을 목표로 만들어지고 있는 오픈 소스 프로젝트 입니다. (나무위키)

 

그냥 쉽게 생각해서 모든 동영상 포맷을 다룰 수 있는 일종의 만능 CLI 프로그램이라고 생각하시면 됩니다.

이 프로그램을 사용하면 동영상의 정보를 가져올 수도 있고, 동영상의 확장자를 변경할 수도 있고, 해상도/프레임도 마음대로 변경할 수 있습니다. (인코딩)

 

하지만 마음대로 갖다 쓰기엔 GPL / LGPL 라이선스가 혼합 되어 있어서 상업적으로 사용할때는 저작권 관련해서 크리티컬을 받을 수도 있습니다. 물론 저는 개인적으로 사용할거라 큰 문제는 없습니다.

 

ffmpeg 를 설치하는 방법은 인터넷에 많이 나와있기 때문에 생략하도록 하겠습니다. 윈도우 패키지 매니저 choco 를 사용하던, 공식 홈페이지에서 받고 환경 변수를 등록하던 아니면 그냥 ffmpeg.exe 만 받아서 단일 실행 파일로 사용하던 어떤 방법을 사용하셔도 됩니다.

 

챕터를 가져오기 위해선 여러가지 방법을 사용할 수 있는데요.

ffmpeg -i "[DB]Gyakkyou Burai Kaiji Ultimate Survivor_01_(10bit_BD1080p_x265).mkv"

 

우선 터미널에서 다음과 같은 명령어를 입력합니다. -i 뒤에 챕터를 읽어올 영상의 경로를 제공해주시면 됩니다. 경로에 공백이 없으면 쌍따옴표로 묶지 않아도 되나, 경로에 공백이 존재하면 문자열 취급을 시켜주기 위해 반드시 쌍따옴표로 묶어주셔야 합니다.

 

ffmpeg version 4.2.3 Copyright (c) 2000-2020 the FFmpeg developers
  built with gcc 9.3.1 (GCC) 20200523
  configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
[matroska,webm @ 00000218479094c0] Could not find codec parameters for stream 2 (Subtitle: hdmv_pgs_subtitle (pgssub)): unspecified size
Consider increasing the value for the 'analyzeduration' and 'probesize' options
Input #0, matroska,webm, from '\\192.168.1.100\?좊땲硫붿씠???꾨컯臾듭떆濡?移댁씠吏€\?꾨컯臾듭떆濡?移댁씠吏€ S1 - ??꼍臾 대ː 移댁씠吏€\[DB]Gyakkyou Burai Kaiji Ultimate Survivor_01_(10bit_BD1080p_x265).mkv':
  Metadata:
    encoder         : libebml v1.4.1 + libmatroska v1.6.2
    creation_time   : 2021-05-10T14:05:26.000000Z
  Duration: 00:22:50.39, start: 0.000000, bitrate: 1647 kb/s
    Chapter #0:0: start 0.000000, end 49.883167
    Metadata:
      title           : Chapter 01
    Chapter #0:1: start 49.883167, end 579.011767
    Metadata:
      title           : Chapter 02
    Chapter #0:2: start 579.011767, end 1282.614667
    Metadata:
      title           : Chapter 03
    Chapter #0:3: start 1282.614667, end 1353.919233
    Metadata:
      title           : Chapter 04
    Chapter #0:4: start 1353.919233, end 1370.369000
    Metadata:
      title           : Chapter 05
    Stream #0:0: Video: hevc (Main 10), yuv420p10le(tv), 1910x1072, SAR 1:1 DAR 955:536, 23.98 fps, 23.98 tbr, 1k tbn, 23.98 tbc (default)
    Metadata:
      BPS-eng         : 1471000
      DURATION-eng    : 00:22:50.369000000
      NUMBER_OF_FRAMES-eng: 32856
      NUMBER_OF_BYTES-eng: 251976704
      _STATISTICS_WRITING_APP-eng: mkvmerge v52.0.0 ('Secret For The Mad') 64-bit
      _STATISTICS_WRITING_DATE_UTC-eng: 2021-05-10 14:05:26
      _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
    Stream #0:1(jpn): Audio: aac (LC), 48000 Hz, stereo, fltp (default)
    Metadata:
      title           : Stereo
      BPS-eng         : 159266
      DURATION-eng    : 00:22:50.368000000
      NUMBER_OF_FRAMES-eng: 64236
      NUMBER_OF_BYTES-eng: 27281692
      _STATISTICS_WRITING_APP-eng: mkvmerge v52.0.0 ('Secret For The Mad') 64-bit
      _STATISTICS_WRITING_DATE_UTC-eng: 2021-05-10 14:05:26
      _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
    Stream #0:2(eng): Subtitle: hdmv_pgs_subtitle (default)
    Metadata:
      title           : Official Subs
      BPS-eng         : 60316
      DURATION-eng    : 00:22:42.728000000
      NUMBER_OF_FRAMES-eng: 648
      NUMBER_OF_BYTES-eng: 10274426
      _STATISTICS_WRITING_APP-eng: mkvmerge v52.0.0 ('Secret For The Mad') 64-bit
      _STATISTICS_WRITING_DATE_UTC-eng: 2021-05-10 14:05:26
      _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES
At least one output file must be specified

그러면 엄청난 양의 정보가 나오게 되는데요. 

 

    Chapter #0:0: start 0.000000, end 49.883167
    Metadata:
      title           : Chapter 01
    Chapter #0:1: start 49.883167, end 579.011767
    Metadata:
      title           : Chapter 02
    Chapter #0:2: start 579.011767, end 1282.614667
    Metadata:
      title           : Chapter 03
    Chapter #0:3: start 1282.614667, end 1353.919233
    Metadata:
      title           : Chapter 04
    Chapter #0:4: start 1353.919233, end 1370.369000
    Metadata:
      title           : Chapter 05

다 무시하고 우리가 관심을 가져야 할 부분은 바로 이 부분입니다.

챕터에 관한 정보는 이 곳에 모여서 표시됩니다.

 

따라서 해야할 것은 파이썬으로 ffmpeg 명령어를 호출한 뒤에 모니터에 출력되는 내용을 읽어오고

그 내용중에 위 처럼 챕터에 관련된 내용을 정규식으로 추출하면 될 것 입니다.

 

At least one output file must be specified

그런데 ffmpeg 프로세스를 실행할 때마다 불편한 오류 메세지가 자꾸 끝에 표시되는데요.

오류메세지를 읽어보니 최소 한 개의 출력 파일이 제공되어야 한다고 표시됩니다.

 

https://stackoverflow.com/questions/11400248/using-ffmpeg-to-get-video-info-why-do-i-need-to-specify-an-output-file

 

Using ffmpeg to get video info - why do I need to specify an output file?

I'm using ffmpeg to get info about a video file and I don't want to save the info into a file. ffmpeg is returning all the video info, but it's returning as an error because I'm not specifying an o...

stackoverflow.com

이 오류가 왜 뜨나 인터넷 검색을 해보니 역시 StackOverFlow에서 정답을 찾을 수 있었습니다.

ffmpeg는 출력 파일을 기본적으로 제공해야 한다고 합니다. 단지 파일에 대한 정보를 얻기 위해서 사용하는건 올바른 사용 용도가 아니라고 합니다. 

ffmpeg 라는 도구 자체가 인코딩을 위해 입력을 받고, 특정 파일로 출력하는 용도기 때문에 그런듯 싶네요.

 

 

ffprobe -i "[DB]Gyakkyou Burai Kaiji Ultimate Survivor_01_(10bit_BD1080p_x265).mkv"

문제를 해결하려면 ffmpeg 가 아니라 ffprobe 라는 도구를 사용하면 된다고 합니다.

 

갑자기 ffmpeg 얘기하다가 ffprobe는 또 뭐냐? 하실 수 있는데 ffprobe는 미디어 파일에 대한 정보를 얻기 위해 설계된, ffmpeg 랑 같이 패키징 되어 있는 프로그램입니다.

 

ffmpeg 가 인코딩용 프로그램 이였다면 ffprobe는 영상 정보를 가져오는 용도의 프로그램이라고 이해하면 될 거 같습니다.

 

    Chapter #0:0: start 0.000000, end 49.883167
    Metadata:
      title           : Chapter 01
    Chapter #0:1: start 49.883167, end 579.011767
    Metadata:
      title           : Chapter 02
    Chapter #0:2: start 579.011767, end 1282.614667
    Metadata:
      title           : Chapter 03
    Chapter #0:3: start 1282.614667, end 1353.919233
    Metadata:
      title           : Chapter 04
    Chapter #0:4: start 1353.919233, end 1370.369000
    Metadata:
      title           : Chapter 05

어쨌던 위와 같이 ffprobe 명령어를 이용하면 아까전의 ffmpeg 를 사용했던 결과와 동일한 출력을 얻을 수 있습니다.

하지만 ffprobe를 이용하면 챕터를 조금 더 정제된 형태(Elegant Way)로 받아낼 수 있습니다.

 

ffprobe -i "파일명" -show_chapters

ffprobe 에 -i 파라미터 이외에 -show_chapters 파라미터를 제공합니다

 

[CHAPTER]
id=1942049836532381281
time_base=1/1000000000
start=0
start_time=0.000000
end=49883166666
end_time=49.883167
TAG:title=Chapter 01
[/CHAPTER]
[CHAPTER]
id=749936452283785529
time_base=1/1000000000
start=49883166666
start_time=49.883167
end=579011766666
end_time=579.011767
TAG:title=Chapter 02
[/CHAPTER]
[CHAPTER]
id=4360544932922373
time_base=1/1000000000
start=579011766666
start_time=579.011767
end=1282614666666
end_time=1282.614667
TAG:title=Chapter 03
[/CHAPTER]
[CHAPTER]
id=-5349351711718223806
time_base=1/1000000000
start=1282614666666
start_time=1282.614667
end=1353919233333
end_time=1353.919233
TAG:title=Chapter 04
[/CHAPTER]
[CHAPTER]
id=9209167242177420794
time_base=1/1000000000
start=1353919233333
start_time=1353.919233
end=1370369000000
end_time=1370.369000
TAG:title=Chapter 05
[/CHAPTER]

그러면 다음과 같은 형태로 챕터 데이터만 받아올 수 있습니다. 데이터 자체는 태그 구조로 묶여있고 안은 변수명 처럼 되어 있습니다. 하지만 이것도 좀 부족합니다. 여기 데이터 형식이 xml, html 같은 형식에 딱 맞는 형태가 아니기 때문에 여전히 외부 파싱 라이브러리를 사용하지 못하고 정규식에 의존하여 파싱해야 합니다.

 

나중에 ffprobe 가 업데이트 되었을때 이 형태가 바뀌면 정규식이 깨질 가능성이 생깁니다.

 

ffprobe -i "파일명" -print_format json -show_chapters -loglevel error

하지만 이렇게 인자를 더 추가해주면..!

 

{
    "chapters": [
        {
            "id": 1942049836532381281,
            "time_base": "1/1000000000",
            "start": 0,
            "start_time": "0.000000",
            "end": 49883166666,
            "end_time": "49.883167",
            "tags": {
                "title": "Chapter 01"
            }
        },
        {
            "id": 749936452283785529,
            "time_base": "1/1000000000",
            "start": 49883166666,
            "start_time": "49.883167",
            "end": 579011766666,
            "end_time": "579.011767",
            "tags": {
                "title": "Chapter 02"
            }
        },
        {
            "id": 4360544932922373,
            "time_base": "1/1000000000",
            "start": 579011766666,
            "start_time": "579.011767",
            "end": 1282614666666,
            "end_time": "1282.614667",
            "tags": {
                "title": "Chapter 03"
            }
        },
        {
            "id": -5349351711718223806,
            "time_base": "1/1000000000",
            "start": 1282614666666,
            "start_time": "1282.614667",
            "end": 1353919233333,
            "end_time": "1353.919233",
            "tags": {
                "title": "Chapter 04"
            }
        },
        {
            "id": 9209167242177420794,
            "time_base": "1/1000000000",
            "start": 1353919233333,
            "start_time": "1353.919233",
            "end": 1370369000000,
            "end_time": "1370.369000",
            "tags": {
                "title": "Chapter 05"
            }
        }
    ]
}

다음과 같이 깔끔한 json 출력을 얻어낼 수 있습니다. 아주 좋습니다 :) 참고로 ffmpeg 에서 똑같이 ffprobe의 명령어를 주면 제대로 실행하지 못합니다 ('print_format' 이라는 인자 자체가 없음)

 

이제 프로세스는 다 정해졌습니다.

  1. 파이썬의 os나 subprocess를 통해 ffprobe 프로세스를 위 명령어대로 호출한다.
  2. 나오는 출력값을 변수에 문자열로 저장한다 문자열로 저장된 json을 파이썬 딕셔너리 데이터로 변환한다. (파이썬의 json 표준 라이브러리 사용하면 됨)
  3. 문자열로 저장된 json을 파이썬 딕셔너리 데이터로 변환한다. (파이썬의 json 표준 라이브러리 사용하면 됨)
  4. 챕터 데이터를 성공적으로 딕셔너리 데이터로 변환했으니 이를 이용해 smi 파일의 싱크를 조정한다.

참 쉽죠?

정규식에 의존하지 않는 깔끔한 프로세스가 완성되었습니다.

이제 코드를 작성해 봅시다.

 

파이썬 자동화 스크립트 작성

https://gist.github.com/dcondrey/469e2850e7f88ac198e8c3ff111bda7c

 

Use ffmpeg to split file by chapters. Python version and bash version

Use ffmpeg to split file by chapters. Python version and bash version - ffmpegchapters-explicit.sh

gist.github.com

원래는 위 링크에서 코드 초안을 작성했었는데, json 데이터로 정제할 수 있다는걸 알고는 ChapGPT랑 위 링크의 코드를 좀 쓰까서 수정했습니다.

 

위에 제공된 코드는 ffmpeg를 활용해서 챕터 별로 동영상을 나누는 예제입니다. 저는 챕터 정보만 필요한거라 ffmpeg를 사용할 필요는 없고 앞에 설명한대로 ffprobe로 데이터만 가져오면 됩니다.

 

# 챕터 리턴 타입에 사용할 Class 타입
# dataclasses를 활용하면 데이터를 저장하는 클래스를 간편하게 만들 수 있다.
from dataclasses import dataclass

# pip install dataclasses-json
from dataclasses_json import dataclass_json
# dataclass_json 을 이용하면 dataclass 를 이용하여 쉽게 json 타입들을 정의할 수 있다. (외부 라이브러리)


@dataclass_json
@dataclass
class Tag:
    title: str


@dataclass_json
@dataclass
class ChapterItem:
    id: int
    time_base: str
    start: int
    start_time: str
    end: int
    end_time: str
    tags: Tag


@dataclass_json
@dataclass
class Chapters:
    chapters: list[ChapterItem]

 

우선 type 폴더 안의 type.py 에 json 데이터를 Class로 정의해보겠습니다. 그냥 Class로 정의 안하고 딕셔너리로 바꿔서 써도 되긴 하는데 그러면 IDE의 자동 완성 기능을 받을 수가 없습니다. (몹시 불편) 타입 안전성을 위해 꼭 필요한 작업입니다.

 

파이썬 3.7 부터 도입된 dataclass라는 녀석을 사용하면 데이터를 저장하는 클래스를 쉽게 만들 수 있습니다. 보통 데이터를 저장하는 class를 만들려면 생성자에서 멤버 변수 초기화 하는것도 만들어야 하고, getter / setter 을 구현하던 해서 프로퍼티도 만들어야 하고 하는데 dataclass 를 사용하면 이걸 한 틱에 해버릴 수 있습니다.

 

사용법은 class 명 위에 그냥 @dataclass 라고 데코레이터 붙이는게 끝입니다. 다만 json 형태의 데이터는 "중첩" 이 가능하기 때문에 (json 데이터 안의 json 데이터) @dataclass로 json 을 정의하기 위해선 중첩이 가능한 구조가 필요합니다.

 

따라서 외부 라이브러리인 dataclasses-json 를 사용해야 합니다.

dataclasses-json 라이브러리를 사용하면 dataclass를 중첩시켜서 json 데이터를 정의할 수 있게 해줍니다.

 

결론 : 외부 라이브러리 dataclasses-json 와 파이썬 dataclass를 이용하면 Class로 json 데이터를 쉽게 정의할 수 있습니다.

 

설명이 좀 어렵긴한데 그냥 ChatGPT 한테 json 데이터 복붙해서 주고 dataclasses-json 랑 dataclass 써서 정의좀 해달라고 해주면 코드 뱉어줍니다.

 

우리는 그거 받아오고 그 Class를 생성해서 json 데이터를 쓰면 됩니다.

 

# ./type/type.py
# 챕터 리턴 타입에 사용할 Class 타입
# dataclasses를 활용하면 데이터를 저장하는 클래스를 간편하게 만들 수 있다.
from dataclasses import dataclass

# pip install dataclasses-json
from dataclasses_json import dataclass_json
# dataclass_json 을 이용하면 dataclass 를 이용하여 쉽게 json 타입들을 정의할 수 있다. (외부 라이브러리)


@dataclass_json
@dataclass
class Tag:
    title: str


@dataclass_json
@dataclass
class ChapterItem:
    id: int
    time_base: str
    start: int
    start_time: str
    end: int
    end_time: str
    tags: Tag


@dataclass_json
@dataclass
class Chapters:
    chapters: list[ChapterItem]
# ./main.py
from pprint import pprint
import subprocess
from easysmi import *
from type.type import ChapterItem, Chapters


def get_chapters(file_name) -> list[ChapterItem]:
    # 실행할 명령어
    command = ['ffprobe', '-i', file_name,
               "-print_format", "json", "-show_chapters", "-loglevel", "error"]

    # 명령어 실행 후, 결과를 output에 저장
    # text = True : output을 text로 저장,
    # encoding은 기본이 cp949라 문제 없이 저장할려면 utf-8로 설정
    output = subprocess.check_output(
        command, stderr=subprocess.STDOUT, encoding='utf-8', text=True)

    # output(json string) 을 파싱하여 딕셔너리로 저장
    # [외부 라이브러리 dataclasses-json 라이브러리에서 제공되는 기능]
    json_dict: Chapters = Chapters.from_json(output)  # type: ignore

    return json_dict.chapters


def sync_shift_by_chapter(shift: int, chapter_start: str, chapter_end: str, p: ParsedSMI) -> None:
    start = int(float(chapter_start) * 1000)
    end = int(float(chapter_end) * 1000)

    # 자막을 파싱하고 오프닝 전까지 자막들을 찾아낸다음에 +1.1초 보정 필요

    for element in p.main:
        if start <= int(element.timeline) and int(element.timeline) <= end:
            print(f"[orig]c-timeline:{element.timeline} text:{element.text}")
            print("=======================================================>")
            element.timeline = str(int(element.timeline) + shift)
            print(f"[edit]c-timeline:{element.timeline} text:{element.text}")


def operation(video_file_name: str, smi_file_name: str) -> None:
    # easysmi 라이브러리를 이용하여 smi 파일을 파싱한다.
    p = parse_smi(smi_file_name)
    chapters = get_chapters(video_file_name)
    pprint(chapters)

    for chapter in chapters:
        # 오프닝 처리 +1.1초 보정 필요
        if chapter.tags.title == "Chapter 01":
            sync_shift_by_chapter(
                1100, chapter.start_time, chapter.end_time, p)
        # 그 다음으로 메인 시작 ~ 검은 화면 잠시 쉬었다 가는 창(?) 전까지 -3.8 초 보정 필요
        elif chapter.tags.title == "Chapter 02":
            sync_shift_by_chapter(-3800, chapter.start_time,
                                  chapter.end_time, p)
        # 검은 화면 ~ 잠시 쉬었다 가는 창까지 -0.7초 보정 필요
        elif chapter.tags.title == "Chapter 03":
            sync_shift_by_chapter(-700, chapter.start_time,
                                  chapter.end_time, p)
        # 검은화면 ~ 엔딩까지 -0.5초
        elif chapter.tags.title == "Chapter 04":
            sync_shift_by_chapter(-500, chapter.start_time,
                                  chapter.end_time, p)
        # 엔딩 ~ 다음화 예고 -0.7초
        elif chapter.tags.title == "Chapter 05":
            sync_shift_by_chapter(-700, chapter.start_time,
                                  chapter.end_time, p)

    # 파싱된 자막을 최종 저장한다.
    smi_file_save(smi_file_name, p)

    # 작업 완료 메세지 출력
    print(video_file_name, smi_file_name, "작업 완료")


if __name__ == '__main__':
    for i in range(1, 27):
        video_file_name = rf"[DB]Gyakkyou Burai Kaiji Ultimate Survivor_{str(i).zfill(2)}_(10bit_BD1080p_x265).mkv"
        smi_file_name = video_file_name.replace(".mkv", ".smi")
        print(video_file_name, smi_file_name)
        operation(video_file_name, smi_file_name)

이후 메인 코드를 main.py에 작성합니다.

main.py 에서는 카이지 1편 ~ 26편에 대해 operation 함수를 호출해서 동영상 파일명과 smi 파일명을 제공합니다.

 

operation 함수 안에선 이를 복사해와서 easysmi 라이브러리를 사용해서 smi 파일을 파싱합니다.

참고로 파이썬에서 smi 파일을 파싱하는 easysmi 라이브러리는 제가 만든겁니다. (자랑 맞음)

 

2년전에 학교다닐때 만들어놨던건데 지금 써보려니 전혀 작동하지 않아서 구조를 싸그리 뜯어 고치고 성능을 100배 넘게 향상시켰습니다. 예전에 썼던 코드 보니깐 참 허접하더라구요 ㅎㅎ; 타입 힌트도 모르고 정규식도 잘 못다룰때 작성한 코드라서.. 일단 급한대로 라이브러리를 수정해놓긴 했는데 버그가 있는지 없는진 아직 잘 모릅니다.

 

어쨌던 챕터 데이터는 get_chapters() 라는 함수에서 가져올 수 있습니다. 챕터 데이터를 가져오는 방법이 궁금하신 분들은 여기서 코드는 다 제거하고 오직 get_chapters() 함수만 사용하시면 됩니다. 가져오는 프로세스는 아까 설명했듯이 ffprobe를 서브 프로세스로 호출하고 결과를 받아온담에 아까 정의해둔 Class로 생성해냅니다.

 

dataclasses-json 라이브러리에서 제공하는 from_json 함수를 이용하면 문자열을 json 으로 받아내 Class로 json 데이터를 생성해낼 수 있습니다.

 

이걸 이용해 챕터 별로 싱크를 조정합니다. 싱크를 조정하는건 easysmi 라이브러리 설명github에서 읽어보시면 될겁니다. 

 

챕터 별로 일괄적으로 싱크를 조정하고 원본 파일에 바로 바로 반영해줍니다. 저는 확신이 있어서 그냥 원본에 바로 반영했는데 당연하지만 작업 전에 백업을 하던가, 테스트 중에는 자막 파일 이름을 다르게 하고 반영해야 합니다. 파일 이름 끝에 -modified 를 붙여서 다르게 자막 파일을 만들어 낸다던지...

 

어쨌던 저 코드를 실행시키고 자막 파일을 확인해보면?

 

자막 싱크가 전부 맞는걸 확인해볼 수 있습니다!!!

영상도 승리에 맞춰 절묘한 장면으로 가져와 봤습니다.

 

일일히 조절했으면 시간이 엄청나게 걸렸겠죠? 빠르게 자막 작업 처리를 완료해냈습니다.

인터넷에 챕터 가져오는 방법이 안나와 있어서 포기할까 싶었는데 그래도 구글링을 열심히 해본 결과 찾아낼 수 있었습니다. 😀

 

정리

from dataclasses import dataclass
import subprocess

# pip install dataclasses-json
from dataclasses_json import dataclass_json


@dataclass_json
@dataclass
class Tag:
    title: str


@dataclass_json
@dataclass
class ChapterItem:
    id: int
    time_base: str
    start: int
    start_time: str
    end: int
    end_time: str
    tags: Tag


@dataclass_json
@dataclass
class Chapters:
    chapters: list[ChapterItem]


def get_chapters(file_name) -> list[ChapterItem]:
    # 실행할 명령어
    command = [
        "ffprobe",
        "-i",
        file_name,
        "-print_format",
        "json",
        "-show_chapters",
        "-loglevel",
        "error",
    ]

    # 명령어 실행 후, 결과를 output에 저장
    # text = True : output을 text로 저장,
    # encoding은 기본이 cp949라 문제 없이 저장할려면 utf-8로 설정
    output = subprocess.check_output(
        command, stderr=subprocess.STDOUT, encoding="utf-8", text=True
    )

    # output(json string) 을 파싱하여 딕셔너리로 저장
    # [외부 라이브러리 dataclasses-json 라이브러리에서 제공되는 기능]
    json_dict: Chapters = Chapters.from_json(output)  # type: ignore

    return json_dict.chapters


if __name__ == "__main__":
    chapters = get_chapters("test.mp4")
    for chapter in chapters:
        print("title", chapter.tags.title)
        print("start_time", chapter.start_time)
        print("end_time", chapter.end_time)
        print("start", chapter.start)
        print("end", chapter.end)
        print("time_base", chapter.time_base)
        print("chapter_id", chapter.id)
        print("=======================================")

위에서 제가 챕터 불러오는 내용만 궁금하신 분들은 get_chapters() 라는 함수만 쓰라고 했죠? 그렇게 말로만 적어놓으면 좀 안와닿으니 코드로 제공하겠습니다.

 

동영상에서 챕터 내역을 읽어오는 예제 코드는 위와 같습니다. 위 템플릿을 기준으로 프로그래밍을 하시면 되겠습니다. 모듈로 사용하실걸 염두에 둬서 if __name__==__main__ 을 사용했습니다.

 

복사 편의를 위해 코드를 파일 하나에 뭉쳤지만 실제 사용할 때는 제가 작성한 것처럼 타입들은 type.py 처럼 별도 모듈로 빼서 관리를 하는걸 적극 권장드립니다.

 

위 코드에서 test.mp4 대신에 챕터를 읽을 영상 파일 경로를 제공하면 코드를 테스트 해볼 수 있습니다.

 

소스 코드

자막 파일 싱크(타임 라인 자동 처리).zip
0.00MB

 

이 파일이 정말 필요하신 분들이 있을 진 모르겠지만 혹시라도 필요한 분들이 있을까봐 전체 소스코드는 여기에 올려두도록 하겠습니다. 참고로 사용할려면 easysmi 와 dataclasses-json 를 pip install 해야 사용할 수 있습니다.

참고

https://stackoverflow.com/questions/30305953/is-there-an-elegant-way-to-split-a-file-by-chapter-using-ffmpeg

 

Is there an elegant way to split a file by chapter using ffmpeg?

In this page, Albert Armea share a code to split videos by chapter using ffmpeg. The code is straight forward, but not quite good-looking. ffmpeg -i "$SOURCE.$EXT" 2>&1 | grep Chap...

stackoverflow.com

https://stackoverflow.com/questions/11400248/using-ffmpeg-to-get-video-info-why-do-i-need-to-specify-an-output-file

 

Using ffmpeg to get video info - why do I need to specify an output file?

I'm using ffmpeg to get info about a video file and I don't want to save the info into a file. ffmpeg is returning all the video info, but it's returning as an error because I'm not specifying an o...

stackoverflow.com

 

SNS 공유하기
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.