Surely cleaning up all this mess wont break anything. surely
This commit is contained in:
parent
563299ba21
commit
4dd9d2bbd4
38 changed files with 16162 additions and 15976 deletions
64
.gitignore
vendored
64
.gitignore
vendored
|
|
@ -1,32 +1,32 @@
|
||||||
|
|
||||||
.env
|
.env
|
||||||
*.tmp
|
*.tmp
|
||||||
*.json
|
*.json
|
||||||
*.js
|
*.js
|
||||||
*.yaml
|
*.yaml
|
||||||
*.yml
|
*.yml
|
||||||
*.txt
|
*.txt
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
*.rpy
|
*.rpy
|
||||||
*.csv
|
*.csv
|
||||||
*.ks
|
*.ks
|
||||||
*.cid
|
*.cid
|
||||||
*.png
|
*.png
|
||||||
*.script
|
*.script
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
!assets/*.png
|
!assets/*.png
|
||||||
!.pre-commit-config.yaml
|
!.pre-commit-config.yaml
|
||||||
!prompt.txt
|
!data/prompt.txt
|
||||||
!vocab_base.txt
|
!data/vocab_base.txt
|
||||||
!requirements.txt
|
!requirements.txt
|
||||||
!util/tl_inspector/TLInspector.js
|
!util/tl_inspector/TLInspector.js
|
||||||
!util/forge/Forge_MZ.js
|
!util/forge/Forge_MZ.js
|
||||||
!util/forge/Forge_MV.js
|
!util/forge/Forge_MV.js
|
||||||
!util/forge/upstream/Forge_MZ.js
|
!util/forge/upstream/Forge_MZ.js
|
||||||
!util/forge/upstream/Forge_MV.js
|
!util/forge/upstream/Forge_MV.js
|
||||||
util/forge/.forge_version.json
|
util/forge/.forge_version.json
|
||||||
util/ace/*.exe
|
util/ace/*.exe
|
||||||
util/ace/.tools_version.json
|
util/ace/.tools_version.json
|
||||||
!util/ace/offline/*.exe
|
!util/ace/offline/*.exe
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
'hooks':
|
'hooks':
|
||||||
- 'id': 'set-defaults'
|
- 'id': 'set-defaults'
|
||||||
'name': 'Set Defaults in rpgmakermvmz.py'
|
'name': 'Set Defaults in rpgmakermvmz.py'
|
||||||
'entry': 'python set_defaults.py'
|
'entry': 'python -m util.defaults'
|
||||||
'language': 'python'
|
'language': 'python'
|
||||||
'files': '^modules/rpgmakermvmz.py$'
|
'files': '^modules/rpgmakermvmz.py$'
|
||||||
|
|
|
||||||
10
DazedMTLTool.desktop
Normal file
10
DazedMTLTool.desktop
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DazedMTLTool
|
||||||
|
GenericName=Translation Tool
|
||||||
|
Comment=AI translation tool for visual novels and RPG games
|
||||||
|
Exec=bash -c 'exec "$(cd "$(dirname "%k")" && pwd)/scripts/launch.sh"'
|
||||||
|
Icon=%k/assets/icon.png
|
||||||
|
Terminal=false
|
||||||
|
Categories=Development;Utility;
|
||||||
|
StartupWMClass=DazedMTLTool
|
||||||
23
README.md
23
README.md
|
|
@ -124,12 +124,11 @@ This means Python wasn't added to your PATH. You have two options:
|
||||||
|
|
||||||
### 3. Launch the GUI
|
### 3. Launch the GUI
|
||||||
|
|
||||||
**Double-click `START.bat`**. It will:
|
**Windows:** Double-click `START.bat`. It will create a virtual environment, install dependencies, and open the GUI.
|
||||||
- Create a virtual environment automatically.
|
|
||||||
- Install all dependencies.
|
|
||||||
- Launch the GUI.
|
|
||||||
|
|
||||||
That's it! From now on, just double-click `START.bat` to open the tool.
|
**Linux/macOS:** Run `./START.sh`, or double-click `DazedMTLTool.desktop` (choose **Allow Launching** when your file manager asks). From then on, either method works.
|
||||||
|
|
||||||
|
That's it! Use the same launcher each time you want to open the tool.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -160,10 +159,10 @@ Specialized tabs with extra options for those specific engines.
|
||||||
|
|
||||||
## Vocab & Prompt
|
## Vocab & Prompt
|
||||||
|
|
||||||
### vocab.txt
|
### data/vocab.txt
|
||||||
This file gives the AI context about your game — character names, genders, recurring terms, etc. The better your vocab file, the more consistent the translation.
|
This file gives the AI context about your game — character names, genders, recurring terms, etc. The better your vocab file, the more consistent the translation.
|
||||||
|
|
||||||
Open `vocab.txt` (or copy `vocab.txt.example` to `vocab.txt` if it doesn't exist) and add entries like:
|
On first run, `data/vocab.txt` is created automatically from `data/vocab_base.txt` if it does not exist yet. Add entries like:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
# Game Characters
|
# Game Characters
|
||||||
|
|
@ -176,14 +175,14 @@ Format: Japanese name, English name in parentheses, then gender.
|
||||||
|
|
||||||
> **Note:** A very large vocab file can increase API costs and potentially reduce quality. Focus on the most important characters and terms.
|
> **Note:** A very large vocab file can increase API costs and potentially reduce quality. Focus on the most important characters and terms.
|
||||||
|
|
||||||
### prompt.txt
|
### data/prompt.txt
|
||||||
This is the system prompt sent to the AI. A default `prompt.txt` is included and works well for most games. You generally don't need to edit it unless you want to customize the translation style.
|
This is the system prompt sent to the AI. A default `data/prompt.txt` is included and works well for most games. You generally don't need to edit it unless you want to customize the translation style.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- **Check `log/translations.txt`** after a run to see what was translated. You can copy useful terms from it into `vocab.txt` for consistency in future runs.
|
- **Check `log/translations.txt`** after a run to see what was translated. You can copy useful terms from it into `data/vocab.txt` for consistency in future runs.
|
||||||
- **Start small** — Translate a few files first to make sure the output looks good before doing the whole game.
|
- **Start small** — Translate a few files first to make sure the output looks good before doing the whole game.
|
||||||
- **Wordwrap** — If text overflows or looks awkward in-game, adjust the `width` setting in `.env` or the Config tab. `60` is a good default for most RPG Maker games.
|
- **Wordwrap** — If text overflows or looks awkward in-game, adjust the `width` setting in `.env` or the Config tab. `60` is a good default for most RPG Maker games.
|
||||||
- **Version control** — Using [Git](https://git-scm.com/) with the game folder is highly recommended. It lets you track every change the translation makes, compare with original files, and roll back if needed.
|
- **Version control** — Using [Git](https://git-scm.com/) with the game folder is highly recommended. It lets you track every change the translation makes, compare with original files, and roll back if needed.
|
||||||
|
|
@ -300,8 +299,8 @@ Here's the recommended step-by-step process for translating an RPG Maker MV/MZ g
|
||||||
|
|
||||||
| Step | Action |
|
| Step | Action |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| **1** | **Parse speakers → vocab.txt** — Use the Parse Speakers feature to pull character names from the game files into `vocab.txt`. |
|
| **1** | **Parse speakers → vocab** — Use the Parse Speakers feature to pull character names from the game files into `data/vocab.txt`. |
|
||||||
| **2** | **Identify speaker genders** — Figure out which characters are male/female and update `vocab.txt` accordingly. This helps the AI use correct pronouns. |
|
| **2** | **Identify speaker genders** — Figure out which characters are male/female and update `data/vocab.txt` accordingly. This helps the AI use correct pronouns. |
|
||||||
| **3** | **Translate Actors.json, MapInfos.json** — These are small files with character and map names. Good to do first. |
|
| **3** | **Translate Actors.json, MapInfos.json** — These are small files with character and map names. Good to do first. |
|
||||||
| **4** | **Translate Items, System, Weapons, etc.** — All the data files that aren't maps or events. Place them in `files/`, translate, then copy results back. |
|
| **4** | **Translate Items, System, Weapons, etc.** — All the data files that aren't maps or events. Place them in `files/`, translate, then copy results back. |
|
||||||
| **5** | **Find speaker names** — Enable CODE 101 (Speakers), check for bracketed names, or use the "First Line = Speaker" option to capture speaker names properly. |
|
| **5** | **Find speaker names** — Enable CODE 101 (Speakers), check for bracketed names, or use the "First Line = Speaker" option to capture speaker names properly. |
|
||||||
|
|
|
||||||
25
START.bat
25
START.bat
|
|
@ -184,28 +184,29 @@ echo Launching DazedMTLTool GUI...
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: Ensure vocab.txt exists (create from example if available)
|
:: Ensure data/vocab.txt exists (create from vocab_base.txt if available)
|
||||||
if not exist "vocab.txt" (
|
if not exist "data\\vocab.txt" (
|
||||||
if exist "vocab.txt.example" (
|
if not exist "data" mkdir data
|
||||||
echo vocab.txt not found - creating from vocab.txt.example...
|
if exist "data\\vocab_base.txt" (
|
||||||
copy /Y "vocab.txt.example" "vocab.txt" >nul 2>&1
|
echo data\vocab.txt not found - creating from data\vocab_base.txt...
|
||||||
|
copy /Y "data\vocab_base.txt" "data\vocab.txt" >nul 2>&1
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to copy vocab.txt.example to vocab.txt.
|
echo ERROR: Failed to copy data\vocab_base.txt to data\vocab.txt.
|
||||||
) else (
|
) else (
|
||||||
echo Created vocab.txt from vocab.txt.example
|
echo Created data\vocab.txt from data\vocab_base.txt
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo vocab.txt and vocab.txt.example not found - creating empty vocab.txt to avoid import errors...
|
echo data\vocab.txt not found - creating empty file to avoid import errors...
|
||||||
type NUL > "vocab.txt"
|
type NUL > "data\vocab.txt"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to create empty vocab.txt.
|
echo ERROR: Failed to create empty data\vocab.txt.
|
||||||
) else (
|
) else (
|
||||||
echo Created empty vocab.txt
|
echo Created empty data\vocab.txt
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
python start_gui.py
|
python scripts/start_gui.py
|
||||||
|
|
||||||
:: Check if GUI launched successfully
|
:: Check if GUI launched successfully
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
|
|
|
||||||
23
START.sh
23
START.sh
|
|
@ -127,23 +127,24 @@ activate_venv() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_vocab_file() {
|
ensure_vocab_file() {
|
||||||
if [[ -f "vocab.txt" ]]; then
|
mkdir -p data
|
||||||
|
if [[ -f "data/vocab.txt" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "vocab.txt.example" ]]; then
|
if [[ -f "data/vocab_base.txt" ]]; then
|
||||||
echo "vocab.txt not found - creating from vocab.txt.example..."
|
echo "data/vocab.txt not found - creating from data/vocab_base.txt..."
|
||||||
if cp "vocab.txt.example" "vocab.txt"; then
|
if cp "data/vocab_base.txt" "data/vocab.txt"; then
|
||||||
echo "Created vocab.txt from vocab.txt.example"
|
echo "Created data/vocab.txt from data/vocab_base.txt"
|
||||||
else
|
else
|
||||||
echo "ERROR: Failed to copy vocab.txt.example to vocab.txt."
|
echo "ERROR: Failed to copy data/vocab_base.txt to data/vocab.txt."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "vocab.txt and vocab.txt.example not found - creating empty vocab.txt to avoid import errors..."
|
echo "data/vocab.txt not found - creating empty file to avoid import errors..."
|
||||||
if : > "vocab.txt"; then
|
if : > "data/vocab.txt"; then
|
||||||
echo "Created empty vocab.txt"
|
echo "Created empty data/vocab.txt"
|
||||||
else
|
else
|
||||||
echo "ERROR: Failed to create empty vocab.txt."
|
echo "ERROR: Failed to create empty data/vocab.txt."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +228,7 @@ echo
|
||||||
|
|
||||||
ensure_vocab_file
|
ensure_vocab_file
|
||||||
|
|
||||||
if ! python start_gui.py; then
|
if ! exec python "$SCRIPT_DIR/scripts/start_gui.py"; then
|
||||||
echo
|
echo
|
||||||
echo "ERROR: Failed to launch GUI."
|
echo "ERROR: Failed to launch GUI."
|
||||||
echo "Check the error messages above."
|
echo "Check the error messages above."
|
||||||
|
|
|
||||||
29
gui/main.py
29
gui/main.py
|
|
@ -19,20 +19,16 @@ from PyQt5.QtWidgets import (
|
||||||
QTextEdit, QSplitter, QGroupBox, QStatusBar, QStackedWidget, QToolButton,
|
QTextEdit, QSplitter, QGroupBox, QStatusBar, QStackedWidget, QToolButton,
|
||||||
QDialog
|
QDialog
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings, QCoreApplication
|
||||||
from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen
|
from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen, QGuiApplication
|
||||||
|
|
||||||
|
|
||||||
def _project_root() -> Path:
|
from util.paths import PROJECT_ROOT, ICON_PATH, LAST_UPDATE_SHA_PATH
|
||||||
"""Directory containing start_gui.py / requirements.txt (parent of gui/)."""
|
|
||||||
return Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
def load_application_icon() -> QIcon:
|
def load_application_icon() -> QIcon:
|
||||||
"""Prefer ICO on Windows; PNG is fine for all platforms. Empty if missing."""
|
"""Prefer ICO on Windows; PNG is fine for all platforms. Empty if missing."""
|
||||||
root = _project_root()
|
for path in (PROJECT_ROOT / "assets/icon.ico", ICON_PATH):
|
||||||
for rel in ("assets/icon.ico", "assets/icon.png"):
|
|
||||||
path = root / rel
|
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
icon = QIcon(str(path))
|
icon = QIcon(str(path))
|
||||||
if not icon.isNull():
|
if not icon.isNull():
|
||||||
|
|
@ -126,14 +122,13 @@ class UpdateThread(QThread):
|
||||||
REPO_USER = "DazedAnon"
|
REPO_USER = "DazedAnon"
|
||||||
REPO_NAME = "DazedMTLTool"
|
REPO_NAME = "DazedMTLTool"
|
||||||
REPO_BRANCH = "main"
|
REPO_BRANCH = "main"
|
||||||
SHA_FILE = "last_update_sha.txt"
|
SHA_FILE = str(LAST_UPDATE_SHA_PATH)
|
||||||
|
|
||||||
# Paths (relative, top-level) that should never be touched during update
|
# Paths (relative, top-level) that should never be touched during update
|
||||||
PROTECTED = {".env", "venv", "log", "files", "translated",
|
PROTECTED = {".env", "venv", "log", "files", "translated", "data"}
|
||||||
"vocab.txt", "last_update_sha.txt"}
|
|
||||||
|
|
||||||
# GitLab zip archives do not preserve Unix execute bits; restore after apply.
|
# GitLab zip archives do not preserve Unix execute bits; restore after apply.
|
||||||
EXECUTABLE_SUFFIXES = {".sh"}
|
EXECUTABLE_SUFFIXES = {".sh", ".desktop"}
|
||||||
|
|
||||||
progress = pyqtSignal(str) # status message
|
progress = pyqtSignal(str) # status message
|
||||||
finished = pyqtSignal(bool, str) # (success, message)
|
finished = pyqtSignal(bool, str) # (success, message)
|
||||||
|
|
@ -902,6 +897,14 @@ def main():
|
||||||
# Set high DPI scale factor policy
|
# Set high DPI scale factor policy
|
||||||
if hasattr(QApplication, 'setHighDpiScaleFactorRoundingPolicy'):
|
if hasattr(QApplication, 'setHighDpiScaleFactorRoundingPolicy'):
|
||||||
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
from util.linux_desktop import ensure_linux_desktop_entry
|
||||||
|
ensure_linux_desktop_entry()
|
||||||
|
QGuiApplication.setDesktopFileName("DazedMTLTool")
|
||||||
|
|
||||||
|
QCoreApplication.setOrganizationName("DazedTranslations")
|
||||||
|
QCoreApplication.setApplicationName("DazedMTLTool")
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
|
@ -934,9 +937,7 @@ def main():
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Set application properties
|
# Set application properties
|
||||||
app.setApplicationName("DazedMTLTool")
|
|
||||||
app.setApplicationVersion("1.0")
|
app.setApplicationVersion("1.0")
|
||||||
app.setOrganizationName("DazedTranslations")
|
|
||||||
|
|
||||||
# Apply dark theme with cleaner, more compact styling
|
# Apply dark theme with cleaner, more compact styling
|
||||||
app.setStyleSheet("""
|
app.setStyleSheet("""
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ except ImportError:
|
||||||
ConfigIntegration = None
|
ConfigIntegration = None
|
||||||
try:
|
try:
|
||||||
# Prefer importing the project's canonical defaults if available
|
# Prefer importing the project's canonical defaults if available
|
||||||
import set_defaults
|
from util.defaults import DEFAULTS as CANONICAL_DEFAULTS
|
||||||
CANONICAL_DEFAULTS = getattr(set_defaults, 'DEFAULTS', None)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
CANONICAL_DEFAULTS = None
|
CANONICAL_DEFAULTS = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import re
|
||||||
from gui.platform_glyph import platform_glyph
|
from gui.platform_glyph import platform_glyph
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import set_defaults
|
from util.defaults import DEFAULTS as CANONICAL_DEFAULTS
|
||||||
CANONICAL_DEFAULTS = getattr(set_defaults, 'DEFAULTS', None)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
CANONICAL_DEFAULTS = None
|
CANONICAL_DEFAULTS = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ from PyQt5.QtCore import pyqtSignal
|
||||||
import re
|
import re
|
||||||
from gui.platform_glyph import platform_glyph
|
from gui.platform_glyph import platform_glyph
|
||||||
try:
|
try:
|
||||||
import set_defaults
|
from util.defaults import DEFAULTS as CANONICAL_DEFAULTS
|
||||||
CANONICAL_DEFAULTS = getattr(set_defaults, 'DEFAULTS', None)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
CANONICAL_DEFAULTS = None
|
CANONICAL_DEFAULTS = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from util.paths import VOCAB_BASE_PATH, VOCAB_PATH
|
||||||
|
|
||||||
import jsbeautifier
|
import jsbeautifier
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QEvent, QSettings, QThread, QTimer, pyqtSignal
|
from PyQt5.QtCore import Qt, QEvent, QSettings, QThread, QTimer, pyqtSignal
|
||||||
|
|
@ -3576,7 +3578,7 @@ class WorkflowTab(QWidget):
|
||||||
|
|
||||||
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
|
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
|
||||||
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
|
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
|
||||||
vocab_path = Path("vocab.txt")
|
vocab_path = VOCAB_PATH
|
||||||
if not vocab_path.exists():
|
if not vocab_path.exists():
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
|
|
@ -3606,7 +3608,7 @@ class WorkflowTab(QWidget):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _reload_vocab(self):
|
def _reload_vocab(self):
|
||||||
vocab_path = Path("vocab.txt")
|
vocab_path = VOCAB_PATH
|
||||||
try:
|
try:
|
||||||
if vocab_path.exists():
|
if vocab_path.exists():
|
||||||
text = vocab_path.read_text(encoding="utf-8")
|
text = vocab_path.read_text(encoding="utf-8")
|
||||||
|
|
@ -3623,10 +3625,10 @@ class WorkflowTab(QWidget):
|
||||||
def _save_vocab(self):
|
def _save_vocab(self):
|
||||||
try:
|
try:
|
||||||
game_text = self.vocab_editor.toPlainText().rstrip("\n")
|
game_text = self.vocab_editor.toPlainText().rstrip("\n")
|
||||||
base_path = Path("vocab_base.txt")
|
base_path = VOCAB_BASE_PATH
|
||||||
base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
||||||
combined = game_text + "\n\n" + self._BASE_SEPARATOR + base_text
|
combined = game_text + "\n\n" + self._BASE_SEPARATOR + base_text
|
||||||
Path("vocab.txt").write_text(combined, encoding="utf-8")
|
VOCAB_PATH.write_text(combined, encoding="utf-8")
|
||||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||||
|
|
@ -3795,7 +3797,7 @@ class WorkflowTab(QWidget):
|
||||||
self._log("⚠ No game folder set. Complete Step 0 first.")
|
self._log("⚠ No game folder set. Complete Step 0 first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
src = Path("vocab.txt")
|
src = VOCAB_PATH
|
||||||
if not src.exists():
|
if not src.exists():
|
||||||
self._log("⚠ vocab.txt not found — save it in Step 3 first.")
|
self._log("⚠ vocab.txt not found — save it in Step 3 first.")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,215 +1,217 @@
|
||||||
# modules/aquedi4.py
|
# modules/aquedi4.py
|
||||||
|
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
# from modules.json import translateSimpleKeyValueJSON
|
# from modules.json import translateSimpleKeyValueJSON
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Boilerplate Globals
|
# Boilerplate Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
ESTIMATE = ""
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
TOKENS = [0, 0]
|
LOCK = threading.Lock()
|
||||||
MISMATCH = []
|
ESTIMATE = ""
|
||||||
FILENAME = None
|
TOKENS = [0, 0]
|
||||||
PBAR = None
|
MISMATCH = []
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
FILENAME = None
|
||||||
|
PBAR = None
|
||||||
# Flag to control line break replacement
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
REPLACE_LINEBREAKS_WITH_PIPES = True
|
|
||||||
|
# Flag to control line break replacement
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
REPLACE_LINEBREAKS_WITH_PIPES = True
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=int(os.getenv("batchsize", 10)),
|
vocab=VOCAB,
|
||||||
maxHistory=10,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False
|
batchSize=int(os.getenv("batchsize", 10)),
|
||||||
)
|
maxHistory=10,
|
||||||
# End Boilerplate
|
estimateMode=False
|
||||||
|
)
|
||||||
def handleAquedi4(filename, estimate):
|
# End Boilerplate
|
||||||
"""Main handler for Aquedi4 JSON files."""
|
|
||||||
global ESTIMATE, TOKENS, FILENAME
|
def handleAquedi4(filename, estimate):
|
||||||
ESTIMATE = estimate
|
"""Main handler for Aquedi4 JSON files."""
|
||||||
FILENAME = filename
|
global ESTIMATE, TOKENS, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
try:
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
try:
|
||||||
|
start = time.time()
|
||||||
if not estimate:
|
translatedData = openFiles(filename)
|
||||||
# Write final result after translation is complete
|
|
||||||
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
if not estimate:
|
||||||
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
# Write final result after translation is complete
|
||||||
|
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
end = time.time()
|
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
except Exception as e:
|
TOKENS[1] += translatedData[1][1]
|
||||||
traceback.print_exc()
|
|
||||||
return "Fail"
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def openFiles(filename):
|
|
||||||
"""Opens and parses the JSON file."""
|
|
||||||
with open("files/" + filename, "r", encoding="UTF-8-sig") as f:
|
def openFiles(filename):
|
||||||
data = json.load(f)
|
"""Opens and parses the JSON file."""
|
||||||
translatedData = parseAquedi4JSON(data, filename)
|
with open("files/" + filename, "r", encoding="UTF-8-sig") as f:
|
||||||
return translatedData
|
data = json.load(f)
|
||||||
|
translatedData = parseAquedi4JSON(data, filename)
|
||||||
|
return translatedData
|
||||||
def parseAquedi4JSON(data, filename):
|
|
||||||
"""Parses and translates the simple key-value JSON."""
|
|
||||||
totalTokens = [0, 0]
|
def parseAquedi4JSON(data, filename):
|
||||||
totalLines = 0
|
"""Parses and translates the simple key-value JSON."""
|
||||||
totalLines = len(data)
|
totalTokens = [0, 0]
|
||||||
global LOCK, PBAR
|
totalLines = 0
|
||||||
|
totalLines = len(data)
|
||||||
stringList = []
|
global LOCK, PBAR
|
||||||
keyList = []
|
|
||||||
if isinstance(data, dict) and all(isinstance(v, str) for v in data.values()):
|
stringList = []
|
||||||
for key, value in data.items():
|
keyList = []
|
||||||
if re.search(LANGREGEX, key):
|
if isinstance(data, dict) and all(isinstance(v, str) for v in data.values()):
|
||||||
stringList.append(key)
|
for key, value in data.items():
|
||||||
keyList.append(key)
|
if re.search(LANGREGEX, key):
|
||||||
|
stringList.append(key)
|
||||||
with tqdm(total=len(stringList), bar_format=BAR_FORMAT, position=0, leave=False) as pbar:
|
keyList.append(key)
|
||||||
pbar.desc = filename
|
|
||||||
PBAR = pbar
|
with tqdm(total=len(stringList), bar_format=BAR_FORMAT, position=0, leave=False) as pbar:
|
||||||
|
pbar.desc = filename
|
||||||
# from modules.json import translateSimpleKeyValueJSON
|
PBAR = pbar
|
||||||
|
|
||||||
try:
|
# from modules.json import translateSimpleKeyValueJSON
|
||||||
if stringList:
|
|
||||||
result = translateSimpleKeyValueJSON(data, filename, stringList, keyList)
|
try:
|
||||||
totalTokens[0] += result[0]
|
if stringList:
|
||||||
totalTokens[1] += result[1]
|
result = translateSimpleKeyValueJSON(data, filename, stringList, keyList)
|
||||||
else:
|
totalTokens[0] += result[0]
|
||||||
pbar.write(f"Warning: {filename} is not in the expected simple key-value format. Skipping.")
|
totalTokens[1] += result[1]
|
||||||
except Exception as e:
|
else:
|
||||||
traceback.print_exc()
|
pbar.write(f"Warning: {filename} is not in the expected simple key-value format. Skipping.")
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return [data, totalTokens, None]
|
return [data, totalTokens, e]
|
||||||
|
|
||||||
|
return [data, totalTokens, None]
|
||||||
def translateSimpleKeyValueJSON(data, filename, stringList, keyList):
|
|
||||||
"""
|
|
||||||
Translate simple key-value JSON format where keys contain Japanese text
|
def translateSimpleKeyValueJSON(data, filename, stringList, keyList):
|
||||||
and values are placeholder strings like "TODO".
|
"""
|
||||||
|
Translate simple key-value JSON format where keys contain Japanese text
|
||||||
Format example:
|
and values are placeholder strings like "TODO".
|
||||||
{
|
|
||||||
"\\r\\nスタビライザー": "TODO",
|
Format example:
|
||||||
"\\r\\nプラグインなし": "TODO"
|
{
|
||||||
}
|
"\\r\\nスタビライザー": "TODO",
|
||||||
"""
|
"\\r\\nプラグインなし": "TODO"
|
||||||
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
}
|
||||||
tokens = [0, 0]
|
"""
|
||||||
|
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
||||||
preparedStringList = []
|
tokens = [0, 0]
|
||||||
if REPLACE_LINEBREAKS_WITH_PIPES:
|
|
||||||
for s in stringList:
|
preparedStringList = []
|
||||||
preparedStringList.append(s.replace("\r\n", "|").replace("\n", "|"))
|
if REPLACE_LINEBREAKS_WITH_PIPES:
|
||||||
else:
|
for s in stringList:
|
||||||
preparedStringList = stringList
|
preparedStringList.append(s.replace("\r\n", "|").replace("\n", "|"))
|
||||||
|
else:
|
||||||
# Translate all keys if any were found
|
preparedStringList = stringList
|
||||||
if preparedStringList:
|
|
||||||
PBAR.total = len(preparedStringList)
|
# Translate all keys if any were found
|
||||||
PBAR.refresh()
|
if preparedStringList:
|
||||||
|
PBAR.total = len(preparedStringList)
|
||||||
response = translateAI(preparedStringList, "Reply with the English Translation")
|
PBAR.refresh()
|
||||||
tokens[0] += response[1][0]
|
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(preparedStringList, "Reply with the English Translation")
|
||||||
translatedList = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Check for mismatch
|
translatedList = response[0]
|
||||||
if len(preparedStringList) != len(translatedList):
|
|
||||||
with LOCK:
|
# Check for mismatch
|
||||||
if FILENAME not in MISMATCH:
|
if len(preparedStringList) != len(translatedList):
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
|
if FILENAME not in MISMATCH:
|
||||||
# Pass 2: Update the values with translations
|
MISMATCH.append(FILENAME)
|
||||||
for i, original_key in enumerate(keyList):
|
|
||||||
if i < len(translatedList):
|
# Pass 2: Update the values with translations
|
||||||
translatedText = translatedList[i]
|
for i, original_key in enumerate(keyList):
|
||||||
|
if i < len(translatedList):
|
||||||
if REPLACE_LINEBREAKS_WITH_PIPES:
|
translatedText = translatedList[i]
|
||||||
translatedText = translatedText.replace("|", "\r\n")
|
|
||||||
# Set the translated text as the value
|
if REPLACE_LINEBREAKS_WITH_PIPES:
|
||||||
data[original_key] = translatedText
|
translatedText = translatedText.replace("|", "\r\n")
|
||||||
|
# Set the translated text as the value
|
||||||
# Save progress after each translation
|
data[original_key] = translatedText
|
||||||
save_progress_json(data, filename)
|
|
||||||
|
# Save progress after each translation
|
||||||
return tokens
|
save_progress_json(data, filename)
|
||||||
|
|
||||||
def save_progress_json(data, filename):
|
return tokens
|
||||||
"""Save current JSON translation progress."""
|
|
||||||
try:
|
def save_progress_json(data, filename):
|
||||||
if ESTIMATE: return
|
"""Save current JSON translation progress."""
|
||||||
os.makedirs("translated", exist_ok=True)
|
try:
|
||||||
tmp_path = os.path.join("translated", f"{filename}.tmp")
|
if ESTIMATE: return
|
||||||
final_path = os.path.join("translated", filename)
|
os.makedirs("translated", exist_ok=True)
|
||||||
with open(tmp_path, "w", encoding="utf-8", newline="\n") as outFile:
|
tmp_path = os.path.join("translated", f"{filename}.tmp")
|
||||||
json.dump(data, outFile, ensure_ascii=False, indent=4)
|
final_path = os.path.join("translated", filename)
|
||||||
os.replace(tmp_path, final_path)
|
with open(tmp_path, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
except Exception:
|
json.dump(data, outFile, ensure_ascii=False, indent=4)
|
||||||
traceback.print_exc()
|
os.replace(tmp_path, final_path)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
|
||||||
totalTokenstring = (
|
def getResultString(translatedData, translationTime, filename):
|
||||||
Fore.YELLOW + f"[Input: {translatedData[1][0]}]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
f"[Output: {translatedData[1][1]}]"
|
totalTokenstring = (
|
||||||
f"[Cost: ${cost:,.4f}]"
|
Fore.YELLOW + f"[Input: {translatedData[1][0]}]"
|
||||||
)
|
f"[Output: {translatedData[1][1]}]"
|
||||||
timeString = Fore.BLUE + f"[{round(translationTime, 1)}s]"
|
f"[Cost: ${cost:,.4f}]"
|
||||||
|
)
|
||||||
if translatedData[2] is None:
|
timeString = Fore.BLUE + f"[{round(translationTime, 1)}s]"
|
||||||
return f"{filename}: {totalTokenstring}{timeString}" + Fore.GREEN + " \u2713 " + Fore.RESET
|
|
||||||
else:
|
if translatedData[2] is None:
|
||||||
errorString = str(translatedData[2]) + Fore.RED
|
return f"{filename}: {totalTokenstring}{timeString}" + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
return f"{filename}: {totalTokenstring}{timeString}" + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
else:
|
||||||
|
errorString = str(translatedData[2]) + Fore.RED
|
||||||
def translateAI(text, history, history_ctx=None):
|
return f"{filename}: {totalTokenstring}{timeString}" + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
"""Legacy wrapper for the shared translation utility."""
|
|
||||||
global PBAR, MISMATCH, FILENAME
|
def translateAI(text, history, history_ctx=None):
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
"""Legacy wrapper for the shared translation utility."""
|
||||||
return sharedtranslateAI(
|
global PBAR, MISMATCH, FILENAME
|
||||||
text=text,
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
)
|
)
|
||||||
918
modules/csv.py
918
modules/csv.py
|
|
@ -1,458 +1,460 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
import csv
|
import csv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = int(os.getenv("noteWidth"))
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = int(os.getenv("noteWidth"))
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = True # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that thdata a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
FILENAME = None
|
IGNORETLTEXT = True # Ignores all translated text.
|
||||||
BRACKETNAMES = False
|
MISMATCH = [] # Lists files that thdata a mismatch error (Length of GPT list response is wrong)
|
||||||
|
FILENAME = None
|
||||||
# CSV Configuration Settings (configurable via GUI)
|
BRACKETNAMES = False
|
||||||
CSV_DELIMITER = " " # CSV delimiter character (comma, semicolon, tab)
|
|
||||||
SOURCE_COLUMN = 2 # Which column has the source text to translate (0-indexed)
|
# CSV Configuration Settings (configurable via GUI)
|
||||||
TARGET_COLUMN = 3 # Which column to write translations to
|
CSV_DELIMITER = " " # CSV delimiter character (comma, semicolon, tab)
|
||||||
SPEAKER_COLUMN = 1 # Which column has speaker names (-1 = none)
|
SOURCE_COLUMN = 2 # Which column has the source text to translate (0-indexed)
|
||||||
SKIP_HEADER_ROW = False # Skip the first row (header)
|
TARGET_COLUMN = 3 # Which column to write translations to
|
||||||
USE_TARGET_IF_NOT_EMPTY = False # Use target column text if not empty (T++ style)
|
SPEAKER_COLUMN = 1 # Which column has speaker names (-1 = none)
|
||||||
WRITE_TO_NEXT_COLUMN = False # Write to column after target instead of overwriting
|
SKIP_HEADER_ROW = False # Skip the first row (header)
|
||||||
PARSE_NAME_TAGS = False # Parse :name[] tags in text
|
USE_TARGET_IF_NOT_EMPTY = False # Use target column text if not empty (T++ style)
|
||||||
PARSE_M_MARKERS = False # Parse \M markers in text
|
WRITE_TO_NEXT_COLUMN = False # Write to column after target instead of overwriting
|
||||||
REMOVE_FURIGANA = True # Remove furigana annotations <=>
|
PARSE_NAME_TAGS = False # Parse :name[] tags in text
|
||||||
SKIP_COMMENT_ROWS = False # Skip rows starting with 'comment'
|
PARSE_M_MARKERS = False # Parse \M markers in text
|
||||||
|
REMOVE_FURIGANA = True # Remove furigana annotations <=>
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
SKIP_COMMENT_ROWS = False # Skip rows starting with 'comment'
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# tqdm Globals
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
ENCODING = "utf8"
|
LEAVE = False
|
||||||
|
PBAR = None
|
||||||
# Initialize Translation Config
|
ENCODING = "utf8"
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleCSV(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, TOKENS, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleCSV(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, TOKENS, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
if not ESTIMATE:
|
FILENAME = filename
|
||||||
with open("translated/" + filename, "w+t", newline="", encoding=ENCODING, errors="xmlcharrefreplace") as writeFile:
|
|
||||||
# Translate
|
if not ESTIMATE:
|
||||||
start = time.time()
|
with open("translated/" + filename, "w+t", newline="", encoding=ENCODING, errors="xmlcharrefreplace") as writeFile:
|
||||||
translatedData = openFiles(filename, writeFile)
|
# Translate
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename, writeFile)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
else:
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Translate
|
TOKENS[1] += translatedData[1][1]
|
||||||
start = time.time()
|
else:
|
||||||
translatedData = openFilesEstimate(filename)
|
# Translate
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFilesEstimate(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
|
return totalString
|
||||||
def openFiles(filename, writeFile):
|
|
||||||
with open("files/" + filename, "r", encoding=ENCODING) as readFile, writeFile:
|
|
||||||
translatedData = parseCSV(readFile, writeFile, filename)
|
def openFiles(filename, writeFile):
|
||||||
|
with open("files/" + filename, "r", encoding=ENCODING) as readFile, writeFile:
|
||||||
return translatedData
|
translatedData = parseCSV(readFile, writeFile, filename)
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def openFilesEstimate(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
|
||||||
translatedData = parseCSV(readFile, "", filename)
|
def openFilesEstimate(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
||||||
return translatedData
|
translatedData = parseCSV(readFile, "", filename)
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] is None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] is None:
|
||||||
else:
|
# Success
|
||||||
# Fail
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def parseCSV(readFile, writeFile, filename):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
totalLines = 0
|
def parseCSV(readFile, writeFile, filename):
|
||||||
global LOCK
|
totalTokens = [0, 0]
|
||||||
|
totalLines = 0
|
||||||
# Get total for progress bar
|
global LOCK
|
||||||
totalLines = len(readFile.readlines())
|
|
||||||
readFile.seek(0)
|
# Get total for progress bar
|
||||||
|
totalLines = len(readFile.readlines())
|
||||||
reader = csv.reader(readFile, delimiter=CSV_DELIMITER)
|
readFile.seek(0)
|
||||||
if not ESTIMATE:
|
|
||||||
writer = csv.writer(
|
reader = csv.reader(readFile, delimiter=CSV_DELIMITER)
|
||||||
writeFile,
|
if not ESTIMATE:
|
||||||
delimiter=CSV_DELIMITER,
|
writer = csv.writer(
|
||||||
)
|
writeFile,
|
||||||
else:
|
delimiter=CSV_DELIMITER,
|
||||||
writer = ""
|
)
|
||||||
|
else:
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar:
|
writer = ""
|
||||||
pbar.desc = filename
|
|
||||||
pbar.total = totalLines
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar:
|
||||||
|
pbar.desc = filename
|
||||||
# Grab All Rows
|
pbar.total = totalLines
|
||||||
data = []
|
|
||||||
for row in reader:
|
# Grab All Rows
|
||||||
data.append(row)
|
data = []
|
||||||
|
for row in reader:
|
||||||
try:
|
data.append(row)
|
||||||
response = translateCSV(data, pbar, writeFile, writer, filename, None)
|
|
||||||
totalTokens[0] = response[0]
|
try:
|
||||||
totalTokens[1] = response[1]
|
response = translateCSV(data, pbar, writeFile, writer, filename, None)
|
||||||
except Exception:
|
totalTokens[0] = response[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] = response[1]
|
||||||
return [data, totalTokens, None]
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, None]
|
||||||
def flush_progress_csv(writeFile, writer, rows):
|
|
||||||
"""Flush current CSV progress to the already-open output file (Windows-safe)."""
|
|
||||||
try:
|
def flush_progress_csv(writeFile, writer, rows):
|
||||||
if ESTIMATE or writeFile is None:
|
"""Flush current CSV progress to the already-open output file (Windows-safe)."""
|
||||||
return
|
try:
|
||||||
with LOCK:
|
if ESTIMATE or writeFile is None:
|
||||||
writeFile.seek(0)
|
return
|
||||||
# Recreate writer at current position to avoid state issues
|
with LOCK:
|
||||||
tmp_writer = csv.writer(writeFile, delimiter=CSV_DELIMITER)
|
writeFile.seek(0)
|
||||||
tmp_writer.writerows(rows)
|
# Recreate writer at current position to avoid state issues
|
||||||
writeFile.truncate()
|
tmp_writer = csv.writer(writeFile, delimiter=CSV_DELIMITER)
|
||||||
writeFile.flush()
|
tmp_writer.writerows(rows)
|
||||||
os.fsync(writeFile.fileno())
|
writeFile.truncate()
|
||||||
except Exception:
|
writeFile.flush()
|
||||||
traceback.print_exc()
|
os.fsync(writeFile.fileno())
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateCSV(data, pbar, writeFile, writer, filename, translatedList):
|
|
||||||
"""
|
|
||||||
Unified CSV translation function using configurable settings.
|
def translateCSV(data, pbar, writeFile, writer, filename, translatedList):
|
||||||
|
"""
|
||||||
Uses global settings:
|
Unified CSV translation function using configurable settings.
|
||||||
- SOURCE_COLUMN: column index for source text
|
|
||||||
- TARGET_COLUMN: column index to write translations
|
Uses global settings:
|
||||||
- SPEAKER_COLUMN: column index for speaker names (-1 = none)
|
- SOURCE_COLUMN: column index for source text
|
||||||
- SKIP_HEADER_ROW: whether to skip first row
|
- TARGET_COLUMN: column index to write translations
|
||||||
- USE_TARGET_IF_NOT_EMPTY: use existing target text if present (T++ style)
|
- SPEAKER_COLUMN: column index for speaker names (-1 = none)
|
||||||
- WRITE_TO_NEXT_COLUMN: write to column after target
|
- SKIP_HEADER_ROW: whether to skip first row
|
||||||
- PARSE_NAME_TAGS: parse :name[] tags
|
- USE_TARGET_IF_NOT_EMPTY: use existing target text if present (T++ style)
|
||||||
- PARSE_M_MARKERS: parse \\M markers
|
- WRITE_TO_NEXT_COLUMN: write to column after target
|
||||||
- REMOVE_FURIGANA: remove furigana
|
- PARSE_NAME_TAGS: parse :name[] tags
|
||||||
- SKIP_COMMENT_ROWS: skip rows with 'comment' in first column
|
- PARSE_M_MARKERS: parse \\M markers
|
||||||
"""
|
- REMOVE_FURIGANA: remove furigana
|
||||||
global LOCK, ESTIMATE, PBAR
|
- SKIP_COMMENT_ROWS: skip rows with 'comment' in first column
|
||||||
PBAR = pbar
|
"""
|
||||||
translatedText = ""
|
global LOCK, ESTIMATE, PBAR
|
||||||
totalTokens = [0, 0]
|
PBAR = pbar
|
||||||
i = 0
|
translatedText = ""
|
||||||
stringList = []
|
totalTokens = [0, 0]
|
||||||
|
i = 0
|
||||||
try:
|
stringList = []
|
||||||
# Translate
|
|
||||||
while i < len(data):
|
try:
|
||||||
# Skip header row if configured
|
# Translate
|
||||||
if SKIP_HEADER_ROW and i == 0:
|
while i < len(data):
|
||||||
i += 1
|
# Skip header row if configured
|
||||||
continue
|
if SKIP_HEADER_ROW and i == 0:
|
||||||
|
i += 1
|
||||||
# Skip comment rows if configured
|
continue
|
||||||
if SKIP_COMMENT_ROWS and len(data[i]) > 0 and 'comment' in str(data[i][0]).lower():
|
|
||||||
i += 1
|
# Skip comment rows if configured
|
||||||
continue
|
if SKIP_COMMENT_ROWS and len(data[i]) > 0 and 'comment' in str(data[i][0]).lower():
|
||||||
|
i += 1
|
||||||
# Check if row has enough columns
|
continue
|
||||||
if len(data[i]) <= SOURCE_COLUMN:
|
|
||||||
i += 1
|
# Check if row has enough columns
|
||||||
continue
|
if len(data[i]) <= SOURCE_COLUMN:
|
||||||
|
i += 1
|
||||||
# Get source text
|
continue
|
||||||
jaString = ""
|
|
||||||
speaker = ""
|
# Get source text
|
||||||
|
jaString = ""
|
||||||
# If USE_TARGET_IF_NOT_EMPTY is enabled (T++ style), check target column first
|
speaker = ""
|
||||||
if USE_TARGET_IF_NOT_EMPTY and len(data[i]) > TARGET_COLUMN and data[i][TARGET_COLUMN]:
|
|
||||||
jaString = data[i][TARGET_COLUMN]
|
# If USE_TARGET_IF_NOT_EMPTY is enabled (T++ style), check target column first
|
||||||
else:
|
if USE_TARGET_IF_NOT_EMPTY and len(data[i]) > TARGET_COLUMN and data[i][TARGET_COLUMN]:
|
||||||
jaString = data[i][SOURCE_COLUMN] if data[i][SOURCE_COLUMN] else ""
|
jaString = data[i][TARGET_COLUMN]
|
||||||
|
else:
|
||||||
# Skip empty strings
|
jaString = data[i][SOURCE_COLUMN] if data[i][SOURCE_COLUMN] else ""
|
||||||
if not jaString:
|
|
||||||
i += 1
|
# Skip empty strings
|
||||||
continue
|
if not jaString:
|
||||||
|
i += 1
|
||||||
# Handle speaker column if configured
|
continue
|
||||||
if SPEAKER_COLUMN >= 0 and len(data[i]) > SPEAKER_COLUMN and data[i][SPEAKER_COLUMN]:
|
|
||||||
speakerResponse = getSpeaker(data[i][SPEAKER_COLUMN])
|
# Handle speaker column if configured
|
||||||
totalTokens[0] += speakerResponse[1][0]
|
if SPEAKER_COLUMN >= 0 and len(data[i]) > SPEAKER_COLUMN and data[i][SPEAKER_COLUMN]:
|
||||||
totalTokens[1] += speakerResponse[1][1]
|
speakerResponse = getSpeaker(data[i][SPEAKER_COLUMN])
|
||||||
speaker = speakerResponse[0]
|
totalTokens[0] += speakerResponse[1][0]
|
||||||
data[i][SPEAKER_COLUMN] = speaker
|
totalTokens[1] += speakerResponse[1][1]
|
||||||
|
speaker = speakerResponse[0]
|
||||||
# Parse :name[] tags if configured
|
data[i][SPEAKER_COLUMN] = speaker
|
||||||
if PARSE_NAME_TAGS and ':name' in jaString:
|
|
||||||
match = re.search(r":name\[([^\]]+?)\]\n([\w\W]*)", jaString)
|
# Parse :name[] tags if configured
|
||||||
if match:
|
if PARSE_NAME_TAGS and ':name' in jaString:
|
||||||
# Translate speaker name
|
match = re.search(r":name\[([^\]]+?)\]\n([\w\W]*)", jaString)
|
||||||
response = getSpeaker(match.group(1))
|
if match:
|
||||||
speaker = response[0]
|
# Translate speaker name
|
||||||
totalTokens[0] += response[1][0]
|
response = getSpeaker(match.group(1))
|
||||||
totalTokens[1] += response[1][1]
|
speaker = response[0]
|
||||||
data[i][SOURCE_COLUMN] = data[i][SOURCE_COLUMN].replace(match.group(1), speaker)
|
totalTokens[0] += response[1][0]
|
||||||
|
totalTokens[1] += response[1][1]
|
||||||
# Extract text portion
|
data[i][SOURCE_COLUMN] = data[i][SOURCE_COLUMN].replace(match.group(1), speaker)
|
||||||
jaString = match.group(2)
|
|
||||||
# Remove voice markers
|
# Extract text portion
|
||||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
jaString = match.group(2)
|
||||||
if voMatch:
|
# Remove voice markers
|
||||||
jaString = jaString.replace(voMatch.group(0), "")
|
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||||
|
if voMatch:
|
||||||
# Parse \M markers if configured
|
jaString = jaString.replace(voMatch.group(0), "")
|
||||||
if PARSE_M_MARKERS and '\\M' in jaString:
|
|
||||||
match = re.search(r"\\M.+\n([\w\W]*)", jaString)
|
# Parse \M markers if configured
|
||||||
if match:
|
if PARSE_M_MARKERS and '\\M' in jaString:
|
||||||
jaString = match.group(1)
|
match = re.search(r"\\M.+\n([\w\W]*)", jaString)
|
||||||
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
if match:
|
||||||
if voMatch:
|
jaString = match.group(1)
|
||||||
jaString = jaString.replace(voMatch.group(0), "")
|
voMatch = re.search(r"\\[vfF]+\[.+]", jaString)
|
||||||
|
if voMatch:
|
||||||
# Remove furigana if configured
|
jaString = jaString.replace(voMatch.group(0), "")
|
||||||
if REMOVE_FURIGANA:
|
|
||||||
jaString = re.sub(r"<(.*)=.*>", r"\1", jaString)
|
# Remove furigana if configured
|
||||||
|
if REMOVE_FURIGANA:
|
||||||
# Store original for replacement
|
jaString = re.sub(r"<(.*)=.*>", r"\1", jaString)
|
||||||
ojaString = jaString
|
|
||||||
|
# Store original for replacement
|
||||||
# Remove textwrap
|
ojaString = jaString
|
||||||
jaString = jaString.replace("\\n", " ")
|
|
||||||
jaString = jaString.replace("\n", " ")
|
# Remove textwrap
|
||||||
|
jaString = jaString.replace("\\n", " ")
|
||||||
# Pass 1: Collect strings
|
jaString = jaString.replace("\n", " ")
|
||||||
if not translatedList:
|
|
||||||
if speaker:
|
# Pass 1: Collect strings
|
||||||
stringList.append(f"[{speaker}]: {jaString}")
|
if not translatedList:
|
||||||
else:
|
if speaker:
|
||||||
stringList.append(jaString)
|
stringList.append(f"[{speaker}]: {jaString}")
|
||||||
|
else:
|
||||||
# Pass 2: Apply translations
|
stringList.append(jaString)
|
||||||
else:
|
|
||||||
# Grab and pop translation
|
# Pass 2: Apply translations
|
||||||
translatedText = translatedList[0]
|
else:
|
||||||
translatedList.pop(0)
|
# Grab and pop translation
|
||||||
|
translatedText = translatedList[0]
|
||||||
# Remove speaker prefix from translation if present
|
translatedList.pop(0)
|
||||||
if speaker:
|
|
||||||
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
# Remove speaker prefix from translation if present
|
||||||
|
if speaker:
|
||||||
# Add wordwrap
|
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
||||||
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
|
||||||
translatedText = translatedText.replace("\n", "\\n")
|
# Add wordwrap
|
||||||
|
translatedText = dazedwrap.wrapText(translatedText, WIDTH)
|
||||||
# Determine target column
|
translatedText = translatedText.replace("\n", "\\n")
|
||||||
actual_target = TARGET_COLUMN + 1 if WRITE_TO_NEXT_COLUMN else TARGET_COLUMN
|
|
||||||
|
# Determine target column
|
||||||
# Ensure row has enough columns
|
actual_target = TARGET_COLUMN + 1 if WRITE_TO_NEXT_COLUMN else TARGET_COLUMN
|
||||||
while len(data[i]) <= actual_target:
|
|
||||||
data[i].append("")
|
# Ensure row has enough columns
|
||||||
|
while len(data[i]) <= actual_target:
|
||||||
# Set data
|
data[i].append("")
|
||||||
if PARSE_NAME_TAGS or PARSE_M_MARKERS:
|
|
||||||
# Replace original text portion
|
# Set data
|
||||||
data[i][actual_target] = data[i][actual_target].replace(ojaString, translatedText) if data[i][actual_target] else translatedText
|
if PARSE_NAME_TAGS or PARSE_M_MARKERS:
|
||||||
else:
|
# Replace original text portion
|
||||||
data[i][actual_target] = translatedText
|
data[i][actual_target] = data[i][actual_target].replace(ojaString, translatedText) if data[i][actual_target] else translatedText
|
||||||
|
else:
|
||||||
flush_progress_csv(writeFile, writer, data)
|
data[i][actual_target] = translatedText
|
||||||
|
|
||||||
# Iterate
|
flush_progress_csv(writeFile, writer, data)
|
||||||
i += 1
|
|
||||||
|
# Iterate
|
||||||
# EOF - Process collected strings
|
i += 1
|
||||||
if len(stringList) > 0:
|
|
||||||
# Set Progress
|
# EOF - Process collected strings
|
||||||
pbar.total = len(stringList)
|
if len(stringList) > 0:
|
||||||
pbar.refresh()
|
# Set Progress
|
||||||
|
pbar.total = len(stringList)
|
||||||
# Translate
|
pbar.refresh()
|
||||||
response = translateAI(stringList, "")
|
|
||||||
totalTokens[0] += response[1][0]
|
# Translate
|
||||||
totalTokens[1] += response[1][1]
|
response = translateAI(stringList, "")
|
||||||
translatedList = response[0]
|
totalTokens[0] += response[1][0]
|
||||||
|
totalTokens[1] += response[1][1]
|
||||||
# Set Strings
|
translatedList = response[0]
|
||||||
if len(stringList) == len(translatedList):
|
|
||||||
translateCSV(data, pbar, writeFile, writer, filename, translatedList)
|
# Set Strings
|
||||||
# Mismatch
|
if len(stringList) == len(translatedList):
|
||||||
else:
|
translateCSV(data, pbar, writeFile, writer, filename, translatedList)
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if filename not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(filename)
|
with LOCK:
|
||||||
|
if filename not in MISMATCH:
|
||||||
# Write all Data
|
MISMATCH.append(filename)
|
||||||
with LOCK:
|
|
||||||
if not ESTIMATE:
|
# Write all Data
|
||||||
for row in data:
|
with LOCK:
|
||||||
writer.writerow(row)
|
if not ESTIMATE:
|
||||||
flush_progress_csv(writeFile, writer, data)
|
for row in data:
|
||||||
|
writer.writerow(row)
|
||||||
except Exception:
|
flush_progress_csv(writeFile, writer, data)
|
||||||
traceback.print_exc()
|
|
||||||
|
except Exception:
|
||||||
# Write all Data
|
traceback.print_exc()
|
||||||
with LOCK:
|
|
||||||
if not ESTIMATE:
|
# Write all Data
|
||||||
for row in data:
|
with LOCK:
|
||||||
writer.writerow(row)
|
if not ESTIMATE:
|
||||||
flush_progress_csv(writeFile, writer, data)
|
for row in data:
|
||||||
return totalTokens
|
writer.writerow(row)
|
||||||
|
flush_progress_csv(writeFile, writer, data)
|
||||||
return totalTokens
|
return totalTokens
|
||||||
|
|
||||||
# Save some money and enter the character before translation
|
return totalTokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
if speaker == "???":
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
return ["???", [0, 0]]
|
|
||||||
|
if speaker == "???":
|
||||||
# Translate and Store Speaker
|
return ["???", [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,439 +1,441 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
PBAR = None
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
LOCK = threading.Lock()
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
PBAR = None
|
||||||
NOTEWIDTH = int(os.getenv("noteWidth"))
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = int(os.getenv("noteWidth"))
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
TOKENS = [0, 0]
|
||||||
FILENAME = None
|
NAMESLIST = []
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
FILENAME = None
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
# tqdm Globals
|
LEAVE = False
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
|
POSITION = 0
|
||||||
|
LEAVE = False
|
||||||
def handleImages(folderName, estimate):
|
|
||||||
global ESTIMATE, TOKENS, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleImages(folderName, estimate):
|
||||||
FILENAME = folderName
|
global ESTIMATE, TOKENS, FILENAME
|
||||||
start = time.time()
|
ESTIMATE = estimate
|
||||||
|
FILENAME = folderName
|
||||||
# Translate Strings
|
start = time.time()
|
||||||
translatedData = openFiles(f"files/{folderName}")
|
|
||||||
|
# Translate Strings
|
||||||
# Custom Names
|
translatedData = openFiles(f"files/{folderName}")
|
||||||
# customList = [[], []]
|
|
||||||
# customList = processImagesDir("Custom", customList)
|
# Custom Names
|
||||||
|
# customList = [[], []]
|
||||||
# Write TL To Images
|
# customList = processImagesDir("Custom", customList)
|
||||||
try:
|
|
||||||
translatedList, originalList, dimensionsList = translatedData[0]
|
# Write TL To Images
|
||||||
for i in range(len(translatedList)):
|
try:
|
||||||
try:
|
translatedList, originalList, dimensionsList = translatedData[0]
|
||||||
# Create image from string
|
for i in range(len(translatedList)):
|
||||||
image = stringToImageOutline(translatedList[i], dimensionsList[i][0], dimensionsList[i][1])
|
try:
|
||||||
# Save image using the corresponding original filename
|
# Create image from string
|
||||||
image.save(rf"translated/{folderName}/{originalList[i]}.png", quality=100)
|
image = stringToImageOutline(translatedList[i], dimensionsList[i][0], dimensionsList[i][1])
|
||||||
except Exception as e:
|
# Save image using the corresponding original filename
|
||||||
# Log error if image saving fails
|
image.save(rf"translated/{folderName}/{originalList[i]}.png", quality=100)
|
||||||
PBAR.write(f"Error processing {translatedList[i]}: {str(e)}")
|
except Exception as e:
|
||||||
except IndexError:
|
# Log error if image saving fails
|
||||||
PBAR.write("Translated data is incomplete. Please check your input.")
|
PBAR.write(f"Error processing {translatedList[i]}: {str(e)}")
|
||||||
|
except IndexError:
|
||||||
# Print File
|
PBAR.write("Translated data is incomplete. Please check your input.")
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, folderName))
|
# Print File
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, folderName))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
|
return totalString
|
||||||
def openFiles(folderName):
|
|
||||||
global PBAR
|
|
||||||
|
def openFiles(folderName):
|
||||||
if os.path.isdir(folderName):
|
global PBAR
|
||||||
imageList = [[], [], []]
|
|
||||||
imageList = processImagesDir(folderName, imageList)
|
if os.path.isdir(folderName):
|
||||||
|
imageList = [[], [], []]
|
||||||
# Start Translation
|
imageList = processImagesDir(folderName, imageList)
|
||||||
with tqdm(
|
|
||||||
bar_format=BAR_FORMAT,
|
# Start Translation
|
||||||
position=POSITION,
|
with tqdm(
|
||||||
leave=LEAVE,
|
bar_format=BAR_FORMAT,
|
||||||
desc=folderName,
|
position=POSITION,
|
||||||
total=len(imageList[0]),
|
leave=LEAVE,
|
||||||
) as PBAR:
|
desc=folderName,
|
||||||
translatedData = translateImages(imageList)
|
total=len(imageList[0]),
|
||||||
translatedData = [
|
) as PBAR:
|
||||||
[translatedData[0], imageList[2], imageList[1]],
|
translatedData = translateImages(imageList)
|
||||||
translatedData[1],
|
translatedData = [
|
||||||
translatedData[2],
|
[translatedData[0], imageList[2], imageList[1]],
|
||||||
]
|
translatedData[1],
|
||||||
|
translatedData[2],
|
||||||
return translatedData
|
]
|
||||||
else:
|
|
||||||
print("The provided directory path does not exist.")
|
return translatedData
|
||||||
|
else:
|
||||||
|
print("The provided directory path does not exist.")
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] is None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] is None:
|
||||||
else:
|
# Success
|
||||||
# Fail
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def getFontSize(text, image_width, image_height, font_path):
|
|
||||||
# Start with a high font size and keep reducing it until the text fits within the image bounds
|
|
||||||
font_size = min(image_width, image_height)
|
def getFontSize(text, image_width, image_height, font_path):
|
||||||
|
# Start with a high font size and keep reducing it until the text fits within the image bounds
|
||||||
while font_size > 0:
|
font_size = min(image_width, image_height)
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
|
||||||
text_bbox = ImageDraw.Draw(Image.new("RGB", (1, 1))).textbbox((0, 0), text, font=font)
|
while font_size > 0:
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
text_height = text_bbox[3] - text_bbox[1]
|
text_bbox = ImageDraw.Draw(Image.new("RGB", (1, 1))).textbbox((0, 0), text, font=font)
|
||||||
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
if text_width <= image_width and text_height <= image_height:
|
text_height = text_bbox[3] - text_bbox[1]
|
||||||
return font_size
|
|
||||||
font_size -= 1
|
if text_width <= image_width and text_height <= image_height:
|
||||||
|
return font_size
|
||||||
return font_size
|
font_size -= 1
|
||||||
|
|
||||||
|
return font_size
|
||||||
def stringToImage(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
|
||||||
# Increase the resolution
|
|
||||||
scaled_width = int(width * scale_factor)
|
def stringToImage(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
||||||
scaled_height = int(height * scale_factor)
|
# Increase the resolution
|
||||||
|
scaled_width = int(width * scale_factor)
|
||||||
# Find the appropriate font size for the scaled up image
|
scaled_height = int(height * scale_factor)
|
||||||
font_size = getFontSize(text, scaled_width, scaled_height, font_path)
|
|
||||||
if font_size == 0:
|
# Find the appropriate font size for the scaled up image
|
||||||
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
font_size = getFontSize(text, scaled_width, scaled_height, font_path)
|
||||||
|
if font_size == 0:
|
||||||
# Create a new image with the scaled width and height and a transparent background
|
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
||||||
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
|
||||||
|
# Create a new image with the scaled width and height and a transparent background
|
||||||
# Create a drawing context
|
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
|
# Create a drawing context
|
||||||
# Load the appropriate font
|
draw = ImageDraw.Draw(image)
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
|
||||||
|
# Load the appropriate font
|
||||||
# Calculate the size of the text to center it
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
# Calculate the size of the text to center it
|
||||||
text_height = text_bbox[3] - text_bbox[1] + 20
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
x = 0
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
|
text_height = text_bbox[3] - text_bbox[1] + 20
|
||||||
x = (scaled_width - text_width) // 2
|
x = 0
|
||||||
y = (scaled_height - text_height) // 2
|
|
||||||
|
x = (scaled_width - text_width) // 2
|
||||||
# Draw the text on the image
|
y = (scaled_height - text_height) // 2
|
||||||
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
|
||||||
|
# Draw the text on the image
|
||||||
# Resize back to the original dimensions to get a clearer text rendering
|
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
||||||
image = image.resize(
|
|
||||||
(width, height),
|
# Resize back to the original dimensions to get a clearer text rendering
|
||||||
Image.LANCZOS,
|
image = image.resize(
|
||||||
)
|
(width, height),
|
||||||
|
Image.LANCZOS,
|
||||||
return image
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
def stringToImageOutline(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
|
||||||
# Outline
|
|
||||||
outline_color = (255, 255, 255, 255)
|
def stringToImageOutline(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
||||||
text_color = (0, 0, 0, 255)
|
# Outline
|
||||||
outline_thickness = 4
|
outline_color = (255, 255, 255, 255)
|
||||||
|
text_color = (0, 0, 0, 255)
|
||||||
# Increase the resolution
|
outline_thickness = 4
|
||||||
scaled_width = int(width * scale_factor)
|
|
||||||
scaled_height = int(height * scale_factor)
|
# Increase the resolution
|
||||||
|
scaled_width = int(width * scale_factor)
|
||||||
# Find the appropriate font size for the scaled up image
|
scaled_height = int(height * scale_factor)
|
||||||
font_size = getFontSize(text, scaled_width, scaled_height, font_path)
|
|
||||||
if font_size == 0:
|
# Find the appropriate font size for the scaled up image
|
||||||
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
font_size = getFontSize(text, scaled_width, scaled_height, font_path)
|
||||||
|
if font_size == 0:
|
||||||
# Create a new image with the scaled width and height and a transparent background
|
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
||||||
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
|
||||||
|
# Create a new image with the scaled width and height and a transparent background
|
||||||
# Create a drawing context
|
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
|
# Create a drawing context
|
||||||
# Load the appropriate font
|
draw = ImageDraw.Draw(image)
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
|
||||||
|
# Load the appropriate font
|
||||||
# Calculate the size of the text to center it
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
# Calculate the size of the text to center it
|
||||||
text_height = text_bbox[3] - text_bbox[1] + 20
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
x = (scaled_width - text_width) // 2
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
y = (scaled_height - text_height) // 2
|
text_height = text_bbox[3] - text_bbox[1] + 20
|
||||||
|
x = (scaled_width - text_width) // 2
|
||||||
# Draw the text outline by applying the text multiple times with small offsets
|
y = (scaled_height - text_height) // 2
|
||||||
for dx in range(-outline_thickness, outline_thickness + 1):
|
|
||||||
for dy in range(-outline_thickness, outline_thickness + 1):
|
# Draw the text outline by applying the text multiple times with small offsets
|
||||||
if dx != 0 or dy != 0:
|
for dx in range(-outline_thickness, outline_thickness + 1):
|
||||||
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
|
for dy in range(-outline_thickness, outline_thickness + 1):
|
||||||
|
if dx != 0 or dy != 0:
|
||||||
# Draw the main text
|
draw.text((x + dx, y + dy), text, font=font, fill=outline_color)
|
||||||
draw.text((x, y), text, font=font, fill=text_color)
|
|
||||||
|
# Draw the main text
|
||||||
# Resize back to the original dimensions to get a clearer text rendering
|
draw.text((x, y), text, font=font, fill=text_color)
|
||||||
image = image.resize((width, height), Image.LANCZOS)
|
|
||||||
|
# Resize back to the original dimensions to get a clearer text rendering
|
||||||
return image
|
image = image.resize((width, height), Image.LANCZOS)
|
||||||
|
|
||||||
|
return image
|
||||||
def stringToImageBox(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
|
||||||
# Increase the resolution
|
|
||||||
scaled_width = int(width * scale_factor)
|
def stringToImageBox(text, width, height, font_path="fonts/TsunagiGothic.ttf", scale_factor=4):
|
||||||
scaled_height = int(height * scale_factor)
|
# Increase the resolution
|
||||||
|
scaled_width = int(width * scale_factor)
|
||||||
# Padding around the text
|
scaled_height = int(height * scale_factor)
|
||||||
padding = 10
|
|
||||||
|
# Padding around the text
|
||||||
# Calculate the dimensions available for text placement
|
padding = 10
|
||||||
available_width = scaled_width - 2 * padding
|
|
||||||
available_height = scaled_height - 2 * padding
|
# Calculate the dimensions available for text placement
|
||||||
|
available_width = scaled_width - 2 * padding
|
||||||
# Determine the best font size to fit within the available dimensions
|
available_height = scaled_height - 2 * padding
|
||||||
font_size = getFontSize(text, available_width, available_height, font_path)
|
|
||||||
if font_size <= 0:
|
# Determine the best font size to fit within the available dimensions
|
||||||
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
font_size = getFontSize(text, available_width, available_height, font_path)
|
||||||
|
if font_size <= 0:
|
||||||
# Create a new image with increased resolution
|
raise ValueError("Text is too long to fit in the supplied dimensions.")
|
||||||
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
|
||||||
draw = ImageDraw.Draw(image)
|
# Create a new image with increased resolution
|
||||||
|
image = Image.new("RGBA", (scaled_width, scaled_height), (255, 255, 255, 0))
|
||||||
# Load the calculated font
|
draw = ImageDraw.Draw(image)
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
|
||||||
|
# Load the calculated font
|
||||||
# Calculate the size and bounding box of the text
|
font = ImageFont.truetype(font_path, font_size)
|
||||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
# Calculate the size and bounding box of the text
|
||||||
text_height = text_bbox[3] - text_bbox[1] + 20
|
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = text_bbox[2] - text_bbox[0]
|
||||||
# Determine centered position for the text while considering padding
|
text_height = text_bbox[3] - text_bbox[1] + 20
|
||||||
# Additional adjustment ensures text appears centrally aligned
|
|
||||||
x = (scaled_width - text_width) // 2
|
# Determine centered position for the text while considering padding
|
||||||
y = (scaled_height - text_height) // 2
|
# Additional adjustment ensures text appears centrally aligned
|
||||||
|
x = (scaled_width - text_width) // 2
|
||||||
# Draw a black box with a white outline that fits the image dimensions precisely
|
y = (scaled_height - text_height) // 2
|
||||||
draw.rectangle([0, 0, scaled_width - 1, scaled_height - 1], outline=(255, 255, 255, 255), width=1)
|
|
||||||
|
# Draw a black box with a white outline that fits the image dimensions precisely
|
||||||
# Fill the inside box with black color
|
draw.rectangle([0, 0, scaled_width - 1, scaled_height - 1], outline=(255, 255, 255, 255), width=1)
|
||||||
draw.rectangle([1, 1, scaled_width - 2, scaled_height - 2], fill=(0, 0, 0, 255))
|
|
||||||
|
# Fill the inside box with black color
|
||||||
# Render the text within the image
|
draw.rectangle([1, 1, scaled_width - 2, scaled_height - 2], fill=(0, 0, 0, 255))
|
||||||
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
|
||||||
|
# Render the text within the image
|
||||||
# Shrink the image back to original dimensions with high-quality interpolation
|
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
||||||
image = image.resize(
|
|
||||||
(width, height),
|
# Shrink the image back to original dimensions with high-quality interpolation
|
||||||
Image.LANCZOS,
|
image = image.resize(
|
||||||
)
|
(width, height),
|
||||||
|
Image.LANCZOS,
|
||||||
return image
|
)
|
||||||
|
|
||||||
|
return image
|
||||||
def getImageDimensions(file_path):
|
|
||||||
try:
|
|
||||||
with Image.open(file_path) as img:
|
def getImageDimensions(file_path):
|
||||||
width, height = img.size
|
try:
|
||||||
return width, height
|
with Image.open(file_path) as img:
|
||||||
except Exception as e:
|
width, height = img.size
|
||||||
print(f"Error reading {file_path}: {e}")
|
return width, height
|
||||||
return None, None
|
except Exception as e:
|
||||||
|
print(f"Error reading {file_path}: {e}")
|
||||||
|
return None, None
|
||||||
def processImagesDir(directory_path, imageList):
|
|
||||||
for file_name in os.listdir(directory_path):
|
|
||||||
# .png and Japanese
|
def processImagesDir(directory_path, imageList):
|
||||||
if ".png" in file_name:
|
for file_name in os.listdir(directory_path):
|
||||||
file_path = os.path.join(directory_path, file_name)
|
# .png and Japanese
|
||||||
if os.path.isfile(file_path):
|
if ".png" in file_name:
|
||||||
# Check if the file is an image
|
file_path = os.path.join(directory_path, file_name)
|
||||||
try:
|
if os.path.isfile(file_path):
|
||||||
width, height = getImageDimensions(file_path)
|
# Check if the file is an image
|
||||||
if width is not None and height is not None:
|
try:
|
||||||
placeholders = {
|
width, height = getImageDimensions(file_path)
|
||||||
".png": "",
|
if width is not None and height is not None:
|
||||||
}
|
placeholders = {
|
||||||
for target, replacement in placeholders.items():
|
".png": "",
|
||||||
file_name = file_name.replace(target, replacement)
|
}
|
||||||
match = re.search(r"[\[【].+?[\]】](.*)", file_name)
|
for target, replacement in placeholders.items():
|
||||||
if match:
|
file_name = file_name.replace(target, replacement)
|
||||||
text = match.group(1)
|
match = re.search(r"[\[【].+?[\]】](.*)", file_name)
|
||||||
else:
|
if match:
|
||||||
text = file_name
|
text = match.group(1)
|
||||||
imageList[0].append(text)
|
else:
|
||||||
imageList[1].append([width, height])
|
text = file_name
|
||||||
imageList[2].append(file_name)
|
imageList[0].append(text)
|
||||||
except Exception as e:
|
imageList[1].append([width, height])
|
||||||
print(f"Error processing {file_name}: {e}")
|
imageList[2].append(file_name)
|
||||||
|
except Exception as e:
|
||||||
if ".txt" in file_name:
|
print(f"Error processing {file_name}: {e}")
|
||||||
try:
|
|
||||||
with open(f"{directory_path}/{file_name}", "r", encoding="utf8") as file:
|
if ".txt" in file_name:
|
||||||
for line in file:
|
try:
|
||||||
line = line.strip()
|
with open(f"{directory_path}/{file_name}", "r", encoding="utf8") as file:
|
||||||
line = line.replace(":", ":")
|
for line in file:
|
||||||
line = line.replace("/", "/")
|
line = line.strip()
|
||||||
line = line.replace("?", "?")
|
line = line.replace(":", ":")
|
||||||
imageList[0].append(line) # Using strip() to remove any extra newlines or spaces
|
line = line.replace("/", "/")
|
||||||
imageList[1].append([104, 15])
|
line = line.replace("?", "?")
|
||||||
except FileNotFoundError:
|
imageList[0].append(line) # Using strip() to remove any extra newlines or spaces
|
||||||
print(f"The file at {file_path} was not found.")
|
imageList[1].append([104, 15])
|
||||||
except IOError:
|
except FileNotFoundError:
|
||||||
print(f"An error occurred while reading the file at {file_path}.")
|
print(f"The file at {file_path} was not found.")
|
||||||
return imageList
|
except IOError:
|
||||||
|
print(f"An error occurred while reading the file at {file_path}.")
|
||||||
|
return imageList
|
||||||
def translateImages(imageList):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
|
def translateImages(imageList):
|
||||||
# Translate GPT
|
totalTokens = [0, 0]
|
||||||
response = translateAI(imageList[0], "Keep the Translation as brief as possible")
|
|
||||||
translatedList = response[0]
|
# Translate GPT
|
||||||
totalTokens[0] += response[1][0]
|
response = translateAI(imageList[0], "Keep the Translation as brief as possible")
|
||||||
totalTokens[1] += response[1][1]
|
translatedList = response[0]
|
||||||
|
totalTokens[0] += response[1][0]
|
||||||
return [translatedList, totalTokens, None]
|
totalTokens[1] += response[1][1]
|
||||||
|
|
||||||
# Save some money and enter the character before translation
|
return [translatedList, totalTokens, None]
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
def translateAI(text, history, history_ctx=None):
|
return response
|
||||||
"""
|
return [speaker, [0, 0]]
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
2720
modules/json.py
2720
modules/json.py
File diff suppressed because it is too large
Load diff
1064
modules/kirikiri.py
1064
modules/kirikiri.py
File diff suppressed because it is too large
Load diff
746
modules/lune.py
746
modules/lune.py
|
|
@ -1,372 +1,374 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
PBAR = None
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
FILENAME = None
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
|
PBAR = None
|
||||||
# tqdm Globals
|
FILENAME = None
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
|
POSITION = 0
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
LEAVE = False
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleLune(filename, estimate):
|
LEAVE = False
|
||||||
global FILENAME, ESTIMATE, totalTokens
|
|
||||||
ESTIMATE = estimate
|
def handleLune(filename, estimate):
|
||||||
FILENAME = filename
|
global FILENAME, ESTIMATE, totalTokens
|
||||||
|
ESTIMATE = estimate
|
||||||
if estimate:
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
if estimate:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
TOKENS[1] += translatedData[1][1]
|
||||||
|
|
||||||
else:
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
try:
|
|
||||||
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
else:
|
||||||
start = time.time()
|
try:
|
||||||
translatedData = openFiles(filename)
|
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
# Print Result
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
end = time.time()
|
||||||
with LOCK:
|
json.dump(translatedData[0], outFile, ensure_ascii=False, indent=4)
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
except Exception:
|
TOKENS[0] += translatedData[1][0]
|
||||||
return "Fail"
|
TOKENS[1] += translatedData[1][1]
|
||||||
|
except Exception:
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="UTF-8-sig") as f:
|
|
||||||
data = json.load(f)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="UTF-8-sig") as f:
|
||||||
# Map Files
|
data = json.load(f)
|
||||||
if ".json" in filename:
|
|
||||||
translatedData = parseJSON(data, filename)
|
# Map Files
|
||||||
|
if ".json" in filename:
|
||||||
else:
|
translatedData = parseJSON(data, filename)
|
||||||
raise NameError(filename + " Not Supported")
|
|
||||||
|
else:
|
||||||
return translatedData
|
raise NameError(filename + " Not Supported")
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def parseJSON(data, filename):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
totalLines = 0
|
def parseJSON(data, filename):
|
||||||
totalLines = len(data)
|
totalTokens = [0, 0]
|
||||||
global LOCK
|
totalLines = 0
|
||||||
|
totalLines = len(data)
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar:
|
global LOCK
|
||||||
pbar.desc = filename
|
|
||||||
pbar.total = totalLines
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, total=totalLines, leave=LEAVE) as pbar:
|
||||||
try:
|
pbar.desc = filename
|
||||||
result = translateJSON(data, pbar)
|
pbar.total = totalLines
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateJSON(data, pbar)
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
return [data, totalTokens, e]
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, None]
|
except Exception as e:
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_json(data, filename):
|
|
||||||
"""Atomically save current JSON translation progress."""
|
|
||||||
try:
|
def save_progress_json(data, filename):
|
||||||
if ESTIMATE:
|
"""Atomically save current JSON translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding="utf-8", newline="\n") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
json.dump(data, tmp_file, ensure_ascii=False, indent=4)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding="utf-8", newline="\n") as tmp_file:
|
||||||
finally:
|
json.dump(data, tmp_file, ensure_ascii=False, indent=4)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateJSON(data, pbar):
|
|
||||||
global PBAR
|
|
||||||
PBAR = pbar
|
def translateJSON(data, pbar):
|
||||||
textHistory = []
|
global PBAR
|
||||||
batch = []
|
PBAR = pbar
|
||||||
maxHistory = MAXHISTORY
|
textHistory = []
|
||||||
tokens = [0, 0]
|
batch = []
|
||||||
speaker = "None"
|
maxHistory = MAXHISTORY
|
||||||
insertBool = False
|
tokens = [0, 0]
|
||||||
i = 0
|
speaker = "None"
|
||||||
batchStartIndex = 0
|
insertBool = False
|
||||||
|
i = 0
|
||||||
while i < len(data):
|
batchStartIndex = 0
|
||||||
item = data[i]
|
|
||||||
# Speaker
|
while i < len(data):
|
||||||
if "name" in item:
|
item = data[i]
|
||||||
if item["name"] not in [None, "-"]:
|
# Speaker
|
||||||
response = getSpeaker(item["name"])
|
if "name" in item:
|
||||||
speaker = response[0]
|
if item["name"] not in [None, "-"]:
|
||||||
tokens[0] += response[1][0]
|
response = getSpeaker(item["name"])
|
||||||
tokens[1] += response[1][1]
|
speaker = response[0]
|
||||||
item["name"] = speaker
|
tokens[0] += response[1][0]
|
||||||
save_progress_json(data, FILENAME or "output.json")
|
tokens[1] += response[1][1]
|
||||||
else:
|
item["name"] = speaker
|
||||||
speaker = "None"
|
save_progress_json(data, FILENAME or "output.json")
|
||||||
|
else:
|
||||||
# Text
|
speaker = "None"
|
||||||
if "message" in item:
|
|
||||||
for text in [
|
# Text
|
||||||
"text",
|
if "message" in item:
|
||||||
"text2",
|
for text in [
|
||||||
"help1",
|
"text",
|
||||||
"help2",
|
"text2",
|
||||||
"help3",
|
"help1",
|
||||||
"like",
|
"help2",
|
||||||
"message",
|
"help3",
|
||||||
"me",
|
"like",
|
||||||
]:
|
"message",
|
||||||
if text in item:
|
"me",
|
||||||
if item[text] != None:
|
]:
|
||||||
jaString = item[text]
|
if text in item:
|
||||||
|
if item[text] != None:
|
||||||
# Remove any textwrap
|
jaString = item[text]
|
||||||
if FIXTEXTWRAP == True:
|
|
||||||
finalJAString = jaString.replace("\n", " ")
|
# Remove any textwrap
|
||||||
|
if FIXTEXTWRAP == True:
|
||||||
# [Passthrough 1] Pulling From File
|
finalJAString = jaString.replace("\n", " ")
|
||||||
if insertBool is False:
|
|
||||||
# Append to List and Clear Values
|
# [Passthrough 1] Pulling From File
|
||||||
batch.append(finalJAString)
|
if insertBool is False:
|
||||||
speaker = ""
|
# Append to List and Clear Values
|
||||||
|
batch.append(finalJAString)
|
||||||
# Translate Batch if Full
|
speaker = ""
|
||||||
if len(batch) == BATCHSIZE:
|
|
||||||
# Translate
|
# Translate Batch if Full
|
||||||
response = translateAI(batch, textHistory)
|
if len(batch) == BATCHSIZE:
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(batch, textHistory)
|
||||||
translatedBatch = response[0]
|
tokens[0] += response[1][0]
|
||||||
textHistory = translatedBatch[-10:]
|
tokens[1] += response[1][1]
|
||||||
|
translatedBatch = response[0]
|
||||||
# Set Values
|
textHistory = translatedBatch[-10:]
|
||||||
if len(batch) == len(translatedBatch):
|
|
||||||
i = batchStartIndex
|
# Set Values
|
||||||
insertBool = True
|
if len(batch) == len(translatedBatch):
|
||||||
|
i = batchStartIndex
|
||||||
# Mismatch
|
insertBool = True
|
||||||
else:
|
|
||||||
pbar.write(f"Mismatch: {batchStartIndex} - {i}")
|
# Mismatch
|
||||||
MISMATCH.append(batch)
|
else:
|
||||||
batchStartIndex = i
|
pbar.write(f"Mismatch: {batchStartIndex} - {i}")
|
||||||
batch.clear()
|
MISMATCH.append(batch)
|
||||||
|
batchStartIndex = i
|
||||||
if insertBool is False:
|
batch.clear()
|
||||||
i += 1
|
|
||||||
|
if insertBool is False:
|
||||||
currentGroup = []
|
i += 1
|
||||||
|
|
||||||
# [Passthrough 2] Setting Data
|
currentGroup = []
|
||||||
else:
|
|
||||||
# Get Text
|
# [Passthrough 2] Setting Data
|
||||||
translatedText = translatedBatch[0]
|
else:
|
||||||
|
# Get Text
|
||||||
# Remove added speaker
|
translatedText = translatedBatch[0]
|
||||||
translatedText = re.sub(r"^.+?:\s", "", translatedText)
|
|
||||||
|
# Remove added speaker
|
||||||
# Textwrap
|
translatedText = re.sub(r"^.+?:\s", "", translatedText)
|
||||||
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
|
# Textwrap
|
||||||
# Set Text
|
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
item[text] = translatedText
|
|
||||||
save_progress_json(data, FILENAME or "output.json")
|
# Set Text
|
||||||
translatedBatch.pop(0)
|
item[text] = translatedText
|
||||||
speaker = ""
|
save_progress_json(data, FILENAME or "output.json")
|
||||||
currentGroup = []
|
translatedBatch.pop(0)
|
||||||
i += 1
|
speaker = ""
|
||||||
|
currentGroup = []
|
||||||
# If Batch is empty. Move on.
|
i += 1
|
||||||
if len(translatedBatch) == 0:
|
|
||||||
insertBool = False
|
# If Batch is empty. Move on.
|
||||||
batchStartIndex = i
|
if len(translatedBatch) == 0:
|
||||||
batch.clear()
|
insertBool = False
|
||||||
else:
|
batchStartIndex = i
|
||||||
i += 1
|
batch.clear()
|
||||||
|
else:
|
||||||
# Translate Batch if not empty and EOF
|
i += 1
|
||||||
if len(batch) != 0 and i >= len(data):
|
|
||||||
# Translate
|
# Translate Batch if not empty and EOF
|
||||||
response = translateAI(batch, textHistory)
|
if len(batch) != 0 and i >= len(data):
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(batch, textHistory)
|
||||||
translatedBatch = response[0]
|
tokens[0] += response[1][0]
|
||||||
textHistory = translatedBatch[-10:]
|
tokens[1] += response[1][1]
|
||||||
|
translatedBatch = response[0]
|
||||||
# Set Values
|
textHistory = translatedBatch[-10:]
|
||||||
if len(batch) == len(translatedBatch):
|
|
||||||
i = batchStartIndex
|
# Set Values
|
||||||
insertBool = True
|
if len(batch) == len(translatedBatch):
|
||||||
|
i = batchStartIndex
|
||||||
# Mismatch
|
insertBool = True
|
||||||
else:
|
|
||||||
pbar.write(f"Mismatch: {batchStartIndex} - {i}")
|
# Mismatch
|
||||||
MISMATCH.append(batch)
|
else:
|
||||||
batchStartIndex = i
|
pbar.write(f"Mismatch: {batchStartIndex} - {i}")
|
||||||
batch.clear()
|
MISMATCH.append(batch)
|
||||||
|
batchStartIndex = i
|
||||||
currentGroup = []
|
batch.clear()
|
||||||
# After applying a batch, persist progress
|
|
||||||
save_progress_json(data, FILENAME or "output.json")
|
currentGroup = []
|
||||||
return tokens
|
# After applying a batch, persist progress
|
||||||
|
save_progress_json(data, FILENAME or "output.json")
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
764
modules/main.py
764
modules/main.py
|
|
@ -1,381 +1,385 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# This needs to be before the module imports as some of them currently try to read and use some of these values
|
# This needs to be before the module imports as some of them currently try to read and use some of these values
|
||||||
# upon import, in which case if they are unset the script will crash before we can output these messages.
|
# upon import, in which case if they are unset the script will crash before we can output these messages.
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
_missing_envs = [
|
from util.paths import migrate_root_data_files, ensure_vocab_file
|
||||||
env for env in [
|
|
||||||
"api",
|
migrate_root_data_files()
|
||||||
"key",
|
ensure_vocab_file()
|
||||||
"model",
|
_missing_envs = [
|
||||||
"language",
|
env for env in [
|
||||||
"timeout",
|
"api",
|
||||||
"fileThreads",
|
"key",
|
||||||
"threads",
|
"model",
|
||||||
"width",
|
"language",
|
||||||
"listWidth",
|
"timeout",
|
||||||
]
|
"fileThreads",
|
||||||
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<"
|
"threads",
|
||||||
]
|
"width",
|
||||||
if _missing_envs:
|
"listWidth",
|
||||||
names = ", ".join(_missing_envs)
|
]
|
||||||
tqdm.write(
|
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<"
|
||||||
Fore.RED
|
]
|
||||||
+ f"Missing required environment variable(s): {names}. "
|
if _missing_envs:
|
||||||
+ "Set them in a .env file (see .env.example)."
|
names = ", ".join(_missing_envs)
|
||||||
)
|
tqdm.write(
|
||||||
|
Fore.RED
|
||||||
from modules.rpgmakermvmz import handleMVMZ, setSpeakerParseMode as setSpeakerParseMVMZ, finalizeSpeakerParse as finalizeSpeakerParseMVMZ
|
+ f"Missing required environment variable(s): {names}. "
|
||||||
from modules.csv import handleCSV
|
+ "Set them in a .env file (see .env.example)."
|
||||||
from modules.tyrano import handleTyrano
|
)
|
||||||
from modules.kirikiri import handleKirikiri
|
|
||||||
from modules.json import handleJSON
|
from modules.rpgmakermvmz import handleMVMZ, setSpeakerParseMode as setSpeakerParseMVMZ, finalizeSpeakerParse as finalizeSpeakerParseMVMZ
|
||||||
from modules.lune import handleLune
|
from modules.csv import handleCSV
|
||||||
from modules.yuris import handleYuris
|
from modules.tyrano import handleTyrano
|
||||||
from modules.nscript import handleOnscripter
|
from modules.kirikiri import handleKirikiri
|
||||||
from modules.wolf import handleWOLF
|
from modules.json import handleJSON
|
||||||
from modules.wolf2 import handleWOLF2
|
from modules.lune import handleLune
|
||||||
from modules.regex import handleRegex
|
from modules.yuris import handleYuris
|
||||||
from modules.text import handleText
|
from modules.nscript import handleOnscripter
|
||||||
from modules.renpy import handleRenpy
|
from modules.wolf import handleWOLF
|
||||||
from modules.unity import handleUnity
|
from modules.wolf2 import handleWOLF2
|
||||||
from modules.images import handleImages
|
from modules.regex import handleRegex
|
||||||
from modules.rpgmakerplugin import handlePlugin
|
from modules.text import handleText
|
||||||
from modules.srpg import handleSRPG
|
from modules.renpy import handleRenpy
|
||||||
from modules.aquedi4 import handleAquedi4
|
from modules.unity import handleUnity
|
||||||
|
from modules.images import handleImages
|
||||||
# For GPT4 rate limit will be hit if you have more than 1 thread.
|
from modules.rpgmakerplugin import handlePlugin
|
||||||
# 1 Thread for each file. Controls how many files are worked on at once.
|
from modules.srpg import handleSRPG
|
||||||
THREADS = int(os.getenv("fileThreads"))
|
from modules.aquedi4 import handleAquedi4
|
||||||
|
|
||||||
# [Display name, file extension, handle function]
|
# For GPT4 rate limit will be hit if you have more than 1 thread.
|
||||||
MODULES = [
|
# 1 Thread for each file. Controls how many files are worked on at once.
|
||||||
["RPGMaker MV/MZ", ["json"], handleMVMZ],
|
THREADS = int(os.getenv("fileThreads"))
|
||||||
["RPGMaker Plugins", ["js", "rb"], handlePlugin],
|
|
||||||
["CSV (From Translator++)", ["csv"], handleCSV],
|
# [Display name, file extension, handle function]
|
||||||
["Tyrano", ["ks"], handleTyrano],
|
MODULES = [
|
||||||
["Kirikiri", ["ks", "tjs", "ssd", "asd"], handleKirikiri],
|
["RPGMaker MV/MZ", ["json"], handleMVMZ],
|
||||||
["JSON", ["json"], handleJSON],
|
["RPGMaker Plugins", ["js", "rb"], handlePlugin],
|
||||||
["Lune", ["json"], handleLune],
|
["CSV (From Translator++)", ["csv"], handleCSV],
|
||||||
["Yuris", ["json"], handleYuris],
|
["Tyrano", ["ks"], handleTyrano],
|
||||||
["NScript", ["txt"], handleOnscripter],
|
["Kirikiri", ["ks", "tjs", "ssd", "asd"], handleKirikiri],
|
||||||
["Wolf", ["json"], handleWOLF],
|
["JSON", ["json"], handleJSON],
|
||||||
["Wolf", ["txt"], handleWOLF2],
|
["Lune", ["json"], handleLune],
|
||||||
["Regex", ["txt", "json", "script", "csv"], handleRegex],
|
["Yuris", ["json"], handleYuris],
|
||||||
["Text", ["txt", "srt"], handleText],
|
["NScript", ["txt"], handleOnscripter],
|
||||||
["Renpy", ["rpy"], handleRenpy],
|
["Wolf", ["json"], handleWOLF],
|
||||||
["Unity", ["txt"], handleUnity],
|
["Wolf", ["txt"], handleWOLF2],
|
||||||
["SRPG Studio", ["json"], handleSRPG],
|
["Regex", ["txt", "json", "script", "csv"], handleRegex],
|
||||||
["Images", [""], handleImages],
|
["Text", ["txt", "srt"], handleText],
|
||||||
["Aquedi4 Prepared JSON", [".json"], handleAquedi4],
|
["Renpy", ["rpy"], handleRenpy],
|
||||||
]
|
["Unity", ["txt"], handleUnity],
|
||||||
|
["SRPG Studio", ["json"], handleSRPG],
|
||||||
# Info Message
|
["Images", [""], handleImages],
|
||||||
tqdm.write(
|
["Aquedi4 Prepared JSON", [".json"], handleAquedi4],
|
||||||
Fore.CYAN
|
]
|
||||||
+ "-Dazed MTL Tool -"
|
|
||||||
+ Fore.RESET,
|
# Info Message
|
||||||
end="\n\n",
|
tqdm.write(
|
||||||
)
|
Fore.CYAN
|
||||||
|
+ "-Dazed MTL Tool -"
|
||||||
|
+ Fore.RESET,
|
||||||
def main():
|
end="\n\n",
|
||||||
from util.translation import clear_cache
|
)
|
||||||
|
|
||||||
estimate = ""
|
|
||||||
batch_mode = False
|
def main():
|
||||||
speaker_parse = False # Deferred until after engine select
|
from util.translation import clear_cache
|
||||||
while estimate == "":
|
|
||||||
estimate = input("Select Mode:\n\n 1. Translate\n 2. Estimate\n 3. Batch Translate (Anthropic Batches API, 50% off)\n")
|
estimate = ""
|
||||||
match estimate:
|
batch_mode = False
|
||||||
case "1":
|
speaker_parse = False # Deferred until after engine select
|
||||||
estimate = False
|
while estimate == "":
|
||||||
case "2":
|
estimate = input("Select Mode:\n\n 1. Translate\n 2. Estimate\n 3. Batch Translate (Anthropic Batches API, 50% off)\n")
|
||||||
estimate = True
|
match estimate:
|
||||||
case "3":
|
case "1":
|
||||||
estimate = False
|
estimate = False
|
||||||
batch_mode = True
|
case "2":
|
||||||
case _:
|
estimate = True
|
||||||
estimate = ""
|
case "3":
|
||||||
|
estimate = False
|
||||||
resume_state = None
|
batch_mode = True
|
||||||
if batch_mode:
|
case _:
|
||||||
from util.translation import isClaudeNative, batchRunState
|
estimate = ""
|
||||||
if not isClaudeNative(os.getenv("model", "")):
|
|
||||||
tqdm.write(
|
resume_state = None
|
||||||
Fore.RED
|
if batch_mode:
|
||||||
+ "Batch Translate requires a Claude model with the 'api' env var unset or pointing at anthropic.com."
|
from util.translation import isClaudeNative, batchRunState
|
||||||
+ Fore.RESET
|
if not isClaudeNative(os.getenv("model", "")):
|
||||||
)
|
tqdm.write(
|
||||||
return
|
Fore.RED
|
||||||
# An interrupted batch run can be resumed instead of re-collecting
|
+ "Batch Translate requires a Claude model with the 'api' env var unset or pointing at anthropic.com."
|
||||||
# (a second submission would be billed again).
|
+ Fore.RESET
|
||||||
resume_state = batchRunState()
|
)
|
||||||
if resume_state:
|
return
|
||||||
confirm = ""
|
# An interrupted batch run can be resumed instead of re-collecting
|
||||||
while confirm not in ("y", "n"):
|
# (a second submission would be billed again).
|
||||||
confirm = input(f"A previous batch run was interrupted ({resume_state}). Resume it? (y/n)\n").strip().lower()
|
resume_state = batchRunState()
|
||||||
if confirm == "n":
|
if resume_state:
|
||||||
resume_state = None
|
confirm = ""
|
||||||
|
while confirm not in ("y", "n"):
|
||||||
# Clear the translation cache at the start of the run. Kept when resuming a
|
confirm = input(f"A previous batch run was interrupted ({resume_state}). Resume it? (y/n)\n").strip().lower()
|
||||||
# batch so names translated during collect stay consistent with the queued
|
if confirm == "n":
|
||||||
# payloads in the consume pass.
|
resume_state = None
|
||||||
if not resume_state:
|
|
||||||
clear_cache()
|
# Clear the translation cache at the start of the run. Kept when resuming a
|
||||||
|
# batch so names translated during collect stay consistent with the queued
|
||||||
version = ""
|
# payloads in the consume pass.
|
||||||
while True:
|
if not resume_state:
|
||||||
tqdm.write("Select game engine:\n")
|
clear_cache()
|
||||||
for position, module in enumerate(MODULES):
|
|
||||||
tqdm.write(f"{str(position + 1).rjust(2)}. {module[0]} (.{module[1]})")
|
version = ""
|
||||||
version = input()
|
while True:
|
||||||
try:
|
tqdm.write("Select game engine:\n")
|
||||||
version = int(version) - 1
|
for position, module in enumerate(MODULES):
|
||||||
except:
|
tqdm.write(f"{str(position + 1).rjust(2)}. {module[0]} (.{module[1]})")
|
||||||
continue
|
version = input()
|
||||||
if version in range(len(MODULES)):
|
try:
|
||||||
break
|
version = int(version) - 1
|
||||||
|
except:
|
||||||
totalCost = (
|
continue
|
||||||
Fore.RED
|
if version in range(len(MODULES)):
|
||||||
+ "Translation module didn't return the total cost. Make sure the \
|
break
|
||||||
files to translate are in the /files folder and that you picked the right game engine."
|
|
||||||
)
|
totalCost = (
|
||||||
|
Fore.RED
|
||||||
# If translating RPGMaker MV/MZ, prompt for speaker parse mode
|
+ "Translation module didn't return the total cost. Make sure the \
|
||||||
speaker_parse = False
|
files to translate are in the /files folder and that you picked the right game engine."
|
||||||
if version == 0 and not estimate and not batch_mode:
|
)
|
||||||
sub = ""
|
|
||||||
while sub == "":
|
# If translating RPGMaker MV/MZ, prompt for speaker parse mode
|
||||||
sub = input("RPGMaker MV/MZ options:\n\n 1. Standard Translate\n 2. Parse Speakers (collect speaker names only)\n")
|
speaker_parse = False
|
||||||
match sub:
|
if version == 0 and not estimate and not batch_mode:
|
||||||
case "1":
|
sub = ""
|
||||||
speaker_parse = False
|
while sub == "":
|
||||||
case "2":
|
sub = input("RPGMaker MV/MZ options:\n\n 1. Standard Translate\n 2. Parse Speakers (collect speaker names only)\n")
|
||||||
speaker_parse = True
|
match sub:
|
||||||
case _:
|
case "1":
|
||||||
sub = ""
|
speaker_parse = False
|
||||||
if speaker_parse:
|
case "2":
|
||||||
setSpeakerParseMVMZ(True)
|
speaker_parse = True
|
||||||
|
case _:
|
||||||
# Open File (Threads) - recursively walk 'files' and preserve directory structure
|
sub = ""
|
||||||
# Prepare per-run log file so CLI runs also write to a run-specific history file
|
if speaker_parse:
|
||||||
try:
|
setSpeakerParseMVMZ(True)
|
||||||
hist_dir = Path("log") / "history"
|
|
||||||
hist_dir.mkdir(parents=True, exist_ok=True)
|
# Open File (Threads) - recursively walk 'files' and preserve directory structure
|
||||||
|
# Prepare per-run log file so CLI runs also write to a run-specific history file
|
||||||
# Clean up old log files, keeping only the 10 most recent
|
try:
|
||||||
try:
|
hist_dir = Path("log") / "history"
|
||||||
log_files = sorted(hist_dir.glob("translationHistory_*.txt"), key=lambda p: p.stat().st_mtime, reverse=True)
|
hist_dir.mkdir(parents=True, exist_ok=True)
|
||||||
# Keep only the 10 most recent, delete the rest
|
|
||||||
for old_log in log_files[10:]:
|
# Clean up old log files, keeping only the 10 most recent
|
||||||
try:
|
try:
|
||||||
old_log.unlink()
|
log_files = sorted(hist_dir.glob("translationHistory_*.txt"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
except Exception:
|
# Keep only the 10 most recent, delete the rest
|
||||||
pass
|
for old_log in log_files[10:]:
|
||||||
except Exception:
|
try:
|
||||||
pass
|
old_log.unlink()
|
||||||
|
except Exception:
|
||||||
fname = datetime.datetime.now().strftime("translationHistory_%Y%m%d_%H%M%S.txt")
|
pass
|
||||||
run_log_path = hist_dir / fname
|
except Exception:
|
||||||
# Don't create the file yet - it will be created when first log is written
|
pass
|
||||||
# Store the path in environment variable
|
|
||||||
try:
|
fname = datetime.datetime.now().strftime("translationHistory_%Y%m%d_%H%M%S.txt")
|
||||||
os.environ['TRANSLATION_RUN_LOG'] = str(run_log_path)
|
run_log_path = hist_dir / fname
|
||||||
except Exception:
|
# Don't create the file yet - it will be created when first log is written
|
||||||
pass
|
# Store the path in environment variable
|
||||||
|
try:
|
||||||
# Try to create a hard link from legacy path to this run file for compatibility
|
os.environ['TRANSLATION_RUN_LOG'] = str(run_log_path)
|
||||||
# This will be created when the run_log_path file is first written to
|
except Exception:
|
||||||
legacy = Path("log") / "translationHistory.txt"
|
pass
|
||||||
try:
|
|
||||||
if legacy.exists():
|
# Try to create a hard link from legacy path to this run file for compatibility
|
||||||
try:
|
# This will be created when the run_log_path file is first written to
|
||||||
legacy.unlink()
|
legacy = Path("log") / "translationHistory.txt"
|
||||||
except Exception:
|
try:
|
||||||
pass
|
if legacy.exists():
|
||||||
except Exception:
|
try:
|
||||||
pass
|
legacy.unlink()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
except Exception:
|
||||||
def runFiles(estimate):
|
pass
|
||||||
runCost = totalCost
|
except Exception:
|
||||||
# Use single worker for estimate mode to prevent race conditions
|
pass
|
||||||
max_workers = 1 if estimate else THREADS
|
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
def runFiles(estimate):
|
||||||
futures = []
|
runCost = totalCost
|
||||||
files_root = "files"
|
# Use single worker for estimate mode to prevent race conditions
|
||||||
|
max_workers = 1 if estimate else THREADS
|
||||||
# Special-case: Images engine expects a folder, not a file; schedule per directory containing assets
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
if MODULES[version][0] == "Images":
|
futures = []
|
||||||
for root, dirs, filenames in os.walk(files_root):
|
files_root = "files"
|
||||||
# Skip hidden/system directories
|
|
||||||
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
|
# Special-case: Images engine expects a folder, not a file; schedule per directory containing assets
|
||||||
|
if MODULES[version][0] == "Images":
|
||||||
# Skip the root 'files' itself to avoid processing everything twice
|
for root, dirs, filenames in os.walk(files_root):
|
||||||
# We'll still allow scheduling for root if it contains assets
|
# Skip hidden/system directories
|
||||||
|
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
|
||||||
# Only schedule directories that contain potential assets
|
|
||||||
has_assets = any(fn.lower().endswith((".png", ".txt")) for fn in filenames)
|
# Skip the root 'files' itself to avoid processing everything twice
|
||||||
if not has_assets:
|
# We'll still allow scheduling for root if it contains assets
|
||||||
continue
|
|
||||||
|
# Only schedule directories that contain potential assets
|
||||||
# Compute relative directory path and ensure translated mirror exists
|
has_assets = any(fn.lower().endswith((".png", ".txt")) for fn in filenames)
|
||||||
rel_dir = os.path.relpath(root, files_root).replace(os.sep, "/")
|
if not has_assets:
|
||||||
if rel_dir == ".":
|
continue
|
||||||
# Represent root as empty string so handler creates files under translated/ directly
|
|
||||||
rel_dir = ""
|
# Compute relative directory path and ensure translated mirror exists
|
||||||
try:
|
rel_dir = os.path.relpath(root, files_root).replace(os.sep, "/")
|
||||||
target_dir = os.path.join("translated", rel_dir.replace("/", os.sep)) if rel_dir else "translated"
|
if rel_dir == ".":
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
# Represent root as empty string so handler creates files under translated/ directly
|
||||||
except Exception:
|
rel_dir = ""
|
||||||
pass
|
try:
|
||||||
|
target_dir = os.path.join("translated", rel_dir.replace("/", os.sep)) if rel_dir else "translated"
|
||||||
futures.append(
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
executor.submit(MODULES[version][2], rel_dir, estimate)
|
except Exception:
|
||||||
)
|
pass
|
||||||
else:
|
|
||||||
# Gather all candidate files recursively
|
futures.append(
|
||||||
for root, dirs, filenames in os.walk(files_root):
|
executor.submit(MODULES[version][2], rel_dir, estimate)
|
||||||
# Skip hidden/system directories if any
|
)
|
||||||
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
|
else:
|
||||||
|
# Gather all candidate files recursively
|
||||||
for fname in filenames:
|
for root, dirs, filenames in os.walk(files_root):
|
||||||
if fname == ".gitkeep":
|
# Skip hidden/system directories if any
|
||||||
continue
|
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
|
||||||
|
|
||||||
abs_path = os.path.join(root, fname)
|
for fname in filenames:
|
||||||
# Build relative path from 'files' root using POSIX-style separators so handlers can do 'files/' + rel
|
if fname == ".gitkeep":
|
||||||
rel_path = os.path.relpath(abs_path, files_root)
|
continue
|
||||||
rel_path_posix = rel_path.replace(os.sep, "/")
|
|
||||||
|
abs_path = os.path.join(root, fname)
|
||||||
# Check extension match for the selected module version
|
# Build relative path from 'files' root using POSIX-style separators so handlers can do 'files/' + rel
|
||||||
for m in MODULES[version][1]:
|
rel_path = os.path.relpath(abs_path, files_root)
|
||||||
if rel_path_posix.endswith(m):
|
rel_path_posix = rel_path.replace(os.sep, "/")
|
||||||
# Ensure the corresponding directory exists under 'translated'
|
|
||||||
rel_dir = os.path.dirname(rel_path_posix)
|
# Check extension match for the selected module version
|
||||||
if rel_dir:
|
for m in MODULES[version][1]:
|
||||||
try:
|
if rel_path_posix.endswith(m):
|
||||||
os.makedirs(os.path.join("translated", rel_dir.replace("/", os.sep)), exist_ok=True)
|
# Ensure the corresponding directory exists under 'translated'
|
||||||
except Exception:
|
rel_dir = os.path.dirname(rel_path_posix)
|
||||||
# Best-effort; handler may attempt write and fail if permissions are insufficient
|
if rel_dir:
|
||||||
pass
|
try:
|
||||||
|
os.makedirs(os.path.join("translated", rel_dir.replace("/", os.sep)), exist_ok=True)
|
||||||
futures.append(
|
except Exception:
|
||||||
executor.submit(MODULES[version][2], rel_path_posix, estimate)
|
# Best-effort; handler may attempt write and fail if permissions are insufficient
|
||||||
)
|
pass
|
||||||
break # Avoid double-adding if multiple ext entries match
|
|
||||||
|
futures.append(
|
||||||
for future in as_completed(futures):
|
executor.submit(MODULES[version][2], rel_path_posix, estimate)
|
||||||
try:
|
)
|
||||||
runCost = future.result()
|
break # Avoid double-adding if multiple ext entries match
|
||||||
except Exception as e:
|
|
||||||
tracebackLineNo = str(traceback.extract_tb(sys.exc_info()[2])[-1].lineno)
|
for future in as_completed(futures):
|
||||||
tqdm.write(Fore.RED + str(e) + "|" + tracebackLineNo + Fore.RESET)
|
try:
|
||||||
return runCost
|
runCost = future.result()
|
||||||
|
except Exception as e:
|
||||||
if batch_mode:
|
tracebackLineNo = str(traceback.extract_tb(sys.exc_info()[2])[-1].lineno)
|
||||||
from util.translation import (
|
tqdm.write(Fore.RED + str(e) + "|" + tracebackLineNo + Fore.RESET)
|
||||||
set_batch_phase,
|
return runCost
|
||||||
clearBatchFiles,
|
|
||||||
pendingBatchRequests,
|
if batch_mode:
|
||||||
estimateBatchCost,
|
from util.translation import (
|
||||||
runTranslationBatches,
|
set_batch_phase,
|
||||||
)
|
clearBatchFiles,
|
||||||
poll = int(os.getenv("batchPollInterval", "60") or 60)
|
pendingBatchRequests,
|
||||||
run_consume = True
|
estimateBatchCost,
|
||||||
|
runTranslationBatches,
|
||||||
if resume_state is None:
|
)
|
||||||
clearBatchFiles()
|
poll = int(os.getenv("batchPollInterval", "60") or 60)
|
||||||
|
run_consume = True
|
||||||
# Pass 1 — queue dialogue for the batch; speaker/variable strings still hit live API.
|
|
||||||
tqdm.write(Fore.CYAN + "[BATCH] Pass 1/2: collecting requests..." + Fore.RESET)
|
if resume_state is None:
|
||||||
tqdm.write(
|
clearBatchFiles()
|
||||||
Fore.YELLOW
|
|
||||||
+ "[BATCH] Note: speaker names and similar short strings translate at live "
|
# Pass 1 — queue dialogue for the batch; speaker/variable strings still hit live API.
|
||||||
"API rates during collect (dialogue is batched after you confirm)."
|
tqdm.write(Fore.CYAN + "[BATCH] Pass 1/2: collecting requests..." + Fore.RESET)
|
||||||
+ Fore.RESET
|
tqdm.write(
|
||||||
)
|
Fore.YELLOW
|
||||||
set_batch_phase("collect")
|
+ "[BATCH] Note: speaker names and similar short strings translate at live "
|
||||||
try:
|
"API rates during collect (dialogue is batched after you confirm)."
|
||||||
totalCost = runFiles(False)
|
+ Fore.RESET
|
||||||
finally:
|
)
|
||||||
set_batch_phase(None)
|
set_batch_phase("collect")
|
||||||
|
try:
|
||||||
if pendingBatchRequests() == 0:
|
totalCost = runFiles(False)
|
||||||
tqdm.write("[BATCH] No requests queued — nothing needed the API.")
|
finally:
|
||||||
run_consume = False
|
set_batch_phase(None)
|
||||||
else:
|
|
||||||
estimateBatchCost()
|
if pendingBatchRequests() == 0:
|
||||||
confirm = ""
|
tqdm.write("[BATCH] No requests queued — nothing needed the API.")
|
||||||
while confirm not in ("y", "n"):
|
run_consume = False
|
||||||
confirm = input("Submit batch? (y/n)\n").strip().lower()
|
else:
|
||||||
if confirm == "n":
|
estimateBatchCost()
|
||||||
tqdm.write("[BATCH] Not submitted. The queue is kept in log/batch_requests.json.")
|
confirm = ""
|
||||||
return
|
while confirm not in ("y", "n"):
|
||||||
runTranslationBatches(poll)
|
confirm = input("Submit batch? (y/n)\n").strip().lower()
|
||||||
elif resume_state == "submitted":
|
if confirm == "n":
|
||||||
tqdm.write(Fore.CYAN + "[BATCH] Resuming the submitted batch..." + Fore.RESET)
|
tqdm.write("[BATCH] Not submitted. The queue is kept in log/batch_requests.json.")
|
||||||
runTranslationBatches(poll)
|
return
|
||||||
else: # "fetched" — results already downloaded, just write the files
|
runTranslationBatches(poll)
|
||||||
tqdm.write(Fore.CYAN + "[BATCH] Resuming from fetched results..." + Fore.RESET)
|
elif resume_state == "submitted":
|
||||||
|
tqdm.write(Fore.CYAN + "[BATCH] Resuming the submitted batch..." + Fore.RESET)
|
||||||
if run_consume:
|
runTranslationBatches(poll)
|
||||||
# Pass 2 — write the translated files from the fetched results.
|
else: # "fetched" — results already downloaded, just write the files
|
||||||
# Anything the batch missed falls back to the live API.
|
tqdm.write(Fore.CYAN + "[BATCH] Resuming from fetched results..." + Fore.RESET)
|
||||||
tqdm.write(Fore.CYAN + "[BATCH] Pass 2/2: writing translated files..." + Fore.RESET)
|
|
||||||
set_batch_phase("consume")
|
if run_consume:
|
||||||
try:
|
# Pass 2 — write the translated files from the fetched results.
|
||||||
totalCost = runFiles(False)
|
# Anything the batch missed falls back to the live API.
|
||||||
finally:
|
tqdm.write(Fore.CYAN + "[BATCH] Pass 2/2: writing translated files..." + Fore.RESET)
|
||||||
set_batch_phase(None)
|
set_batch_phase("consume")
|
||||||
else:
|
try:
|
||||||
totalCost = runFiles(estimate)
|
totalCost = runFiles(False)
|
||||||
|
finally:
|
||||||
# Finalize speaker parse mode by writing collected speakers to vocab
|
set_batch_phase(None)
|
||||||
if speaker_parse:
|
else:
|
||||||
finalizeSpeakerParseMVMZ()
|
totalCost = runFiles(estimate)
|
||||||
|
|
||||||
# Delete Tmp Files
|
# Finalize speaker parse mode by writing collected speakers to vocab
|
||||||
if os.path.isfile("csv.tmp"):
|
if speaker_parse:
|
||||||
os.remove("csv.tmp")
|
finalizeSpeakerParseMVMZ()
|
||||||
|
|
||||||
# Sweep any leftover temp files in translated/
|
# Delete Tmp Files
|
||||||
try:
|
if os.path.isfile("csv.tmp"):
|
||||||
translated_dir = os.path.join("translated")
|
os.remove("csv.tmp")
|
||||||
if os.path.isdir(translated_dir):
|
|
||||||
for fname in os.listdir(translated_dir):
|
# Sweep any leftover temp files in translated/
|
||||||
if fname.endswith(".tmp"):
|
try:
|
||||||
fpath = os.path.join(translated_dir, fname)
|
translated_dir = os.path.join("translated")
|
||||||
try:
|
if os.path.isdir(translated_dir):
|
||||||
os.remove(fpath)
|
for fname in os.listdir(translated_dir):
|
||||||
except Exception:
|
if fname.endswith(".tmp"):
|
||||||
# Best-effort cleanup; ignore files locked by other processes
|
fpath = os.path.join(translated_dir, fname)
|
||||||
pass
|
try:
|
||||||
except Exception:
|
os.remove(fpath)
|
||||||
pass
|
except Exception:
|
||||||
|
# Best-effort cleanup; ignore files locked by other processes
|
||||||
# Finish
|
pass
|
||||||
if totalCost != "Fail":
|
except Exception:
|
||||||
# if estimate is False:
|
pass
|
||||||
# This is to encourage people to grab what's in /translated instead
|
|
||||||
# deleteFolderFiles("files")
|
# Finish
|
||||||
|
if totalCost != "Fail":
|
||||||
tqdm.write(str(totalCost))
|
# if estimate is False:
|
||||||
|
# This is to encourage people to grab what's in /translated instead
|
||||||
|
# deleteFolderFiles("files")
|
||||||
def deleteFolderFiles(folderPath):
|
|
||||||
for filename in os.listdir(folderPath):
|
tqdm.write(str(totalCost))
|
||||||
file_path = os.path.join(folderPath, filename)
|
|
||||||
if file_path.endswith((".json", ".ks")):
|
|
||||||
|
def deleteFolderFiles(folderPath):
|
||||||
|
for filename in os.listdir(folderPath):
|
||||||
|
file_path = os.path.join(folderPath, filename)
|
||||||
|
if file_path.endswith((".json", ".ks")):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
|
|
@ -1,426 +1,428 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
CONVERTTOWIDE = False # Default (False)
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# tqdm Globals
|
CONVERTTOWIDE = False # Default (False)
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
FILENAME = None
|
LEAVE = False
|
||||||
|
PBAR = None
|
||||||
# Full Width
|
FILENAME = None
|
||||||
ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F))
|
|
||||||
ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus
|
# Full Width
|
||||||
wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F))
|
ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F))
|
||||||
wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus
|
ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus
|
||||||
|
wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F))
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleOnscripter(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleOnscripter(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
if ESTIMATE:
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
if ESTIMATE:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
else:
|
return totalString
|
||||||
try:
|
|
||||||
with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile:
|
else:
|
||||||
start = time.time()
|
try:
|
||||||
translatedData = openFiles(filename)
|
with open("translated/" + filename, "w", encoding="cp932", errors="ignore") as outFile:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
outFile.writelines(translatedData[0])
|
# Print Result
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
end = time.time()
|
||||||
with LOCK:
|
outFile.writelines(translatedData[0])
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
except Exception:
|
TOKENS[0] += translatedData[1][0]
|
||||||
traceback.print_exc()
|
TOKENS[1] += translatedData[1][1]
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="cp932") as readFile:
|
|
||||||
translatedData = parseOnscripter(readFile, filename)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="cp932") as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseOnscripter(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseOnscripter(readFile, filename):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
|
def parseOnscripter(readFile, filename):
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
try:
|
pbar.desc = filename
|
||||||
result = translateOnscripter(data, pbar, filename, [])
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateOnscripter(data, pbar, filename, [])
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="cp932"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="cp932"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateOnscripter(data, pbar, filename, translatedList):
|
|
||||||
stringList = []
|
|
||||||
currentGroup = []
|
def translateOnscripter(data, pbar, filename, translatedList):
|
||||||
tokens = [0, 0]
|
stringList = []
|
||||||
speaker = ""
|
currentGroup = []
|
||||||
voice = False
|
tokens = [0, 0]
|
||||||
global LOCK, ESTIMATE, PBAR
|
speaker = ""
|
||||||
PBAR = pbar
|
voice = False
|
||||||
i = 0
|
global LOCK, ESTIMATE, PBAR
|
||||||
|
PBAR = pbar
|
||||||
# Dialogue
|
i = 0
|
||||||
while i < len(data):
|
|
||||||
# Lines
|
# Dialogue
|
||||||
regex = r"^([\u3000「(【[][^\n]+)"
|
while i < len(data):
|
||||||
match = re.search(regex, data[i])
|
# Lines
|
||||||
if match != None and match.group(1) != "":
|
regex = r"^([\u3000「(【[][^\n]+)"
|
||||||
originalString = match.group(1)
|
match = re.search(regex, data[i])
|
||||||
# Pass 1
|
if match != None and match.group(1) != "":
|
||||||
if translatedList == []:
|
originalString = match.group(1)
|
||||||
# Grab Consecutive Strings
|
# Pass 1
|
||||||
jaString = match.group(1)
|
if translatedList == []:
|
||||||
while len(data) > i + 1 and re.match(regex, data[i + 1]):
|
# Grab Consecutive Strings
|
||||||
data[i] = ""
|
jaString = match.group(1)
|
||||||
i += 1
|
while len(data) > i + 1 and re.match(regex, data[i + 1]):
|
||||||
jaString = f"{jaString} {data[i]}"
|
data[i] = ""
|
||||||
|
i += 1
|
||||||
# Convert from Wide
|
jaString = f"{jaString} {data[i]}"
|
||||||
jaString = jaString.translate(wide_to_ascii)
|
|
||||||
|
# Convert from Wide
|
||||||
# Remove any textwrap and \u3000 and \
|
jaString = jaString.translate(wide_to_ascii)
|
||||||
jaString = jaString.replace("\n", "")
|
|
||||||
jaString = jaString.replace("\u3000", "")
|
# Remove any textwrap and \u3000 and \
|
||||||
jaString = jaString.replace("\\", "")
|
jaString = jaString.replace("\n", "")
|
||||||
jaString = jaString.replace(" >", ")")
|
jaString = jaString.replace("\u3000", "")
|
||||||
jaString = jaString.replace("< ", "(")
|
jaString = jaString.replace("\\", "")
|
||||||
|
jaString = jaString.replace(" >", ")")
|
||||||
# Remove Furigana
|
jaString = jaString.replace("< ", "(")
|
||||||
furiMatch = re.findall(r"({(.+?)\/(.+?)})", jaString)
|
|
||||||
if furiMatch:
|
# Remove Furigana
|
||||||
for match in furiMatch:
|
furiMatch = re.findall(r"({(.+?)\/(.+?)})", jaString)
|
||||||
jaString = jaString.replace(match[0], match[2])
|
if furiMatch:
|
||||||
|
for match in furiMatch:
|
||||||
# Add String
|
jaString = jaString.replace(match[0], match[2])
|
||||||
stringList.append(jaString.strip())
|
|
||||||
|
# Add String
|
||||||
# Pass 2
|
stringList.append(jaString.strip())
|
||||||
else:
|
|
||||||
# Get Text
|
# Pass 2
|
||||||
if translatedList:
|
else:
|
||||||
# Grab and Pop
|
# Get Text
|
||||||
translatedText = translatedList[0]
|
if translatedList:
|
||||||
translatedList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = translatedList[0]
|
||||||
# Set to None if empty list
|
translatedList.pop(0)
|
||||||
if len(translatedList) <= 0:
|
|
||||||
translatedList = None
|
# Set to None if empty list
|
||||||
|
if len(translatedList) <= 0:
|
||||||
# Textwrap & Other Text
|
translatedList = None
|
||||||
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
translatedText = translatedText.replace("\n", "\n\u3000")
|
# Textwrap & Other Text
|
||||||
|
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
# Split the string into lines
|
translatedText = translatedText.replace("\n", "\n\u3000")
|
||||||
lines = translatedText.split("\n")
|
|
||||||
|
# Split the string into lines
|
||||||
# Add a backslash after every 3rd line
|
lines = translatedText.split("\n")
|
||||||
j = 0
|
|
||||||
while j < len(lines):
|
# Add a backslash after every 3rd line
|
||||||
if j == 4:
|
j = 0
|
||||||
lines[j - 1] = f"{lines[j-1]}\\"
|
while j < len(lines):
|
||||||
lines[j] = f"\n{lines[j]}"
|
if j == 4:
|
||||||
j += 1
|
lines[j - 1] = f"{lines[j-1]}\\"
|
||||||
|
lines[j] = f"\n{lines[j]}"
|
||||||
# Join the lines back into a single string
|
j += 1
|
||||||
translatedText = "\n".join(lines)
|
|
||||||
|
# Join the lines back into a single string
|
||||||
# Remove Double Spaces
|
translatedText = "\n".join(lines)
|
||||||
translatedText = translatedText.replace(" ", " ")
|
|
||||||
|
# Remove Double Spaces
|
||||||
# Convert to Wide
|
translatedText = translatedText.replace(" ", " ")
|
||||||
if CONVERTTOWIDE:
|
|
||||||
translatedText = translatedText.translate(ascii_to_wide)
|
# Convert to Wide
|
||||||
|
if CONVERTTOWIDE:
|
||||||
# Fix Formatting
|
translatedText = translatedText.translate(ascii_to_wide)
|
||||||
translatedText = fixText(translatedText)
|
|
||||||
|
# Fix Formatting
|
||||||
# Set Data
|
translatedText = fixText(translatedText)
|
||||||
data[i] = data[i].replace(originalString, f"{translatedText}")
|
|
||||||
save_progress_lines(data, filename)
|
# Set Data
|
||||||
i += 1
|
data[i] = data[i].replace(originalString, f"{translatedText}")
|
||||||
|
save_progress_lines(data, filename)
|
||||||
# Choices
|
i += 1
|
||||||
elif "csel" in data[i] and translatedList != []:
|
|
||||||
choiceList = []
|
# Choices
|
||||||
jaString = data[i]
|
elif "csel" in data[i] and translatedList != []:
|
||||||
|
choiceList = []
|
||||||
choiceList = re.findall(r"\"(.*?)\"", jaString)
|
jaString = data[i]
|
||||||
if len(choiceList) > 0:
|
|
||||||
# Translate
|
choiceList = re.findall(r"\"(.*?)\"", jaString)
|
||||||
response = translateAI(choiceList, "This will be a dialogue option")
|
if len(choiceList) > 0:
|
||||||
translatedTextList = response[0]
|
# Translate
|
||||||
tokens[0] += response[1][0]
|
response = translateAI(choiceList, "This will be a dialogue option")
|
||||||
tokens[1] += response[1][1]
|
translatedTextList = response[0]
|
||||||
|
tokens[0] += response[1][0]
|
||||||
# Set Data
|
tokens[1] += response[1][1]
|
||||||
for j in range(len(translatedTextList)):
|
|
||||||
# Convert to Wide
|
# Set Data
|
||||||
if CONVERTTOWIDE:
|
for j in range(len(translatedTextList)):
|
||||||
translatedText = translatedTextList[j].translate(ascii_to_wide)
|
# Convert to Wide
|
||||||
|
if CONVERTTOWIDE:
|
||||||
# Set
|
translatedText = translatedTextList[j].translate(ascii_to_wide)
|
||||||
data[i] = data[i].replace(choiceList[j], translatedText)
|
|
||||||
save_progress_lines(data, filename)
|
# Set
|
||||||
i += 1
|
data[i] = data[i].replace(choiceList[j], translatedText)
|
||||||
|
save_progress_lines(data, filename)
|
||||||
# Nothing relevant. Skip Line.
|
i += 1
|
||||||
else:
|
|
||||||
i += 1
|
# Nothing relevant. Skip Line.
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if len(stringList) > 0:
|
|
||||||
# Set Progress
|
# EOF
|
||||||
pbar.total = len(stringList)
|
if len(stringList) > 0:
|
||||||
pbar.refresh()
|
# Set Progress
|
||||||
|
pbar.total = len(stringList)
|
||||||
# Translate
|
pbar.refresh()
|
||||||
response = translateAI(stringList, "")
|
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "")
|
||||||
translatedList = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Set Strings
|
translatedList = response[0]
|
||||||
if len(stringList) == len(translatedList):
|
|
||||||
translateOnscripter(data, pbar, filename, translatedList)
|
# Set Strings
|
||||||
|
if len(stringList) == len(translatedList):
|
||||||
# Mismatch
|
translateOnscripter(data, pbar, filename, translatedList)
|
||||||
else:
|
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if filename not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(filename)
|
with LOCK:
|
||||||
return tokens
|
if filename not in MISMATCH:
|
||||||
|
MISMATCH.append(filename)
|
||||||
|
return tokens
|
||||||
def fixText(translatedText):
|
|
||||||
# Add Break
|
|
||||||
translatedText = translatedText.replace('"', "'")
|
def fixText(translatedText):
|
||||||
translatedText = f"\u3000{translatedText}\\"
|
# Add Break
|
||||||
|
translatedText = translatedText.replace('"', "'")
|
||||||
# Unconvert Codes
|
translatedText = f"\u3000{translatedText}\\"
|
||||||
matchList = re.findall(r"([$].+?)[^\w]", translatedText)
|
|
||||||
if matchList:
|
# Unconvert Codes
|
||||||
for match in matchList:
|
matchList = re.findall(r"([$].+?)[^\w]", translatedText)
|
||||||
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
if matchList:
|
||||||
|
for match in matchList:
|
||||||
# Unconvert Color Codes
|
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
||||||
matchList = re.findall(r"([#][\w\d]{6})", translatedText)
|
|
||||||
if matchList:
|
# Unconvert Color Codes
|
||||||
for match in matchList:
|
matchList = re.findall(r"([#][\w\d]{6})", translatedText)
|
||||||
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
if matchList:
|
||||||
|
for match in matchList:
|
||||||
# Unconvert Variables
|
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
||||||
matchList = re.findall(r"([%]\w.+?)[^\w_]", translatedText)
|
|
||||||
if matchList:
|
# Unconvert Variables
|
||||||
for match in matchList:
|
matchList = re.findall(r"([%]\w.+?)[^\w_]", translatedText)
|
||||||
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
if matchList:
|
||||||
|
for match in matchList:
|
||||||
# Unconvert Backslashes
|
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
||||||
matchList = re.findall(r"\", translatedText)
|
|
||||||
if matchList:
|
# Unconvert Backslashes
|
||||||
for match in matchList:
|
matchList = re.findall(r"\", translatedText)
|
||||||
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
if matchList:
|
||||||
|
for match in matchList:
|
||||||
return translatedText
|
translatedText = translatedText.replace(match, match.translate(wide_to_ascii))
|
||||||
|
|
||||||
# Save some money and enter the character before translation
|
return translatedText
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
|
return [speaker, [0, 0]]
|
||||||
def translateAI(text, history, history_ctx=None):
|
|
||||||
"""
|
|
||||||
Translate text using the shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This function maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Translate text using the shared translation utility.
|
||||||
return sharedtranslateAI(
|
This function maintains compatibility with existing code while using the new shared implementation.
|
||||||
text=text,
|
"""
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
1056
modules/regex.py
1056
modules/regex.py
File diff suppressed because it is too large
Load diff
704
modules/renpy.py
704
modules/renpy.py
|
|
@ -1,351 +1,353 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
FILENAME = None
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# tqdm Globals
|
FILENAME = None
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
|
LEAVE = False
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
PBAR = None
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleRenpy(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE
|
|
||||||
global FILENAME
|
def handleRenpy(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE
|
||||||
ESTIMATE = estimate
|
global FILENAME
|
||||||
|
FILENAME = filename
|
||||||
if ESTIMATE:
|
ESTIMATE = estimate
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
if ESTIMATE:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
else:
|
return totalString
|
||||||
try:
|
|
||||||
with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile:
|
else:
|
||||||
start = time.time()
|
try:
|
||||||
translatedData = openFiles(filename)
|
with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
outFile.writelines(translatedData[0])
|
# Print Result
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
end = time.time()
|
||||||
with LOCK:
|
outFile.writelines(translatedData[0])
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
except Exception:
|
TOKENS[0] += translatedData[1][0]
|
||||||
traceback.print_exc()
|
TOKENS[1] += translatedData[1][1]
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
|
||||||
translatedData = parseRenpy(readFile, filename)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseRenpy(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseRenpy(readFile, filename):
|
|
||||||
global PBAR
|
|
||||||
totalTokens = [0, 0]
|
def parseRenpy(readFile, filename):
|
||||||
|
global PBAR
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
PBAR = pbar
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
|
pbar.desc = filename
|
||||||
try:
|
PBAR = pbar
|
||||||
result = translateRenpy(data, filename, [])
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateRenpy(data, filename, [])
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="utf-8"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="utf-8"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateRenpy(data, filename, translatedList):
|
|
||||||
stringList = []
|
|
||||||
currentGroup = []
|
def translateRenpy(data, filename, translatedList):
|
||||||
tokens = [0, 0]
|
stringList = []
|
||||||
speaker = ""
|
currentGroup = []
|
||||||
voice = False
|
tokens = [0, 0]
|
||||||
global LOCK, ESTIMATE, FILENAME, PBAR
|
speaker = ""
|
||||||
i = 0
|
voice = False
|
||||||
|
global LOCK, ESTIMATE, FILENAME, PBAR
|
||||||
while i < len(data):
|
i = 0
|
||||||
voice = False
|
|
||||||
speaker = ""
|
while i < len(data):
|
||||||
lineRegexNoSpeaker = r'^\s\s\s\s"(.*)"'
|
voice = False
|
||||||
lineRegexSpeaker = r'^\s\s\s\s(.+?)\s"(.*)"'
|
speaker = ""
|
||||||
|
lineRegexNoSpeaker = r'^\s\s\s\s"(.*)"'
|
||||||
# Grab Line
|
lineRegexSpeaker = r'^\s\s\s\s(.+?)\s"(.*)"'
|
||||||
match = re.search(lineRegexSpeaker, data[i])
|
|
||||||
if match:
|
# Grab Line
|
||||||
response = getSpeaker(match.group(1))
|
match = re.search(lineRegexSpeaker, data[i])
|
||||||
jaString = match.group(2)
|
if match:
|
||||||
speaker = response[0]
|
response = getSpeaker(match.group(1))
|
||||||
tokens[0] += response[1][0]
|
jaString = match.group(2)
|
||||||
tokens[1] += response[1][1]
|
speaker = response[0]
|
||||||
else:
|
tokens[0] += response[1][0]
|
||||||
match = re.search(lineRegexNoSpeaker, data[i])
|
tokens[1] += response[1][1]
|
||||||
if match:
|
else:
|
||||||
jaString = match.group(1)
|
match = re.search(lineRegexNoSpeaker, data[i])
|
||||||
|
if match:
|
||||||
# Valid Line
|
jaString = match.group(1)
|
||||||
if match and "voice" not in data[i]:
|
|
||||||
originalString = jaString
|
# Valid Line
|
||||||
# Pass 1
|
if match and "voice" not in data[i]:
|
||||||
if translatedList == []:
|
originalString = jaString
|
||||||
# Remove any textwrap
|
# Pass 1
|
||||||
jaString = jaString.replace("\\n", " ")
|
if translatedList == []:
|
||||||
|
# Remove any textwrap
|
||||||
# Add String
|
jaString = jaString.replace("\\n", " ")
|
||||||
if speaker:
|
|
||||||
stringList.append(f"[{speaker}]: {jaString.strip()}")
|
# Add String
|
||||||
else:
|
if speaker:
|
||||||
stringList.append(jaString.strip())
|
stringList.append(f"[{speaker}]: {jaString.strip()}")
|
||||||
|
else:
|
||||||
# Pass 2
|
stringList.append(jaString.strip())
|
||||||
else:
|
|
||||||
# Get Text
|
# Pass 2
|
||||||
if translatedList:
|
else:
|
||||||
# Grab and Pop
|
# Get Text
|
||||||
translatedText = translatedList[0]
|
if translatedList:
|
||||||
translatedList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = translatedList[0]
|
||||||
# Set to None if empty list
|
translatedList.pop(0)
|
||||||
if len(translatedList) <= 0:
|
|
||||||
translatedList = None
|
# Set to None if empty list
|
||||||
|
if len(translatedList) <= 0:
|
||||||
# Remove speaker
|
translatedList = None
|
||||||
if speaker != "":
|
|
||||||
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
|
# Remove speaker
|
||||||
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
if speaker != "":
|
||||||
|
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
|
||||||
# Escape Quotes
|
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
||||||
translatedText = re.sub(r'[\\]*(")', '\\"', translatedText)
|
|
||||||
translatedText = re.sub(r"[\\]*(')", "\\'", translatedText)
|
# Escape Quotes
|
||||||
|
translatedText = re.sub(r'[\\]*(")', '\\"', translatedText)
|
||||||
# Textwrap
|
translatedText = re.sub(r"[\\]*(')", "\\'", translatedText)
|
||||||
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
translatedText = translatedText.replace("\n", "\\n")
|
# Textwrap
|
||||||
|
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
# Set Data
|
translatedText = translatedText.replace("\n", "\\n")
|
||||||
data[i] = data[i].replace(originalString, translatedText)
|
|
||||||
save_progress_lines(data, filename)
|
# Set Data
|
||||||
i += 1
|
data[i] = data[i].replace(originalString, translatedText)
|
||||||
else:
|
save_progress_lines(data, filename)
|
||||||
i += 1
|
i += 1
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if len(stringList) > 0:
|
|
||||||
# Set Progress
|
# EOF
|
||||||
PBAR.total = len(stringList)
|
if len(stringList) > 0:
|
||||||
PBAR.refresh()
|
# Set Progress
|
||||||
|
PBAR.total = len(stringList)
|
||||||
# Translate
|
PBAR.refresh()
|
||||||
response = translateAI(stringList, "Reply with the English TL of the NPC Name")
|
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "Reply with the English TL of the NPC Name")
|
||||||
translatedList = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Set Strings
|
translatedList = response[0]
|
||||||
if len(stringList) == len(translatedList):
|
|
||||||
translateRenpy(data, filename, translatedList)
|
# Set Strings
|
||||||
|
if len(stringList) == len(translatedList):
|
||||||
# Mismatch
|
translateRenpy(data, filename, translatedList)
|
||||||
else:
|
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if FILENAME not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
return tokens
|
if FILENAME not in MISMATCH:
|
||||||
|
MISMATCH.append(FILENAME)
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ from util.translation import TranslationConfig, translateAI as sharedtranslateAI
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LOCK = threading.Lock()
|
LOCK = threading.Lock()
|
||||||
THREAD_CTX = threading.local()
|
THREAD_CTX = threading.local()
|
||||||
WIDTH = int(os.getenv("width"))
|
WIDTH = int(os.getenv("width"))
|
||||||
|
|
@ -803,7 +805,7 @@ def update_vocab_section(category: str, pairs: list[tuple[str, str]]):
|
||||||
The existing section is replaced entirely; other sections are preserved.
|
The existing section is replaced entirely; other sections are preserved.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
vocab_path = Path("vocab.txt")
|
vocab_path = VOCAB_PATH
|
||||||
|
|
||||||
# Helper: normalized comparison to detect no-op translations
|
# Helper: normalized comparison to detect no-op translations
|
||||||
def _norm(s: str) -> str:
|
def _norm(s: str) -> str:
|
||||||
|
|
@ -5071,7 +5073,7 @@ def finalizeSpeakerParse():
|
||||||
_speakerCache[orig] = norm
|
_speakerCache[orig] = norm
|
||||||
NAMESLIST.append([orig, norm])
|
NAMESLIST.append([orig, norm])
|
||||||
|
|
||||||
vocab_path = Path("vocab.txt")
|
vocab_path = VOCAB_PATH
|
||||||
if not vocab_path.exists():
|
if not vocab_path.exists():
|
||||||
return
|
return
|
||||||
content = vocab_path.read_text(encoding="utf-8")
|
content = vocab_path.read_text(encoding="utf-8")
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
4524
modules/srpg.py
4524
modules/srpg.py
File diff suppressed because it is too large
Load diff
726
modules/text.py
726
modules/text.py
|
|
@ -1,363 +1,365 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
FILENAME = None
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# tqdm Globals
|
FILENAME = None
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
|
LEAVE = False
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
PBAR = None
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleText(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, TOKENS, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleText(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, TOKENS, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
# Translate
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
# Translate
|
||||||
|
start = time.time()
|
||||||
# Translate
|
translatedData = openFiles(filename)
|
||||||
if not estimate:
|
|
||||||
try:
|
# Translate
|
||||||
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
if not estimate:
|
||||||
outFile.writelines(translatedData[0])
|
try:
|
||||||
except Exception:
|
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
traceback.print_exc()
|
outFile.writelines(translatedData[0])
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
# Print File
|
return "Fail"
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print File
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
|
return totalString
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
|
||||||
translatedData = parseText(readFile, filename)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseText(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseText(readFile, filename):
|
|
||||||
global PBAR
|
|
||||||
totalTokens = [0, 0]
|
def parseText(readFile, filename):
|
||||||
|
global PBAR
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
PBAR = pbar
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
|
pbar.desc = filename
|
||||||
try:
|
PBAR = pbar
|
||||||
result = translateTxt(data, filename, [])
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateTxt(data, filename, [])
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="utf-8"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="utf-8"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
|
||||||
"""Save progress only when tokens indicate work or when explicitly called after a mutation."""
|
|
||||||
try:
|
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
||||||
if tokens is not None:
|
"""Save progress only when tokens indicate work or when explicitly called after a mutation."""
|
||||||
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
try:
|
||||||
return
|
if tokens is not None:
|
||||||
save_progress_lines(lines, filename, encoding=encoding)
|
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
||||||
except Exception:
|
return
|
||||||
traceback.print_exc()
|
save_progress_lines(lines, filename, encoding=encoding)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateTxt(data, filename, translatedList):
|
|
||||||
if translatedList:
|
|
||||||
stringList = translatedList[0]
|
def translateTxt(data, filename, translatedList):
|
||||||
choiceList = translatedList[1]
|
if translatedList:
|
||||||
else:
|
stringList = translatedList[0]
|
||||||
stringList = []
|
choiceList = translatedList[1]
|
||||||
choiceList = []
|
else:
|
||||||
tokens = [0, 0]
|
stringList = []
|
||||||
speaker = ""
|
choiceList = []
|
||||||
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
tokens = [0, 0]
|
||||||
i = 0
|
speaker = ""
|
||||||
|
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
||||||
# Regex
|
i = 0
|
||||||
lineTextRegex = r"(?:^\[.+?\]:)?(.+)"
|
|
||||||
speakerTextRegex = r"^\[(.+?)\]"
|
# Regex
|
||||||
|
lineTextRegex = r"(?:^\[.+?\]:)?(.+)"
|
||||||
while i < len(data):
|
speakerTextRegex = r"^\[(.+?)\]"
|
||||||
# Speaker
|
|
||||||
match = re.search(speakerTextRegex, data[i])
|
while i < len(data):
|
||||||
if match:
|
# Speaker
|
||||||
# Get Speaker
|
match = re.search(speakerTextRegex, data[i])
|
||||||
speakerData = getSpeaker(match.group(1))
|
if match:
|
||||||
speaker = speakerData[0]
|
# Get Speaker
|
||||||
tokens[0] += speakerData[1][0]
|
speakerData = getSpeaker(match.group(1))
|
||||||
tokens[1] += speakerData[1][1]
|
speaker = speakerData[0]
|
||||||
data[i] = data[i].replace(match.group(1), speaker)
|
tokens[0] += speakerData[1][0]
|
||||||
|
tokens[1] += speakerData[1][1]
|
||||||
# Dialogue
|
data[i] = data[i].replace(match.group(1), speaker)
|
||||||
match = re.search(lineTextRegex, data[i])
|
|
||||||
jaString = None
|
# Dialogue
|
||||||
if match:
|
match = re.search(lineTextRegex, data[i])
|
||||||
# Set String
|
jaString = None
|
||||||
jaString = match.group(1)
|
if match:
|
||||||
|
# Set String
|
||||||
# Pass 1
|
jaString = match.group(1)
|
||||||
if not translatedList:
|
|
||||||
# Strip Spaces
|
# Pass 1
|
||||||
jaString = jaString.strip()
|
if not translatedList:
|
||||||
|
# Strip Spaces
|
||||||
if jaString:
|
jaString = jaString.strip()
|
||||||
if speaker:
|
|
||||||
stringList.append(f"[{speaker}]: {jaString}")
|
if jaString:
|
||||||
else:
|
if speaker:
|
||||||
stringList.append(jaString)
|
stringList.append(f"[{speaker}]: {jaString}")
|
||||||
|
else:
|
||||||
# Pass 2
|
stringList.append(jaString)
|
||||||
else:
|
|
||||||
# Get Text
|
# Pass 2
|
||||||
if stringList:
|
else:
|
||||||
# Grab and Pop
|
# Get Text
|
||||||
translatedText = stringList[0]
|
if stringList:
|
||||||
stringList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = stringList[0]
|
||||||
# Set to None if empty list
|
stringList.pop(0)
|
||||||
if len(stringList) <= 0:
|
|
||||||
stringList = None
|
# Set to None if empty list
|
||||||
|
if len(stringList) <= 0:
|
||||||
# Remove speaker
|
stringList = None
|
||||||
translatedText = re.sub(r"(^\[.+?\]\s?[|:]\s?)", "", translatedText)
|
|
||||||
|
# Remove speaker
|
||||||
# # Textwrap
|
translatedText = re.sub(r"(^\[.+?\]\s?[|:]\s?)", "", translatedText)
|
||||||
# translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
# translatedText = translatedText.replace("\n", "\\n")
|
# # Textwrap
|
||||||
|
# translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
# Set Data
|
# translatedText = translatedText.replace("\n", "\\n")
|
||||||
data[i] = f"{translatedText}\n"
|
|
||||||
saveCheckLines(data, filename)
|
# Set Data
|
||||||
|
data[i] = f"{translatedText}\n"
|
||||||
i += 1
|
saveCheckLines(data, filename)
|
||||||
else:
|
|
||||||
i += 1
|
i += 1
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if not translatedList:
|
|
||||||
stringListTL = []
|
# EOF
|
||||||
choiceListTL = []
|
if not translatedList:
|
||||||
|
stringListTL = []
|
||||||
# String List
|
choiceListTL = []
|
||||||
if stringList:
|
|
||||||
PBAR.total = len(stringList)
|
# String List
|
||||||
PBAR.refresh()
|
if stringList:
|
||||||
response = translateAI(stringList, "Reply with the English Translation")
|
PBAR.total = len(stringList)
|
||||||
tokens[0] += response[1][0]
|
PBAR.refresh()
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "Reply with the English Translation")
|
||||||
stringListTL = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
if len(stringList) != len(stringListTL):
|
stringListTL = response[0]
|
||||||
# Mismatch
|
|
||||||
with LOCK:
|
if len(stringList) != len(stringListTL):
|
||||||
if FILENAME not in MISMATCH:
|
# Mismatch
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
|
if FILENAME not in MISMATCH:
|
||||||
# Choice List
|
MISMATCH.append(FILENAME)
|
||||||
if choiceList:
|
|
||||||
response = translateAI(choiceList, "Reply with the English TL of the Dialogue Choice")
|
# Choice List
|
||||||
tokens[0] += response[1][0]
|
if choiceList:
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(choiceList, "Reply with the English TL of the Dialogue Choice")
|
||||||
choiceListTL = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
if len(choiceList) != len(choiceListTL):
|
choiceListTL = response[0]
|
||||||
# Mismatch
|
|
||||||
with LOCK:
|
if len(choiceList) != len(choiceListTL):
|
||||||
if FILENAME not in MISMATCH:
|
# Mismatch
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
|
if FILENAME not in MISMATCH:
|
||||||
# Set Strings
|
MISMATCH.append(FILENAME)
|
||||||
translateTxt(data, filename, [stringListTL, choiceListTL])
|
|
||||||
return tokens
|
# Set Strings
|
||||||
|
translateTxt(data, filename, [stringListTL, choiceListTL])
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
)
|
)
|
||||||
|
|
@ -1,457 +1,459 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# OpenAI initialization centralized in util/translation.py
|
# OpenAI initialization centralized in util/translation.py
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
FILENAME = None
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# tqdm Globals
|
FILENAME = None
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
|
LEAVE = False
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
PBAR = None
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleTyrano(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, TOKENS, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleTyrano(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, TOKENS, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
# Translate
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
# Translate
|
||||||
|
start = time.time()
|
||||||
# Translate
|
translatedData = openFiles(filename)
|
||||||
if not estimate:
|
|
||||||
try:
|
# Translate
|
||||||
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
if not estimate:
|
||||||
outFile.writelines(translatedData[0])
|
try:
|
||||||
except Exception:
|
with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile:
|
||||||
traceback.print_exc()
|
outFile.writelines(translatedData[0])
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
# Print File
|
return "Fail"
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print File
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
|
return totalString
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
|
||||||
translatedData = parseTyrano(readFile, filename)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseTyrano(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseTyrano(readFile, filename):
|
|
||||||
global PBAR
|
|
||||||
totalTokens = [0, 0]
|
def parseTyrano(readFile, filename):
|
||||||
|
global PBAR
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
PBAR = pbar
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
|
pbar.desc = filename
|
||||||
try:
|
PBAR = pbar
|
||||||
result = translateTyrano(data, filename, [])
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateTyrano(data, filename, [])
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="utf-8"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="utf-8"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
|
||||||
"""Save progress only when tokens indicate work or when tokens is None but we know a mutation occurred."""
|
|
||||||
try:
|
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
||||||
# If explicit tokens provided, gate on tokens; otherwise assume we were called on actual mutation
|
"""Save progress only when tokens indicate work or when tokens is None but we know a mutation occurred."""
|
||||||
if tokens is not None:
|
try:
|
||||||
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
# If explicit tokens provided, gate on tokens; otherwise assume we were called on actual mutation
|
||||||
return
|
if tokens is not None:
|
||||||
save_progress_lines(lines, filename, encoding=encoding)
|
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
||||||
except Exception:
|
return
|
||||||
traceback.print_exc()
|
save_progress_lines(lines, filename, encoding=encoding)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateTyrano(data, filename, translatedList):
|
|
||||||
if translatedList:
|
|
||||||
stringList = translatedList[0]
|
def translateTyrano(data, filename, translatedList):
|
||||||
choiceList = translatedList[1]
|
if translatedList:
|
||||||
else:
|
stringList = translatedList[0]
|
||||||
stringList = []
|
choiceList = translatedList[1]
|
||||||
choiceList = []
|
else:
|
||||||
tokens = [0, 0]
|
stringList = []
|
||||||
speaker = ""
|
choiceList = []
|
||||||
stringListTL = []
|
tokens = [0, 0]
|
||||||
choiceListTL = []
|
speaker = ""
|
||||||
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
stringListTL = []
|
||||||
i = 0
|
choiceListTL = []
|
||||||
|
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
|
||||||
while i < len(data):
|
i = 0
|
||||||
voice = False
|
|
||||||
lineRegexNoSpeaker = r"^([^\[#;*@\n]+)\[p\]|^([^\[#;*@\n]+)\[l\]\[[rp]\]|^([^\[#;*@\n]+)\[[rpl]\]|^([^\[#;*@_\n]+)\n$"
|
while i < len(data):
|
||||||
lineRegexSpeaker = r"^#(.*)"
|
voice = False
|
||||||
furiganaRegex = r"(\[ruby\stext=(.*?)\])"
|
lineRegexNoSpeaker = r"^([^\[#;*@\n]+)\[p\]|^([^\[#;*@\n]+)\[l\]\[[rp]\]|^([^\[#;*@\n]+)\[[rpl]\]|^([^\[#;*@_\n]+)\n$"
|
||||||
choiceRegex = r'\[glink.+?text="(.*?)"'
|
lineRegexSpeaker = r"^#(.*)"
|
||||||
|
furiganaRegex = r"(\[ruby\stext=(.*?)\])"
|
||||||
# Speaker
|
choiceRegex = r'\[glink.+?text="(.*?)"'
|
||||||
match = re.search(lineRegexSpeaker, data[i])
|
|
||||||
if match:
|
# Speaker
|
||||||
if match.group(1):
|
match = re.search(lineRegexSpeaker, data[i])
|
||||||
response = getSpeaker(match.group(1))
|
if match:
|
||||||
speaker = response[0]
|
if match.group(1):
|
||||||
tokens[0] += response[1][0]
|
response = getSpeaker(match.group(1))
|
||||||
tokens[1] += response[1][1]
|
speaker = response[0]
|
||||||
data[i] = data[i].replace(match.group(1), speaker)
|
tokens[0] += response[1][0]
|
||||||
else:
|
tokens[1] += response[1][1]
|
||||||
speaker = None
|
data[i] = data[i].replace(match.group(1), speaker)
|
||||||
|
else:
|
||||||
# Furigana
|
speaker = None
|
||||||
match = re.search(r"^\[ruby\stext", data[i])
|
|
||||||
furiganaList = []
|
# Furigana
|
||||||
if match:
|
match = re.search(r"^\[ruby\stext", data[i])
|
||||||
# Check next line and combine
|
furiganaList = []
|
||||||
while match:
|
if match:
|
||||||
furiganaList.append(data[i].replace("\n", ""))
|
# Check next line and combine
|
||||||
del data[i]
|
while match:
|
||||||
match = re.search(r"^\[ruby\stext", data[i])
|
furiganaList.append(data[i].replace("\n", ""))
|
||||||
jaString = "".join(furiganaList)
|
del data[i]
|
||||||
|
match = re.search(r"^\[ruby\stext", data[i])
|
||||||
# Ruby Text
|
jaString = "".join(furiganaList)
|
||||||
furiganaList = re.findall(furiganaRegex, jaString)
|
|
||||||
for furigana in furiganaList:
|
# Ruby Text
|
||||||
jaString = jaString.replace(furigana[0], furigana[1])
|
furiganaList = re.findall(furiganaRegex, jaString)
|
||||||
|
for furigana in furiganaList:
|
||||||
data.insert(i, f"{jaString}[r]")
|
jaString = jaString.replace(furigana[0], furigana[1])
|
||||||
|
|
||||||
# Dialogue
|
data.insert(i, f"{jaString}[r]")
|
||||||
match = re.search(lineRegexNoSpeaker, data[i])
|
|
||||||
jaString = None
|
# Dialogue
|
||||||
if match:
|
match = re.search(lineRegexNoSpeaker, data[i])
|
||||||
# Find which group matched
|
jaString = None
|
||||||
for group_num in range(1, 5):
|
if match:
|
||||||
try:
|
# Find which group matched
|
||||||
if match.group(group_num):
|
for group_num in range(1, 5):
|
||||||
jaString = match.group(group_num)
|
try:
|
||||||
break
|
if match.group(group_num):
|
||||||
except IndexError:
|
jaString = match.group(group_num)
|
||||||
break
|
break
|
||||||
|
except IndexError:
|
||||||
# Skip if no valid string found
|
break
|
||||||
if not jaString:
|
|
||||||
i += 1
|
# Skip if no valid string found
|
||||||
continue
|
if not jaString:
|
||||||
|
i += 1
|
||||||
# Combine w/ next line if necessary
|
continue
|
||||||
repeatRegex = r"(.+?)\[[rpl]\]"
|
|
||||||
if i + 1 < len(data):
|
# Combine w/ next line if necessary
|
||||||
match = re.search(repeatRegex, data[i + 1])
|
repeatRegex = r"(.+?)\[[rpl]\]"
|
||||||
while match and "[p]" not in data[i]:
|
if i + 1 < len(data):
|
||||||
jaString = jaString + match.group(1)
|
match = re.search(repeatRegex, data[i + 1])
|
||||||
jaString = jaString.replace("_ ", "")
|
while match and "[p]" not in data[i]:
|
||||||
if "[p]" in data[i + 1]:
|
jaString = jaString + match.group(1)
|
||||||
data[i] = f"{jaString}[p]\n"
|
jaString = jaString.replace("_ ", "")
|
||||||
else:
|
if "[p]" in data[i + 1]:
|
||||||
data[i] = jaString
|
data[i] = f"{jaString}[p]\n"
|
||||||
del data[i + 1]
|
else:
|
||||||
if i + 1 < len(data):
|
data[i] = jaString
|
||||||
match = re.search(repeatRegex, data[i + 1])
|
del data[i + 1]
|
||||||
else:
|
if i + 1 < len(data):
|
||||||
break
|
match = re.search(repeatRegex, data[i + 1])
|
||||||
|
else:
|
||||||
originalString = jaString
|
break
|
||||||
|
|
||||||
# Pass 1
|
originalString = jaString
|
||||||
if not translatedList:
|
|
||||||
# Remove any textwrap and commands
|
# Pass 1
|
||||||
jaString = jaString.replace("[r]", " ")
|
if not translatedList:
|
||||||
jaString = jaString.replace("[l]", "")
|
# Remove any textwrap and commands
|
||||||
|
jaString = jaString.replace("[r]", " ")
|
||||||
# Ruby Text
|
jaString = jaString.replace("[l]", "")
|
||||||
furiganaList = re.findall(furiganaRegex, jaString)
|
|
||||||
for furigana in furiganaList:
|
# Ruby Text
|
||||||
jaString = jaString.replace(furigana[0], furigana[1])
|
furiganaList = re.findall(furiganaRegex, jaString)
|
||||||
|
for furigana in furiganaList:
|
||||||
# Strip Spaces
|
jaString = jaString.replace(furigana[0], furigana[1])
|
||||||
jaString = jaString.strip()
|
|
||||||
|
# Strip Spaces
|
||||||
if jaString:
|
jaString = jaString.strip()
|
||||||
if speaker:
|
|
||||||
stringList.append(f"[{speaker}]: {jaString}")
|
if jaString:
|
||||||
else:
|
if speaker:
|
||||||
stringList.append(jaString)
|
stringList.append(f"[{speaker}]: {jaString}")
|
||||||
|
else:
|
||||||
# Pass 2
|
stringList.append(jaString)
|
||||||
else:
|
|
||||||
# Get Text
|
# Pass 2
|
||||||
if stringList:
|
else:
|
||||||
# Grab and Pop
|
# Get Text
|
||||||
translatedText = stringList[0]
|
if stringList:
|
||||||
stringList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = stringList[0]
|
||||||
# Set to None if empty list
|
stringList.pop(0)
|
||||||
if len(stringList) <= 0:
|
|
||||||
stringList = None
|
# Set to None if empty list
|
||||||
|
if len(stringList) <= 0:
|
||||||
# Remove speaker
|
stringList = None
|
||||||
if speaker != "":
|
|
||||||
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
|
# Remove speaker
|
||||||
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
if speaker != "":
|
||||||
|
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
|
||||||
# Avoid Crashes
|
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
|
||||||
translatedText = translatedText.replace("[", "(")
|
|
||||||
translatedText = translatedText.replace("]", ")")
|
# Avoid Crashes
|
||||||
|
translatedText = translatedText.replace("[", "(")
|
||||||
# Textwrap
|
translatedText = translatedText.replace("]", ")")
|
||||||
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
translatedText = translatedText.replace("\n", "[r]")
|
# Textwrap
|
||||||
|
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
# Set Data
|
translatedText = translatedText.replace("\n", "[r]")
|
||||||
data[i] = data[i].replace(originalString, translatedText)
|
|
||||||
# Save progress after each line change (we mutated, so tokens gate optional)
|
# Set Data
|
||||||
saveCheckLines(data, filename)
|
data[i] = data[i].replace(originalString, translatedText)
|
||||||
|
# Save progress after each line change (we mutated, so tokens gate optional)
|
||||||
# Choices
|
saveCheckLines(data, filename)
|
||||||
match = re.search(choiceRegex, data[i])
|
|
||||||
if match:
|
# Choices
|
||||||
# Pass 1
|
match = re.search(choiceRegex, data[i])
|
||||||
if not translatedList:
|
if match:
|
||||||
choiceList.append(match.group(1))
|
# Pass 1
|
||||||
match = re.search(choiceRegex, data[i + 1])
|
if not translatedList:
|
||||||
|
choiceList.append(match.group(1))
|
||||||
# Pass 2
|
match = re.search(choiceRegex, data[i + 1])
|
||||||
else:
|
|
||||||
# Grab and Pop
|
# Pass 2
|
||||||
translatedText = choiceList[0]
|
else:
|
||||||
choiceList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = choiceList[0]
|
||||||
# Replace Spaces
|
choiceList.pop(0)
|
||||||
translatedText = translatedText.replace(" ", "\u3000")
|
|
||||||
|
# Replace Spaces
|
||||||
# Set
|
translatedText = translatedText.replace(" ", "\u3000")
|
||||||
data[i] = data[i].replace(match.group(1), translatedText)
|
|
||||||
saveCheckLines(data, filename)
|
# Set
|
||||||
|
data[i] = data[i].replace(match.group(1), translatedText)
|
||||||
i += 1
|
saveCheckLines(data, filename)
|
||||||
else:
|
|
||||||
i += 1
|
i += 1
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if not translatedList:
|
|
||||||
# String List
|
# EOF
|
||||||
if stringList:
|
if not translatedList:
|
||||||
PBAR.total = len(stringList)
|
# String List
|
||||||
PBAR.refresh()
|
if stringList:
|
||||||
response = translateAI(stringList, "Reply with the English Translation")
|
PBAR.total = len(stringList)
|
||||||
tokens[0] += response[1][0]
|
PBAR.refresh()
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "Reply with the English Translation")
|
||||||
stringListTL = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
if len(stringList) != len(stringListTL):
|
stringListTL = response[0]
|
||||||
# Mismatch
|
|
||||||
with LOCK:
|
if len(stringList) != len(stringListTL):
|
||||||
if FILENAME not in MISMATCH:
|
# Mismatch
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
|
if FILENAME not in MISMATCH:
|
||||||
# Choice List
|
MISMATCH.append(FILENAME)
|
||||||
if choiceList:
|
|
||||||
response = translateAI(choiceList, "Reply with the English TL of the Dialogue Choice")
|
# Choice List
|
||||||
tokens[0] += response[1][0]
|
if choiceList:
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(choiceList, "Reply with the English TL of the Dialogue Choice")
|
||||||
choiceListTL = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
if len(choiceList) != len(choiceListTL):
|
choiceListTL = response[0]
|
||||||
# Mismatch
|
|
||||||
with LOCK:
|
if len(choiceList) != len(choiceListTL):
|
||||||
if FILENAME not in MISMATCH:
|
# Mismatch
|
||||||
MISMATCH.append(FILENAME)
|
with LOCK:
|
||||||
|
if FILENAME not in MISMATCH:
|
||||||
# Recursive call for Pass 2 with translated strings
|
MISMATCH.append(FILENAME)
|
||||||
result = translateTyrano(data, filename, [stringListTL, choiceListTL])
|
|
||||||
tokens[0] += result[0]
|
# Recursive call for Pass 2 with translated strings
|
||||||
tokens[1] += result[1]
|
result = translateTyrano(data, filename, [stringListTL, choiceListTL])
|
||||||
|
tokens[0] += result[0]
|
||||||
return tokens
|
tokens[1] += result[1]
|
||||||
|
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
708
modules/unity.py
708
modules/unity.py
|
|
@ -1,353 +1,355 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# OpenAI initialization centralized in util/translation.py
|
# OpenAI initialization centralized in util/translation.py
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
# tqdm Globals
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
PBAR = None
|
POSITION = 0
|
||||||
FILENAME = None
|
LEAVE = False
|
||||||
|
PBAR = None
|
||||||
# Full Width
|
FILENAME = None
|
||||||
ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F))
|
|
||||||
ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus
|
# Full Width
|
||||||
wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F))
|
ascii_to_wide = dict((i, chr(i + 0xFEE0)) for i in range(0x21, 0x7F))
|
||||||
wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus
|
ascii_to_wide.update({0x20: "\u3000", 0x2D: "\u2212"}) # space and minus
|
||||||
|
wide_to_ascii = dict((i, chr(i - 0xFEE0)) for i in range(0xFF01, 0xFF5F))
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
wide_to_ascii.update({0x3000: " ", 0x2212: "-"}) # space and minus
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleUnity(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleUnity(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
if ESTIMATE:
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
if ESTIMATE:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
else:
|
return totalString
|
||||||
try:
|
|
||||||
with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile:
|
else:
|
||||||
start = time.time()
|
try:
|
||||||
translatedData = openFiles(filename)
|
with open("translated/" + filename, "w", encoding="utf8", errors="ignore") as outFile:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
outFile.writelines(translatedData[0])
|
# Print Result
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
end = time.time()
|
||||||
with LOCK:
|
outFile.writelines(translatedData[0])
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
except Exception:
|
TOKENS[0] += translatedData[1][0]
|
||||||
traceback.print_exc()
|
TOKENS[1] += translatedData[1][1]
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
|
||||||
translatedData = parseUnity(readFile, filename)
|
def openFiles(filename):
|
||||||
|
with open("files/" + filename, "r", encoding="utf8") as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseUnity(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseUnity(readFile, filename):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
|
def parseUnity(readFile, filename):
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
try:
|
pbar.desc = filename
|
||||||
result = translateUnity(data, pbar, filename, [])
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateUnity(data, pbar, filename, [])
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="utf-8"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="utf-8"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
|
||||||
"""Save progress only when tokens indicate work or when explicitly called after a mutation."""
|
|
||||||
try:
|
def saveCheckLines(lines, filename, tokens=None, encoding="utf-8"):
|
||||||
if tokens is not None:
|
"""Save progress only when tokens indicate work or when explicitly called after a mutation."""
|
||||||
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
try:
|
||||||
return
|
if tokens is not None:
|
||||||
save_progress_lines(lines, filename, encoding=encoding)
|
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
||||||
except Exception:
|
return
|
||||||
traceback.print_exc()
|
save_progress_lines(lines, filename, encoding=encoding)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateUnity(data, pbar, filename, translatedList):
|
|
||||||
stringList = []
|
|
||||||
tokens = [0, 0]
|
def translateUnity(data, pbar, filename, translatedList):
|
||||||
global LOCK, ESTIMATE, PBAR
|
stringList = []
|
||||||
PBAR = pbar
|
tokens = [0, 0]
|
||||||
i = 0
|
global LOCK, ESTIMATE, PBAR
|
||||||
|
PBAR = pbar
|
||||||
# Dialogue
|
i = 0
|
||||||
while i < len(data):
|
|
||||||
# Lines
|
# Dialogue
|
||||||
regex = r"(.*?)=(.*)"
|
while i < len(data):
|
||||||
match = re.search(regex, data[i])
|
# Lines
|
||||||
if match:
|
regex = r"(.*?)=(.*)"
|
||||||
leftString = match.group(1)
|
match = re.search(regex, data[i])
|
||||||
rightString = match.group(2)
|
if match:
|
||||||
|
leftString = match.group(1)
|
||||||
# Validate Japanese Text
|
rightString = match.group(2)
|
||||||
if not re.search(LANGREGEX, leftString) and IGNORETLTEXT:
|
|
||||||
i += 1
|
# Validate Japanese Text
|
||||||
continue
|
if not re.search(LANGREGEX, leftString) and IGNORETLTEXT:
|
||||||
|
i += 1
|
||||||
# Pass 1
|
continue
|
||||||
if translatedList == []:
|
|
||||||
jaString = leftString
|
# Pass 1
|
||||||
|
if translatedList == []:
|
||||||
# Remove textwrap
|
jaString = leftString
|
||||||
# jaString = jaString.replace("\\n", " ")
|
|
||||||
|
# Remove textwrap
|
||||||
# Add String
|
# jaString = jaString.replace("\\n", " ")
|
||||||
stringList.append(jaString.strip())
|
|
||||||
|
# Add String
|
||||||
# Pass 2
|
stringList.append(jaString.strip())
|
||||||
else:
|
|
||||||
# Get Text
|
# Pass 2
|
||||||
if translatedList:
|
else:
|
||||||
# Grab and Pop
|
# Get Text
|
||||||
translatedText = translatedList[0]
|
if translatedList:
|
||||||
translatedList.pop(0)
|
# Grab and Pop
|
||||||
|
translatedText = translatedList[0]
|
||||||
# Set to None if empty list
|
translatedList.pop(0)
|
||||||
if len(translatedList) <= 0:
|
|
||||||
translatedList = None
|
# Set to None if empty list
|
||||||
|
if len(translatedList) <= 0:
|
||||||
# Textwrap
|
translatedList = None
|
||||||
# translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
# translatedText = translatedText.replace("\n", "\\n")
|
# Textwrap
|
||||||
|
# translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
# Remove Double Spaces and =
|
# translatedText = translatedText.replace("\n", "\\n")
|
||||||
translatedText = translatedText.replace(" ", " ")
|
|
||||||
translatedText = translatedText.replace("=", "->")
|
# Remove Double Spaces and =
|
||||||
|
translatedText = translatedText.replace(" ", " ")
|
||||||
# Set Data
|
translatedText = translatedText.replace("=", "->")
|
||||||
data[i] = f"{leftString}={translatedText}\n"
|
|
||||||
saveCheckLines(data, filename)
|
# Set Data
|
||||||
i += 1
|
data[i] = f"{leftString}={translatedText}\n"
|
||||||
|
saveCheckLines(data, filename)
|
||||||
# Nothing relevant. Skip Line.
|
i += 1
|
||||||
else:
|
|
||||||
i += 1
|
# Nothing relevant. Skip Line.
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if len(stringList) > 0:
|
|
||||||
# Set Progress
|
# EOF
|
||||||
pbar.total = len(stringList)
|
if len(stringList) > 0:
|
||||||
pbar.refresh()
|
# Set Progress
|
||||||
|
pbar.total = len(stringList)
|
||||||
# Translate
|
pbar.refresh()
|
||||||
response = translateAI(stringList, "")
|
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "")
|
||||||
translatedList = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Set Strings
|
translatedList = response[0]
|
||||||
if len(stringList) == len(translatedList):
|
|
||||||
translateUnity(data, pbar, filename, translatedList)
|
# Set Strings
|
||||||
|
if len(stringList) == len(translatedList):
|
||||||
# Mismatch
|
translateUnity(data, pbar, filename, translatedList)
|
||||||
else:
|
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if filename not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(filename)
|
with LOCK:
|
||||||
return tokens
|
if filename not in MISMATCH:
|
||||||
|
MISMATCH.append(filename)
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
5400
modules/wolf.py
5400
modules/wolf.py
File diff suppressed because it is too large
Load diff
842
modules/wolf2.py
842
modules/wolf2.py
|
|
@ -1,420 +1,422 @@
|
||||||
# Libraries
|
# Libraries
|
||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# OpenAI initialization centralized in util/translation.py
|
# OpenAI initialization centralized in util/translation.py
|
||||||
|
|
||||||
# Globals
|
# Globals
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
TIMEOUT = int(os.getenv("timeout"))
|
TIMEOUT = int(os.getenv("timeout"))
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
LOCK = threading.Lock()
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
WIDTH = int(os.getenv("width"))
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LISTWIDTH = int(os.getenv("listWidth"))
|
LOCK = threading.Lock()
|
||||||
NOTEWIDTH = 70
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
LISTWIDTH = int(os.getenv("listWidth"))
|
||||||
ESTIMATE = ""
|
NOTEWIDTH = 70
|
||||||
TOKENS = [0, 0]
|
MAXHISTORY = 10
|
||||||
NAMESLIST = []
|
ESTIMATE = ""
|
||||||
NAMES = False # Output a list of all the character names found
|
TOKENS = [0, 0]
|
||||||
BRFLAG = False # If the game uses <br> instead
|
NAMESLIST = []
|
||||||
FIXTEXTWRAP = True # Overwrites textwrap
|
NAMES = False # Output a list of all the character names found
|
||||||
IGNORETLTEXT = False # Ignores all translated text.
|
BRFLAG = False # If the game uses <br> instead
|
||||||
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
FIXTEXTWRAP = True # Overwrites textwrap
|
||||||
FILENAME = "" # Current file being processed, used by translateAI wrapper
|
IGNORETLTEXT = False # Ignores all translated text.
|
||||||
|
MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong)
|
||||||
# tqdm Globals
|
FILENAME = "" # Current file being processed, used by translateAI wrapper
|
||||||
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
|
||||||
POSITION = 0
|
# tqdm Globals
|
||||||
LEAVE = False
|
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
|
||||||
|
POSITION = 0
|
||||||
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
LEAVE = False
|
||||||
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
|
||||||
|
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
|
||||||
# Get pricing configuration based on the model
|
LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+"
|
||||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
|
||||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
# Get pricing configuration based on the model
|
||||||
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||||
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||||
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"]
|
||||||
|
BATCHSIZE = PRICING_CONFIG["batchSize"]
|
||||||
# Initialize Translation Config
|
FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
|
||||||
TRANSLATION_CONFIG = TranslationConfig(
|
|
||||||
model=MODEL,
|
# Initialize Translation Config
|
||||||
language=LANGUAGE,
|
TRANSLATION_CONFIG = TranslationConfig(
|
||||||
prompt=PROMPT,
|
model=MODEL,
|
||||||
vocab=VOCAB,
|
language=LANGUAGE,
|
||||||
langRegex=LANGREGEX,
|
prompt=PROMPT,
|
||||||
batchSize=BATCHSIZE,
|
vocab=VOCAB,
|
||||||
maxHistory=MAXHISTORY,
|
langRegex=LANGREGEX,
|
||||||
estimateMode=False # Will be set dynamically based on ESTIMATE
|
batchSize=BATCHSIZE,
|
||||||
)
|
maxHistory=MAXHISTORY,
|
||||||
LEAVE = False
|
estimateMode=False # Will be set dynamically based on ESTIMATE
|
||||||
|
)
|
||||||
def handleWOLF2(filename, estimate):
|
LEAVE = False
|
||||||
global ESTIMATE, FILENAME
|
|
||||||
ESTIMATE = estimate
|
def handleWOLF2(filename, estimate):
|
||||||
FILENAME = filename
|
global ESTIMATE, FILENAME
|
||||||
|
ESTIMATE = estimate
|
||||||
if ESTIMATE:
|
FILENAME = filename
|
||||||
start = time.time()
|
|
||||||
translatedData = openFiles(filename)
|
if ESTIMATE:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
# Print Result
|
||||||
with LOCK:
|
end = time.time()
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
|
TOKENS[0] += translatedData[1][0]
|
||||||
# Print Total
|
TOKENS[1] += translatedData[1][1]
|
||||||
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
|
||||||
|
# Print Total
|
||||||
# Print any errors on maps
|
totalString = getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
if len(MISMATCH) > 0:
|
|
||||||
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
# Print any errors on maps
|
||||||
else:
|
if len(MISMATCH) > 0:
|
||||||
return totalString
|
return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET
|
||||||
|
else:
|
||||||
else:
|
return totalString
|
||||||
try:
|
|
||||||
with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile:
|
else:
|
||||||
start = time.time()
|
try:
|
||||||
translatedData = openFiles(filename)
|
with open("translated/" + filename, "w", encoding="shift_jis", errors="ignore") as outFile:
|
||||||
|
start = time.time()
|
||||||
# Print Result
|
translatedData = openFiles(filename)
|
||||||
end = time.time()
|
|
||||||
outFile.writelines(translatedData[0])
|
# Print Result
|
||||||
tqdm.write(getResultString(translatedData, end - start, filename))
|
end = time.time()
|
||||||
with LOCK:
|
outFile.writelines(translatedData[0])
|
||||||
TOKENS[0] += translatedData[1][0]
|
tqdm.write(getResultString(translatedData, end - start, filename))
|
||||||
TOKENS[1] += translatedData[1][1]
|
with LOCK:
|
||||||
except Exception:
|
TOKENS[0] += translatedData[1][0]
|
||||||
traceback.print_exc()
|
TOKENS[1] += translatedData[1][1]
|
||||||
return "Fail"
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
return "Fail"
|
||||||
|
|
||||||
|
return getResultString(["", TOKENS, None], end - start, "TOTAL")
|
||||||
def getResultString(translatedData, translationTime, filename):
|
|
||||||
# File Print String
|
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
def getResultString(translatedData, translationTime, filename):
|
||||||
totalTokenstring = (
|
# File Print String
|
||||||
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
"[Output: "
|
totalTokenstring = (
|
||||||
+ str(translatedData[1][1])
|
Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]"
|
||||||
+ "]" "[Cost: ${:,.4f}".format(cost)
|
"[Output: "
|
||||||
+ "]"
|
+ str(translatedData[1][1])
|
||||||
)
|
+ "]" "[Cost: ${:,.4f}".format(cost)
|
||||||
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
+ "]"
|
||||||
|
)
|
||||||
if translatedData[2] == None:
|
timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]"
|
||||||
# Success
|
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
if translatedData[2] == None:
|
||||||
|
# Success
|
||||||
else:
|
return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET
|
||||||
# Fail
|
|
||||||
try:
|
else:
|
||||||
raise translatedData[2]
|
# Fail
|
||||||
except Exception as e:
|
try:
|
||||||
traceback.print_exc()
|
raise translatedData[2]
|
||||||
errorString = str(e) + Fore.RED
|
except Exception as e:
|
||||||
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
traceback.print_exc()
|
||||||
|
errorString = str(e) + Fore.RED
|
||||||
|
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
|
||||||
def openFiles(filename):
|
|
||||||
# Use a robust reader to handle cp932/Shift_JIS variants and occasional bad bytes
|
|
||||||
def read_file_lines_with_fallback(path: str):
|
def openFiles(filename):
|
||||||
encodings = ["cp932", "shift_jis", "utf-8-sig", "utf-8"]
|
# Use a robust reader to handle cp932/Shift_JIS variants and occasional bad bytes
|
||||||
for enc in encodings:
|
def read_file_lines_with_fallback(path: str):
|
||||||
try:
|
encodings = ["cp932", "shift_jis", "utf-8-sig", "utf-8"]
|
||||||
with open(path, "r", encoding=enc) as f:
|
for enc in encodings:
|
||||||
return f.readlines()
|
try:
|
||||||
except UnicodeDecodeError:
|
with open(path, "r", encoding=enc) as f:
|
||||||
continue
|
return f.readlines()
|
||||||
# Last resort: ignore undecodable bytes under cp932
|
except UnicodeDecodeError:
|
||||||
with open(path, "rb") as f:
|
continue
|
||||||
raw = f.read()
|
# Last resort: ignore undecodable bytes under cp932
|
||||||
try:
|
with open(path, "rb") as f:
|
||||||
text = raw.decode("cp932", errors="ignore")
|
raw = f.read()
|
||||||
except Exception:
|
try:
|
||||||
text = raw.decode("latin-1", errors="ignore")
|
text = raw.decode("cp932", errors="ignore")
|
||||||
return text.splitlines(keepends=True)
|
except Exception:
|
||||||
|
text = raw.decode("latin-1", errors="ignore")
|
||||||
path = os.path.join("files", filename)
|
return text.splitlines(keepends=True)
|
||||||
lines = read_file_lines_with_fallback(path)
|
|
||||||
|
path = os.path.join("files", filename)
|
||||||
# Keep parseWOLF API by wrapping lines in a file-like object
|
lines = read_file_lines_with_fallback(path)
|
||||||
with io.StringIO("".join(lines)) as readFile:
|
|
||||||
translatedData = parseWOLF(readFile, filename)
|
# Keep parseWOLF API by wrapping lines in a file-like object
|
||||||
|
with io.StringIO("".join(lines)) as readFile:
|
||||||
# Delete lines marked for deletion
|
translatedData = parseWOLF(readFile, filename)
|
||||||
finalData = []
|
|
||||||
for line in translatedData[0]:
|
# Delete lines marked for deletion
|
||||||
if line != "\\d\n":
|
finalData = []
|
||||||
finalData.append(line)
|
for line in translatedData[0]:
|
||||||
translatedData[0] = finalData
|
if line != "\\d\n":
|
||||||
|
finalData.append(line)
|
||||||
return translatedData
|
translatedData[0] = finalData
|
||||||
|
|
||||||
|
return translatedData
|
||||||
def parseWOLF(readFile, filename):
|
|
||||||
totalTokens = [0, 0]
|
|
||||||
|
def parseWOLF(readFile, filename):
|
||||||
# Read File into data
|
totalTokens = [0, 0]
|
||||||
data = readFile.readlines()
|
|
||||||
|
# Read File into data
|
||||||
# Create Progress Bar
|
data = readFile.readlines()
|
||||||
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
|
||||||
pbar.desc = filename
|
# Create Progress Bar
|
||||||
|
with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
|
||||||
try:
|
pbar.desc = filename
|
||||||
result = translateWOLF(data, [], pbar, filename)
|
|
||||||
totalTokens[0] += result[0]
|
try:
|
||||||
totalTokens[1] += result[1]
|
result = translateWOLF(data, [], pbar, filename)
|
||||||
except Exception as e:
|
totalTokens[0] += result[0]
|
||||||
traceback.print_exc()
|
totalTokens[1] += result[1]
|
||||||
return [data, totalTokens, e]
|
except Exception as e:
|
||||||
return [data, totalTokens, None]
|
traceback.print_exc()
|
||||||
|
return [data, totalTokens, e]
|
||||||
|
return [data, totalTokens, None]
|
||||||
def save_progress_lines(lines, filename, encoding="shift_jis"):
|
|
||||||
"""Atomically save current line-based translation progress."""
|
|
||||||
try:
|
def save_progress_lines(lines, filename, encoding="shift_jis"):
|
||||||
if ESTIMATE:
|
"""Atomically save current line-based translation progress."""
|
||||||
return
|
try:
|
||||||
os.makedirs("translated", exist_ok=True)
|
if ESTIMATE:
|
||||||
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
return
|
||||||
try:
|
os.makedirs("translated", exist_ok=True)
|
||||||
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix=f"{filename}.", suffix=".tmp", dir="translated")
|
||||||
tmp_file.writelines(lines)
|
try:
|
||||||
os.replace(tmp_path, os.path.join("translated", filename))
|
with os.fdopen(tmp_fd, "w", encoding=encoding, newline="\n", errors="ignore") as tmp_file:
|
||||||
finally:
|
tmp_file.writelines(lines)
|
||||||
if os.path.exists(tmp_path):
|
os.replace(tmp_path, os.path.join("translated", filename))
|
||||||
try:
|
finally:
|
||||||
os.remove(tmp_path)
|
if os.path.exists(tmp_path):
|
||||||
except OSError:
|
try:
|
||||||
pass
|
os.remove(tmp_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
traceback.print_exc()
|
pass
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def saveCheckLines(lines, filename, tokens=None, encoding="shift_jis"):
|
|
||||||
try:
|
|
||||||
if tokens is not None:
|
def saveCheckLines(lines, filename, tokens=None, encoding="shift_jis"):
|
||||||
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
try:
|
||||||
return
|
if tokens is not None:
|
||||||
save_progress_lines(lines, filename, encoding=encoding)
|
if not (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])):
|
||||||
except Exception:
|
return
|
||||||
traceback.print_exc()
|
save_progress_lines(lines, filename, encoding=encoding)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
def translateWOLF(data, translatedList, pbar, filename):
|
|
||||||
stringList = []
|
|
||||||
currentGroup = []
|
def translateWOLF(data, translatedList, pbar, filename):
|
||||||
tokens = [0, 0]
|
stringList = []
|
||||||
speaker = ""
|
currentGroup = []
|
||||||
global LOCK, ESTIMATE, PBAR
|
tokens = [0, 0]
|
||||||
PBAR = pbar
|
speaker = ""
|
||||||
i = 0
|
global LOCK, ESTIMATE, PBAR
|
||||||
|
PBAR = pbar
|
||||||
while i < len(data):
|
i = 0
|
||||||
# Speaker
|
|
||||||
matchList = re.findall(r"^([^/]*):", data[i])
|
while i < len(data):
|
||||||
if len(matchList) != 0:
|
# Speaker
|
||||||
response = getSpeaker(matchList[0])
|
matchList = re.findall(r"^([^/]*):", data[i])
|
||||||
speaker = response[0]
|
if len(matchList) != 0:
|
||||||
tokens[0] += response[1][0]
|
response = getSpeaker(matchList[0])
|
||||||
tokens[1] += response[1][1]
|
speaker = response[0]
|
||||||
data[i] = data[i].replace(matchList[0], f"{speaker}")
|
tokens[0] += response[1][0]
|
||||||
saveCheckLines(data, filename)
|
tokens[1] += response[1][1]
|
||||||
i += 1
|
data[i] = data[i].replace(matchList[0], f"{speaker}")
|
||||||
else:
|
saveCheckLines(data, filename)
|
||||||
speaker = ""
|
i += 1
|
||||||
|
else:
|
||||||
# Options
|
speaker = ""
|
||||||
if "//選択肢" in data[i]:
|
|
||||||
i += 1
|
# Options
|
||||||
choiceList = []
|
if "//選択肢" in data[i]:
|
||||||
initialIndex = i
|
i += 1
|
||||||
while "//" in data[i] and "の場合" not in data[i]:
|
choiceList = []
|
||||||
choiceList.append(re.search(r"\/\/(.*)", data[i]).group(1))
|
initialIndex = i
|
||||||
i += 1
|
while "//" in data[i] and "の場合" not in data[i]:
|
||||||
|
choiceList.append(re.search(r"\/\/(.*)", data[i]).group(1))
|
||||||
# Translate
|
i += 1
|
||||||
response = translateAI(choiceList, "This will be a dialogue option")
|
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(choiceList, "This will be a dialogue option")
|
||||||
choiceListTL = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Set Data
|
choiceListTL = response[0]
|
||||||
if len(choiceList) == len(choiceListTL):
|
|
||||||
# Set Data
|
# Set Data
|
||||||
i = initialIndex
|
if len(choiceList) == len(choiceListTL):
|
||||||
while "//" in data[i] and "の場合" not in data[i]:
|
# Set Data
|
||||||
choiceListTL[0] = choiceListTL[0].replace(", ", "、")
|
i = initialIndex
|
||||||
data[i] = f"//{choiceListTL[0]}\n"
|
while "//" in data[i] and "の場合" not in data[i]:
|
||||||
choiceListTL.pop(0)
|
choiceListTL[0] = choiceListTL[0].replace(", ", "、")
|
||||||
i += 1
|
data[i] = f"//{choiceListTL[0]}\n"
|
||||||
saveCheckLines(data, filename)
|
choiceListTL.pop(0)
|
||||||
|
i += 1
|
||||||
# Mismatch
|
saveCheckLines(data, filename)
|
||||||
else:
|
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if filename not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(filename)
|
with LOCK:
|
||||||
|
if filename not in MISMATCH:
|
||||||
# Lines
|
MISMATCH.append(filename)
|
||||||
if r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
|
||||||
# Pass 1
|
# Lines
|
||||||
if translatedList == []:
|
if r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
||||||
# Grab Consecutive Strings
|
# Pass 1
|
||||||
currentGroup.append(data[i])
|
if translatedList == []:
|
||||||
i += 1
|
# Grab Consecutive Strings
|
||||||
while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
currentGroup.append(data[i])
|
||||||
currentGroup.append(data[i])
|
i += 1
|
||||||
i += 1
|
while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
||||||
|
currentGroup.append(data[i])
|
||||||
# Join up 401 groups for better translation.
|
i += 1
|
||||||
if len(currentGroup) > 0:
|
|
||||||
jaString = "".join(currentGroup)
|
# Join up 401 groups for better translation.
|
||||||
currentGroup = []
|
if len(currentGroup) > 0:
|
||||||
|
jaString = "".join(currentGroup)
|
||||||
# Remove any textwrap
|
currentGroup = []
|
||||||
jaString = jaString.replace("\n", " ")
|
|
||||||
|
# Remove any textwrap
|
||||||
# Add Speaker (If there is one)
|
jaString = jaString.replace("\n", " ")
|
||||||
if speaker != "":
|
|
||||||
jaString = f"[{speaker}]: {jaString}"
|
# Add Speaker (If there is one)
|
||||||
|
if speaker != "":
|
||||||
# Add String
|
jaString = f"[{speaker}]: {jaString}"
|
||||||
stringList.append(jaString)
|
|
||||||
i += 1
|
# Add String
|
||||||
|
stringList.append(jaString)
|
||||||
# Pass 2
|
i += 1
|
||||||
else:
|
|
||||||
# Insert Strings
|
# Pass 2
|
||||||
while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
else:
|
||||||
data.pop(i)
|
# Insert Strings
|
||||||
|
while i < len(data) and r"/" not in data[i] and "@" not in data[i] and data[i] != "\n":
|
||||||
# Get Text
|
data.pop(i)
|
||||||
translatedText = translatedList[0]
|
|
||||||
translatedList.pop(0)
|
# Get Text
|
||||||
|
translatedText = translatedList[0]
|
||||||
if len(translatedList) <= 0:
|
translatedList.pop(0)
|
||||||
translatedList = None
|
|
||||||
|
if len(translatedList) <= 0:
|
||||||
# Remove speaker
|
translatedList = None
|
||||||
matchSpeakerList = re.findall(r"^(\[.+?\]\s?[|:]\s?)\s?", translatedText)
|
|
||||||
if len(matchSpeakerList) > 0:
|
# Remove speaker
|
||||||
translatedText = translatedText.replace(matchSpeakerList[0], "")
|
matchSpeakerList = re.findall(r"^(\[.+?\]\s?[|:]\s?)\s?", translatedText)
|
||||||
|
if len(matchSpeakerList) > 0:
|
||||||
# Textwrap
|
translatedText = translatedText.replace(matchSpeakerList[0], "")
|
||||||
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
|
||||||
|
# Textwrap
|
||||||
# Set Data
|
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
|
||||||
data.insert(i, f"{translatedText}\n")
|
|
||||||
saveCheckLines(data, filename)
|
# Set Data
|
||||||
i += 1
|
data.insert(i, f"{translatedText}\n")
|
||||||
|
saveCheckLines(data, filename)
|
||||||
# Nothing relevant. Skip Line.
|
i += 1
|
||||||
else:
|
|
||||||
i += 1
|
# Nothing relevant. Skip Line.
|
||||||
|
else:
|
||||||
# EOF
|
i += 1
|
||||||
if len(stringList) > 0:
|
|
||||||
# Set Progress
|
# EOF
|
||||||
pbar.total = len(stringList)
|
if len(stringList) > 0:
|
||||||
pbar.refresh()
|
# Set Progress
|
||||||
|
pbar.total = len(stringList)
|
||||||
# Translate
|
pbar.refresh()
|
||||||
response = translateAI(stringList, "")
|
|
||||||
tokens[0] += response[1][0]
|
# Translate
|
||||||
tokens[1] += response[1][1]
|
response = translateAI(stringList, "")
|
||||||
translatedList = response[0]
|
tokens[0] += response[1][0]
|
||||||
|
tokens[1] += response[1][1]
|
||||||
# Set Strings
|
translatedList = response[0]
|
||||||
if len(stringList) == len(translatedList):
|
|
||||||
translateWOLF(data, translatedList, pbar, filename)
|
# Set Strings
|
||||||
|
if len(stringList) == len(translatedList):
|
||||||
# Mismatch
|
translateWOLF(data, translatedList, pbar, filename)
|
||||||
else:
|
|
||||||
with LOCK:
|
# Mismatch
|
||||||
if filename not in MISMATCH:
|
else:
|
||||||
MISMATCH.append(filename)
|
with LOCK:
|
||||||
return tokens
|
if filename not in MISMATCH:
|
||||||
|
MISMATCH.append(filename)
|
||||||
# Save some money and enter the character before translation
|
return tokens
|
||||||
def getSpeaker(speaker):
|
|
||||||
match speaker:
|
# Save some money and enter the character before translation
|
||||||
case "ファイン":
|
def getSpeaker(speaker):
|
||||||
return ["Fine", [0, 0]]
|
match speaker:
|
||||||
case "":
|
case "ファイン":
|
||||||
return ["", [0, 0]]
|
return ["Fine", [0, 0]]
|
||||||
case _:
|
case "":
|
||||||
# Find Speaker
|
return ["", [0, 0]]
|
||||||
for i in range(len(NAMESLIST)):
|
case _:
|
||||||
if speaker == NAMESLIST[i][0]:
|
# Find Speaker
|
||||||
return [NAMESLIST[i][1], [0, 0]]
|
for i in range(len(NAMESLIST)):
|
||||||
|
if speaker == NAMESLIST[i][0]:
|
||||||
# Translate and Store Speaker
|
return [NAMESLIST[i][1], [0, 0]]
|
||||||
response = translateAI(
|
|
||||||
f"{speaker}",
|
# Translate and Store Speaker
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
response[0] = response[0].replace("Speaker: ", "")
|
response[0] = response[0].title()
|
||||||
|
response[0] = response[0].replace("'S", "'s")
|
||||||
# Retry if name doesn't translate for some reason
|
response[0] = response[0].replace("Speaker: ", "")
|
||||||
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
|
||||||
response = translateAI(
|
# Retry if name doesn't translate for some reason
|
||||||
f"{speaker}",
|
if re.search(r"([a-zA-Z??])", response[0]) == None:
|
||||||
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
response = translateAI(
|
||||||
False,
|
f"{speaker}",
|
||||||
)
|
"Reply with the " + LANGUAGE + " translation of the NPC name.",
|
||||||
response[0] = response[0].title()
|
False,
|
||||||
response[0] = response[0].replace("'S", "'s")
|
)
|
||||||
|
response[0] = response[0].title()
|
||||||
speakerList = [speaker, response[0]]
|
response[0] = response[0].replace("'S", "'s")
|
||||||
NAMESLIST.append(speakerList)
|
|
||||||
return response
|
speakerList = [speaker, response[0]]
|
||||||
return [speaker, [0, 0]]
|
NAMESLIST.append(speakerList)
|
||||||
|
return response
|
||||||
def translateAI(text, history, history_ctx=None):
|
return [speaker, [0, 0]]
|
||||||
"""
|
|
||||||
Legacy wrapper function for the new shared translation utility.
|
def translateAI(text, history, history_ctx=None):
|
||||||
This maintains compatibility with existing code while using the new shared implementation.
|
"""
|
||||||
"""
|
Legacy wrapper function for the new shared translation utility.
|
||||||
global PBAR, MISMATCH, FILENAME
|
This maintains compatibility with existing code while using the new shared implementation.
|
||||||
|
"""
|
||||||
# Update config estimate mode based on global ESTIMATE
|
global PBAR, MISMATCH, FILENAME
|
||||||
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
|
||||||
|
# Update config estimate mode based on global ESTIMATE
|
||||||
# Call the new shared translation function
|
TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE)
|
||||||
return sharedtranslateAI(
|
|
||||||
text=text,
|
# Call the new shared translation function
|
||||||
history=history,
|
return sharedtranslateAI(
|
||||||
config=TRANSLATION_CONFIG,
|
text=text,
|
||||||
filename=FILENAME,
|
history=history,
|
||||||
pbar=PBAR,
|
config=TRANSLATION_CONFIG,
|
||||||
lock=LOCK,
|
filename=FILENAME,
|
||||||
mismatchList=MISMATCH
|
pbar=PBAR,
|
||||||
)
|
lock=LOCK,
|
||||||
|
mismatchList=MISMATCH
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ from util.translation import (
|
||||||
|
|
||||||
MODEL = os.getenv("model")
|
MODEL = os.getenv("model")
|
||||||
LANGUAGE = os.getenv("language").capitalize()
|
LANGUAGE = os.getenv("language").capitalize()
|
||||||
PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
|
from util.paths import PROMPT_PATH, VOCAB_PATH
|
||||||
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
|
|
||||||
|
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
|
||||||
|
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||||
LOCK = threading.Lock()
|
LOCK = threading.Lock()
|
||||||
WIDTH = int(os.getenv("width"))
|
WIDTH = int(os.getenv("width"))
|
||||||
MAXHISTORY = 10
|
MAXHISTORY = 10
|
||||||
|
|
|
||||||
13
scripts/launch.sh
Normal file
13
scripts/launch.sh
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Launcher for DazedMTLTool.desktop (finds venv, then starts the GUI).
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT" || exit 1
|
||||||
|
|
||||||
|
PYTHON="python3"
|
||||||
|
if [[ -x "$ROOT/.venv/bin/python" ]]; then
|
||||||
|
PYTHON="$ROOT/.venv/bin/python"
|
||||||
|
elif [[ -x "$ROOT/venv/bin/python" ]]; then
|
||||||
|
PYTHON="$ROOT/venv/bin/python"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$PYTHON" "$ROOT/scripts/start_gui.py"
|
||||||
|
|
@ -1,72 +1,74 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""Launch script for DazedMTLTool GUI."""
|
||||||
Launch script for DazedMTLTool GUI
|
|
||||||
"""
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
|
||||||
import os
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
from pathlib import Path
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
# Add the project root to Python path
|
|
||||||
project_root = Path(__file__).parent
|
def check_dependencies():
|
||||||
sys.path.insert(0, str(project_root))
|
"""Check if required dependencies are installed."""
|
||||||
|
missing_deps = []
|
||||||
def check_dependencies():
|
|
||||||
"""Check if required dependencies are installed."""
|
try:
|
||||||
missing_deps = []
|
import PyQt5 # noqa: F401
|
||||||
|
except ImportError:
|
||||||
try:
|
missing_deps.append("PyQt5")
|
||||||
import PyQt5
|
|
||||||
except ImportError:
|
try:
|
||||||
missing_deps.append("PyQt5")
|
from dotenv import load_dotenv # noqa: F401
|
||||||
|
except ImportError:
|
||||||
try:
|
missing_deps.append("python-dotenv")
|
||||||
from dotenv import load_dotenv
|
|
||||||
except ImportError:
|
if missing_deps:
|
||||||
missing_deps.append("python-dotenv")
|
print("Missing dependencies:")
|
||||||
|
for dep in missing_deps:
|
||||||
if missing_deps:
|
print(f" - {dep}")
|
||||||
print("Missing dependencies:")
|
print("\nPlease install them using:")
|
||||||
for dep in missing_deps:
|
print(" pip install -r requirements.txt")
|
||||||
print(f" - {dep}")
|
return False
|
||||||
print("\nPlease install them using:")
|
|
||||||
print(" pip install -r requirements_gui.txt")
|
return True
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
def main():
|
print("DazedMTLTool GUI Launcher")
|
||||||
"""Main entry point."""
|
print("=" * 40)
|
||||||
print("DazedMTLTool GUI Launcher")
|
|
||||||
print("=" * 40)
|
from util.paths import ensure_vocab_file, migrate_root_data_files
|
||||||
|
|
||||||
# Check dependencies
|
migrate_root_data_files()
|
||||||
if not check_dependencies():
|
ensure_vocab_file()
|
||||||
sys.exit(1)
|
|
||||||
|
if not check_dependencies():
|
||||||
try:
|
sys.exit(1)
|
||||||
from util.ace.update_tools import seed_ace_tools
|
|
||||||
seed_ace_tools()
|
try:
|
||||||
except Exception as exc:
|
from util.ace.update_tools import seed_ace_tools
|
||||||
print(f"Warning: Ace tool setup failed ({exc}). Ace features may be unavailable.")
|
seed_ace_tools()
|
||||||
|
except Exception as exc:
|
||||||
try:
|
print(f"Warning: Ace tool setup failed ({exc}). Ace features may be unavailable.")
|
||||||
from util.forge.update_tools import seed_forge_plugins
|
|
||||||
seed_forge_plugins()
|
try:
|
||||||
except Exception as exc:
|
from util.forge.update_tools import seed_forge_plugins
|
||||||
print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.")
|
seed_forge_plugins()
|
||||||
|
except Exception as exc:
|
||||||
# Import and run GUI
|
print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.")
|
||||||
try:
|
|
||||||
from gui.main import main as gui_main
|
try:
|
||||||
gui_main()
|
from gui.main import main as gui_main
|
||||||
except ImportError as e:
|
gui_main()
|
||||||
print(f"Error importing GUI modules: {e}")
|
except ImportError as e:
|
||||||
print("Make sure all GUI files are in the 'gui' directory")
|
print(f"Error importing GUI modules: {e}")
|
||||||
sys.exit(1)
|
print("Make sure all GUI files are in the 'gui' directory")
|
||||||
except Exception as e:
|
sys.exit(1)
|
||||||
print(f"Error starting GUI: {e}")
|
except Exception as e:
|
||||||
sys.exit(1)
|
print(f"Error starting GUI: {e}")
|
||||||
|
sys.exit(1)
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
# Canonical defaults used by the GUI and the `set_defaults` script.
|
from util.paths import PROJECT_ROOT
|
||||||
|
|
||||||
|
# Canonical defaults used by the GUI and the pre-commit hook.
|
||||||
# Stored as booleans for easier consumption by the GUI.
|
# Stored as booleans for easier consumption by the GUI.
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
'FIRSTLINESPEAKERS': False,
|
'FIRSTLINESPEAKERS': False,
|
||||||
|
|
@ -56,4 +58,4 @@ def set_defaults(file_path):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
set_defaults('modules/rpgmakermvmz.py')
|
set_defaults(PROJECT_ROOT / "modules/rpgmakermvmz.py")
|
||||||
72
util/linux_desktop.py
Normal file
72
util/linux_desktop.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""Install/update the Linux .desktop entry for taskbar and window icons."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from util.paths import ICON_PATH, PROJECT_ROOT
|
||||||
|
|
||||||
|
DESKTOP_ID = "DazedMTLTool"
|
||||||
|
LAUNCH_SCRIPT = PROJECT_ROOT / "scripts" / "launch.sh"
|
||||||
|
|
||||||
|
|
||||||
|
def installed_desktop_path() -> Path:
|
||||||
|
data_home = os.environ.get("XDG_DATA_HOME", "")
|
||||||
|
if data_home:
|
||||||
|
base = Path(data_home)
|
||||||
|
else:
|
||||||
|
base = Path.home() / ".local" / "share"
|
||||||
|
return base / "applications" / f"{DESKTOP_ID}.desktop"
|
||||||
|
|
||||||
|
|
||||||
|
def _desktop_content(root: Path, icon: Path, launch: Path) -> str:
|
||||||
|
return (
|
||||||
|
"[Desktop Entry]\n"
|
||||||
|
"Type=Application\n"
|
||||||
|
f"Name={DESKTOP_ID}\n"
|
||||||
|
"GenericName=Translation Tool\n"
|
||||||
|
"Comment=AI translation tool for visual novels and RPG games\n"
|
||||||
|
f"Exec={launch}\n"
|
||||||
|
f"Icon={icon}\n"
|
||||||
|
f"Path={root}\n"
|
||||||
|
"Terminal=false\n"
|
||||||
|
"Categories=Development;Utility;\n"
|
||||||
|
f"StartupWMClass={DESKTOP_ID}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_linux_desktop_entry() -> None:
|
||||||
|
"""Write ~/.local/share/applications/DazedMTLTool.desktop with absolute paths."""
|
||||||
|
if not sys.platform.startswith("linux"):
|
||||||
|
return
|
||||||
|
if not LAUNCH_SCRIPT.is_file() or not ICON_PATH.is_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
desktop_path = installed_desktop_path()
|
||||||
|
content = _desktop_content(PROJECT_ROOT, ICON_PATH, LAUNCH_SCRIPT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if desktop_path.is_file() and desktop_path.read_text(encoding="utf-8") == content:
|
||||||
|
return
|
||||||
|
desktop_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
desktop_path.write_text(content, encoding="utf-8")
|
||||||
|
desktop_path.chmod(0o755)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Warning: could not install Linux desktop entry ({exc})")
|
||||||
|
return
|
||||||
|
|
||||||
|
_refresh_desktop_database(desktop_path.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_desktop_database(applications_dir: Path) -> None:
|
||||||
|
for cmd in (
|
||||||
|
["update-desktop-database", str(applications_dir)],
|
||||||
|
["kbuildsycoca5", "--noincremental"],
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
45
util/paths.py
Normal file
45
util/paths.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Canonical project paths (repo root, data files, config)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DATA_DIR = PROJECT_ROOT / "data"
|
||||||
|
VOCAB_PATH = DATA_DIR / "vocab.txt"
|
||||||
|
VOCAB_BASE_PATH = DATA_DIR / "vocab_base.txt"
|
||||||
|
PROMPT_PATH = DATA_DIR / "prompt.txt"
|
||||||
|
LAST_UPDATE_SHA_PATH = DATA_DIR / "last_update_sha.txt"
|
||||||
|
ENV_PATH = PROJECT_ROOT / ".env"
|
||||||
|
ICON_PATH = PROJECT_ROOT / "assets" / "icon.png"
|
||||||
|
|
||||||
|
_ROOT_DATA_FILES = (
|
||||||
|
"vocab.txt",
|
||||||
|
"vocab_base.txt",
|
||||||
|
"prompt.txt",
|
||||||
|
"last_update_sha.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_root_data_files() -> None:
|
||||||
|
"""Move legacy root-level data files into data/ on first run."""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name in _ROOT_DATA_FILES:
|
||||||
|
src = PROJECT_ROOT / name
|
||||||
|
dst = DATA_DIR / name
|
||||||
|
if src.is_file() and not dst.exists():
|
||||||
|
src.rename(dst)
|
||||||
|
|
||||||
|
|
||||||
|
migrate_root_data_files()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_vocab_file() -> None:
|
||||||
|
"""Create data/vocab.txt from vocab_base.txt when missing."""
|
||||||
|
migrate_root_data_files()
|
||||||
|
if VOCAB_PATH.is_file():
|
||||||
|
return
|
||||||
|
if VOCAB_BASE_PATH.is_file():
|
||||||
|
VOCAB_PATH.write_text(VOCAB_BASE_PATH.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
else:
|
||||||
|
VOCAB_PATH.write_text("", encoding="utf-8")
|
||||||
6119
util/translation.py
6119
util/translation.py
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue