supervertaler 1.9.174__py3-none-any.whl → 1.9.176b0__py3-none-any.whl
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.
- Supervertaler.py +503 -27
- modules/database_migrations.py +54 -7
- modules/termbase_manager.py +105 -1
- modules/unified_prompt_library.py +2 -2
- modules/unified_prompt_manager_qt.py +35 -18
- supervertaler-1.9.176b0.dist-info/METADATA +151 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.176b0.dist-info}/RECORD +11 -11
- supervertaler-1.9.174.dist-info/METADATA +0 -939
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.176b0.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.176b0.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.176b0.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.176b0.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -34,7 +34,7 @@ License: MIT
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
37
|
+
__version__ = "1.9.176-beta"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
39
|
__release_date__ = "2026-01-28"
|
|
40
40
|
__edition__ = "Qt"
|
|
@@ -5794,11 +5794,14 @@ class PreTranslationWorker(QThread):
|
|
|
5794
5794
|
custom_prompt = None
|
|
5795
5795
|
if self.prompt_manager:
|
|
5796
5796
|
try:
|
|
5797
|
+
# Get glossary terms for AI injection
|
|
5798
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5797
5799
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5798
5800
|
source_text=segment.source,
|
|
5799
5801
|
source_lang=source_lang,
|
|
5800
5802
|
target_lang=target_lang,
|
|
5801
|
-
mode="single"
|
|
5803
|
+
mode="single",
|
|
5804
|
+
glossary_terms=glossary_terms
|
|
5802
5805
|
)
|
|
5803
5806
|
# Extract just the instruction part (without the source text section)
|
|
5804
5807
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -5857,11 +5860,14 @@ class PreTranslationWorker(QThread):
|
|
|
5857
5860
|
if self.prompt_manager and batch_segments:
|
|
5858
5861
|
try:
|
|
5859
5862
|
first_segment = batch_segments[0][1]
|
|
5863
|
+
# Get glossary terms for AI injection
|
|
5864
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5860
5865
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5861
5866
|
source_text=first_segment.source,
|
|
5862
5867
|
source_lang=source_lang,
|
|
5863
5868
|
target_lang=target_lang,
|
|
5864
|
-
mode="single"
|
|
5869
|
+
mode="single",
|
|
5870
|
+
glossary_terms=glossary_terms
|
|
5865
5871
|
)
|
|
5866
5872
|
# Extract just the instruction part
|
|
5867
5873
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -6308,6 +6314,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6308
6314
|
# TM Metadata Manager - needed for TM list in Superlookup
|
|
6309
6315
|
from modules.tm_metadata_manager import TMMetadataManager
|
|
6310
6316
|
self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
|
|
6317
|
+
|
|
6318
|
+
# Termbase Manager - needed for glossary AI injection
|
|
6319
|
+
from modules.termbase_manager import TermbaseManager
|
|
6320
|
+
self.termbase_mgr = TermbaseManager(self.db_manager, self.log)
|
|
6311
6321
|
|
|
6312
6322
|
# Spellcheck Manager for target language spell checking
|
|
6313
6323
|
self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
|
|
@@ -6425,13 +6435,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6425
6435
|
from PyQt6.QtCore import QTimer
|
|
6426
6436
|
QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
|
|
6427
6437
|
|
|
6428
|
-
# First-run check - show
|
|
6429
|
-
if self._needs_data_location_dialog:
|
|
6438
|
+
# First-run check - show unified setup wizard
|
|
6439
|
+
if self._needs_data_location_dialog or not general_settings.get('first_run_completed', False):
|
|
6430
6440
|
from PyQt6.QtCore import QTimer
|
|
6431
|
-
QTimer.singleShot(300, self.
|
|
6432
|
-
elif not general_settings.get('first_run_completed', False):
|
|
6433
|
-
from PyQt6.QtCore import QTimer
|
|
6434
|
-
QTimer.singleShot(500, self._show_first_run_welcome)
|
|
6441
|
+
QTimer.singleShot(300, lambda: self._show_setup_wizard(is_first_run=True))
|
|
6435
6442
|
|
|
6436
6443
|
def _show_data_location_dialog(self):
|
|
6437
6444
|
"""Show dialog to let user choose their data folder location on first run."""
|
|
@@ -6662,7 +6669,273 @@ class SupervertalerQt(QMainWindow):
|
|
|
6662
6669
|
self.log("✅ First-run welcome shown (will show again next time)")
|
|
6663
6670
|
except Exception as e:
|
|
6664
6671
|
self.log(f"⚠️ First-run welcome error: {e}")
|
|
6665
|
-
|
|
6672
|
+
|
|
6673
|
+
def _show_setup_wizard(self, is_first_run: bool = False):
|
|
6674
|
+
"""
|
|
6675
|
+
Show unified setup wizard that combines data folder selection and features intro.
|
|
6676
|
+
|
|
6677
|
+
Args:
|
|
6678
|
+
is_first_run: If True, this is an automatic first-run trigger. If False, user
|
|
6679
|
+
manually invoked from menu (skip data folder if already configured).
|
|
6680
|
+
"""
|
|
6681
|
+
try:
|
|
6682
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
|
6683
|
+
QPushButton, QLineEdit, QFileDialog, QStackedWidget,
|
|
6684
|
+
QWidget, QFrame, QCheckBox)
|
|
6685
|
+
from PyQt6.QtCore import Qt
|
|
6686
|
+
|
|
6687
|
+
dialog = QDialog(self)
|
|
6688
|
+
dialog.setWindowTitle("Supervertaler Setup Wizard")
|
|
6689
|
+
dialog.setMinimumWidth(600)
|
|
6690
|
+
dialog.setMinimumHeight(450)
|
|
6691
|
+
dialog.setModal(True)
|
|
6692
|
+
|
|
6693
|
+
main_layout = QVBoxLayout(dialog)
|
|
6694
|
+
main_layout.setSpacing(15)
|
|
6695
|
+
main_layout.setContentsMargins(20, 20, 20, 20)
|
|
6696
|
+
|
|
6697
|
+
# Stacked widget for wizard pages
|
|
6698
|
+
stacked = QStackedWidget()
|
|
6699
|
+
|
|
6700
|
+
# Determine if we need to show data folder page
|
|
6701
|
+
show_data_folder_page = is_first_run and self._needs_data_location_dialog
|
|
6702
|
+
|
|
6703
|
+
# ==================== PAGE 1: Data Folder Selection ====================
|
|
6704
|
+
page1 = QWidget()
|
|
6705
|
+
page1_layout = QVBoxLayout(page1)
|
|
6706
|
+
page1_layout.setSpacing(15)
|
|
6707
|
+
|
|
6708
|
+
# Step indicator
|
|
6709
|
+
step1_indicator = QLabel("<span style='color: #888;'>Step 1 of 2</span>")
|
|
6710
|
+
page1_layout.addWidget(step1_indicator)
|
|
6711
|
+
|
|
6712
|
+
# Title
|
|
6713
|
+
page1_title = QLabel("<h2>📁 Choose Your Data Folder</h2>")
|
|
6714
|
+
page1_layout.addWidget(page1_title)
|
|
6715
|
+
|
|
6716
|
+
# Explanation
|
|
6717
|
+
page1_msg = QLabel(
|
|
6718
|
+
"Supervertaler stores your data in a folder of your choice:<br><br>"
|
|
6719
|
+
"• <b>API keys</b> – Your LLM provider credentials<br>"
|
|
6720
|
+
"• <b>Translation memories</b> – Reusable translation pairs<br>"
|
|
6721
|
+
"• <b>Glossaries</b> – Terminology databases<br>"
|
|
6722
|
+
"• <b>Prompts</b> – Custom AI prompts<br>"
|
|
6723
|
+
"• <b>Settings</b> – Application configuration<br><br>"
|
|
6724
|
+
"Choose a location that's easy to find and backup."
|
|
6725
|
+
)
|
|
6726
|
+
page1_msg.setWordWrap(True)
|
|
6727
|
+
page1_layout.addWidget(page1_msg)
|
|
6728
|
+
|
|
6729
|
+
# Path input with browse button
|
|
6730
|
+
path_layout = QHBoxLayout()
|
|
6731
|
+
path_edit = QLineEdit()
|
|
6732
|
+
default_path = get_default_user_data_path()
|
|
6733
|
+
path_edit.setText(str(default_path))
|
|
6734
|
+
path_edit.setMinimumWidth(350)
|
|
6735
|
+
path_layout.addWidget(path_edit)
|
|
6736
|
+
|
|
6737
|
+
browse_btn = QPushButton("Browse...")
|
|
6738
|
+
def browse_folder():
|
|
6739
|
+
folder = QFileDialog.getExistingDirectory(
|
|
6740
|
+
dialog,
|
|
6741
|
+
"Choose Data Folder",
|
|
6742
|
+
str(Path.home())
|
|
6743
|
+
)
|
|
6744
|
+
if folder:
|
|
6745
|
+
folder_path = Path(folder)
|
|
6746
|
+
if folder_path.name != "Supervertaler":
|
|
6747
|
+
folder_path = folder_path / "Supervertaler"
|
|
6748
|
+
path_edit.setText(str(folder_path))
|
|
6749
|
+
|
|
6750
|
+
browse_btn.clicked.connect(browse_folder)
|
|
6751
|
+
path_layout.addWidget(browse_btn)
|
|
6752
|
+
page1_layout.addLayout(path_layout)
|
|
6753
|
+
|
|
6754
|
+
# Tip
|
|
6755
|
+
page1_tip = QLabel(
|
|
6756
|
+
"💡 <b>Tip:</b> The default location is in your home folder, "
|
|
6757
|
+
"making it easy to find and backup."
|
|
6758
|
+
)
|
|
6759
|
+
page1_tip.setWordWrap(True)
|
|
6760
|
+
page1_tip.setStyleSheet("color: #666;")
|
|
6761
|
+
page1_layout.addWidget(page1_tip)
|
|
6762
|
+
|
|
6763
|
+
page1_layout.addStretch()
|
|
6764
|
+
stacked.addWidget(page1)
|
|
6765
|
+
|
|
6766
|
+
# ==================== PAGE 2: Features Introduction ====================
|
|
6767
|
+
page2 = QWidget()
|
|
6768
|
+
page2_layout = QVBoxLayout(page2)
|
|
6769
|
+
page2_layout.setSpacing(15)
|
|
6770
|
+
|
|
6771
|
+
# Step indicator
|
|
6772
|
+
step2_label = "Step 2 of 2" if show_data_folder_page else "Setup"
|
|
6773
|
+
step2_indicator = QLabel(f"<span style='color: #888;'>{step2_label}</span>")
|
|
6774
|
+
page2_layout.addWidget(step2_indicator)
|
|
6775
|
+
|
|
6776
|
+
# Data folder info (shown when skipping page 1)
|
|
6777
|
+
if not show_data_folder_page:
|
|
6778
|
+
from PyQt6.QtGui import QDesktopServices
|
|
6779
|
+
from PyQt6.QtCore import QUrl
|
|
6780
|
+
|
|
6781
|
+
data_folder_path = str(self.user_data_path)
|
|
6782
|
+
data_folder_info = QLabel(
|
|
6783
|
+
f"<b>📁 Data Folder:</b> <a href='file:///{data_folder_path}' "
|
|
6784
|
+
f"style='color: #3b82f6;'>{data_folder_path}</a><br>"
|
|
6785
|
+
"<span style='color: #666; font-size: 0.9em;'>"
|
|
6786
|
+
"Your settings, TMs, glossaries and prompts are stored here. "
|
|
6787
|
+
"Change in Settings → General.</span>"
|
|
6788
|
+
)
|
|
6789
|
+
data_folder_info.setWordWrap(True)
|
|
6790
|
+
data_folder_info.setTextFormat(Qt.TextFormat.RichText)
|
|
6791
|
+
data_folder_info.setOpenExternalLinks(False) # Handle clicks ourselves
|
|
6792
|
+
data_folder_info.linkActivated.connect(
|
|
6793
|
+
lambda url: QDesktopServices.openUrl(QUrl.fromLocalFile(data_folder_path))
|
|
6794
|
+
)
|
|
6795
|
+
data_folder_info.setStyleSheet(
|
|
6796
|
+
"background: #f0f4ff; padding: 12px; border-radius: 6px; "
|
|
6797
|
+
"border-left: 4px solid #3b82f6; margin-bottom: 10px;"
|
|
6798
|
+
)
|
|
6799
|
+
page2_layout.addWidget(data_folder_info)
|
|
6800
|
+
|
|
6801
|
+
# Title
|
|
6802
|
+
page2_title = QLabel("<h2>✨ Modular Features</h2>")
|
|
6803
|
+
page2_layout.addWidget(page2_title)
|
|
6804
|
+
|
|
6805
|
+
# Message
|
|
6806
|
+
page2_msg = QLabel(
|
|
6807
|
+
"Supervertaler uses a <b>modular architecture</b> – you can install "
|
|
6808
|
+
"only the features you need.<br><br>"
|
|
6809
|
+
"<b>Core features</b> (always available):<br>"
|
|
6810
|
+
"• AI translation with OpenAI, Claude, Gemini, Ollama<br>"
|
|
6811
|
+
"• Translation Memory and Glossaries<br>"
|
|
6812
|
+
"• XLIFF, SDLXLIFF, memoQ support<br>"
|
|
6813
|
+
"• Basic spellchecking<br><br>"
|
|
6814
|
+
"<b>Optional features</b> (install via pip):<br>"
|
|
6815
|
+
"• <code>openai-whisper</code> – Local voice dictation (no API needed)<br><br>"
|
|
6816
|
+
"You can view and manage features in <b>Settings → Features</b>."
|
|
6817
|
+
)
|
|
6818
|
+
page2_msg.setWordWrap(True)
|
|
6819
|
+
page2_msg.setTextFormat(Qt.TextFormat.RichText)
|
|
6820
|
+
page2_layout.addWidget(page2_msg)
|
|
6821
|
+
|
|
6822
|
+
# Checkbox
|
|
6823
|
+
dont_show_checkbox = CheckmarkCheckBox("Don't show this wizard on startup")
|
|
6824
|
+
dont_show_checkbox.setChecked(True)
|
|
6825
|
+
page2_layout.addWidget(dont_show_checkbox)
|
|
6826
|
+
|
|
6827
|
+
# Open Features tab checkbox
|
|
6828
|
+
open_features_checkbox = CheckmarkCheckBox("Open Features tab after closing")
|
|
6829
|
+
open_features_checkbox.setChecked(True)
|
|
6830
|
+
page2_layout.addWidget(open_features_checkbox)
|
|
6831
|
+
|
|
6832
|
+
page2_layout.addStretch()
|
|
6833
|
+
stacked.addWidget(page2)
|
|
6834
|
+
|
|
6835
|
+
main_layout.addWidget(stacked)
|
|
6836
|
+
|
|
6837
|
+
# ==================== Navigation Buttons ====================
|
|
6838
|
+
nav_layout = QHBoxLayout()
|
|
6839
|
+
|
|
6840
|
+
back_btn = QPushButton("← Back")
|
|
6841
|
+
back_btn.setVisible(False) # Hidden on first page
|
|
6842
|
+
|
|
6843
|
+
next_btn = QPushButton("Next →")
|
|
6844
|
+
finish_btn = QPushButton("Finish")
|
|
6845
|
+
finish_btn.setVisible(False)
|
|
6846
|
+
finish_btn.setDefault(True)
|
|
6847
|
+
|
|
6848
|
+
# Use Default button (only on page 1)
|
|
6849
|
+
default_btn = QPushButton("Use Default")
|
|
6850
|
+
default_btn.clicked.connect(lambda: path_edit.setText(str(default_path)))
|
|
6851
|
+
|
|
6852
|
+
nav_layout.addWidget(default_btn)
|
|
6853
|
+
nav_layout.addStretch()
|
|
6854
|
+
nav_layout.addWidget(back_btn)
|
|
6855
|
+
nav_layout.addWidget(next_btn)
|
|
6856
|
+
nav_layout.addWidget(finish_btn)
|
|
6857
|
+
|
|
6858
|
+
main_layout.addLayout(nav_layout)
|
|
6859
|
+
|
|
6860
|
+
# Track chosen path for later
|
|
6861
|
+
chosen_path_holder = [None]
|
|
6862
|
+
|
|
6863
|
+
def go_to_page(page_index):
|
|
6864
|
+
stacked.setCurrentIndex(page_index)
|
|
6865
|
+
if page_index == 0:
|
|
6866
|
+
back_btn.setVisible(False)
|
|
6867
|
+
next_btn.setVisible(True)
|
|
6868
|
+
finish_btn.setVisible(False)
|
|
6869
|
+
default_btn.setVisible(True)
|
|
6870
|
+
else:
|
|
6871
|
+
back_btn.setVisible(show_data_folder_page)
|
|
6872
|
+
next_btn.setVisible(False)
|
|
6873
|
+
finish_btn.setVisible(True)
|
|
6874
|
+
default_btn.setVisible(False)
|
|
6875
|
+
|
|
6876
|
+
def on_next():
|
|
6877
|
+
# Save the data folder choice
|
|
6878
|
+
chosen_path = Path(path_edit.text())
|
|
6879
|
+
chosen_path_holder[0] = chosen_path
|
|
6880
|
+
|
|
6881
|
+
# Create the folder and save config
|
|
6882
|
+
chosen_path.mkdir(parents=True, exist_ok=True)
|
|
6883
|
+
save_user_data_path(chosen_path)
|
|
6884
|
+
|
|
6885
|
+
# Update our path if different
|
|
6886
|
+
if chosen_path != self.user_data_path:
|
|
6887
|
+
self.user_data_path = chosen_path
|
|
6888
|
+
self._reinitialize_with_new_data_path()
|
|
6889
|
+
else:
|
|
6890
|
+
if hasattr(self, 'db_manager') and self.db_manager and not self.db_manager.connection:
|
|
6891
|
+
self.db_manager.connect()
|
|
6892
|
+
|
|
6893
|
+
self.log(f"📁 Data folder set to: {chosen_path}")
|
|
6894
|
+
go_to_page(1)
|
|
6895
|
+
|
|
6896
|
+
def on_back():
|
|
6897
|
+
go_to_page(0)
|
|
6898
|
+
|
|
6899
|
+
def on_finish():
|
|
6900
|
+
# Save first_run preference
|
|
6901
|
+
if dont_show_checkbox.isChecked():
|
|
6902
|
+
settings = self.load_general_settings()
|
|
6903
|
+
settings['first_run_completed'] = True
|
|
6904
|
+
self.save_general_settings(settings)
|
|
6905
|
+
self.log("✅ Setup wizard completed (won't show again on startup)")
|
|
6906
|
+
else:
|
|
6907
|
+
self.log("✅ Setup wizard shown (will show again next time)")
|
|
6908
|
+
|
|
6909
|
+
dialog.accept()
|
|
6910
|
+
|
|
6911
|
+
# Navigate to Features tab if checkbox is checked
|
|
6912
|
+
if open_features_checkbox.isChecked():
|
|
6913
|
+
self.main_tabs.setCurrentIndex(4) # Settings tab
|
|
6914
|
+
if hasattr(self, 'settings_tabs'):
|
|
6915
|
+
for i in range(self.settings_tabs.count()):
|
|
6916
|
+
if "Features" in self.settings_tabs.tabText(i):
|
|
6917
|
+
self.settings_tabs.setCurrentIndex(i)
|
|
6918
|
+
break
|
|
6919
|
+
|
|
6920
|
+
back_btn.clicked.connect(on_back)
|
|
6921
|
+
next_btn.clicked.connect(on_next)
|
|
6922
|
+
finish_btn.clicked.connect(on_finish)
|
|
6923
|
+
|
|
6924
|
+
# Start on appropriate page
|
|
6925
|
+
if show_data_folder_page:
|
|
6926
|
+
go_to_page(0)
|
|
6927
|
+
else:
|
|
6928
|
+
# Skip to features page if data folder already configured
|
|
6929
|
+
go_to_page(1)
|
|
6930
|
+
step2_indicator.setText("<span style='color: #888;'>Supervertaler Setup</span>")
|
|
6931
|
+
|
|
6932
|
+
dialog.exec()
|
|
6933
|
+
|
|
6934
|
+
except Exception as e:
|
|
6935
|
+
self.log(f"⚠️ Setup wizard error: {e}")
|
|
6936
|
+
import traceback
|
|
6937
|
+
traceback.print_exc()
|
|
6938
|
+
|
|
6666
6939
|
def _check_for_new_models(self, force: bool = False):
|
|
6667
6940
|
"""
|
|
6668
6941
|
Check for new LLM models from providers
|
|
@@ -7926,6 +8199,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
7926
8199
|
superdocs_action.triggered.connect(lambda: self._open_url("https://supervertaler.gitbook.io/superdocs/"))
|
|
7927
8200
|
help_menu.addAction(superdocs_action)
|
|
7928
8201
|
|
|
8202
|
+
setup_wizard_action = QAction("🚀 Setup Wizard...", self)
|
|
8203
|
+
setup_wizard_action.setToolTip("Run the initial setup wizard (data folder location, features overview)")
|
|
8204
|
+
setup_wizard_action.triggered.connect(lambda: self._show_setup_wizard(is_first_run=False))
|
|
8205
|
+
help_menu.addAction(setup_wizard_action)
|
|
8206
|
+
|
|
7929
8207
|
help_menu.addSeparator()
|
|
7930
8208
|
|
|
7931
8209
|
shortcuts_action = QAction("⌨️ Keyboard Shortcuts", self)
|
|
@@ -12750,7 +13028,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12750
13028
|
"💡 <b>Glossaries</b><br>"
|
|
12751
13029
|
"• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
|
|
12752
13030
|
"• <b>Write</b> (blue ✓): Glossary is updated with new terms<br>"
|
|
12753
|
-
"• <b>Priority</b>: Manually set 1-N (lower = higher priority).
|
|
13031
|
+
"• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
|
|
13032
|
+
"• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
|
|
12754
13033
|
)
|
|
12755
13034
|
help_msg.setWordWrap(True)
|
|
12756
13035
|
help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
|
|
@@ -12772,8 +13051,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12772
13051
|
# Termbase list with table
|
|
12773
13052
|
termbase_table = QTableWidget()
|
|
12774
13053
|
self.termbase_table = termbase_table # Store for external access (Superlookup navigation)
|
|
12775
|
-
termbase_table.setColumnCount(
|
|
12776
|
-
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority"])
|
|
13054
|
+
termbase_table.setColumnCount(8)
|
|
13055
|
+
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
|
|
12777
13056
|
termbase_table.horizontalHeader().setStretchLastSection(False)
|
|
12778
13057
|
termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
|
|
12779
13058
|
termbase_table.setColumnWidth(1, 180) # Name
|
|
@@ -12782,6 +13061,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
12782
13061
|
termbase_table.setColumnWidth(4, 50) # Read checkbox
|
|
12783
13062
|
termbase_table.setColumnWidth(5, 50) # Write checkbox
|
|
12784
13063
|
termbase_table.setColumnWidth(6, 60) # Priority
|
|
13064
|
+
termbase_table.setColumnWidth(7, 40) # AI checkbox
|
|
12785
13065
|
termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
12786
13066
|
termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
|
12787
13067
|
termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
|
|
@@ -13475,7 +13755,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
13475
13755
|
priority_item.setToolTip("No priority - glossary not readable")
|
|
13476
13756
|
priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
13477
13757
|
termbase_table.setItem(row, 6, priority_item)
|
|
13478
|
-
|
|
13758
|
+
|
|
13759
|
+
# AI checkbox (purple/orange) - whether to inject terms into LLM prompts
|
|
13760
|
+
ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
|
|
13761
|
+
ai_checkbox = OrangeCheckmarkCheckBox()
|
|
13762
|
+
ai_checkbox.setChecked(ai_enabled)
|
|
13763
|
+
ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
|
|
13764
|
+
|
|
13765
|
+
def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
|
|
13766
|
+
if checked:
|
|
13767
|
+
# Show warning when enabling
|
|
13768
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
13769
|
+
msg = QMessageBox()
|
|
13770
|
+
msg.setWindowTitle("Enable AI Injection")
|
|
13771
|
+
msg.setText(f"Enable AI injection for '{tb_name}'?")
|
|
13772
|
+
msg.setInformativeText(
|
|
13773
|
+
"When enabled, ALL terms from this glossary will be sent to the LLM "
|
|
13774
|
+
"with every translation request.\n\n"
|
|
13775
|
+
"This helps the AI consistently use your preferred terminology "
|
|
13776
|
+
"throughout the translation.\n\n"
|
|
13777
|
+
"Recommended for small, curated glossaries (< 500 terms)."
|
|
13778
|
+
)
|
|
13779
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
13780
|
+
msg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
|
13781
|
+
if msg.exec() != QMessageBox.StandardButton.Yes:
|
|
13782
|
+
# User cancelled - revert checkbox
|
|
13783
|
+
sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
|
|
13784
|
+
if sender:
|
|
13785
|
+
sender.blockSignals(True)
|
|
13786
|
+
sender.setChecked(False)
|
|
13787
|
+
sender.blockSignals(False)
|
|
13788
|
+
return
|
|
13789
|
+
termbase_mgr.set_termbase_ai_inject(tb_id, checked)
|
|
13790
|
+
self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
|
|
13791
|
+
|
|
13792
|
+
ai_checkbox.toggled.connect(on_ai_toggle)
|
|
13793
|
+
termbase_table.setCellWidget(row, 7, ai_checkbox)
|
|
13794
|
+
|
|
13479
13795
|
# Update header checkbox states based on current selection
|
|
13480
13796
|
tb_read_header_checkbox.blockSignals(True)
|
|
13481
13797
|
tb_write_header_checkbox.blockSignals(True)
|
|
@@ -31565,9 +31881,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31565
31881
|
|
|
31566
31882
|
# Get source text
|
|
31567
31883
|
source_text = current_segment.source
|
|
31568
|
-
|
|
31884
|
+
|
|
31885
|
+
# Get glossary terms for AI injection
|
|
31886
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
31887
|
+
|
|
31569
31888
|
# Build combined prompt
|
|
31570
|
-
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31889
|
+
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31890
|
+
source_text, source_lang, target_lang, glossary_terms=glossary_terms
|
|
31891
|
+
)
|
|
31571
31892
|
|
|
31572
31893
|
# Check for figure/image context
|
|
31573
31894
|
figure_info = ""
|
|
@@ -31594,11 +31915,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31594
31915
|
composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
|
|
31595
31916
|
|
|
31596
31917
|
if self.prompt_manager_qt.library.active_primary_prompt:
|
|
31597
|
-
composition_parts.append(f"✓
|
|
31918
|
+
composition_parts.append(f"✓ Custom prompt attached")
|
|
31598
31919
|
|
|
31599
31920
|
if self.prompt_manager_qt.library.attached_prompts:
|
|
31600
31921
|
composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
|
|
31601
|
-
|
|
31922
|
+
|
|
31923
|
+
if glossary_terms:
|
|
31924
|
+
composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
|
|
31925
|
+
|
|
31602
31926
|
if figure_info:
|
|
31603
31927
|
composition_parts.append(figure_info)
|
|
31604
31928
|
|
|
@@ -31633,12 +31957,59 @@ class SupervertalerQt(QMainWindow):
|
|
|
31633
31957
|
image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
|
|
31634
31958
|
layout.addWidget(image_notice)
|
|
31635
31959
|
|
|
31636
|
-
# Text editor for preview
|
|
31960
|
+
# Text editor for preview with syntax highlighting
|
|
31637
31961
|
text_edit = QTextEdit()
|
|
31638
|
-
text_edit.setPlainText(combined)
|
|
31639
31962
|
text_edit.setReadOnly(True)
|
|
31640
31963
|
text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
|
|
31641
31964
|
text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
|
|
31965
|
+
|
|
31966
|
+
# Format the prompt with color highlighting
|
|
31967
|
+
import html
|
|
31968
|
+
import re
|
|
31969
|
+
|
|
31970
|
+
# Escape HTML entities first
|
|
31971
|
+
formatted_html = html.escape(combined)
|
|
31972
|
+
|
|
31973
|
+
# Replace newlines with <br> for HTML
|
|
31974
|
+
formatted_html = formatted_html.replace('\n', '<br>')
|
|
31975
|
+
|
|
31976
|
+
# Make "# SYSTEM PROMPT" bold and red
|
|
31977
|
+
formatted_html = re.sub(
|
|
31978
|
+
r'(#\s*SYSTEM\s*PROMPT)',
|
|
31979
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31980
|
+
formatted_html,
|
|
31981
|
+
flags=re.IGNORECASE
|
|
31982
|
+
)
|
|
31983
|
+
|
|
31984
|
+
# Make "# CUSTOM PROMPT" bold and red
|
|
31985
|
+
formatted_html = re.sub(
|
|
31986
|
+
r'(#\s*CUSTOM\s*PROMPT)',
|
|
31987
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31988
|
+
formatted_html,
|
|
31989
|
+
flags=re.IGNORECASE
|
|
31990
|
+
)
|
|
31991
|
+
|
|
31992
|
+
# Make "# GLOSSARY" bold and orange
|
|
31993
|
+
formatted_html = re.sub(
|
|
31994
|
+
r'(#\s*GLOSSARY)',
|
|
31995
|
+
r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31996
|
+
formatted_html,
|
|
31997
|
+
flags=re.IGNORECASE
|
|
31998
|
+
)
|
|
31999
|
+
|
|
32000
|
+
# Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
|
|
32001
|
+
# Match language code + " text:" followed by content until "# " or end
|
|
32002
|
+
formatted_html = re.sub(
|
|
32003
|
+
r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
|
|
32004
|
+
r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
|
|
32005
|
+
formatted_html,
|
|
32006
|
+
flags=re.DOTALL
|
|
32007
|
+
)
|
|
32008
|
+
|
|
32009
|
+
# Wrap in pre-like styling div
|
|
32010
|
+
formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
|
|
32011
|
+
|
|
32012
|
+
text_edit.setHtml(formatted_html)
|
|
31642
32013
|
layout.addWidget(text_edit, 1)
|
|
31643
32014
|
|
|
31644
32015
|
# Close button
|
|
@@ -35380,7 +35751,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35380
35751
|
primary_prompt_text = f"✅ {prompt_path}"
|
|
35381
35752
|
attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
|
|
35382
35753
|
|
|
35383
|
-
ai_layout.addWidget(QLabel(f"<b>
|
|
35754
|
+
ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
|
|
35384
35755
|
if attached_count > 0:
|
|
35385
35756
|
ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
|
|
35386
35757
|
|
|
@@ -40466,13 +40837,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40466
40837
|
except Exception as e:
|
|
40467
40838
|
self.log(f"⚠ Could not add surrounding segments: {e}")
|
|
40468
40839
|
|
|
40840
|
+
# Get glossary terms for AI injection
|
|
40841
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
40842
|
+
|
|
40469
40843
|
custom_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
40470
40844
|
source_text=segment.source,
|
|
40471
40845
|
source_lang=self.current_project.source_lang,
|
|
40472
40846
|
target_lang=self.current_project.target_lang,
|
|
40473
|
-
mode="single"
|
|
40847
|
+
mode="single",
|
|
40848
|
+
glossary_terms=glossary_terms
|
|
40474
40849
|
)
|
|
40475
|
-
|
|
40850
|
+
|
|
40476
40851
|
# Add surrounding context before the translation delimiter
|
|
40477
40852
|
if surrounding_context:
|
|
40478
40853
|
# Insert before the "YOUR TRANSLATION" delimiter
|
|
@@ -42023,11 +42398,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42023
42398
|
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
|
|
42024
42399
|
try:
|
|
42025
42400
|
first_segment = batch_segments[0][1]
|
|
42401
|
+
# Get glossary terms for AI injection
|
|
42402
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
42026
42403
|
base_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
42027
42404
|
source_text=first_segment.source,
|
|
42028
42405
|
source_lang=source_lang,
|
|
42029
42406
|
target_lang=target_lang,
|
|
42030
|
-
mode="single"
|
|
42407
|
+
mode="single",
|
|
42408
|
+
glossary_terms=glossary_terms
|
|
42031
42409
|
)
|
|
42032
42410
|
if "**SOURCE TEXT:**" in base_prompt:
|
|
42033
42411
|
base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
|
|
@@ -42455,11 +42833,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42455
42833
|
# Access parent through closure
|
|
42456
42834
|
parent = self
|
|
42457
42835
|
if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
|
|
42836
|
+
# Get glossary terms for AI injection
|
|
42837
|
+
glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
|
|
42458
42838
|
custom_prompt = parent.prompt_manager_qt.build_final_prompt(
|
|
42459
42839
|
source_text=source_text,
|
|
42460
42840
|
source_lang=source_lang,
|
|
42461
42841
|
target_lang=target_lang,
|
|
42462
|
-
mode="single"
|
|
42842
|
+
mode="single",
|
|
42843
|
+
glossary_terms=glossary_terms
|
|
42463
42844
|
)
|
|
42464
42845
|
except Exception as e:
|
|
42465
42846
|
self.log(f"⚠ Could not build LLM prompt from manager: {e}")
|
|
@@ -42942,7 +43323,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42942
43323
|
api_keys['google'] = api_keys['gemini']
|
|
42943
43324
|
|
|
42944
43325
|
return api_keys
|
|
42945
|
-
|
|
43326
|
+
|
|
43327
|
+
def get_ai_inject_glossary_terms(self) -> list:
|
|
43328
|
+
"""Get glossary terms from AI-inject-enabled termbases for the current project.
|
|
43329
|
+
|
|
43330
|
+
Returns:
|
|
43331
|
+
List of term dictionaries with source_term, target_term, forbidden keys
|
|
43332
|
+
"""
|
|
43333
|
+
if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
|
|
43334
|
+
return []
|
|
43335
|
+
|
|
43336
|
+
project_id = None
|
|
43337
|
+
if hasattr(self, 'current_project') and self.current_project:
|
|
43338
|
+
project_id = getattr(self.current_project, 'id', None)
|
|
43339
|
+
|
|
43340
|
+
return self.termbase_mgr.get_ai_inject_terms(project_id)
|
|
43341
|
+
|
|
42946
43342
|
def ensure_example_api_keys(self):
|
|
42947
43343
|
"""Create example API keys file on first launch for new users"""
|
|
42948
43344
|
example_file = self.user_data_path / "api_keys.example.txt"
|
|
@@ -48257,7 +48653,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
|
|
|
48257
48653
|
|
|
48258
48654
|
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48259
48655
|
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48260
|
-
|
|
48656
|
+
|
|
48657
|
+
painter.end()
|
|
48658
|
+
|
|
48659
|
+
|
|
48660
|
+
class OrangeCheckmarkCheckBox(QCheckBox):
|
|
48661
|
+
"""Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
|
|
48662
|
+
|
|
48663
|
+
def __init__(self, text="", parent=None):
|
|
48664
|
+
super().__init__(text, parent)
|
|
48665
|
+
self.setCheckable(True)
|
|
48666
|
+
self.setEnabled(True)
|
|
48667
|
+
self.setStyleSheet("""
|
|
48668
|
+
QCheckBox {
|
|
48669
|
+
font-size: 9pt;
|
|
48670
|
+
spacing: 6px;
|
|
48671
|
+
}
|
|
48672
|
+
QCheckBox::indicator {
|
|
48673
|
+
width: 16px;
|
|
48674
|
+
height: 16px;
|
|
48675
|
+
border: 2px solid #999;
|
|
48676
|
+
border-radius: 3px;
|
|
48677
|
+
background-color: white;
|
|
48678
|
+
}
|
|
48679
|
+
QCheckBox::indicator:checked {
|
|
48680
|
+
background-color: #FF9800;
|
|
48681
|
+
border-color: #FF9800;
|
|
48682
|
+
}
|
|
48683
|
+
QCheckBox::indicator:hover {
|
|
48684
|
+
border-color: #666;
|
|
48685
|
+
}
|
|
48686
|
+
QCheckBox::indicator:checked:hover {
|
|
48687
|
+
background-color: #F57C00;
|
|
48688
|
+
border-color: #F57C00;
|
|
48689
|
+
}
|
|
48690
|
+
""")
|
|
48691
|
+
|
|
48692
|
+
def paintEvent(self, event):
|
|
48693
|
+
"""Override paint event to draw white checkmark when checked"""
|
|
48694
|
+
super().paintEvent(event)
|
|
48695
|
+
|
|
48696
|
+
if self.isChecked():
|
|
48697
|
+
from PyQt6.QtWidgets import QStyleOptionButton
|
|
48698
|
+
from PyQt6.QtGui import QPainter, QPen, QColor
|
|
48699
|
+
from PyQt6.QtCore import QPointF
|
|
48700
|
+
|
|
48701
|
+
opt = QStyleOptionButton()
|
|
48702
|
+
self.initStyleOption(opt)
|
|
48703
|
+
indicator_rect = self.style().subElementRect(
|
|
48704
|
+
self.style().SubElement.SE_CheckBoxIndicator,
|
|
48705
|
+
opt,
|
|
48706
|
+
self
|
|
48707
|
+
)
|
|
48708
|
+
|
|
48709
|
+
if indicator_rect.isValid():
|
|
48710
|
+
painter = QPainter(self)
|
|
48711
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
48712
|
+
pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
|
|
48713
|
+
painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
|
|
48714
|
+
painter.setBrush(QColor(255, 255, 255))
|
|
48715
|
+
|
|
48716
|
+
x = indicator_rect.x()
|
|
48717
|
+
y = indicator_rect.y()
|
|
48718
|
+
w = indicator_rect.width()
|
|
48719
|
+
h = indicator_rect.height()
|
|
48720
|
+
|
|
48721
|
+
padding = min(w, h) * 0.15
|
|
48722
|
+
x += padding
|
|
48723
|
+
y += padding
|
|
48724
|
+
w -= padding * 2
|
|
48725
|
+
h -= padding * 2
|
|
48726
|
+
|
|
48727
|
+
check_x1 = x + w * 0.10
|
|
48728
|
+
check_y1 = y + h * 0.50
|
|
48729
|
+
check_x2 = x + w * 0.35
|
|
48730
|
+
check_y2 = y + h * 0.70
|
|
48731
|
+
check_x3 = x + w * 0.90
|
|
48732
|
+
check_y3 = y + h * 0.25
|
|
48733
|
+
|
|
48734
|
+
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48735
|
+
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48736
|
+
|
|
48261
48737
|
painter.end()
|
|
48262
48738
|
|
|
48263
48739
|
|