개요
지난번에 이어 계속 작업해 보자.
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> </P>
<P Class=ENUSCC> </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 태그로 텍스트를 꾸미고 있다.
자막이 끝나는 시점을 설정할 수 없는데, 자막을 지우려면 공백 문자
를 추가하면 된다.
다음은 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
, ruby
는 srt
포맷에서도 어느 정도 지원하므로 그대로 사용하면 될 것 같고, <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 \n',
'<Sync Start=8700><P Class=KRCC>\n까는 스레드가 세워졌잖아!?\n',
'<Sync Start=10544><P Class=KRCC>\n너무해!!\n',
'<Sync Start=11583><P Class=KRCC>\n \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', ' '),
(8700, 'KRCC', '까는 스레드가 세워졌잖아!?'),
(10544, 'KRCC', '너무해!!'),
(11583, 'KRCC', ' '),
(11950, 'KRCC', '시끄럽네'),
(13750, 'KRCC', '인터넷 찾아보고선\n발광하지 좀 말아주겠니?'),
(16564, 'KRCC', '어떻게!?\n무슨 수로!?'),
(17850, 'KRCC', '우리가 만든 게임이\n비판당하고 있는데!?'),
(20750, 'KRCC', '까이고 있는 건\n대부분 내 시나리오잖아'),
(24550, 'KRCC', '어째서 네가 신경쓰는 거야?'),
(26659, 'KRCC', ' '),
(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):
# ` `는 무시
if data[i][2] == ' ':
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
