Home

Gabriel's Blog

<맨 위로>
09 Feb 2022   | #애니 #분석

본격 애니메이션 감상을 위한 여정(2)

개요

지난번에 이어 계속 작업해 보자.

SubViewer에서 smi 파일을 정상적으로 불러올 수 없는 이슈를 해결해야 한다.

smi -> srt 변환 사이트를 통해 변환 과정을 거친 뒤 다시 실행해 봤으나 여러 가지 문제가 발생.

직접 만들어 보자.


smi와 srt의 구조

다음은 smi포맷의 일반적인 구조다.

<SAMI>

<HEAD>
<Title>자막 제목</Title>
<STYLE TYPE="text/css">
<!--
P {margin-left:8pt; margin-right:8pt; margin-bottom:2pt; margin-top:2pt;
  text-align:center; font-size:20pt; font-family:arial, sans-serif;
  font-weight:normal; color:White;}
  .KRCC {Name:한국어; lang:ko-KR; SAMIType:CC;}
  .ENUSCC { name: English; lang: en-US ; SAMIType: CC ; }
-->
</STYLE>
</HEAD>

<BODY>

<SYNC Start=0>
<P Class=KRCC>한국어 자막</P>
<P Class=ENUSCC>English Subtitle</P>

<SYNC Start=1000>
<P Class=KRCC>&nbsp;</P>
<P Class=ENUSCC>&nbsp;</P>

<SYNC Start=2000>
<P Class=KRCC><font color="#79F3AE">자막 색상</font></P>
<P Class=ENUSCC><font color="#79F3AE">Font Color</font></P>

</BODY>
</SAMI>

중요한 부분은 <BODY> 내부에 있다.

<SYNC>태그로 자막의 시작 지점을 설정,

<P>태그의 Class 속성으로 자막 언어를 설정,

그 외 다른 HTML 태그로 텍스트를 꾸미고 있다.

자막이 끝나는 시점을 설정할 수 없는데, 자막을 지우려면 공백 문자 &nbsp;를 추가하면 된다.

다음은 srt포맷의 일반적인 구조다.

1
00:00:10,500 → 00:00:13,000
Elephant's Dream

2
00:00:15,000 → 00:00:18,000
At the left we can see...

3
...

번호 + 시작 시각 + 끝 시각 + 내용 + 공백

으로 한 문장의 자막이 구성된다. 자막이 끝나는 지점을 설정할 수 있다.


태그 분석

사용되는 태그 네임을 추출해서 어떤 종류의 태그가 사용되는지 알아볼 것이다.

늘 그렇듯 파이썬을 사용한다.

smi 파일 불러오기

with open('caption.smi', 'r') as f:
    text_list = f.readlines()
text = ''.join(text_list)

파싱하기 편하도록 모든 줄바꿈을 제거한다. 어차피 자막의 줄바꿈은 <br>태그를 통해서만 이루어진다.

정규 표현식 모듈 불러오기

import re

태그 추출 표현식 작성

p = re.compile('<.*?>')
res = p.findall(text)
unique = list(set(res))  # 중복 요소 제거
print(unique)
#------
['<Sync Start=1178016>', '<Sync Start=1287900>', '<Sync Start=1206878>', '<Sync Start=501961>', '<Sync Start=1267976>', '<Sync Start=587713>', '<TITLE>', '<Sync Start=59019>', '<Sync Start=479785>', '<Sync Start=957630>', '<Sync Start=1081757>', '<Sync Start=1110300>', '<Sync Start=508177>', ... ]

태그 가공

  # 태그 패턴에서 네임만 추출하는 표현식
tag_p = re.compile('<(\s*)(?P<tag>[^>\s]*)(\s+[^>]*)?(\s*)>')
tag_names = []
for i in unique:
    try:
        tag = tag_p.match(i).group('tag')
        if not tag in tag_names:
            tag_names.append(tag)
    except:
        print('정규식이 맞지 않음')
        print(i)
for i in tag_names:
    print(i)
#------
Sync
TITLE
/BODY
STYLE
/FONT
/font
rt
font
/b
/STYLE
/SAMI
SAMI
/rb
/HEAD
br
/ruby
p
b
P
/TITLE
rb
BR
BODY
/rt
HEAD
ruby

여기서 중요한 것만 정리하면 다음과 같다.

Sync
p, P

br, BR

font, /font, /FONT

b, /b

ruby, /ruby
rb, /rb
rt, /rt

대소문자가 제각각인 점에 유의하자.

텍스트를 꾸미는 태그 font, b, rubysrt 포맷에서도 어느 정도 지원하므로 그대로 사용하면 될 것 같고, <br>태그는 \n으로 치환하는 과정이 필요해 보인다.

분석은 이 정도로 하자.


이제 만들어 보자

필자가 소유한 내여귀 자막파일을 기준으로 한 변환과정이므로, 처리 로직이 범용적이지 않음에 주의하자.

1차 가공

<body> 내부에 있는 핵심 텍스트만 추출하는 과정이다.

valid_text = text[text.find('<Sync', 0):text.find('</BODY>', 0)]
valid_text+='<Sync'

마지막에 더미 텍스트를 넣으면 끝 예외 처리를 따로 할 필요가 없다.

2차 가공

전체 자막을 동시에 출력되는 것끼리 각각 나눈다. <Sync>태그를 기준으로 나누면 된다.

sub = []
pos = valid_text.find('<Sync', 0)
while True:
    pos_prev = pos
    pos = valid_text.find('<Sync', pos_prev+1)
    if pos==-1:
        break
    sub.append(valid_text[pos_prev:pos])
print(sub)
#------예시 출력
['<Sync Start=6000><P Class=KRCC>\n"이럴 수가아--!!"\n',
 '<Sync Start=8545><P Class=KRCC>\n&nbsp;\n',
 '<Sync Start=8700><P Class=KRCC>\n까는 스레드가 세워졌잖아!?\n',
 '<Sync Start=10544><P Class=KRCC>\n너무해!!\n',
 '<Sync Start=11583><P Class=KRCC>\n&nbsp;\n',
 '<Sync Start=11950><P Class=KRCC>\n시끄럽네\n',
 '<Sync Start=13750><P Class=KRCC>\n인터넷 찾아보고선<BR>\n발광하지 좀 말아주겠니?\n',
 '<Sync Start=16564><P Class=KRCC>\n어떻게!?<BR>\n무슨 수로!?\n',
 '<Sync Start=17850><P Class=KRCC>\n우리가 만든 게임이<BR>\n비판당하고 있는데!?\n',
 ...
]

3차 가공

  • <Sync>태그와 <P>태그에서 필요한 정보를 추출
  • 텍스트의 필요없는 여백을 제거
  • <br>태그를 공백으로 치환
  • 하나의 자막 세트 중간에 아무 글자가 없는 라인이 있을 경우 공백 추가 (\n\n같은 게 있으면 오류가 난다. 이런 형태의 줄은 각 자막 세트를 구분하기 위해서만 사용된다)

마지막이 무슨 의미냐면

1
00:00:10,500  00:00:13,000
  // 이런   된다는 뜻!
안녕

2
...
1
00:00:10,500  00:00:13,000
(공백)  // 아무튼 뭐라도  줘야 한다
안녕

2
...

코드를 보자.

# Sync 태그를 파싱하는 표현식
p_sync = re.compile('<(Sync|sync) Start=(?P<time>[\d]*)>')
# P 태그를 파싱하는 표현식
p_p = re.compile('<(P|p) Class=(?P<lang>[\w]*)>')
# 3차 가공 결과를 저장하는 리스트
data = []

for i in sub:
    tmp=p_sync.search(i)
    time = tmp.group('time')
    i=i[:tmp.start()] + i[tmp.end():]  # Sync 태그를 원문에서 제거
    
    tmp=p_p.search(i)
    lang = tmp.group('lang')
    i=i[:tmp.start()] + i[tmp.end():]  # P 태그를 원문에서 제거
    
    i=i.strip()  # 필요없는 공백 제거
    
    # <br>태그 치환
    i=i.replace('<br>\n', '\n')
    i=i.replace('\n<br>', '\n')
    i=i.replace('<BR>\n', '\n')
    i=i.replace('\n<BR>', '\n')
    
    # 아무것도 없는 줄에 공백 추가
    i=i.replace('\n\n', '\n \n')
    i=i.replace('\n\n', '\n \n')
    if i[0]=='\n':
        i = ' ' + i
    
    data.append((int(time), lang, i))

