Skip to content

Commit 38e5f03

Browse files
committed
feature: DeepL翻訳からGPT4o miniへ変更、main.pyにのみ書いていたコードを複数ファイルに分割、その他可読性の向上
1 parent 5ede796 commit 38e5f03

File tree

14 files changed

+674
-523
lines changed

14 files changed

+674
-523
lines changed

README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# MinecraftModsLocalizer ユーザーガイド
22

3-
このドキュメントでは、MinecraftModsLocalizerの使用方法について詳しく説明します。このツールは、MinecraftのModとModPackのQuestsを日本語に翻訳するためのものです。
3+
4+
# **New!! 翻訳に使用するAIをChatGPTに変更しました!API料金が大幅に安くなり、精度も格段に良くなりました!ファイル構造の崩壊、特殊文字無視とはバイバイ!**
5+
6+
7+
8+
このツールは、MinecraftのModとModPackのQuestsを日本語に翻訳するためのものです。
49
## 注意事項はより良い翻訳のために読むことを推奨します
510

611
## 目次
@@ -20,7 +25,7 @@
2025

2126
**ソフト名:** MinecraftModsLocalizer
2227

23-
このソフトウェアは、DEEPL翻訳を使用して、MinecraftのMod本体とftbquestsのQuestを日本語に翻訳する機能を提供します。
28+
このソフトウェアは、ChatGPTを使用して、MinecraftのMod本体とftbquestsのQuestを日本語に翻訳する機能を提供します。
2429

2530
ModPackなどの一括翻訳などにご利用ください
2631

@@ -31,12 +36,15 @@ ModPackなどの一括翻訳などにご利用ください
3136

3237
**テスト済み環境:**
3338
- Windows
39+
40+
**テスト済みModPack:**
41+
- DawnCraft (Forge)
3442
- ATM9 (Forge)
3543
- Create Astral (Fabric)
3644

3745
## インストール前の要件
3846

39-
- DEEPL翻訳のAPI_KEY(認証キー)が必要です
47+
- OpenAIのAPI_KEYが必要です
4048

4149
## インストール方法
4250

@@ -59,18 +67,22 @@ ModPackなどの一括翻訳などにご利用ください
5967

6068
## 使い方
6169

62-
1. DEEPLのAPI_KEY(認証キー)を取得し、ソフトウェアにキーを提供します
70+
1. OpenAIのAPI KEYを取得し、ソフトウェアに提供します(ググると取得方法はいっぱい出ると思います)
6371
2. ソフトウェアを起動し、指示に従ってModまたはQuestsの翻訳を開始します。
6472

73+
**超巨大なModPack(ATM9のような)でも0.5ドル以下で翻訳できると思います。本当に安くなった。**
74+
6575
特定のmod(.jar)やquestファイル(.snbt)のみを翻訳したい場合は、それらのファイルを取り除いてください。
6676

6777
- modは`mods`フォルダ内にあります。
6878
- questsは`kubejs/assets/kubejs/lang/`または`config/ftbquests/quests/chapters`(両方ある場合はlangの方が翻訳元になります)の中にあります。
6979

7080
### 各項目について
7181
- **Translate Target:** 翻訳対象を選択します。Mod本体の翻訳、Questsの翻訳、または両方を選択できます。
72-
- **API_KEY:** DEEPLのAPI_KEY(認証キー)を入力してください。
73-
- **Use Free API:** DEEPLの無料APIを使用するかどうかを選択します。契約しているプランで変更してください。Freeプランの方はチェック、Proプランなどそれ以外の方はチェックを外してください。
82+
- **OpenAI API KEY:** OpenAIのAPI_KEYを入力してください。
83+
- **Chunk Size** ファイルを分割して翻訳するため、一つあたりの行数を指定します。下げると翻訳速度が低下しますが精度が上昇します。また、下げると翻訳失敗時に翻訳されなかった部分が減ります。単体mod翻訳やクエストのみの翻訳では1、ModPackで大量のModを一括で翻訳するときは100くらいまで上げることをお勧めします(翻訳時間がすごいことになります)
84+
- **Model** 弄らないことをお勧めします。OpenAIからより良いモデルが出たときは変更してもよいかもしれません。
85+
- **Prompt** 知識のある方はプロンプトを弄るとさらなる精度向上が見込めるかもしれません。特にどうしても一部翻訳が正常に成功しないことがあり、成功しなかった場合はチャンクを丸ごと翻訳しないことになります。
7486

