Surely cleaning up all this mess wont break anything. surely

This commit is contained in:
DazedAnon 2026-06-15 17:34:32 -05:00
parent 563299ba21
commit 4dd9d2bbd4
38 changed files with 16162 additions and 15976 deletions

4
.gitignore vendored
View file

@ -18,8 +18,8 @@ __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

View file

@ -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
View 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

View file

@ -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. |

View file

@ -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 (

View file

@ -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."

View file

@ -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)
@ -903,6 +898,14 @@ def main():
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)
# Windows taskbar groups by executable (python.exe) unless we set an explicit # Windows taskbar groups by executable (python.exe) unless we set an explicit
@ -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("""

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -22,8 +22,10 @@ import tempfile
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()
ESTIMATE = "" ESTIMATE = ""
TOKENS = [0, 0] TOKENS = [0, 0]

View file

@ -20,8 +20,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -18,8 +18,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()
PBAR = None PBAR = None
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -11,6 +11,10 @@ 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()
from util.paths import migrate_root_data_files, ensure_vocab_file
migrate_root_data_files()
ensure_vocab_file()
_missing_envs = [ _missing_envs = [
env for env in [ env for env in [
"api", "api",

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -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")

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -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()
VOCAB_LOCK = threading.Lock() # Dedicated lock for vocab.txt updates VOCAB_LOCK = threading.Lock() # Dedicated lock for vocab.txt updates
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
@ -118,7 +120,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:

View file

@ -19,8 +19,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -21,8 +21,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -21,8 +21,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -21,8 +21,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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -22,8 +22,10 @@ import tempfile
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()
WIDTH = int(os.getenv("width")) WIDTH = int(os.getenv("width"))
LISTWIDTH = int(os.getenv("listWidth")) LISTWIDTH = int(os.getenv("listWidth"))

View file

@ -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
View 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"

View file

@ -1,27 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """Launch script for DazedMTLTool GUI."""
Launch script for DazedMTLTool GUI
"""
import sys import sys
import os
from pathlib import Path from pathlib import Path
# Add the project root to Python path PROJECT_ROOT = Path(__file__).resolve().parent.parent
project_root = Path(__file__).parent sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(project_root))
def check_dependencies(): def check_dependencies():
"""Check if required dependencies are installed.""" """Check if required dependencies are installed."""
missing_deps = [] missing_deps = []
try: try:
import PyQt5 import PyQt5 # noqa: F401
except ImportError: except ImportError:
missing_deps.append("PyQt5") missing_deps.append("PyQt5")
try: try:
from dotenv import load_dotenv from dotenv import load_dotenv # noqa: F401
except ImportError: except ImportError:
missing_deps.append("python-dotenv") missing_deps.append("python-dotenv")
@ -30,17 +27,22 @@ def check_dependencies():
for dep in missing_deps: for dep in missing_deps:
print(f" - {dep}") print(f" - {dep}")
print("\nPlease install them using:") print("\nPlease install them using:")
print(" pip install -r requirements_gui.txt") print(" pip install -r requirements.txt")
return False return False
return True return True
def main(): def main():
"""Main entry point.""" """Main entry point."""
print("DazedMTLTool GUI Launcher") print("DazedMTLTool GUI Launcher")
print("=" * 40) print("=" * 40)
# Check dependencies from util.paths import ensure_vocab_file, migrate_root_data_files
migrate_root_data_files()
ensure_vocab_file()
if not check_dependencies(): if not check_dependencies():
sys.exit(1) sys.exit(1)
@ -56,7 +58,6 @@ def main():
except Exception as exc: except Exception as exc:
print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.") print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.")
# Import and run GUI
try: try:
from gui.main import main as gui_main from gui.main import main as gui_main
gui_main() gui_main()
@ -68,5 +69,6 @@ def main():
print(f"Error starting GUI: {e}") print(f"Error starting GUI: {e}")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -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
View 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
View 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")

View file

@ -1316,7 +1316,8 @@ class TranslationConfig:
# Load prompt and vocab files if not provided # Load prompt and vocab files if not provided
if prompt is None: if prompt is None:
try: try:
self.prompt = Path("prompt.txt").read_text(encoding="utf-8") from util.paths import PROMPT_PATH, VOCAB_PATH
self.prompt = PROMPT_PATH.read_text(encoding="utf-8")
except FileNotFoundError: except FileNotFoundError:
self.prompt = "" self.prompt = ""
else: else:
@ -1324,7 +1325,7 @@ class TranslationConfig:
if vocab is None: if vocab is None:
try: try:
self.vocab = Path("vocab.txt").read_text(encoding="utf-8") self.vocab = VOCAB_PATH.read_text(encoding="utf-8")
except FileNotFoundError: except FileNotFoundError:
self.vocab = "" self.vocab = ""
else: else: