AI を使用して映画の字幕を翻訳する

この記事では、Python を使用して ffmpeg と Gemini を呼び出して映画の字幕を翻訳する方法を紹介します。エフェクトは「エフェクト表示」セクションで確認できます。

背景

少し前に前の会社を辞めて、また独立しましたが、今回はあまり考えずに、ゆっくりと自分が楽しいと思える仕事に取り組むことにしました。 NAS を購入し、仕事での IT スキルがついに生活に活かされたことがわかりました。最初は映画の中国語字幕についてでした。

NAS を入手するための最初のステップは、4K 映画を必死にダウンロードし始めることです。これらの映画にはすべて字幕が付いていますが、一部の映画には中国語の字幕がないか、うまく翻訳されていません。さらに、購入した NAS ソフトウェアは完全に機能しておらず、中国語字幕をダウンロードするのは面倒なので、自動化された解決策があればと思っています。評価後は、ChatGPT や Gemini などの現在の AI を使用して英語字幕を翻訳できると思います。良い結果が得られるはずです。

Poetry を使用してプロジェクトを管理する

ここ数年はあまり Python プロジェクトを行っていませんでしたが、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。これは、ほとんどの動画の字幕タグが比較的標準化されているためですが、字幕にまったくタグがない動画も実際にあるため、推測するしかありません。他の状況は、実際の状況に基づいてのみ対処できるものです。

英語字幕処理

最初は、字幕を Gemini に投げて結果を保存するだけで十分だと思っていましたが、実際には機能しませんでした。次のような問題がありました。

  1. 多くの英語字幕には多くのタグがあり、翻訳時の効果に影響します
  2. サブタイトルが大きすぎるとジェミニは処理できず、コンテキストが長すぎると問題が発生する可能性があります。
  3. 字幕のタイムスタンプが長すぎるため、プロンプトが長すぎます。

このため、上記の問題に対処するために、字幕クラス 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映画の字幕には特にデリケートな言葉が頻繁に登場することが非常に重要であり、双子座はそれを可能な限り許容するように知らされる必要があります。ドキュメントによると、有料アカウントでのみ実行可能ですがBLOCK_NONE、映画を翻訳するときに上記の設定ではあまり問題が発生しなかったようですが、時々問題が発生しましたが、再試行すると問題はなくなりました。

その後、3 回の再試行が追加され、再試行によって問題が解決される場合があります。

最後に、API キーは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 メソッドを実行して翻訳を完了します。プロンプト ワード テンプレートの prompt_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
哦,阿宝在哪儿?

結果は驚くほど良好で、prmopt はそれ以上のコンテキストを提供しませんでしたが、Gemini は本物の翻訳を提供してくれました。

要約する

映画の場合、上記のコードは比較的安定して実行されます。しかし、字幕自体があまり良くない場合、翻訳結果もあまり良くなく、異常も多く、多くの改善が必要です。最近、私のビデオ アカウント (Yunyunzhongshengs) がいくつかの技術ビデオを共有しました。これらは改良されたコードで実装されています。後で共有します。

テクノロジーを利用して生活を変えることができるのは素晴らしいことだと思います。誰もがもっと注意を払い、コミュニケーションを取れるようになれば幸いです。

この記事はYunyunzhongsheng ( https://yylives.cc/ ) で最初に公開されたもので、どなたでもご覧いただけます。

私はオープンソース紅蒙を諦めることにしました 、オープンソース紅蒙の父である王成露氏:オープンソース紅蒙は 中国の基本ソフトウェア分野における唯一の建築革新産業ソフトウェアイベントです - OGG 1.0がリリースされ、ファーウェイがすべてのソースコードを提供します。 Google Readerが「コードクソ山」に殺される Ubuntu 24.04 LTSが正式リリース Fedora Linux 40の正式リリースを前に、 Microsoft開発者ら:Windows 11のパフォーマンスは「ばかばかしいほど悪い」、 馬化騰氏と周宏毅氏が握手し「恨みを晴らす」 有名ゲーム会社が新たな規定を発行:従業員の結婚祝いは10万元を超えてはならない 拼多多は不正競争で有罪判決 賠償金500万元
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/6919515/blog/11054239