ゲームの海外ローカライズにおける生成AI活用事例

スポンサーリンク

chatGPTなど使ったことあればそれなりの翻訳が瞬時に行えることは周知の通り。

ただ、ゲームにおいてはキャラのエネルギーがドラクエならMP、ナルトはチャクラになってないと世界観を壊してしまうのでかなりセンシティブ。

一人称一つとっても悟空なら「オラ」以外あり得ない。(たまに「オレ」というときにも意味があるし)

そういった、「以下の文章を翻訳して。XXXXX」という適当なプロンプトでの翻訳じゃだめわけだ。

結果的にやはりシナリオの世界観を残したままの翻訳にはやはり劣るが、一定世界観を考慮した翻訳を実装した例を紹介。実務的には初稿の初稿(ver0.1相当)としてのレベルにはあるということで既存の海外ローカライズ時の翻訳工数を40%程度削減できた。

この実装をいくらか紹介。

スポンサーリンク

処理フロー概要

  • ファイルアップロードと準備
    •  ユーザーからアップロードされたExcelファイルを読み込み、データフレームに変換します。
    • 翻訳対象の列を設定し、用語や占位文字(プレースホルダー)の解析
  • 用語集の初期化
    翻訳プロセスの前に既存の用語集を読み込み、新しい用語があれば追加して更新します。
  • 翻訳プロセス
    • 各テキストを並行処理で翻訳し、用語集を参考にしながら翻訳結果を生成します。
      初期翻訳結果をレビューし、必要に応じて改善を行います。
  • 結果の生成
    • 翻訳結果と更新された用語集を出力します。

コード詳細

処理ステップ概要は以下のような感じ。
特徴は用語集になっている。

触ってみる中で気づいたのは固有名詞翻訳の難しさ。都度翻訳時に同じ語がバラバラの翻訳にならないようにして全体で統一感を出すのが最も難しい。

前提

用語集がなければフローに則って全文見ながらやっていくが、企画書やメディアミックスの場合は他メディアの情報などを事前情報としてシステムプロンプトに入れることを推奨。

実装

# ライブラリimport ioimport pandas as pdfrom tqdm import tqdmfrom typing import Generatorfrom fastapi.datastructures import UploadFile

大枠

uploadされたエクセルに原文があるとして大枠は以下のようになっている。
後続で個別の関数について紹介。

引数については、
grossary_file:用語集(任意だが、ナルトにおける「チャクラ」のように外したくないものはここで指定しておきたい)
source_language, target_language:翻訳元言語、翻訳対象言語だが、ここはpromptに入れ込むのでわかるようになっていれば良い。(EN, JP, CN-ZHとか日本語、英語とかどんな書き方でも大丈夫)
keys:翻訳の単位。(実際のゲームテキストだと利用箇所やシナリオ上位のシーンの区切りごとに翻訳したほうが良いのでそれを特定するための候補キーを指定)
source_col:翻訳対象テキスト(原文)

def process_xlsx(    xlsx_file: UploadFile,    glossary_file: UploadFile | None,    sheet_name: str,    source_language: str,    target_language: str,    keys: list[str],    source_col: str,) -> Generator[dict, None, tuple[pd.DataFrame, pd.DataFrame]]:    """    主な処理のエントリーポイント。    アップロードされたExcelファイルと用語集を処理し、翻訳を実行します。    """    # ファイルの読み込みと準備    df = read_and_prepare_file(xlsx_file, sheet_name, keys, source_col, target_language)    # 用語や占位文字の解析    parse_texts_in_dataframe(df, source_language, agent_classes)    # 用語集の読み込み    df_glossary = read_glossary(glossary_file, source_language, target_language)    # 用語集の初期化    if target_language not in df_glossary.columns:        df_glossary = init_glossary(df, df_glossary, source_language, target_language)        yield {            "message": "[初始化词汇表完成]",            "df_translated": None,            "df_glossary": df_glossary.to_dict(),            "is_last": False,        }    # 翻訳と結果生成    for result in process_translations(df, df_glossary, source_language, target_language):        yield result    return df, df_glossary

Excelファイルを読み込み、データフレームに変換する処理

def read_and_prepare_file(    xlsx_file: UploadFile,    sheet_name: str,    keys: list[str],    source_col: str,    target_language: str,) -> pd.DataFrame:    """Excelファイルを読み込み、データフレームを準備する"""    origin_contents = xlsx_file.file.read()    buffer = io.BytesIO(origin_contents)    df = pd.read_excel(buffer, sheet_name=sheet_name).dropna(subset=[source_col])    df = df.set_index(keys)    df[target_language] = ""  # 初期化    return df

用語集やプレースホルダーの解析

def parse_texts_in_dataframe(df: pd.DataFrame, source_language: str, agent_classes: dict):    """データフレーム内の各テキストを解析する"""    df["split_words"] = ""    df["is_word"] = False    df["has_placeholder"] = False    for i in tqdm(df.index):        df.at[i, "split_words"] = word_split(            source_language,            source_text=df.loc[i, source_col],            agent_classes=agent_classes,        )        parse_res = parse_text(            source_language=source_language,            source_text=df.loc[i, source_col],            agent_classes=agent_classes,        )        df.at[i, "is_word"] = parse_res["is_word"]        df.at[i, "has_placeholder"] = parse_res["has_placeholder"]

各テキストを解析して単語の分解やプレースホルダー有無を確認している。

ゲーム翻訳では、「キャラクターの一人称」や「特定の用語」だけでなく、「占位文字」(プレースホルダー)も非常に重要な要素です。これらを適切に解析し、翻訳プロセスに組み込むことで、翻訳品質を高めることができます。