# 더미 데이터 추가
data.append((data[-1][0]+2000, 'KRCC', 'Dummy'))
print(data)
#------예시 출력
[(6000, 'KRCC', '"이럴 수가아--!!"'),
 (8545, 'KRCC', '&nbsp;'),
 (8700, 'KRCC', '까는 스레드가 세워졌잖아!?'),
 (10544, 'KRCC', '너무해!!'),
 (11583, 'KRCC', '&nbsp;'),
 (11950, 'KRCC', '시끄럽네'),
 (13750, 'KRCC', '인터넷 찾아보고선\n발광하지 좀 말아주겠니?'),
 (16564, 'KRCC', '어떻게!?\n무슨 수로!?'),
 (17850, 'KRCC', '우리가 만든 게임이\n비판당하고 있는데!?'),
 (20750, 'KRCC', '까이고 있는 건\n대부분 내 시나리오잖아'),
 (24550, 'KRCC', '어째서 네가 신경쓰는 거야?'),
 (26659, 'KRCC', '&nbsp;'),
 (26900, 'KRCC', '하고 싶은 걸 한 결과,\n받아들여 주질 않았다...'),
 (31250, 'KRCC', '책임은 내게 있어'),
 ...
]

마찬가지로 마지막에 더미 데이터를 추가했다.

4차 가공

smi 포맷에 대응하는 최종 문자열로 변환하는 과정이다.

# 시간 형태를 ms에서 HH:MM:SS:ms로 변환한다.
def tfc(time):
    s, ms = divmod(time, 1000)
    m, s = divmod(s, 60)
    h, m = divmod(m, 60)
    return f'{h:02}:{m:02}:{s:02},{ms:03}'
# srt 포맷에 대은하는 문자열이 각 줄별로 저장되는 리스트
srt = []

for i in range(len(data)-1):
    # `&nbsp;`는 무시
    if data[i][2] == '&nbsp;':
        continue
    srt.append(str(i))
    srt.append(tfc(data[i][0]) + '-->' + tfc(data[i+1][0]))
    srt.append(data[i][2])
    srt.append('')

# 모든 문장의 끝에 줄바꿈 기호를 추가
srt = [i+'\n' for i in srt]

저장

with open('caption.srt', 'w') as f:
    f.writelines(srt)

결과

0
00:00:06,000-->00:00:08,545
"이럴 수가아--!!"

2
00:00:08,700-->00:00:10,544
까는 스레드가 세워졌잖아!?

3
00:00:10,544-->00:00:11,583
너무해!!

5
00:00:11,950-->00:00:13,750
시끄럽네

6
00:00:13,750-->00:00:16,564
인터넷 찾아보고선
발광하지 좀 말아주겠니?

7
00:00:16,564-->00:00:17,850
어떻게!?
무슨 수로!?

8
00:00:17,850-->00:00:20,750
우리가 만든 게임이
비판당하고 있는데!?

잘 변환 된다.

이제 PrimeVideo 스트리밍에 자막을 띄워 보자.

스틸컷

아마존 프라임 비디오 - 내 여동생이 이렇게 귀여울 리가 없어! 15화 중

마침내 원하던 화면을 얻었다. 실로 감동적인 장면이 아닐 수 없다.

감동

사족으로, 인용을 위한 스틸컷을 저작권법에 저촉되지 않는다고 한다. 대신 출처를 밝혀야 한다나.


결론

국내 판권이 없어도 어떻게든 볼 방법은 있다. 단지 많은 수고가 필요할 뿐이다.

과연 자기 돈을 들여가면서 이렇게까지 노력할 사람이 얼마나 될지는 모르겠지만.

시간 때우기로 적당한 프로젝트였다.

제대로 된 결말을 봤으니 이제 2기를 보러 가야겠다.

이만.

그럼 다음에,
Gabriel-Dropout at 16:59

scribble