snbt-tr 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snbt_tr-1.0.0/LICENSE +21 -0
- snbt_tr-1.0.0/MANIFEST.in +3 -0
- snbt_tr-1.0.0/PKG-INFO +144 -0
- snbt_tr-1.0.0/README.md +129 -0
- snbt_tr-1.0.0/config.py +366 -0
- snbt_tr-1.0.0/core.py +1516 -0
- snbt_tr-1.0.0/gui.py +2807 -0
- snbt_tr-1.0.0/main.py +1363 -0
- snbt_tr-1.0.0/pyproject.toml +26 -0
- snbt_tr-1.0.0/resources/logo.png +0 -0
- snbt_tr-1.0.0/resources/mod_rules.json +17 -0
- snbt_tr-1.0.0/setup.cfg +4 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/PKG-INFO +144 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/SOURCES.txt +19 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/dependency_links.txt +1 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/entry_points.txt +2 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/requires.txt +3 -0
- snbt_tr-1.0.0/snbt_tr.egg-info/top_level.txt +4 -0
- snbt_tr-1.0.0/tests/test_config_logic.py +48 -0
- snbt_tr-1.0.0/tests/test_gui_cli.py +1177 -0
- snbt_tr-1.0.0/tests/test_kubejs.py +148 -0
snbt_tr-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ronnikols
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
snbt_tr-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snbt-tr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Advanced SNBT quest localizer using AI key pools
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: PyQt6>=6.4.0
|
|
12
|
+
Requires-Dist: httpx>=0.24.0
|
|
13
|
+
Requires-Dist: aiofiles>=23.1.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="resources/logo.png" alt="SNBT AI Localizer Logo" width="200" height="200">
|
|
18
|
+
</p> # SNBT AI Localizer
|
|
19
|
+
|
|
20
|
+
English | [Русский](README_RU.md)
|
|
21
|
+
|
|
22
|
+
Advanced asynchronous translator for Minecraft FTB Quests files (`.snbt`) with support for multiple local and cloud translation engines, featuring a 4-tab GUI and intelligent SQLite caching system.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Recommended Methods
|
|
27
|
+
| Platform | Command |
|
|
28
|
+
|----------------|----------------------------------|
|
|
29
|
+
| Arch Linux AUR | `yay -S snbt-tr` |
|
|
30
|
+
| Flatpak | `flatpak install org.mineai.snbt-tr` |
|
|
31
|
+
| pipx | `pipx install snbt-tr` |
|
|
32
|
+
| PyPI | `pip install snbt-tr` |
|
|
33
|
+
|
|
34
|
+
### Manual Installation
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/ronnikols/SNBT-AI-Localizer.git
|
|
37
|
+
cd SNBT-AI-Localizer
|
|
38
|
+
pip install -r requirements.txt
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Launch Methods
|
|
44
|
+
- `snbt-tr` - Launches the interactive CLI setup wizard
|
|
45
|
+
- `snbt-tr --gui` - Launches the PyQt6 GUI with dual-tab interface
|
|
46
|
+
- `snbt-tr [options]` - Unattended batch execution
|
|
47
|
+
|
|
48
|
+
### Examples
|
|
49
|
+
```bash
|
|
50
|
+
snbt-tr --list-models -p groq
|
|
51
|
+
snbt-tr -d /path/to/quests -p groq -k YOUR_API_KEY
|
|
52
|
+
snbt-tr --mix -d /path/to/quests
|
|
53
|
+
snbt-tr --clear-cache
|
|
54
|
+
snbt-tr --fastdir
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
### 4-Tab GUI Architecture
|
|
60
|
+
- **Workspace Tab**: Primary translation interface with provider/model selection, API key pool management (up to 10 keys), custom context input, target language dropdown, batch processing controls (Start/Pause/Stop), and live logging output
|
|
61
|
+
- **Translation Memory Tab**: Visual cache manager with:
|
|
62
|
+
- Real-time search across original and translated text with 300ms debounce
|
|
63
|
+
- Modpack combobox filter for isolating translations by modpack
|
|
64
|
+
- Target language filter synchronized with Workspace tab
|
|
65
|
+
- 4-column QTableWidget (Original, Translation, Modpack, Added) with auto-stretch column width layout
|
|
66
|
+
- Inline editing of translations directly in the table
|
|
67
|
+
- Bulk operations: Load More (pagination), Delete Selected, Save Changes, Clear Cache
|
|
68
|
+
- **Settings Tab**: Configuration management for providers, models, API keys, and application settings
|
|
69
|
+
- **Credits Tab**: Displays project credits and acknowledgments
|
|
70
|
+
|
|
71
|
+
### Asynchronous Processing
|
|
72
|
+
- Non-blocking I/O with `asyncio` and `httpx.AsyncClient`
|
|
73
|
+
- Configurable concurrency (1-10 threads)
|
|
74
|
+
- Adaptive chunking:
|
|
75
|
+
- 5 items per chunk for Ollama (local inference)
|
|
76
|
+
- Configurable batch size (default: 50) for cloud providers
|
|
77
|
+
- Automatic delays between chunks to prevent rate limiting
|
|
78
|
+
|
|
79
|
+
### Fault Tolerance
|
|
80
|
+
- Round-Robin multi-key pool with automatic load balancing
|
|
81
|
+
- Invalid keys (HTTP 401/403) are permanently removed from the pool
|
|
82
|
+
- Rate-limited keys (HTTP 429) enter exponential backoff (0.2s to 64s)
|
|
83
|
+
- Fallback to single-item translation on chunk failure
|
|
84
|
+
- Mixed Provider Mode: Auto-detects provider from key prefix and uses saved defaults
|
|
85
|
+
- Binary split fallback for parse errors with recursive chunk splitting
|
|
86
|
+
|
|
87
|
+
### Smart Caching System
|
|
88
|
+
- SQLite-based cache with WAL mode for concurrent access
|
|
89
|
+
- Per-language tables (`cache_{lang_code}`) to prevent cross-contamination
|
|
90
|
+
- Modpack isolation: Translations can be filtered and managed by modpack
|
|
91
|
+
- Thread-safe operations with locking
|
|
92
|
+
- Real-time persistence between chunks
|
|
93
|
+
- Defensive sanitization: Automatic cleanup of nested dictionaries in SNBT and protection against malformed JSON in responses
|
|
94
|
+
|
|
95
|
+
### 🛡️ Pluralization Guard
|
|
96
|
+
**Prevents redundant API calls for highly similar strings with a 90% similarity threshold**
|
|
97
|
+
|
|
98
|
+
The Pluralization Guard is a sophisticated fuzzy-matching system that automatically detects and reuses translations for similar phrases, preventing duplicate API calls and saving costs. It works by:
|
|
99
|
+
|
|
100
|
+
1. **Exact Match Check**: First attempts to find an exact match in the cache
|
|
101
|
+
2. **Length Filtering**: Searches for candidates within ±15% length of the input text
|
|
102
|
+
3. **Number Preservation**: Ensures strings with different numbers (e.g., "item 1" vs "item 2") are NOT matched
|
|
103
|
+
4. **Fuzzy Matching**: Uses `difflib.SequenceMatcher` with a **90% similarity threshold**
|
|
104
|
+
5. **Tail Preservation**: Maintains Minecraft formatting codes at the end of strings (e.g., "hello§a" → cached "привет" + "§a")
|
|
105
|
+
|
|
106
|
+
**Example:**
|
|
107
|
+
- Input: `"Get 5 diamonds"` → Cache miss
|
|
108
|
+
- Input: `"Get 6 diamonds"` → **Cache hit!** (90%+ similarity, same structure)
|
|
109
|
+
- Input: `"Get diamond"` → Cache miss (different number count)
|
|
110
|
+
- Input: `"Get 5 diamonds§a"` → **Cache hit!** (matches "Get 5 diamonds" + preserves "§a")
|
|
111
|
+
|
|
112
|
+
This prevents charging for translations of:
|
|
113
|
+
- Plural variations: "apple" → "apples"
|
|
114
|
+
- Number variations: "item 1" → "item 2" (if structure matches)
|
|
115
|
+
- Minor formatting differences: "text" → "text§a"
|
|
116
|
+
|
|
117
|
+
### Minecraft Formatting Protection
|
|
118
|
+
- Regex shielding for namespace tags (`#c:ender_pearl_dusts`), UUIDs, and entity IDs
|
|
119
|
+
- Temporary placeholder substitution (`__TAG_N__`) during translation
|
|
120
|
+
- Preserves all Minecraft-specific formatting codes
|
|
121
|
+
|
|
122
|
+
## Supported Providers
|
|
123
|
+
|
|
124
|
+
| Provider | Default Model | Free Tier | Notes |
|
|
125
|
+
|----------|----------------|-----------|-------|
|
|
126
|
+
| Groq Cloud (Fast) | llama-3.3-70b-versatile | Yes | Low-latency inference |
|
|
127
|
+
| NVIDIA NIM | nvidia/nemotron-4-340b-instruct | Yes | Enterprise-grade models |
|
|
128
|
+
| OpenRouter (Cloud AI) | google/gemma-4-31b:free | Yes | 100+ free models |
|
|
129
|
+
| Google Gemini (Free API) | models/gemini-3.1-flash-lite | Yes | Google's latest free model |
|
|
130
|
+
| Sambanova | DeepSeek-V3.1 | No | High-performance inference |
|
|
131
|
+
| OpenAI | gpt-4o-mini | No | Optimized for speed |
|
|
132
|
+
| Mistral AI | mistral-large-latest | Yes | Open-source frontier models |
|
|
133
|
+
| Anthropic (Claude) | claude-3-5-sonnet-20241022 | No | High-quality responses |
|
|
134
|
+
| Cohere | command-r-plus | No | Production-ready |
|
|
135
|
+
| Ollama (Local / Free) | qwen2.5:7b | Yes | Self-hosted, no API key |
|
|
136
|
+
| Google Translate (Free) | N/A | Yes | Traditional MT, no API key |
|
|
137
|
+
| Local LLM / Custom | (Custom) | Yes | Custom local models |
|
|
138
|
+
|
|
139
|
+
## Feedback & Community
|
|
140
|
+
For feedback, suggestions, and bug reports, please contact us via:
|
|
141
|
+
- **Telegram**: [https://t.me/ronnikols](https://t.me/ronnikols)
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
snbt_tr-1.0.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="resources/logo.png" alt="SNBT AI Localizer Logo" width="200" height="200">
|
|
3
|
+
</p> # SNBT AI Localizer
|
|
4
|
+
|
|
5
|
+
English | [Русский](README_RU.md)
|
|
6
|
+
|
|
7
|
+
Advanced asynchronous translator for Minecraft FTB Quests files (`.snbt`) with support for multiple local and cloud translation engines, featuring a 4-tab GUI and intelligent SQLite caching system.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Recommended Methods
|
|
12
|
+
| Platform | Command |
|
|
13
|
+
|----------------|----------------------------------|
|
|
14
|
+
| Arch Linux AUR | `yay -S snbt-tr` |
|
|
15
|
+
| Flatpak | `flatpak install org.mineai.snbt-tr` |
|
|
16
|
+
| pipx | `pipx install snbt-tr` |
|
|
17
|
+
| PyPI | `pip install snbt-tr` |
|
|
18
|
+
|
|
19
|
+
### Manual Installation
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/ronnikols/SNBT-AI-Localizer.git
|
|
22
|
+
cd SNBT-AI-Localizer
|
|
23
|
+
pip install -r requirements.txt
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Launch Methods
|
|
29
|
+
- `snbt-tr` - Launches the interactive CLI setup wizard
|
|
30
|
+
- `snbt-tr --gui` - Launches the PyQt6 GUI with dual-tab interface
|
|
31
|
+
- `snbt-tr [options]` - Unattended batch execution
|
|
32
|
+
|
|
33
|
+
### Examples
|
|
34
|
+
```bash
|
|
35
|
+
snbt-tr --list-models -p groq
|
|
36
|
+
snbt-tr -d /path/to/quests -p groq -k YOUR_API_KEY
|
|
37
|
+
snbt-tr --mix -d /path/to/quests
|
|
38
|
+
snbt-tr --clear-cache
|
|
39
|
+
snbt-tr --fastdir
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
### 4-Tab GUI Architecture
|
|
45
|
+
- **Workspace Tab**: Primary translation interface with provider/model selection, API key pool management (up to 10 keys), custom context input, target language dropdown, batch processing controls (Start/Pause/Stop), and live logging output
|
|
46
|
+
- **Translation Memory Tab**: Visual cache manager with:
|
|
47
|
+
- Real-time search across original and translated text with 300ms debounce
|
|
48
|
+
- Modpack combobox filter for isolating translations by modpack
|
|
49
|
+
- Target language filter synchronized with Workspace tab
|
|
50
|
+
- 4-column QTableWidget (Original, Translation, Modpack, Added) with auto-stretch column width layout
|
|
51
|
+
- Inline editing of translations directly in the table
|
|
52
|
+
- Bulk operations: Load More (pagination), Delete Selected, Save Changes, Clear Cache
|
|
53
|
+
- **Settings Tab**: Configuration management for providers, models, API keys, and application settings
|
|
54
|
+
- **Credits Tab**: Displays project credits and acknowledgments
|
|
55
|
+
|
|
56
|
+
### Asynchronous Processing
|
|
57
|
+
- Non-blocking I/O with `asyncio` and `httpx.AsyncClient`
|
|
58
|
+
- Configurable concurrency (1-10 threads)
|
|
59
|
+
- Adaptive chunking:
|
|
60
|
+
- 5 items per chunk for Ollama (local inference)
|
|
61
|
+
- Configurable batch size (default: 50) for cloud providers
|
|
62
|
+
- Automatic delays between chunks to prevent rate limiting
|
|
63
|
+
|
|
64
|
+
### Fault Tolerance
|
|
65
|
+
- Round-Robin multi-key pool with automatic load balancing
|
|
66
|
+
- Invalid keys (HTTP 401/403) are permanently removed from the pool
|
|
67
|
+
- Rate-limited keys (HTTP 429) enter exponential backoff (0.2s to 64s)
|
|
68
|
+
- Fallback to single-item translation on chunk failure
|
|
69
|
+
- Mixed Provider Mode: Auto-detects provider from key prefix and uses saved defaults
|
|
70
|
+
- Binary split fallback for parse errors with recursive chunk splitting
|
|
71
|
+
|
|
72
|
+
### Smart Caching System
|
|
73
|
+
- SQLite-based cache with WAL mode for concurrent access
|
|
74
|
+
- Per-language tables (`cache_{lang_code}`) to prevent cross-contamination
|
|
75
|
+
- Modpack isolation: Translations can be filtered and managed by modpack
|
|
76
|
+
- Thread-safe operations with locking
|
|
77
|
+
- Real-time persistence between chunks
|
|
78
|
+
- Defensive sanitization: Automatic cleanup of nested dictionaries in SNBT and protection against malformed JSON in responses
|
|
79
|
+
|
|
80
|
+
### 🛡️ Pluralization Guard
|
|
81
|
+
**Prevents redundant API calls for highly similar strings with a 90% similarity threshold**
|
|
82
|
+
|
|
83
|
+
The Pluralization Guard is a sophisticated fuzzy-matching system that automatically detects and reuses translations for similar phrases, preventing duplicate API calls and saving costs. It works by:
|
|
84
|
+
|
|
85
|
+
1. **Exact Match Check**: First attempts to find an exact match in the cache
|
|
86
|
+
2. **Length Filtering**: Searches for candidates within ±15% length of the input text
|
|
87
|
+
3. **Number Preservation**: Ensures strings with different numbers (e.g., "item 1" vs "item 2") are NOT matched
|
|
88
|
+
4. **Fuzzy Matching**: Uses `difflib.SequenceMatcher` with a **90% similarity threshold**
|
|
89
|
+
5. **Tail Preservation**: Maintains Minecraft formatting codes at the end of strings (e.g., "hello§a" → cached "привет" + "§a")
|
|
90
|
+
|
|
91
|
+
**Example:**
|
|
92
|
+
- Input: `"Get 5 diamonds"` → Cache miss
|
|
93
|
+
- Input: `"Get 6 diamonds"` → **Cache hit!** (90%+ similarity, same structure)
|
|
94
|
+
- Input: `"Get diamond"` → Cache miss (different number count)
|
|
95
|
+
- Input: `"Get 5 diamonds§a"` → **Cache hit!** (matches "Get 5 diamonds" + preserves "§a")
|
|
96
|
+
|
|
97
|
+
This prevents charging for translations of:
|
|
98
|
+
- Plural variations: "apple" → "apples"
|
|
99
|
+
- Number variations: "item 1" → "item 2" (if structure matches)
|
|
100
|
+
- Minor formatting differences: "text" → "text§a"
|
|
101
|
+
|
|
102
|
+
### Minecraft Formatting Protection
|
|
103
|
+
- Regex shielding for namespace tags (`#c:ender_pearl_dusts`), UUIDs, and entity IDs
|
|
104
|
+
- Temporary placeholder substitution (`__TAG_N__`) during translation
|
|
105
|
+
- Preserves all Minecraft-specific formatting codes
|
|
106
|
+
|
|
107
|
+
## Supported Providers
|
|
108
|
+
|
|
109
|
+
| Provider | Default Model | Free Tier | Notes |
|
|
110
|
+
|----------|----------------|-----------|-------|
|
|
111
|
+
| Groq Cloud (Fast) | llama-3.3-70b-versatile | Yes | Low-latency inference |
|
|
112
|
+
| NVIDIA NIM | nvidia/nemotron-4-340b-instruct | Yes | Enterprise-grade models |
|
|
113
|
+
| OpenRouter (Cloud AI) | google/gemma-4-31b:free | Yes | 100+ free models |
|
|
114
|
+
| Google Gemini (Free API) | models/gemini-3.1-flash-lite | Yes | Google's latest free model |
|
|
115
|
+
| Sambanova | DeepSeek-V3.1 | No | High-performance inference |
|
|
116
|
+
| OpenAI | gpt-4o-mini | No | Optimized for speed |
|
|
117
|
+
| Mistral AI | mistral-large-latest | Yes | Open-source frontier models |
|
|
118
|
+
| Anthropic (Claude) | claude-3-5-sonnet-20241022 | No | High-quality responses |
|
|
119
|
+
| Cohere | command-r-plus | No | Production-ready |
|
|
120
|
+
| Ollama (Local / Free) | qwen2.5:7b | Yes | Self-hosted, no API key |
|
|
121
|
+
| Google Translate (Free) | N/A | Yes | Traditional MT, no API key |
|
|
122
|
+
| Local LLM / Custom | (Custom) | Yes | Custom local models |
|
|
123
|
+
|
|
124
|
+
## Feedback & Community
|
|
125
|
+
For feedback, suggestions, and bug reports, please contact us via:
|
|
126
|
+
- **Telegram**: [https://t.me/ronnikols](https://t.me/ronnikols)
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
snbt_tr-1.0.0/config.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Optional
|
|
7
|
+
from core import is_valid_custom_instance, PROVIDER_DEFAULTS
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from PyQt6.QtCore import QSettings
|
|
11
|
+
except ImportError:
|
|
12
|
+
QSettings = None
|
|
13
|
+
|
|
14
|
+
PROVIDER_ALIASES = {
|
|
15
|
+
"google": "Google Translate (Free)",
|
|
16
|
+
"google_free": "Google Translate (Free)",
|
|
17
|
+
"gemini": "Google Gemini (Free API)",
|
|
18
|
+
"ollama": "Ollama (Local / Free)",
|
|
19
|
+
"groq": "Groq Cloud (Fast)",
|
|
20
|
+
"openrouter": "OpenRouter (Cloud AI)",
|
|
21
|
+
"nvidia": "NVIDIA NIM",
|
|
22
|
+
"nim": "NVIDIA NIM",
|
|
23
|
+
"sambanova": "Sambanova",
|
|
24
|
+
"mixed": "Mixed Providers",
|
|
25
|
+
"mix": "Mixed Providers",
|
|
26
|
+
"openai": "OpenAI",
|
|
27
|
+
"mistral": "Mistral AI",
|
|
28
|
+
"anthropic": "Anthropic (Claude)",
|
|
29
|
+
"claude": "Anthropic (Claude)",
|
|
30
|
+
"cohere": "Cohere",
|
|
31
|
+
"local": "Local LLM / Custom",
|
|
32
|
+
"custom": "Local LLM / Custom",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
PROVIDER_ENDPOINTS = {
|
|
36
|
+
"Google Gemini (Free API)": "https://generativelanguage.googleapis.com/v1beta/openai/models",
|
|
37
|
+
"Groq Cloud (Fast)": "https://api.groq.com/openai/v1/models",
|
|
38
|
+
"OpenRouter (Cloud AI)": "https://openrouter.ai/api/v1/models",
|
|
39
|
+
"NVIDIA NIM": "https://integrate.api.nvidia.com/v1/models",
|
|
40
|
+
"Ollama (Local / Free)": "http://localhost:11434/v1/models",
|
|
41
|
+
"Sambanova": "https://api.sambanova.ai/v1/models",
|
|
42
|
+
"OpenAI": "https://api.openai.com/v1/models",
|
|
43
|
+
"Mistral AI": "https://api.mistral.ai/v1/models",
|
|
44
|
+
"Anthropic (Claude)": "https://api.anthropic.com/v1/messages",
|
|
45
|
+
"Cohere": "https://api.cohere.ai/v1/chat",
|
|
46
|
+
"Local LLM / Custom": "",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
LANG_ALIASES = {
|
|
50
|
+
"ru": ("Russian", "ru_ru"),
|
|
51
|
+
"en": ("English", "en_us"),
|
|
52
|
+
"es": ("Spanish", "es_es"),
|
|
53
|
+
"zh": ("Chinese Simplified", "zh_cn"),
|
|
54
|
+
"zh-cn": ("Chinese Simplified", "zh_cn"),
|
|
55
|
+
"zh-tw": ("Chinese Traditional", "zh_tw"),
|
|
56
|
+
"de": ("German", "de_de"),
|
|
57
|
+
"fr": ("French", "fr_fr"),
|
|
58
|
+
"pt": ("Portuguese", "pt_br"),
|
|
59
|
+
"pt-br": ("Portuguese", "pt_br"),
|
|
60
|
+
"ja": ("Japanese", "ja_jp"),
|
|
61
|
+
"ko": ("Korean", "ko_kr"),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ENV_KEY_MAP = {
|
|
65
|
+
"Google Gemini (Free API)": "GEMINI_API_KEY",
|
|
66
|
+
"Groq Cloud (Fast)": "GROQ_API_KEY",
|
|
67
|
+
"OpenRouter (Cloud AI)": "OPENROUTER_API_KEY",
|
|
68
|
+
"NVIDIA NIM": "NVIDIA_API_KEY",
|
|
69
|
+
"Sambanova": "SAMBANOVA_API_KEY",
|
|
70
|
+
"OpenAI": "OPENAI_API_KEY",
|
|
71
|
+
"Mistral AI": "MISTRAL_API_KEY",
|
|
72
|
+
"Anthropic (Claude)": "ANTHROPIC_API_KEY",
|
|
73
|
+
"Cohere": "COHERE_API_KEY",
|
|
74
|
+
"Local LLM / Custom": "",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
SETTINGS_KEY_MAP = {
|
|
78
|
+
"Google Gemini (Free API)": "gemini_api_key",
|
|
79
|
+
"Groq Cloud (Fast)": "groq_api_key",
|
|
80
|
+
"OpenRouter (Cloud AI)": "openrouter_api_key",
|
|
81
|
+
"NVIDIA NIM": "nvidia_api_key",
|
|
82
|
+
"Sambanova": "sambanova_api_key",
|
|
83
|
+
"OpenAI": "openai_api_key",
|
|
84
|
+
"Mistral AI": "mistral_api_key",
|
|
85
|
+
"Anthropic (Claude)": "anthropic_api_key",
|
|
86
|
+
"Cohere": "cohere_api_key",
|
|
87
|
+
"Local LLM / Custom": "",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class ConfigManager:
|
|
91
|
+
AVAILABLE_PROVIDERS = [
|
|
92
|
+
"Groq Cloud (Fast)",
|
|
93
|
+
"NVIDIA NIM",
|
|
94
|
+
"OpenRouter (Cloud AI)",
|
|
95
|
+
"Google Gemini (Free API)",
|
|
96
|
+
"Sambanova",
|
|
97
|
+
"OpenAI",
|
|
98
|
+
"Mistral AI",
|
|
99
|
+
"Anthropic (Claude)",
|
|
100
|
+
"Cohere",
|
|
101
|
+
"Google Translate (Free)",
|
|
102
|
+
"Ollama (Local / Free)",
|
|
103
|
+
"Local LLM / Custom"
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
def __init__(self):
|
|
107
|
+
self.quest_dir: Optional[Path] = None
|
|
108
|
+
self.target_lang: str = "ru_ru"
|
|
109
|
+
self.concurrency: int = 2
|
|
110
|
+
self.provider: str = "Google Translate (Free)"
|
|
111
|
+
self.model: Optional[str] = None
|
|
112
|
+
self.custom_context: str = ""
|
|
113
|
+
self.policy: str = "Complement (Дополнить)"
|
|
114
|
+
self.resource_pack_mode: bool = False
|
|
115
|
+
self.api_keys_pool: Dict[str, List[str]] = {prov: [] for prov in PROVIDER_ALIASES.values()}
|
|
116
|
+
self.custom_instances_paths: List[str] = []
|
|
117
|
+
self.batch_size: int = 50
|
|
118
|
+
self.min_batch_size: int = 1
|
|
119
|
+
self.max_concurrent_requests: int = 10
|
|
120
|
+
|
|
121
|
+
self.load_from_settings()
|
|
122
|
+
|
|
123
|
+
def _settings(self):
|
|
124
|
+
if QSettings is None:
|
|
125
|
+
return None
|
|
126
|
+
return QSettings("MineAI", "SNBT-Localizer")
|
|
127
|
+
|
|
128
|
+
def load_from_settings(self):
|
|
129
|
+
s = self._settings()
|
|
130
|
+
if s is None:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
self.provider = s.value("provider", self.provider)
|
|
134
|
+
self.model = s.value("model", self.model)
|
|
135
|
+
if self.provider and self.provider != "Mixed Providers":
|
|
136
|
+
provider_model = s.value(f"model_{self.provider}", "")
|
|
137
|
+
if provider_model:
|
|
138
|
+
self.model = provider_model
|
|
139
|
+
self.target_lang = s.value("target_lang", self.target_lang)
|
|
140
|
+
self.concurrency = int(s.value("concurrency_limit", self.concurrency))
|
|
141
|
+
self.custom_context = s.value("custom_context", self.custom_context)
|
|
142
|
+
self.policy = s.value("policy", self.policy)
|
|
143
|
+
|
|
144
|
+
raw = s.value("custom_instances_paths", "")
|
|
145
|
+
if raw:
|
|
146
|
+
self.custom_instances_paths = [p.strip() for p in str(raw).splitlines() if p.strip()]
|
|
147
|
+
|
|
148
|
+
self.batch_size = int(s.value("batch_size", 50))
|
|
149
|
+
self.min_batch_size = int(s.value("min_batch_size", 1))
|
|
150
|
+
self.max_concurrent_requests = int(s.value("max_concurrent_requests", 10))
|
|
151
|
+
self.resource_pack_mode = s.value("resource_pack_mode", self.resource_pack_mode)
|
|
152
|
+
if isinstance(self.resource_pack_mode, str):
|
|
153
|
+
self.resource_pack_mode = self.resource_pack_mode.lower() == "true"
|
|
154
|
+
quest_dir_value = s.value("quest_dir", "")
|
|
155
|
+
self.quest_dir = Path(quest_dir_value) if quest_dir_value else None
|
|
156
|
+
|
|
157
|
+
for prov in PROVIDER_ALIASES.values():
|
|
158
|
+
raw = s.value(f"api_keys_pool_{prov}", "")
|
|
159
|
+
if raw:
|
|
160
|
+
lines = [line.strip() for line in str(raw).splitlines() if line.strip()]
|
|
161
|
+
seen = set()
|
|
162
|
+
unique = []
|
|
163
|
+
for k in lines:
|
|
164
|
+
if k not in seen:
|
|
165
|
+
seen.add(k)
|
|
166
|
+
unique.append(k)
|
|
167
|
+
self.api_keys_pool[prov] = unique[:10]
|
|
168
|
+
if prov not in self.api_keys_pool:
|
|
169
|
+
self.api_keys_pool[prov] = []
|
|
170
|
+
|
|
171
|
+
def save_to_settings(self):
|
|
172
|
+
s = self._settings()
|
|
173
|
+
if s is None:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
s.setValue("provider", self.provider)
|
|
177
|
+
s.setValue("model", self.model or "")
|
|
178
|
+
if self.provider and self.provider != "Mixed Providers":
|
|
179
|
+
s.setValue(f"model_{self.provider}", self.model or "")
|
|
180
|
+
s.setValue("target_lang", self.target_lang)
|
|
181
|
+
s.setValue("concurrency_limit", self.concurrency)
|
|
182
|
+
s.setValue("custom_context", self.custom_context)
|
|
183
|
+
s.setValue("policy", self.policy)
|
|
184
|
+
s.setValue("custom_instances_paths", "\n".join(self.custom_instances_paths))
|
|
185
|
+
|
|
186
|
+
for prov, keys in self.api_keys_pool.items():
|
|
187
|
+
s.setValue(f"api_keys_pool_{prov}", "\n".join(keys))
|
|
188
|
+
|
|
189
|
+
s.setValue("batch_size", self.batch_size)
|
|
190
|
+
s.setValue("min_batch_size", self.min_batch_size)
|
|
191
|
+
s.setValue("max_concurrent_requests", self.max_concurrent_requests)
|
|
192
|
+
s.setValue("resource_pack_mode", self.resource_pack_mode)
|
|
193
|
+
s.setValue("quest_dir", str(self.quest_dir) if self.quest_dir else "")
|
|
194
|
+
s.sync()
|
|
195
|
+
|
|
196
|
+
def parse_cli_args(self, args: Optional[List[str]] = None):
|
|
197
|
+
parser = argparse.ArgumentParser(prog='snbt-tr', add_help=False)
|
|
198
|
+
parser.add_argument("-h", "--help", action="help", help="Show this help message and exit")
|
|
199
|
+
parser.add_argument("-p", "--provider", help="Provider alias (google, gemini, groq, openrouter, nvidia, nim, sambanova)")
|
|
200
|
+
parser.add_argument("-m", "--model", help="Model name (default: provider default)")
|
|
201
|
+
parser.add_argument("-k", "--key", help="API key(s), comma-separated (env/QSettings fallback)")
|
|
202
|
+
parser.add_argument("-l", "--lang", default="ru", help="Language code (default: ru)")
|
|
203
|
+
parser.add_argument("-d", "--dir", default=".", help="Path to quests directory (default: .)")
|
|
204
|
+
parser.add_argument("-c", "--context", default="", help="Custom translation context")
|
|
205
|
+
parser.add_argument("--policy", default="complement", choices=["complement", "overwrite", "skip"],
|
|
206
|
+
help="Existing files policy: complement, overwrite, skip (default: complement)")
|
|
207
|
+
parser.add_argument("--concurrency", type=int, default=2,
|
|
208
|
+
help="Number of parallel translation threads (1-10, default: 3)")
|
|
209
|
+
parser.add_argument("--list-models", action="store_true", help="List available models for provider and exit")
|
|
210
|
+
parser.add_argument("--fastdir", "--fd", action="store_true", help="Scan launcher paths for instances and exit")
|
|
211
|
+
parser.add_argument("--clear-cache", "--clear", action="store_true", help="Clear translation cache and exit")
|
|
212
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging to console")
|
|
213
|
+
parser.add_argument('--mix', action='store_true', help='Enable Mixed Provider mode using QSettings key pool')
|
|
214
|
+
parser.add_argument('--gui', action='store_true', help='Launch Graphical User Interface (GUI)')
|
|
215
|
+
parser.add_argument("--batch-size", type=int, default=50,
|
|
216
|
+
help="Batch size for translation (default: 50)")
|
|
217
|
+
parser.add_argument("--min-batch-size", type=int, default=1,
|
|
218
|
+
help="Minimum batch size before failing (default: 1)")
|
|
219
|
+
parser.add_argument("--max-concurrent-requests", type=int, default=10,
|
|
220
|
+
help="Max concurrent API requests (default: 10)")
|
|
221
|
+
parser.add_argument("--resource-pack", "-r", action="store_true", help="Enable Resource Pack Mode for JSON translation")
|
|
222
|
+
|
|
223
|
+
parsed = parser.parse_args(args)
|
|
224
|
+
|
|
225
|
+
if parsed.provider:
|
|
226
|
+
alias = parsed.provider.lower()
|
|
227
|
+
if alias in PROVIDER_ALIASES:
|
|
228
|
+
self.provider = PROVIDER_ALIASES[alias]
|
|
229
|
+
else:
|
|
230
|
+
logging.getLogger("snbt_localizer.cli").error(f"Unknown provider alias: {parsed.provider}")
|
|
231
|
+
self.provider = "Google Translate (Free)"
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
if parsed.mix:
|
|
235
|
+
self.provider = "Mixed Providers"
|
|
236
|
+
|
|
237
|
+
if parsed.model:
|
|
238
|
+
self.model = parsed.model
|
|
239
|
+
|
|
240
|
+
if parsed.lang:
|
|
241
|
+
lang_input = parsed.lang.lower()
|
|
242
|
+
if lang_input in LANG_ALIASES:
|
|
243
|
+
self.target_lang = LANG_ALIASES[lang_input][1]
|
|
244
|
+
else:
|
|
245
|
+
for alias, (name, code) in LANG_ALIASES.items():
|
|
246
|
+
if lang_input == code or lang_input == name.lower():
|
|
247
|
+
self.target_lang = code
|
|
248
|
+
break
|
|
249
|
+
else:
|
|
250
|
+
logging.getLogger("snbt_localizer.cli").error(f"Unknown language: {parsed.lang}")
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
if parsed.dir:
|
|
254
|
+
self.quest_dir = Path(parsed.dir)
|
|
255
|
+
|
|
256
|
+
if parsed.context:
|
|
257
|
+
self.custom_context = parsed.context
|
|
258
|
+
|
|
259
|
+
if parsed.policy:
|
|
260
|
+
mapping = {
|
|
261
|
+
"complement": "Complement (Дополнить)",
|
|
262
|
+
"overwrite": "Overwrite (Перезаписать)",
|
|
263
|
+
"skip": "Skip (Пропустить)",
|
|
264
|
+
}
|
|
265
|
+
self.policy = mapping.get(parsed.policy, "Complement (Дополнить)")
|
|
266
|
+
|
|
267
|
+
if parsed.concurrency:
|
|
268
|
+
self.concurrency = max(1, min(parsed.concurrency, 10))
|
|
269
|
+
|
|
270
|
+
if parsed.batch_size:
|
|
271
|
+
self.batch_size = max(1, parsed.batch_size)
|
|
272
|
+
if parsed.min_batch_size:
|
|
273
|
+
self.min_batch_size = max(1, parsed.min_batch_size)
|
|
274
|
+
if parsed.max_concurrent_requests:
|
|
275
|
+
self.max_concurrent_requests = max(1, parsed.max_concurrent_requests)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if parsed.key:
|
|
279
|
+
keys = [k.strip() for k in parsed.key.split(",") if k.strip()]
|
|
280
|
+
seen = set()
|
|
281
|
+
unique = []
|
|
282
|
+
for k in keys:
|
|
283
|
+
if k not in seen:
|
|
284
|
+
seen.add(k)
|
|
285
|
+
unique.append(k)
|
|
286
|
+
self.api_keys_pool[self.provider] = unique[:10]
|
|
287
|
+
|
|
288
|
+
return parsed
|
|
289
|
+
|
|
290
|
+
def get_api_keys(self, provider: Optional[str] = None) -> List[str]:
|
|
291
|
+
prov = provider or self.provider
|
|
292
|
+
return self.api_keys_pool.get(prov, [])
|
|
293
|
+
|
|
294
|
+
def set_api_keys(self, provider: str, keys: List[str]):
|
|
295
|
+
seen = set()
|
|
296
|
+
unique = []
|
|
297
|
+
for k in keys:
|
|
298
|
+
k = k.strip()
|
|
299
|
+
if k and k not in seen:
|
|
300
|
+
seen.add(k)
|
|
301
|
+
unique.append(k)
|
|
302
|
+
self.api_keys_pool[provider] = unique[:10]
|
|
303
|
+
|
|
304
|
+
def smart_parse_key(self, entry: str) -> tuple[str, str]:
|
|
305
|
+
entry = entry.strip()
|
|
306
|
+
if "\\" in entry:
|
|
307
|
+
parts = entry.split("\\", 1)
|
|
308
|
+
key = parts[0].strip()
|
|
309
|
+
model = parts[1].strip() if parts[1].strip() else ""
|
|
310
|
+
if model:
|
|
311
|
+
return key, model
|
|
312
|
+
# Trailing backslash with no model - use key part for prefix detection
|
|
313
|
+
entry = key
|
|
314
|
+
|
|
315
|
+
if entry.startswith("gsk_"):
|
|
316
|
+
return entry, "llama-3.3-70b-versatile"
|
|
317
|
+
if entry.startswith("nvapi-"):
|
|
318
|
+
return entry, "nvidia/nemotron-3-ultra"
|
|
319
|
+
if entry.startswith("sk-or-"):
|
|
320
|
+
return entry, "meta-llama/llama-3.3-70b-instruct:free"
|
|
321
|
+
|
|
322
|
+
raise ValueError(f"Unknown key format or provider prefix: {entry[:10]}...")
|
|
323
|
+
|
|
324
|
+
def get_all_available_keys(self) -> list[tuple[str, str, str]]:
|
|
325
|
+
result = []
|
|
326
|
+
for provider in self.AVAILABLE_PROVIDERS:
|
|
327
|
+
keys = self.get_api_keys(provider)
|
|
328
|
+
if not keys:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
saved_model = ""
|
|
332
|
+
if QSettings is not None:
|
|
333
|
+
settings = QSettings("MineAI", "SNBT-Localizer")
|
|
334
|
+
saved_model = settings.value(f"model_{provider}", "") or ""
|
|
335
|
+
|
|
336
|
+
default_model = PROVIDER_DEFAULTS.get(provider, "")
|
|
337
|
+
model = saved_model if saved_model else default_model
|
|
338
|
+
|
|
339
|
+
for key in keys:
|
|
340
|
+
result.append((provider, key, model))
|
|
341
|
+
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
def resolve_language(self, lang_input: str) -> tuple:
|
|
345
|
+
lang_input = lang_input.lower()
|
|
346
|
+
if lang_input in LANG_ALIASES:
|
|
347
|
+
return LANG_ALIASES[lang_input]
|
|
348
|
+
for alias, (name, code) in LANG_ALIASES.items():
|
|
349
|
+
if lang_input == code or lang_input == name.lower():
|
|
350
|
+
return (name, code)
|
|
351
|
+
raise ValueError(f"Unknown language: {lang_input}")
|
|
352
|
+
|
|
353
|
+
def add_custom_path(self, path: str):
|
|
354
|
+
path = str(Path(path).resolve())
|
|
355
|
+
if path not in self.custom_instances_paths:
|
|
356
|
+
self.custom_instances_paths.append(path)
|
|
357
|
+
self.save_to_settings()
|
|
358
|
+
|
|
359
|
+
def prune_invalid_paths(self):
|
|
360
|
+
valid_paths = []
|
|
361
|
+
for p in self.custom_instances_paths:
|
|
362
|
+
if is_valid_custom_instance(Path(p)):
|
|
363
|
+
valid_paths.append(p)
|
|
364
|
+
if valid_paths != self.custom_instances_paths:
|
|
365
|
+
self.custom_instances_paths = valid_paths
|
|
366
|
+
self.save_to_settings()
|