supervertaler 1.9.175__py3-none-any.whl → 1.9.177b0__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.
Potentially problematic release.
This version of supervertaler might be problematic. Click here for more details.
- Supervertaler.py +347 -30
- modules/database_manager.py +169 -64
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.dist-info}/METADATA +1 -1
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.dist-info}/RECORD +8 -8
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.175.dist-info → supervertaler-1.9.177b0.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"
|
|
@@ -6435,13 +6435,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6435
6435
|
from PyQt6.QtCore import QTimer
|
|
6436
6436
|
QTimer.singleShot(2000, lambda: self._check_for_new_models(force=False)) # 2 second delay
|
|
6437
6437
|
|
|
6438
|
-
# First-run check - show
|
|
6439
|
-
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):
|
|
6440
6440
|
from PyQt6.QtCore import QTimer
|
|
6441
|
-
QTimer.singleShot(300, self.
|
|
6442
|
-
elif not general_settings.get('first_run_completed', False):
|
|
6443
|
-
from PyQt6.QtCore import QTimer
|
|
6444
|
-
QTimer.singleShot(500, self._show_first_run_welcome)
|
|
6441
|
+
QTimer.singleShot(300, lambda: self._show_setup_wizard(is_first_run=True))
|
|
6445
6442
|
|
|
6446
6443
|
def _show_data_location_dialog(self):
|
|
6447
6444
|
"""Show dialog to let user choose their data folder location on first run."""
|
|
@@ -6672,7 +6669,273 @@ class SupervertalerQt(QMainWindow):
|
|
|
6672
6669
|
self.log("✅ First-run welcome shown (will show again next time)")
|
|
6673
6670
|
except Exception as e:
|
|
6674
6671
|
self.log(f"⚠️ First-run welcome error: {e}")
|
|
6675
|
-
|
|
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
|
+
|
|
6676
6939
|
def _check_for_new_models(self, force: bool = False):
|
|
6677
6940
|
"""
|
|
6678
6941
|
Check for new LLM models from providers
|
|
@@ -7936,6 +8199,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
7936
8199
|
superdocs_action.triggered.connect(lambda: self._open_url("https://supervertaler.gitbook.io/superdocs/"))
|
|
7937
8200
|
help_menu.addAction(superdocs_action)
|
|
7938
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
|
+
|
|
7939
8207
|
help_menu.addSeparator()
|
|
7940
8208
|
|
|
7941
8209
|
shortcuts_action = QAction("⌨️ Keyboard Shortcuts", self)
|
|
@@ -22051,20 +22319,24 @@ class SupervertalerQt(QMainWindow):
|
|
|
22051
22319
|
"""
|
|
22052
22320
|
Search termbases using a provided cursor (thread-safe for background threads).
|
|
22053
22321
|
This method allows background workers to query the database without SQLite threading errors.
|
|
22054
|
-
|
|
22322
|
+
|
|
22323
|
+
Implements BIDIRECTIONAL matching: searches both source_term and target_term columns.
|
|
22324
|
+
When a match is found on target_term, source and target are swapped in the result.
|
|
22325
|
+
This matches memoQ/Trados behavior where a NL→EN termbase also works for EN→NL projects.
|
|
22326
|
+
|
|
22055
22327
|
Args:
|
|
22056
22328
|
source_text: The source text to search for
|
|
22057
22329
|
cursor: A database cursor from a thread-local connection
|
|
22058
22330
|
source_lang: Source language code
|
|
22059
22331
|
target_lang: Target language code
|
|
22060
22332
|
project_id: Current project ID (required to filter by activated termbases)
|
|
22061
|
-
|
|
22333
|
+
|
|
22062
22334
|
Returns:
|
|
22063
22335
|
Dictionary of {term: translation} matches
|
|
22064
22336
|
"""
|
|
22065
22337
|
if not source_text or not cursor:
|
|
22066
22338
|
return {}
|
|
22067
|
-
|
|
22339
|
+
|
|
22068
22340
|
try:
|
|
22069
22341
|
# Convert language names to codes (match interactive search logic)
|
|
22070
22342
|
source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
|
|
@@ -22084,20 +22356,26 @@ class SupervertalerQt(QMainWindow):
|
|
|
22084
22356
|
try:
|
|
22085
22357
|
# JOIN termbases AND termbase_activation to filter by activated termbases
|
|
22086
22358
|
# This matches the logic in database_manager.py search_termbases()
|
|
22359
|
+
# BIDIRECTIONAL: Search both source_term (forward) and target_term (reverse)
|
|
22360
|
+
# Using UNION to combine both directions
|
|
22087
22361
|
query = """
|
|
22088
|
-
SELECT
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
|
|
22093
|
-
|
|
22094
|
-
|
|
22095
|
-
|
|
22096
|
-
|
|
22097
|
-
|
|
22362
|
+
SELECT * FROM (
|
|
22363
|
+
-- Forward match: search source_term
|
|
22364
|
+
SELECT
|
|
22365
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
22366
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22367
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22368
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22369
|
+
'source' as match_direction
|
|
22370
|
+
FROM termbase_terms t
|
|
22371
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22372
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22373
|
+
WHERE LOWER(t.source_term) LIKE ?
|
|
22374
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22098
22375
|
"""
|
|
22099
22376
|
params = [project_id if project_id else 0, f"%{clean_word.lower()}%"]
|
|
22100
22377
|
|
|
22378
|
+
# Language filters for forward query
|
|
22101
22379
|
if source_lang_code:
|
|
22102
22380
|
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
22103
22381
|
params.extend([source_lang_code, source_lang_code])
|
|
@@ -22105,15 +22383,45 @@ class SupervertalerQt(QMainWindow):
|
|
|
22105
22383
|
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
22106
22384
|
params.extend([target_lang_code, target_lang_code])
|
|
22107
22385
|
|
|
22108
|
-
#
|
|
22109
|
-
query += "
|
|
22386
|
+
# Reverse match: search target_term, swap source/target in output
|
|
22387
|
+
query += """
|
|
22388
|
+
UNION ALL
|
|
22389
|
+
-- Reverse match: search target_term, swap columns
|
|
22390
|
+
SELECT
|
|
22391
|
+
t.id, t.target_term as source_term, t.source_term as target_term,
|
|
22392
|
+
t.termbase_id, t.priority,
|
|
22393
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22394
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22395
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
22396
|
+
'target' as match_direction
|
|
22397
|
+
FROM termbase_terms t
|
|
22398
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22399
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
22400
|
+
WHERE LOWER(t.target_term) LIKE ?
|
|
22401
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22402
|
+
"""
|
|
22403
|
+
params.extend([project_id if project_id else 0, f"%{clean_word.lower()}%"])
|
|
22404
|
+
|
|
22405
|
+
# Language filters for reverse query (swapped)
|
|
22406
|
+
if source_lang_code:
|
|
22407
|
+
# For reverse: source_lang filters target_lang column
|
|
22408
|
+
query += " AND (t.target_lang = ? OR (t.target_lang IS NULL AND tb.target_lang = ?) OR (t.target_lang IS NULL AND tb.target_lang IS NULL))"
|
|
22409
|
+
params.extend([source_lang_code, source_lang_code])
|
|
22410
|
+
if target_lang_code:
|
|
22411
|
+
# For reverse: target_lang filters source_lang column
|
|
22412
|
+
query += " AND (t.source_lang = ? OR (t.source_lang IS NULL AND tb.source_lang = ?) OR (t.source_lang IS NULL AND tb.source_lang IS NULL))"
|
|
22413
|
+
params.extend([target_lang_code, target_lang_code])
|
|
22414
|
+
|
|
22415
|
+
# Close UNION and limit
|
|
22416
|
+
query += ") combined LIMIT 30"
|
|
22110
22417
|
cursor.execute(query, params)
|
|
22111
22418
|
results = cursor.fetchall()
|
|
22112
22419
|
|
|
22113
22420
|
for row in results:
|
|
22114
|
-
# Uniform access
|
|
22421
|
+
# Uniform access (columns are already swapped for reverse matches)
|
|
22115
22422
|
source_term = row[1] if isinstance(row, tuple) else row['source_term']
|
|
22116
22423
|
target_term = row[2] if isinstance(row, tuple) else row['target_term']
|
|
22424
|
+
match_direction = row[13] if isinstance(row, tuple) else row.get('match_direction', 'source')
|
|
22117
22425
|
if not source_term or not target_term:
|
|
22118
22426
|
continue
|
|
22119
22427
|
|
|
@@ -22137,8 +22445,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
22137
22445
|
existing = matches.get(source_term.strip())
|
|
22138
22446
|
# Deduplicate: keep numerically lowest ranking (highest priority)
|
|
22139
22447
|
# For project termbases, ranking is None so they always win
|
|
22448
|
+
# Also prefer 'source' matches over 'target' matches when equal
|
|
22140
22449
|
if existing:
|
|
22141
22450
|
existing_ranking = existing.get('ranking', None)
|
|
22451
|
+
existing_direction = existing.get('match_direction', 'source')
|
|
22142
22452
|
if is_project_tb:
|
|
22143
22453
|
# Project termbase always wins
|
|
22144
22454
|
pass
|
|
@@ -22147,8 +22457,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
22147
22457
|
continue
|
|
22148
22458
|
elif existing_ranking is not None and ranking is not None:
|
|
22149
22459
|
# Both have rankings, keep lower (higher priority)
|
|
22150
|
-
if existing_ranking
|
|
22460
|
+
if existing_ranking < ranking:
|
|
22151
22461
|
continue
|
|
22462
|
+
elif existing_ranking == ranking:
|
|
22463
|
+
# Same ranking: prefer source match over target match
|
|
22464
|
+
if existing_direction == 'source' and match_direction == 'target':
|
|
22465
|
+
continue
|
|
22152
22466
|
|
|
22153
22467
|
matches[source_term.strip()] = {
|
|
22154
22468
|
'translation': target_term.strip(),
|
|
@@ -22163,15 +22477,18 @@ class SupervertalerQt(QMainWindow):
|
|
|
22163
22477
|
'forbidden': forbidden or False,
|
|
22164
22478
|
'is_project_termbase': bool(is_project_tb),
|
|
22165
22479
|
'termbase_name': termbase_name or '',
|
|
22166
|
-
'target_synonyms': [] # Will be populated below
|
|
22480
|
+
'target_synonyms': [], # Will be populated below
|
|
22481
|
+
'match_direction': match_direction # Track if this was a reverse match
|
|
22167
22482
|
}
|
|
22168
|
-
|
|
22483
|
+
|
|
22169
22484
|
# Fetch synonyms for this term
|
|
22485
|
+
# For reverse matches, fetch 'source' synonyms since they become targets
|
|
22170
22486
|
try:
|
|
22487
|
+
synonym_lang = 'source' if match_direction == 'target' else 'target'
|
|
22171
22488
|
cursor.execute("""
|
|
22172
|
-
SELECT synonym_text FROM termbase_synonyms
|
|
22173
|
-
WHERE term_id = ? AND language =
|
|
22174
|
-
""", (term_id,))
|
|
22489
|
+
SELECT synonym_text FROM termbase_synonyms
|
|
22490
|
+
WHERE term_id = ? AND language = ? AND forbidden = 0
|
|
22491
|
+
""", (term_id, synonym_lang))
|
|
22175
22492
|
synonym_rows = cursor.fetchall()
|
|
22176
22493
|
for syn_row in synonym_rows:
|
|
22177
22494
|
synonym = syn_row[0] if isinstance(syn_row, tuple) else syn_row['synonym_text']
|
modules/database_manager.py
CHANGED
|
@@ -1477,120 +1477,225 @@ class DatabaseManager:
|
|
|
1477
1477
|
# TODO: Implement in Phase 3
|
|
1478
1478
|
pass
|
|
1479
1479
|
|
|
1480
|
-
def search_termbases(self, search_term: str, source_lang: str = None,
|
|
1480
|
+
def search_termbases(self, search_term: str, source_lang: str = None,
|
|
1481
1481
|
target_lang: str = None, project_id: str = None,
|
|
1482
|
-
min_length: int = 0) -> List[Dict]:
|
|
1482
|
+
min_length: int = 0, bidirectional: bool = True) -> List[Dict]:
|
|
1483
1483
|
"""
|
|
1484
|
-
Search termbases for matching
|
|
1485
|
-
|
|
1484
|
+
Search termbases for matching terms (bidirectional by default)
|
|
1485
|
+
|
|
1486
1486
|
Args:
|
|
1487
|
-
search_term:
|
|
1487
|
+
search_term: Term to search for
|
|
1488
1488
|
source_lang: Filter by source language (optional)
|
|
1489
1489
|
target_lang: Filter by target language (optional)
|
|
1490
1490
|
project_id: Filter by project (optional)
|
|
1491
1491
|
min_length: Minimum term length to return
|
|
1492
|
-
|
|
1492
|
+
bidirectional: If True, also search target_term and swap results (default True)
|
|
1493
|
+
|
|
1493
1494
|
Returns:
|
|
1494
1495
|
List of termbase hits, sorted by priority (lower = higher priority)
|
|
1496
|
+
Each result includes 'match_direction' ('source' or 'target') indicating
|
|
1497
|
+
which column matched. For 'target' matches, source_term and target_term
|
|
1498
|
+
are swapped so results are always oriented correctly for the current project.
|
|
1495
1499
|
"""
|
|
1496
1500
|
# Build query with filters - include termbase name and ranking via JOIN
|
|
1497
1501
|
# Note: termbase_id is stored as TEXT in termbase_terms but INTEGER in termbases
|
|
1498
1502
|
# Use CAST to ensure proper comparison
|
|
1499
1503
|
# IMPORTANT: Join with termbase_activation to get the ACTUAL priority for this project
|
|
1500
1504
|
# CRITICAL FIX: Also match when search_term starts with the glossary term
|
|
1501
|
-
# This handles cases like searching for "ca." when glossary has "ca."
|
|
1505
|
+
# This handles cases like searching for "ca." when glossary has "ca."
|
|
1502
1506
|
# AND searching for "ca" when glossary has "ca."
|
|
1503
1507
|
# We also strip trailing punctuation from glossary terms for comparison
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1508
|
+
|
|
1509
|
+
# Build matching conditions for a given column
|
|
1510
|
+
def build_match_conditions(column: str) -> str:
|
|
1511
|
+
return f"""(
|
|
1512
|
+
LOWER(t.{column}) = LOWER(?) OR
|
|
1513
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1514
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1515
|
+
LOWER(t.{column}) LIKE LOWER(?) OR
|
|
1516
|
+
LOWER(RTRIM(t.{column}, '.!?,;:')) = LOWER(?) OR
|
|
1517
|
+
LOWER(?) LIKE LOWER(t.{column}) || '%' OR
|
|
1518
|
+
LOWER(?) = LOWER(RTRIM(t.{column}, '.!?,;:'))
|
|
1519
|
+
)"""
|
|
1520
|
+
|
|
1521
|
+
# Build match params for one direction
|
|
1522
|
+
def build_match_params() -> list:
|
|
1523
|
+
return [
|
|
1524
|
+
search_term,
|
|
1525
|
+
f"{search_term} %",
|
|
1526
|
+
f"% {search_term}",
|
|
1527
|
+
f"% {search_term} %",
|
|
1528
|
+
search_term, # For RTRIM comparison
|
|
1529
|
+
search_term, # For reverse LIKE
|
|
1530
|
+
search_term # For reverse RTRIM comparison
|
|
1531
|
+
]
|
|
1532
|
+
|
|
1533
|
+
# Matching patterns:
|
|
1534
|
+
# 1. Exact match: column = search_term
|
|
1535
|
+
# 2. Glossary term starts with search: column LIKE "search_term %"
|
|
1536
|
+
# 3. Glossary term ends with search: column LIKE "% search_term"
|
|
1537
|
+
# 4. Glossary term contains search: column LIKE "% search_term %"
|
|
1538
|
+
# 5. Glossary term (stripped) = search_term: RTRIM(column) = search_term (handles "ca." = "ca")
|
|
1539
|
+
# 6. Search starts with glossary term: search_term LIKE column || '%'
|
|
1540
|
+
# 7. Search = glossary term stripped: search_term = RTRIM(column)
|
|
1541
|
+
|
|
1542
|
+
# Base SELECT for forward matches (source_term matches)
|
|
1543
|
+
base_select_forward = """
|
|
1544
|
+
SELECT
|
|
1545
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
1507
1546
|
t.forbidden, t.source_lang, t.target_lang, t.definition, t.domain,
|
|
1508
1547
|
t.notes, t.project, t.client,
|
|
1509
1548
|
tb.name as termbase_name,
|
|
1510
1549
|
tb.source_lang as termbase_source_lang,
|
|
1511
1550
|
tb.target_lang as termbase_target_lang,
|
|
1512
1551
|
tb.is_project_termbase,
|
|
1513
|
-
COALESCE(ta.priority, tb.ranking) as ranking
|
|
1552
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
1553
|
+
'source' as match_direction
|
|
1514
1554
|
FROM termbase_terms t
|
|
1515
1555
|
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
1516
1556
|
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
1517
|
-
WHERE
|
|
1518
|
-
LOWER(t.source_term) = LOWER(?) OR
|
|
1519
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1520
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1521
|
-
LOWER(t.source_term) LIKE LOWER(?) OR
|
|
1522
|
-
LOWER(RTRIM(t.source_term, '.!?,;:')) = LOWER(?) OR
|
|
1523
|
-
LOWER(?) LIKE LOWER(t.source_term) || '%' OR
|
|
1524
|
-
LOWER(?) = LOWER(RTRIM(t.source_term, '.!?,;:'))
|
|
1525
|
-
)
|
|
1557
|
+
WHERE {match_conditions}
|
|
1526
1558
|
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
1527
|
-
"""
|
|
1528
|
-
|
|
1529
|
-
#
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1559
|
+
""".format(match_conditions=build_match_conditions('source_term'))
|
|
1560
|
+
|
|
1561
|
+
# Base SELECT for reverse matches (target_term matches) - swap source/target in output
|
|
1562
|
+
base_select_reverse = """
|
|
1563
|
+
SELECT
|
|
1564
|
+
t.id, t.target_term as source_term, t.source_term as target_term,
|
|
1565
|
+
t.termbase_id, t.priority,
|
|
1566
|
+
t.forbidden, t.target_lang as source_lang, t.source_lang as target_lang,
|
|
1567
|
+
t.definition, t.domain,
|
|
1568
|
+
t.notes, t.project, t.client,
|
|
1569
|
+
tb.name as termbase_name,
|
|
1570
|
+
tb.target_lang as termbase_source_lang,
|
|
1571
|
+
tb.source_lang as termbase_target_lang,
|
|
1572
|
+
tb.is_project_termbase,
|
|
1573
|
+
COALESCE(ta.priority, tb.ranking) as ranking,
|
|
1574
|
+
'target' as match_direction
|
|
1575
|
+
FROM termbase_terms t
|
|
1576
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
1577
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id AND ta.project_id = ? AND ta.is_active = 1
|
|
1578
|
+
WHERE {match_conditions}
|
|
1579
|
+
AND (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
1580
|
+
""".format(match_conditions=build_match_conditions('target_term'))
|
|
1581
|
+
|
|
1582
|
+
# Build params
|
|
1583
|
+
project_param = project_id if project_id else 0
|
|
1584
|
+
forward_params = [project_param] + build_match_params()
|
|
1585
|
+
reverse_params = [project_param] + build_match_params()
|
|
1586
|
+
|
|
1587
|
+
# Build language filter conditions
|
|
1588
|
+
lang_conditions_forward = ""
|
|
1589
|
+
lang_conditions_reverse = ""
|
|
1590
|
+
lang_params_forward = []
|
|
1591
|
+
lang_params_reverse = []
|
|
1592
|
+
|
|
1548
1593
|
if source_lang:
|
|
1549
|
-
|
|
1550
|
-
|
|
1594
|
+
# For forward: filter on source_lang
|
|
1595
|
+
lang_conditions_forward += """ AND (
|
|
1596
|
+
t.source_lang = ? OR
|
|
1551
1597
|
(t.source_lang IS NULL AND tb.source_lang = ?) OR
|
|
1552
1598
|
(t.source_lang IS NULL AND tb.source_lang IS NULL)
|
|
1553
1599
|
)"""
|
|
1554
|
-
|
|
1555
|
-
|
|
1600
|
+
lang_params_forward.extend([source_lang, source_lang])
|
|
1601
|
+
# For reverse: source_lang becomes target_lang (swapped)
|
|
1602
|
+
lang_conditions_reverse += """ AND (
|
|
1603
|
+
t.target_lang = ? OR
|
|
1604
|
+
(t.target_lang IS NULL AND tb.target_lang = ?) OR
|
|
1605
|
+
(t.target_lang IS NULL AND tb.target_lang IS NULL)
|
|
1606
|
+
)"""
|
|
1607
|
+
lang_params_reverse.extend([source_lang, source_lang])
|
|
1608
|
+
|
|
1556
1609
|
if target_lang:
|
|
1557
|
-
|
|
1558
|
-
|
|
1610
|
+
# For forward: filter on target_lang
|
|
1611
|
+
lang_conditions_forward += """ AND (
|
|
1612
|
+
t.target_lang = ? OR
|
|
1559
1613
|
(t.target_lang IS NULL AND tb.target_lang = ?) OR
|
|
1560
1614
|
(t.target_lang IS NULL AND tb.target_lang IS NULL)
|
|
1561
1615
|
)"""
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1616
|
+
lang_params_forward.extend([target_lang, target_lang])
|
|
1617
|
+
# For reverse: target_lang becomes source_lang (swapped)
|
|
1618
|
+
lang_conditions_reverse += """ AND (
|
|
1619
|
+
t.source_lang = ? OR
|
|
1620
|
+
(t.source_lang IS NULL AND tb.source_lang = ?) OR
|
|
1621
|
+
(t.source_lang IS NULL AND tb.source_lang IS NULL)
|
|
1622
|
+
)"""
|
|
1623
|
+
lang_params_reverse.extend([target_lang, target_lang])
|
|
1624
|
+
|
|
1625
|
+
# Project filter conditions
|
|
1626
|
+
project_conditions = ""
|
|
1627
|
+
project_params = []
|
|
1565
1628
|
if project_id:
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1629
|
+
project_conditions = " AND (t.project_id = ? OR t.project_id IS NULL)"
|
|
1630
|
+
project_params = [project_id]
|
|
1631
|
+
|
|
1632
|
+
# Min length conditions
|
|
1633
|
+
min_len_forward = ""
|
|
1634
|
+
min_len_reverse = ""
|
|
1569
1635
|
if min_length > 0:
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
#
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1636
|
+
min_len_forward = f" AND LENGTH(t.source_term) >= {min_length}"
|
|
1637
|
+
min_len_reverse = f" AND LENGTH(t.target_term) >= {min_length}"
|
|
1638
|
+
|
|
1639
|
+
# Build forward query
|
|
1640
|
+
forward_query = base_select_forward + lang_conditions_forward + project_conditions + min_len_forward
|
|
1641
|
+
forward_params.extend(lang_params_forward)
|
|
1642
|
+
forward_params.extend(project_params)
|
|
1643
|
+
|
|
1644
|
+
if bidirectional:
|
|
1645
|
+
# Build reverse query
|
|
1646
|
+
reverse_query = base_select_reverse + lang_conditions_reverse + project_conditions + min_len_reverse
|
|
1647
|
+
reverse_params.extend(lang_params_reverse)
|
|
1648
|
+
reverse_params.extend(project_params)
|
|
1649
|
+
|
|
1650
|
+
# Combine with UNION and sort
|
|
1651
|
+
query = f"""
|
|
1652
|
+
SELECT * FROM (
|
|
1653
|
+
{forward_query}
|
|
1654
|
+
UNION ALL
|
|
1655
|
+
{reverse_query}
|
|
1656
|
+
) combined
|
|
1657
|
+
ORDER BY COALESCE(ranking, -1) ASC, source_term ASC
|
|
1658
|
+
"""
|
|
1659
|
+
params = forward_params + reverse_params
|
|
1660
|
+
else:
|
|
1661
|
+
# Original forward-only behavior
|
|
1662
|
+
query = forward_query + " ORDER BY COALESCE(ranking, -1) ASC, source_term ASC"
|
|
1663
|
+
params = forward_params
|
|
1664
|
+
|
|
1577
1665
|
self.cursor.execute(query, params)
|
|
1578
1666
|
results = []
|
|
1667
|
+
seen_combinations = set() # Track (source_term, target_term, termbase_id) to avoid duplicates
|
|
1668
|
+
|
|
1579
1669
|
for row in self.cursor.fetchall():
|
|
1580
1670
|
result_dict = dict(row)
|
|
1671
|
+
|
|
1672
|
+
# Deduplicate: same term pair from same termbase should only appear once
|
|
1673
|
+
# Prefer 'source' match over 'target' match
|
|
1674
|
+
combo_key = (
|
|
1675
|
+
result_dict.get('source_term', '').lower(),
|
|
1676
|
+
result_dict.get('target_term', '').lower(),
|
|
1677
|
+
result_dict.get('termbase_id')
|
|
1678
|
+
)
|
|
1679
|
+
if combo_key in seen_combinations:
|
|
1680
|
+
continue
|
|
1681
|
+
seen_combinations.add(combo_key)
|
|
1682
|
+
|
|
1581
1683
|
# SQLite stores booleans as 0/1, explicitly convert to Python bool
|
|
1582
1684
|
if 'is_project_termbase' in result_dict:
|
|
1583
1685
|
result_dict['is_project_termbase'] = bool(result_dict['is_project_termbase'])
|
|
1584
|
-
|
|
1686
|
+
|
|
1585
1687
|
# Fetch target synonyms for this term and include them in the result
|
|
1586
1688
|
term_id = result_dict.get('id')
|
|
1689
|
+
match_direction = result_dict.get('match_direction', 'source')
|
|
1587
1690
|
if term_id:
|
|
1588
1691
|
try:
|
|
1692
|
+
# For reverse matches, fetch 'source' synonyms since they become targets
|
|
1693
|
+
synonym_lang = 'source' if match_direction == 'target' else 'target'
|
|
1589
1694
|
self.cursor.execute("""
|
|
1590
1695
|
SELECT synonym_text, forbidden FROM termbase_synonyms
|
|
1591
|
-
WHERE term_id = ? AND language =
|
|
1696
|
+
WHERE term_id = ? AND language = ?
|
|
1592
1697
|
ORDER BY display_order ASC
|
|
1593
|
-
""", (term_id,))
|
|
1698
|
+
""", (term_id, synonym_lang))
|
|
1594
1699
|
synonyms = []
|
|
1595
1700
|
for syn_row in self.cursor.fetchall():
|
|
1596
1701
|
syn_text = syn_row[0]
|
|
@@ -1600,7 +1705,7 @@ class DatabaseManager:
|
|
|
1600
1705
|
result_dict['target_synonyms'] = synonyms
|
|
1601
1706
|
except Exception:
|
|
1602
1707
|
result_dict['target_synonyms'] = []
|
|
1603
|
-
|
|
1708
|
+
|
|
1604
1709
|
results.append(result_dict)
|
|
1605
1710
|
return results
|
|
1606
1711
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supervertaler
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.177b0
|
|
4
4
|
Summary: Professional AI-enhanced translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
|
|
5
5
|
Home-page: https://supervertaler.com
|
|
6
6
|
Author: Michael Beijer
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Supervertaler.py,sha256=
|
|
1
|
+
Supervertaler.py,sha256=4cGlqNtBnj95ts-qLw6W-aNSJqjqkeFDTCbBAWkx5cI,2322643
|
|
2
2
|
modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
|
|
3
3
|
modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
|
|
4
4
|
modules/ai_attachment_manager.py,sha256=juZlrW3UPkIkcnj0SREgOQkQROLf0fcu3ShZcKXMxsI,11361
|
|
@@ -6,7 +6,7 @@ modules/ai_file_viewer_dialog.py,sha256=lKKqUUlOEVgHmmu6aRxqH7P6ds-7dRLk4ltDyjCw
|
|
|
6
6
|
modules/autofingers_engine.py,sha256=eJ7tBi7YJvTToe5hYTfnyGXB-qme_cHrOPZibaoR2Xw,17061
|
|
7
7
|
modules/cafetran_docx_handler.py,sha256=_F7Jh0WPVaDnMhdxEsVSXuD1fN9r-S_V6i0gr86Pdfc,14076
|
|
8
8
|
modules/config_manager.py,sha256=MkPY3xVFgFDkcwewLREg4BfyKueO0OJkT1cTLxehcjM,17894
|
|
9
|
-
modules/database_manager.py,sha256=
|
|
9
|
+
modules/database_manager.py,sha256=yNtaJNAKtICBBSc5iyhIufzDn25k7OqkOuFeojmWuM4,87319
|
|
10
10
|
modules/database_migrations.py,sha256=tndJ4wV_2JBfPggMgO1tQRwdfRFZ9zwvADllCZE9CCk,15663
|
|
11
11
|
modules/dejavurtf_handler.py,sha256=8NZPPYtHga40SZCypHjPoJPmZTvm9rD-eEUUab7mjtg,28156
|
|
12
12
|
modules/document_analyzer.py,sha256=t1rVvqLaTcpQTEja228C7zZnh8dXshK4wA9t1E9aGVk,19524
|
|
@@ -77,9 +77,9 @@ modules/unified_prompt_manager_qt.py,sha256=U89UFGG-M7BLetoaLAlma0x-n8SIyx682DhS
|
|
|
77
77
|
modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
|
|
78
78
|
modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
|
|
79
79
|
modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
|
|
80
|
-
supervertaler-1.9.
|
|
81
|
-
supervertaler-1.9.
|
|
82
|
-
supervertaler-1.9.
|
|
83
|
-
supervertaler-1.9.
|
|
84
|
-
supervertaler-1.9.
|
|
85
|
-
supervertaler-1.9.
|
|
80
|
+
supervertaler-1.9.177b0.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
|
|
81
|
+
supervertaler-1.9.177b0.dist-info/METADATA,sha256=KeUutsoVb7QmekOgaHFTiDxTdrgrJLs0yLcThPa0lAQ,5727
|
|
82
|
+
supervertaler-1.9.177b0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
83
|
+
supervertaler-1.9.177b0.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
|
|
84
|
+
supervertaler-1.9.177b0.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
|
|
85
|
+
supervertaler-1.9.177b0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|