7587
## 出力ファイル
7688

@@ -85,11 +97,14 @@ ModPackなどの一括翻訳などにご利用ください
8597

8698
## 注意事項
8799

88-
- **API料金には十分注意してください特に巨大なModPackでは無料枠を超えないよう分割翻訳を行うことを推奨します。**
89-
- 身内で分担して翻訳し、ファイルを共有する方法もいいかもしれません。
90-
- 契約しているプランで「Use Free API」チェックボックスを切り替えてください(特にPro以降のプランの方はチェックボックスを外す必要があります)。
91-
- DEEPL翻訳を使用しているため、翻訳の精度は非常に高いと思われますが、"%s"などの変数を含む文字列の場合、挙動に問題が生じる可能性があります。不自然または誤った翻訳を見つけた場合、ファイルを直接編集することで修正してください。(**(大抵は先頭に%sがついているときに%が残らず、sだけになる問題があるため、%を補ってあげてください)**)
100+
- **Chunk Sizeは単体mod翻訳やクエストのみの翻訳では1、ModPackで大量のModを一括で翻訳するときは100くらいまで上げることをお勧めします(翻訳時間がすごいことになります)**
101+
- OpenAIのAPI_KEYの取り扱いには十分注意してください。
102+
- GPT4o miniにモデルを変更したことによってAPI料金は気にしなくてもいいレベルで安くなりましたが、代償として翻訳が超遅くなりました。気長に待ってください
92103
- Mod本体の翻訳に関して、リソースパックのpack.mcmetaがインデントが崩れている場合、正常に読み込まれない可能性があります。リソースパックに候補が出てこない場合は、pack.mcmetaを確認してください。
104+
- どうしても翻訳が失敗してしまうことがあるので、気になる方は
105+
- Mod: `logs/localizer/error`から手動でjsonを編集し、`resourcepacks/japanese/lang/ja_jp.json`に追記してください
106+
- Quest: `logs/localizer/error`から手動でjsonを編集し、`/kubejs/assets/kubejs/lang/ja_jp.json`に追記してください
107+
- ※なお、Questの場合snbtファイルに直書き形式であった場合errorディレクトリに記録が残りません。
93108

94109
## 内部実装について
95110

@@ -99,9 +114,9 @@ ModPackなどの一括翻訳などにご利用ください
99114
- 存在する場合kubejs/assets/kubejs/lang/en_us.jsonを読み込み翻訳を行います
100115
- 存在しない場合直接config/ftbquests/quests/chapters/ファイル(.snbt)を書き換え翻訳します。
101116
- また、kubejs/assets/kubejs/lang/en_us.jsonに本来jsonとして無効なコメントが含まれている場合、改行コード(\n)がクエスト内容に存在する場合消し飛ばします(Create Astralで確認)。扱いめんどくさかった。許して❤
117+
- 翻訳前と後の行数が異なる場合最大5回までもう一度翻訳を試みます。それでもダメな場合はそのチャンクは翻訳されません。
102118

103119
## 将来のアップデートと余談
104120

105-
- 完璧な翻訳を求める場合、将来的にはGPT-4のAPIを使用することが最善の選択かもしれません。GPT-4の対応は、開発者の意向やコスト面の変化に応じて実施される可能性があります。
106-
- コミュニティ内でAPIコストを分担し、翻訳ファイルを共有する方法がコストパフォーマンスの観点から有効であると考えられます。
107-
- 一日で作った手抜きソフトなのでテスト不足、バグも散見され、コードも汚いと思いますが、ご了承ください。Github Issuesにバグ報告や機能要望を投稿していただけると幸いです。
121+
- コード綺麗にしました。forkなども大歓迎です。init.pyを弄ればだいたいのパラメーターは弄れます。プロンプトが気に入らないときはどうぞ
122+
- Github Issuesにバグ報告や機能要望を投稿していただけると幸いです。