処理の具体的な流れ

  • 正規表現パターンの定義:
    • placeholder_pattern = re.compile(r'{\w+}’):占位文字を検出するための正規表現パターンを定義します。この例では、{}で囲まれた単語にマッチします。
  • テキストの解析:
    • text = df.loc[i, source_col]:対象のテキストを取得します。
      split_words = text.split():テキストを空白で分割し、単語リストを作成します。
      has_placeholder = bool(placeholder_pattern.search(text)):テキストに占位文字が含まれているかをチェックします。
  • 解析結果の格納:
    • df.at[i, “split_words”] = split_words:分割された単語リストをデータフレームに格納します。
    • df.at[i, “is_word”] = len(split_words) == 1:単語が一つだけかどうかをチェックし、その結果をデータフレームに格納します。
    • df.at[i, “has_placeholder”] = has_placeholder:占位文字が含まれているかどうかの結果をデータフレームに格納します。

以上の処理を行うことで、各テキストが占位文字を含んでいるかどうか、どのように分割されるかを解析し、それらをデータフレームに格納します。この解析結果は、後の翻訳プロセスで非常に役立ちます。占位文字を考慮した翻訳が行われ、テキストの整合性が保たれます。

占位文字(プレースホルダー)とは?

占位文字(プレースホルダー)は、動的に変更される部分を指示するために使われる特別な記号や文字列のことです。例えば、以下のようなケースがよくあります:

{playerName}:プレイヤーの名前が動的に挿入される部分。
%d:整数が動的に挿入される部分(例:カウントやスコア)。
{item}:取得したアイテムの名称が挿入される部分。
こうした占位文字がある場合、それを適切に扱わないと、翻訳後のテキストに不整合が生じ、ユーザーが違和感を覚える原因になります。

占位文字の解析と処理の意義

占位文字の解析と処理の目的は以下の通りです:
占位文字の正確な翻訳:占位文字が含まれる文を適切に解析して、翻訳時に正しく保持することで、動的なコンテンツが正確に表示されるようにする。
テキストの整合性の維持:占位文字を適切に保持することで、文章全体の整合性を保つ。
誤訳の防止:占位文字を無視しないことで、ユーザーが混乱しないようにする。

用語集の初期化(uploadがなかった場合)

最初のprocess_excel()にgrossaryがなかった場合の初期化処理

def init_glossary(df: pd.DataFrame, df_glossary: pd.DataFrame, source_language: str, target_language: str) -> pd.DataFrame:    """辞書の初期化を行い、新しい用語を追加する"""    chunk = []    size = 0    for i, index in enumerate(df.index):        words: list = df.loc[index, "split_words"]        chunk.extend(words)        size += len(words)        if size > 200:            df_glossary = ai_glossary(                chunk,                df_glossary,                source_language,                target_language,                agent_classes,            )            chunk = []            size = 0    if size > 0:        df_glossary = ai_glossary(            chunk,            df_glossary,            source_language,            target_language,            agent_classes,        )    return df_glossary

翻訳プロセス

翻訳プロセスの目的は、与えられたテキストを指定された言語に翻訳し、その結果をデータフレームに追加していくことです。これにより、元のデータと翻訳されたデータを一目で比較できるようになります。

翻訳プロセスの主なステップは以下の通りです:

  • 各テキストを逐次処理
    • データフレーム内の各行を一つずつ取り出し、それを翻訳します。
  • 翻訳結果の追加
    • 翻訳結果を元のデータフレームの対応する列に追加します。
  • 途中経過の出力
    • 進行状況を把握するため、逐次結果を出力します。
  • 翻訳完了後の後処理
    • 全ての翻訳が完了した後、最終的な翻訳結果と更新された用語集を出力します。
def process_translations(    df: pd.DataFrame,    df_glossary: pd.DataFrame,    source_language: str,    target_language: str,    source_col: str) -> Generator[dict, None, None]:    """各テキストを翻訳し、結果をデータフレームに追加する処理"""        # tqdmは進行状況バーを表示するためのライブラリ    for i in tqdm(df.index):        # 翻訳関数を呼び出し、翻訳結果と更新された用語集を取得        translation, df_glossary = ai_translate(            i,            source_col,            df,            df_glossary,            source_language,            target_language,            agent_classes=agent_classes,        )        # 翻訳結果をデータフレームの該当する列に追加        df.loc[i, target_language] = translation                # 進行状況を出力(オプション)        yield {            "message": f"{df.loc[i, source_col]} -> {df.loc[i, target_language]}",            "df_translated": df.loc[i].to_dict(),            "df_glossary": None,            "is_last": False,        }    # 翻訳が全て完了した後の最終出力    yield {        "message": "[翻訳完成]",        "df_translated": df.drop(columns=["split_words", "is_word", "has_placeholder"]).to_dict(),        "df_glossary": df_glossary.to_dict(),        "is_last": True,    }

この翻訳プロセスは、効率的にテキストを翻訳し、進行状況と結果を把握できるように設計されています。特に以下の点に注意しています:

逐次処理:各テキストを一つずつ処理することで、進行状況を正確に追跡可能。
翻訳結果の管理:翻訳結果がデータフレームに追加されることで、元データとの比較が容易。
リアルタイムのフィードバック:進行状況を逐次出力することで、ユーザーに対してリアルタイムのフィードバックを提供。
最終結果の出力:最終的なデータと更新された用語集を出力することで、翻訳プロジェクト全体を把握可能。
このプロセスを通じて、効率的かつ正確にゲームの世界観を保持した翻訳が実現できます。

おわり

このように特定の用語や表現が持つ文化的な背景を理解し、それを正確に再現することが求められます。提案した翻訳プロセスは、用語集の活用と占位文字の正確な取り扱いを通じて、翻訳の一貫性と品質を高めることを目的としています。

今回の実装では、正直製品レベルの翻訳hには至っていない。また、にも関わらずAPIコール量がかさばってしまうため固有名詞抽出などはQwen2.5:32Bなどのろーかるモデルを活用した。

また、品詞ごとに翻訳方法を分けるエージェントの作成なども検討したがかかる労力に見合った成果が得られる見込みが低いという判断で断念した。

精度や処理時間の面では課題がまだまだ残ったが、実務的には従来のローカライズ作業の工数を大幅に削減でき、価値の高い取り組みとなった。ゼロからの翻訳と、最低限の逐語訳があるところから始めるとでは翻訳家の普段がかなり軽くなるとのことだ。

今後はスタートアップなどが特化的に取り組んでよりよいサービスが生まれるかもしれないが2024年秋時点の現場業務として紹介。

契約上深くはかけないところ、ぼかして書いた処理などもあるが参考になると嬉しい。