이 기사에서는 Python을 사용하여 ffmpeg 및 Gemini를 호출하여 영화 자막을 번역하는 방법을 소개합니다. 효과는 "효과 표시" 섹션에서 확인할 수 있습니다.
배경
얼마 전 전 다니던 회사를 그만두고 다시 독립하게 됐어요. 별다른 생각 없이 거의 자연스럽게 이직을 하게 됐어요. 이번에는 신중하게 생각하고 마음 편히 일할 수 있는 일을 하기로 했어요. NAS를 구입하고 직장에서 배운 IT기술이 드디어 생활에 쓰이게 되었는데, 그 첫 번째가 영화 중국어 자막에 관한 내용이었습니다.
NAS를 구입하는 첫 번째 단계는 4K 영화를 미친듯이 다운로드하는 것입니다. 이 영화는 모두 자막이 제공되지만 일부는 중국어 자막이 없거나 잘 번역되지 않습니다. 게다가 제가 구입한 NAS 소프트웨어가 제대로 작동하지 않고 중국어 자막을 다운로드하는 것도 번거로워서 자동화된 솔루션이 있었으면 좋겠습니다. 평가 결과 ChatGPT, Gemini 등 현재 AI를 활용해 영어 자막을 번역할 수 있다면 좋은 결과가 있을 것으로 생각됩니다.
Poetry를 사용하여 프로젝트 관리
지난 몇 년간 파이썬 프로젝트를 많이 해본 적이 없는데 시를 활용한 프로젝트를 본 적이 있어서 이번 프로젝트에서는 사용하기로 했습니다. 평가판 경험은 매우 좋으며 이전에 사용했던 Pipenv보다 훨씬 좋습니다.
내 pyproject.toml 파일의 내용은 다음과 같습니다.
[tool.poetry]
name = "upbox"
version = "0.1.0"
description = ""
authors = ["rocksun <[email protected]>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
ffmpeg-python = "^0.2.0"
llama-index = "^0.10.25"
llama-index-llms-gemini = "^0.1.6"
pysubs2 = "^1.6.1"
# yt-dlp = "^2024.4.9"
# typer = "^0.12.3"
# faster-whisper = "^1.0.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
여기서는 시의 사용에 대해 자세히 설명하지 않겠습니다. 스스로 배울 수 있습니다. ffmpeg의 패키징 라이브러리가 여기에 인용되어 있습니다(경로에 ffmpeg 명령이 필요함). 실제로 llama-index를 사용하는 것과 사용하지 않는 것 사이에는 큰 차이가 없습니다. llama-index의 기능을 너무 많이 사용하지 마세요. 마지막으로 자막 처리 라이브러리인 pysubs2입니다. 한때 자막을 직접 구문 분석할지 고민했지만 나중에 pysubs2를 사용하면 여전히 많은 시간을 절약할 수 있다는 것을 알았습니다.
영어 자막 추출
ffmpeg를 통해 비디오에 포함된 자막을 추출하는 것은 쉽습니다. 다음 명령을 실행하면 됩니다.
ffmpeg -i my_file.mkv outfile.vtt
하지만 실제로는 동영상에 자막이 여러 개 있어서 정확하지 않으므로 확인이 필요합니다. 나는 여전히 ffmpeg 라이브러리, 즉 ffmpeg-python 사용을 고려하고 있습니다 . 이 라이브러리를 사용하여 영어 자막을 추출하는 코드는 다음과 같습니다.
def _guess_eng_subtitle_index(video_path):
probe = ffmpeg.probe(video_path)
streams = probe['streams']
for index, stream in enumerate(streams):
if stream.get('codec_type') == 'subtitle' and stream.get('tags', {}).get('language') == 'eng':
return index
for index, stream in enumerate(streams):
if stream['codec_type'] == 'subtitle' and stream.get('tags', {}).get('title', "").lower().find("english")!=-1 :
return index
return -1
def _extract_subtitle_by_index(video_path, output_path, index):
return ffmpeg.input(video_path).output(output_path, map='0:'+str(index)).run()
def extract_subtitle(video_path, en_subtitle_path):
# get the streams from video with ffprobe
index = _guess_eng_subtitle_index(video_path)
if index == -1:
return -1
return _extract_subtitle_by_index(video_path, en_subtitle_path, index)
영어 자막의 색인을 판별하는 방법이 추가되었습니다 _guess_eng_subtitle_index
. 왜냐하면 대부분의 동영상의 자막 태그가 상대적으로 표준화되어 있음에도 불구하고 자막에 태그가 전혀 없는 동영상도 있기 때문에 추측만 할 수 있기 때문입니다. 여전히 일부 상황은 실제 상황에 따라 처리될 수 있습니다.
영어 자막 처리
처음에는 제미니에 자막을 던지고 결과를 저장하는 것만으로도 충분하다고 생각했는데, 실제로는 몇 가지 문제가 있었습니다.
- 많은 영어 자막에는 태그가 많아 번역 시 효과에 영향을 미칩니다.
- 자막이 너무 크면 제미니가 감당할 수 없고, 내용이 너무 길면 문제가 발생할 수 있습니다.
- 자막의 타임스탬프가 너무 길어 프롬프트가 너무 길어집니다.
이러한 이유로 위의 문제를 해결하기 위해 자막 클래스 UpSubs를 추가해야 했습니다.
class UpSubs:
def __init__(self, subs_path):
self.subs = pysubs2.load(subs_path)
def get_subtitle_text(self):
text = ""
for sub in self.subs:
text += sub.text + "\n\n"
return text
def get_subtitle_text_with_index(self):
text = ""
for i, sub in enumerate(self.subs):
text += "chunk-"+str(i) + ":\n" + sub.text.replace("\\N", " ") + "\n\n"
return text
def save(self, output_path):
self.subs.save(output_path)
def clean(self):
indexes = []
for i, sub in enumerate(self.subs):
# remove xml tag and line change in sub text
sub.text = re.sub(r"<[^>]+>", "", sub.text)
sub.text = sub.text.replace("\\N", " ")
def fill(self, text):
text = text.strip()
pattern = r"\n\s*\n"
paragraphs = re.split(pattern, text)
for para in paragraphs:
try:
firtline = para.split("\n")[0]
countstr = firtline[6:len(firtline)-1]
# print(countstr)
index = int(countstr)
p = "\n".join(para.split("\n")[1:])
self.subs[index].text = p
except Exception as e:
print(f"Error merge paragraph : \n {para} \n with exception: \n {e}")
raise(e)
def merge_dual(self, subspath):
second_subs = pysubs2.load(subspath)
merged_subs = SSAFile()
if len(self.subs.events) == len(second_subs.events):
for i, first_event in enumerate(self.subs.events):
second_event = second_subs[i]
if first_event.text == second_event.text:
merged_event = SSAEvent(first_event.start, first_event.end, first_event.text)
else:
merged_event = SSAEvent(first_event.start, first_event.end, first_event.text + '\n' + second_event.text)
merged_subs.append(merged_event)
return merged_subs
return None
clean
이 방법은 단순히 자막을 정리할 수 있으며, save 방법은 자막을 저장하는 데 사용할 수 있으며 merge_dual
이중 언어 자막을 병합하는 데 사용할 수 있습니다. 이는 상대적으로 간단하며 나중에 자막 텍스트 처리에 중점을 둘 것입니다.
원본 srt 파일 형식은 다음과 같습니다.
12
00:02:30,776 --> 00:02:34,780
Not even the great Dragon Warrior.
13
00:02:43,830 --> 00:02:45,749
Oh, where is Po?
14
00:02:45,749 --> 00:02:48,502
He was supposed to be here hours ago.
방법은 다음과 같습니다 get_subtitle_text_with_index
.
chunk-12
Not even the great Dragon Warrior.
chunk-13
Oh, where is Po?
chunk-14
He was supposed to be here hours ago.
이는 단어와 청크의 수를 줄이기 위해 수행됩니다. 또한 이 방법을 통해 각 자막의 수를 추적할 수 있으며 fill
번역된 텍스트에서 자막을 복원할 수 있습니다.
쌍둥이자리에게 전화하기
Gemini에 전화하는 데는 몇 가지 문제가 있습니다.
- 액세스 키가 필요합니다
- 국내 방문에는 적합한 대리인이 필요합니다.
- 특정 내결함성이 있어야 함
- Gemini의 보안 메커니즘을 우회할 필요도 있습니다.
따라서 complete
이러한 문제를 해결하기 위해 특별한 방법이 작성되었습니다.
def complete(prompt, max_tokens=32760):
prompt = prompt.strip()
if not prompt:
return ""
safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE"
},
]
retries = 3
for _ in range(retries):
try:
return Gemini(max_tokens=max_tokens, safety_settings=safety_settings, temperature = 0.01).complete(prompt).text
except Exception as e:
print(f"Error completing prompt: {prompt} \n with error: \n ")
traceback.print_exc()
return ""
safety_settings
특히 민감한 언어가 영화 자막에 자주 등장하는 것이 매우 중요하며, Gemini는 이를 최대한 용인하도록 알려야 합니다. 문서에 따르면 유료 계정만 할 수 있지만 BLOCK_NONE
영화를 번역할 때 가끔 문제가 발생했지만 다시 시도하면 사라지는 문제가 위 구성에서 많이 발생하지 않은 것 같습니다.
그런 다음 3번의 재시도가 추가됩니다. 재시도하면 일부 문제가 해결될 수 있습니다.
마지막으로 API Key는 Google AI Studio를 통해 얻을 수 있습니다. 그런 다음 프로젝트에 .env 파일을 추가합니다.
http_proxy=http://192.168.0.107:7890
https_proxy=http://192.168.0.107:7890
GOOGLE_API_KEY=[your-api-key]
프로그램은 API 키와 프록시 설정을 읽을 수 있습니다.
호출 프로세스
tran_subtitles
가장 바깥쪽 메소드를 먼저 살펴보자
def tran_subtitles(fixed_subtitle, zh_subtitle=None, cncf = False, chunk_size=3000):
subtitle_base = os.path.splitext(fixed_subtitle)[0]
video_base = os.path.splitext(subtitle_base)[0]
if zh_subtitle is None:
zh_subtitle = video_base + ".zh-fixed.vtt"
if os.path.exists(zh_subtitle):
print(f"zh subtitle {zh_subtitle} already translated, skip to translate.")
return 1
prompt_tpl = MOVIE_TRAN_PROMPT_TPL
opts = { }
srtp = UpSubs(fixed_subtitle)
text = srtp.get_subtitle_text_with_index()
process_text(srtp, text, prompt_tpl, opts, chunk_size = chunk_size)
srtp.save(zh_subtitle)
return zh_subtitle
로직은 비교적 간단합니다. 영어 자막을 읽고 get_subtitle_text_with_index
메소드를 사용하여 번역할 텍스트로 변환한 다음 process_text 메소드를 실행하여 번역을 완료합니다. 프롬프트 단어 템플릿 프롬프트_tpl은 다음을 포함하는 MOVIE_TRAN_PROMPT_TPL을 직접 참조합니다.
MOVIE_TRAN_PROMPT_TPL = """你是个专业电影字幕翻译,你需要将一份英文字幕翻译成中文。
[需要翻译的英文字幕]:
{content}
# [中文字幕]:"""
이 프롬프트는 매우 간단하다는 것을 알 수 있습니다.
그런 다음 다음 방법에 주의할 수 있습니다 process_text
.
def process_text(subs, text, prompt_tpl, opts, chunk_size=2500):
# ret = ""
chunks = _split_subtitles(text, chunk_size)
for(i, chunk) in enumerate(chunks):
print("process chunk ({}/{})".format(i+1,len(chunks)))
# if i==4:
# break
# format string with all the field in a dict
opts["content"] = chunk
prompt = prompt_tpl.format(**opts)
print(prompt)
out = complete(prompt, max_tokens=32760)
subs.fill(out)
print(out)
메소드를 통해 자막 텍스트를 여러 덩어리로 분할한 _split_subtitles
후 위에서 언급한 메소드에 던집니다 complete
.
결과 보여줘
처음에는 자막 번역에 대한 기대가 별로 없었는데, 최종 효과가 의외로 좋았습니다. 쿵푸팬더 4를 예로 들면 다음과 같습니다.
영어 자막:
10
00:02:22,184 --> 00:02:27,606
Let it be known from the highest mountain
to the lowest valley that Tai Lung lives,
11
00:02:27,606 --> 00:02:30,776
and no one will stand in his way.
12
00:02:30,776 --> 00:02:34,780
Not even the great Dragon Warrior.
13
00:02:43,830 --> 00:02:45,749
Oh, where is Po?
중국어 자막:
10
00:02:22,184 --> 00:02:27,606
让最高的山峰和最低的山谷都知道,泰隆还活着,
11
00:02:27,606 --> 00:02:30,776
没人能阻挡他。
12
00:02:30,776 --> 00:02:34,780
即使是伟大的神龙大侠也不行。
13
00:02:43,830 --> 00:02:45,749
哦,阿宝在哪儿?
결과는 놀라울 정도로 좋았습니다. 내 서문은 더 많은 맥락을 제공하지 않았지만 Gemini는 확실한 번역을 제공했습니다.
요약하다
영화의 경우 위 코드는 비교적 안정적으로 실행됩니다. 하지만 일부 자막 자체가 별로 좋지 않은 경우에는 번역 결과도 별로 좋지 않고, 변칙적인 부분도 많아 개선이 많이 필요합니다. 최근 내 비디오 계정(Yunyunzhongshengs)에서 개선된 코드로 구현된 일부 기술 비디오를 공유했는데 나중에 공유하겠습니다.
기술을 사용하여 삶을 변화시킬 수 있다는 것은 멋진 일이라고 생각합니다. 더 많은 발전의 기회가 있었으면 좋겠습니다. 모두가 더 많은 관심을 갖고 소통할 수 있기를 바랍니다.
오픈 소스 Hongmeng을 포기하기로 결정했습니다 . 오픈 소스 Hongmeng의 아버지 Wang Chenglu: 오픈 소스 Hongmeng은 중국에서 유일하게 기초 소프트웨어 분야의 건축 혁신 산업 소프트웨어 행사입니다. OGG 1.0이 출시되고 Huawei는 모든 소스 코드를 제공합니다. Google 리더가 "코드 똥 산"에 의해 사망했습니다 Ubuntu 24.04 LTS 공식 출시 Fedora Linux 40 공식 출시 전에 Microsoft 개발자 : Windows 11 성능이 "어리석을 정도로 나쁩니다", Ma Huateng과 Zhou Hongyi가 악수하며 "원한을 제거합니다" 유명 게임 회사가 새로운 규정을 발표했습니다. 직원의 결혼 선물은 10만 위안을 초과할 수 없습니다. 핀둬둬는 부정 경쟁 혐의로 판결을 받았습니다. 보상금은 500만 위안입니다.이 기사는 Yunyunzhongsheng ( https://yylives.cc/ ) 에 처음 게재되었습니다 . 누구나 방문하실 수 있습니다.