dev/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
1515
COPY . /src
1616

1717
# 必要なPythonパッケージをインストール
18-
RUN pip install --no-cache-dir pyzipper requests pyinstaller PySimpleGUI
18+
RUN pip install --no-cache-dir pyzipper requests pyinstaller TkEasyGUI openai

linux/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ RUN rm -rf /var/lib/apt/lists/*
1515

1616
# Copy code and install Python dependencies
1717
COPY ./src /src
18-
RUN pip install --no-cache-dir pyzipper requests pyinstaller PySimpleGUI
18+
RUN pip install --no-cache-dir pyzipper requests pyinstaller TkEasyGUI openai

src/chatgpt.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
import time
3+
from openai import OpenAI
4+
5+
from src.provider import provide_api_key, provide_model, provide_prompt
6+
7+
8+
def translate_with_chatgpt(split_target, timeout):
9+
start_time = time.time()
10+
result = []
11+
12+
# 改行を削除(翻訳時扱いがめんどくさいため)
13+
split_target = [line.replace('\\n', '').replace('\n', '') for line in split_target]
14+
15+
# APIキーとクライアントの初期化
16+
client = OpenAI(api_key=provide_api_key())
17+
18+
try:
19+
# ChatGPTを用いて翻訳を行う
20+
response = client.chat.completions.create(
21+
model=provide_model(),
22+
messages=[
23+
{
24+
"role": "system",
25+
"content": [
26+
{"type": "text", "text": provide_prompt().replace('{line_count}', str(len(split_target)))}]
27+
},
28+
{
29+
"role": "user",
30+
"content": [{"type": "text", "text": '\n'.join(split_target)}]
31+
}
32+
],
33+
)
34+
35+
# 翻訳結果を取得
36+
if response.choices and response.choices[0].message:
37+
translated_text = response.choices[0].message.content
38+
result = translated_text.splitlines() if len(split_target) > 1 else [translated_text.replace('\n', '')]
39+
else:
40+
logging.error("Failed to get a valid response from the ChatGPT model.")
41+
42+
except Exception as e:
43+
elapsed_time = time.time() - start_time
44+
if elapsed_time > timeout:
45+
logging.error("Timeout reached while waiting for translation.")
46+
logging.error(f"Error during translation: {str(e)}")
47+
48+
return result

src/deepl.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import logging
2+
import time
3+
from io import BytesIO
4+
import requests
5+
6+
from src.main import API_KEY
7+
8+
# DEEPL_API_URL = f'https://api{suffix}.deepl.com/v2/translate'
9+
# UPLOAD_URL = f"https://api{suffix}.deepl.com/v2/document"
10+
# CHECK_STATUS_URL_TEMPLATE = f"https://api{suffix}.deepl.com/v2/document/{{}}"
11+
# DOWNLOAD_URL_TEMPLATE = f"https://api{suffix}.deepl.com/v2/document/{{}}/result"
12+
13+
# def translate_with_deepl(part, timeout):
14+
# start_time = time.time()
15+
# part_values = []
16+
#
17+
# with open(part, 'rb') as f:
18+
# response = requests.post(
19+
# UPLOAD_URL,
20+
# headers={'Authorization': f'DeepL-Auth-Key {API_KEY}'},
21+
# data={'source_lang': 'EN', 'target_lang': 'JA'},
22+
# files={'file': f}
23+
# )
24+
# response_data = response.json()
25+
# document_id = response_data.get('document_id')
26+
# document_key = response_data.get('document_key')
27+
#
28+
# while True:
29+
# elapsed_time = time.time() - start_time
30+
# if elapsed_time > timeout:
31+
# logging.error(f"Timeout reached while waiting for translation.")
32+
# break
33+
#
34+
# status_response = requests.post(
35+
# CHECK_STATUS_URL_TEMPLATE.format(document_id),
36+
# headers={'Authorization': f'DeepL-Auth-Key {API_KEY}', 'Content-Type': 'application/json'},
37+
# json={'document_key': document_key}
38+
# )
39+
# status_data = status_response.json()
40+
#
41+
# if status_data['status'] == "done":
42+
# download_response = requests.post(
43+
# DOWNLOAD_URL_TEMPLATE.format(document_id),
44+
# headers={'Authorization': f'DeepL-Auth-Key {API_KEY}', 'Content-Type': 'application/json'},
45+
# json={'document_key': document_key}
46+
# )
47+
# buffer = BytesIO(download_response.content)
48+
# buffer.seek(0)
49+
# text_data = buffer.read().decode('utf-8')
50+
# part_values.extend(text_data.splitlines())
51+
# buffer.close()
52+
# break
53+
# else:
54+
# time.sleep(10) # DeepL APIの翻訳状態をポーリング
55+
#
56+
# return part_values
57+

src/init.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pathlib import Path
2+
3+
RESOURCE_DIR = Path('./resourcepacks/japanese')
4+
MODS_DIR = Path('./mods')
5+
QUESTS_DIR1 = Path('./kubejs/assets/kubejs/lang')
6+
QUESTS_DIR2 = Path('./kubejs/assets/ftbquests/lang')
7+
QUESTS_DIR3 = Path('./config/ftbquests/quests/chapters')
8+
9+
MAX_ATTEMPTS = 5
10+
11+
USER = 'Y-RyuZU'
12+
REPO = 'MinecraftModsLocalizer'
13+
VERSION = 'v1.5.3'

src/jar.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import json
2+
import logging
3+
import os
4+
import re
5+
import zipfile
6+
from pathlib import Path
7+
8+
from src.init import RESOURCE_DIR, MODS_DIR
9+
from src.provider import provide_log_directory
10+
from src.prepare import extract_map_from_json, prepare_translation
11+
12+
13+
def process_jar_file(jar_path):
14+
mod_name = get_mod_name_from_jar(jar_path)
15+
if mod_name is None:
16+
logging.info(f"Could not determine mod name for {jar_path}")
17+
return {}
18+
19+
lang_path_in_jar = Path(f'assets/{mod_name}/lang/')
20+
ja_jp_path_in_jar = os.path.join(lang_path_in_jar, 'ja_jp.json')
21+
en_us_path_in_jar = os.path.join(lang_path_in_jar, 'en_us.json')
22+
ja_jp_path_in_jar_str = str(ja_jp_path_in_jar).replace('\\', '/')
23+
en_us_path_in_jar_str = str(en_us_path_in_jar).replace('\\', '/')
24+
25+
logging.info(f"Extract en_us.json or ja_jp.json in {jar_path / lang_path_in_jar}")
26+
with zipfile.ZipFile(jar_path, 'r') as zip_ref:
27+
if en_us_path_in_jar_str in zip_ref.namelist():
28+
extract_specific_file(jar_path, en_us_path_in_jar_str, provide_log_directory())
29+
if ja_jp_path_in_jar_str in zip_ref.namelist():
30+
extract_specific_file(jar_path, ja_jp_path_in_jar_str, provide_log_directory())
31+
32+
en_us_path = os.path.join(provide_log_directory(), en_us_path_in_jar)
33+
ja_jp_path = os.path.join(provide_log_directory(), ja_jp_path_in_jar)
34+
35+
return extract_map_from_json(ja_jp_path) if os.path.exists(ja_jp_path) else extract_map_from_json(en_us_path)
36+
37+
38+
def translate_from_jar():
39+
if not os.path.exists(RESOURCE_DIR):
40+
os.makedirs(os.path.join(RESOURCE_DIR, 'assets', 'japanese', 'lang'))
41+
42+
targets = {}
43+
44+
extracted_pack_mcmeta = False
45+
for filename in os.listdir(MODS_DIR):
46+
if filename.endswith('.jar'):
47+
# Extract pack.mcmeta if it exists in the jar
48+
if not extracted_pack_mcmeta:
49+
extracted_pack_mcmeta = extract_specific_file(os.path.join(MODS_DIR, filename), 'pack.mcmeta',
50+
RESOURCE_DIR)
51+
update_resourcepack_description(os.path.join(RESOURCE_DIR, 'pack.mcmeta'), '日本語化パック')
52+
53+
targets.update(process_jar_file(os.path.join(MODS_DIR, filename)))
54+
55+
translated_map = prepare_translation(list(targets.values()))
56+
57+
translated_targets = {json_key: translated_map[original] for json_key, original in targets.items() if
58+
original in translated_map}
59+
60+
untranslated_items = {json_key: original for json_key, original in targets.items() if
61+
original not in translated_map}
62+
63+
with open(os.path.join(RESOURCE_DIR, 'assets', 'japanese', 'lang', 'ja_jp.json'), 'w', encoding="utf-8") as f:
64+
json.dump(dict(sorted(translated_targets.items())), f, ensure_ascii=False, indent=4)
65+
66+
error_directory = os.path.join(provide_log_directory(), 'error')
67+
68+
if not os.path.exists(error_directory):
69+
os.makedirs(error_directory)
70+
71+
with open(os.path.join(error_directory, 'mod_ja_jp.json'), 'w', encoding="utf-8") as f:
72+
json.dump(dict(sorted(untranslated_items.items())), f, ensure_ascii=False, indent=4)
73+
74+
75+
def update_resourcepack_description(file_path, new_description):
76+
# ファイルが存在するか確認
77+
if not os.path.exists(file_path):
78+
return
79+
80+
with open(file_path, 'r', encoding='utf-8') as file:
81+
try:
82+
data = json.load(file)
83+
except json.JSONDecodeError as e:
84+
return
85+
86+
# 'description'の'text'を新しい値に更新
87+
try:
88+
if 'pack' in data and 'description' in data['pack'] and 'text' in data['pack']['description']:
89+
data['pack']['description']['text'] = new_description
90+
else:
91+
return
92+
except Exception as e:
93+
return
94+
95+
# 変更を加えたデータを同じファイルに書き戻す
96+
with open(file_path, 'w', encoding='utf-8') as file:
97+
try:
98+
json.dump(data, file, ensure_ascii=False, indent=2) # JSONを整形して書き込み
99+
except Exception as e:
100+
return
101+
102+
103+
def get_mod_name_from_jar(jar_path):
104+
with zipfile.ZipFile(jar_path, 'r') as zip_ref:
105+
asset_dirs_with_lang = set()
106+
for name in zip_ref.namelist():
107+
parts = name.split('/')
108+
if len(parts) > 3 and parts[0] == 'assets' and parts[2] == 'lang' and parts[1] != 'minecraft':
109+
asset_dirs_with_lang.add(parts[1])
110+
if asset_dirs_with_lang:
111+
return list(asset_dirs_with_lang)[0]
112+
return None
113+
114+
115+
def extract_specific_file(zip_filepath, file_name, dest_dir):
116+
with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:
117+
if file_name in zip_ref.namelist():
118+
zip_ref.extract(file_name, dest_dir)
119+
return True
120+
else:
121+
logging.info(f"The file {file_name} in {zip_filepath} was not found in the ZIP archive.")
122+
return False

src/log.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import logging
2+
import os
3+
import sys
4+
5+
6+
def setup_logging(directory):
7+
log_file = "translate.log"
8+
9+
# ディレクトリが存在しない場合は作成
10+
if not os.path.exists(directory):
11+
os.makedirs(directory)
12+
13+
# ログファイルのフルパス
14+
log_path = os.path.join(directory, log_file)
15+
16+
# ロガーの設定
17+
logging.basicConfig(
18+
level=logging.INFO, # INFOレベル以上のログを取得
19+
format='%(asctime)s %(levelname)s %(message)s', # ログのフォーマット
20+
handlers=[
21+
logging.FileHandler(log_path), # ログをファイルに出力
22+
logging.StreamHandler(sys.stdout) # ログをコンソールに出力
23+
]
24+
)

0 commit comments

Comments
 (0)