supervertaler 1.9.180__py3-none-any.whl → 1.9.183__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 +783 -434
- modules/extract_tm.py +518 -0
- modules/project_tm.py +320 -0
- modules/termbase_manager.py +0 -1
- modules/termview_widget.py +22 -14
- modules/translation_memory.py +3 -12
- modules/translation_results_panel.py +0 -7
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/METADATA +1 -1
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/RECORD +13 -11
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.180.dist-info → supervertaler-1.9.183.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -32,9 +32,9 @@ License: MIT
|
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
# Version Information.
|
|
35
|
-
__version__ = "1.9.
|
|
35
|
+
__version__ = "1.9.183"
|
|
36
36
|
__phase__ = "0.9"
|
|
37
|
-
__release_date__ = "2026-01-
|
|
37
|
+
__release_date__ = "2026-01-31"
|
|
38
38
|
__edition__ = "Qt"
|
|
39
39
|
|
|
40
40
|
import sys
|
|
@@ -1659,16 +1659,12 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
1659
1659
|
matches_dict: Dictionary of {term: {'translation': str, 'priority': int}} or {term: str}
|
|
1660
1660
|
"""
|
|
1661
1661
|
from PyQt6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont
|
|
1662
|
-
|
|
1663
|
-
print(f"[HIGHLIGHT DEBUG] highlight_termbase_matches called with {len(matches_dict) if matches_dict else 0} matches")
|
|
1664
|
-
|
|
1662
|
+
|
|
1665
1663
|
# Get the document and create a cursor
|
|
1666
1664
|
doc = self.document()
|
|
1667
1665
|
text = self.toPlainText()
|
|
1668
1666
|
text_lower = text.lower()
|
|
1669
1667
|
|
|
1670
|
-
print(f"[HIGHLIGHT DEBUG] Widget text length: {len(text)}, text preview: {text[:60]}...")
|
|
1671
|
-
|
|
1672
1668
|
# IMPORTANT: Always clear all previous formatting first to prevent inconsistent highlighting
|
|
1673
1669
|
cursor = QTextCursor(doc)
|
|
1674
1670
|
cursor.select(QTextCursor.SelectionType.Document)
|
|
@@ -1677,7 +1673,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
1677
1673
|
|
|
1678
1674
|
# If no matches, we're done (highlighting has been cleared)
|
|
1679
1675
|
if not matches_dict:
|
|
1680
|
-
print(f"[HIGHLIGHT DEBUG] No matches, returning after clear")
|
|
1681
1676
|
return
|
|
1682
1677
|
|
|
1683
1678
|
# Get highlight style from main window settings
|
|
@@ -1695,9 +1690,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
1695
1690
|
dotted_color = settings.get('termbase_dotted_color', '#808080')
|
|
1696
1691
|
break
|
|
1697
1692
|
parent = parent.parent() if hasattr(parent, 'parent') else None
|
|
1698
|
-
|
|
1699
|
-
print(f"[HIGHLIGHT DEBUG] Using style: {highlight_style}")
|
|
1700
|
-
|
|
1693
|
+
|
|
1701
1694
|
# Sort matches by source term length (longest first) to avoid partial matches
|
|
1702
1695
|
# Since dict keys are now term_ids, we need to extract source terms first
|
|
1703
1696
|
term_entries = []
|
|
@@ -1706,11 +1699,7 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
1706
1699
|
source_term = match_info.get('source', '')
|
|
1707
1700
|
if source_term:
|
|
1708
1701
|
term_entries.append((source_term, term_id, match_info))
|
|
1709
|
-
|
|
1710
|
-
print(f"[HIGHLIGHT DEBUG] Built {len(term_entries)} term entries from matches")
|
|
1711
|
-
if term_entries:
|
|
1712
|
-
print(f"[HIGHLIGHT DEBUG] First few terms to search: {[t[0] for t in term_entries[:3]]}")
|
|
1713
|
-
|
|
1702
|
+
|
|
1714
1703
|
# Sort by source term length (longest first)
|
|
1715
1704
|
term_entries.sort(key=lambda x: len(x[0]), reverse=True)
|
|
1716
1705
|
|
|
@@ -1835,8 +1824,6 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
1835
1824
|
highlighted_ranges.append((idx, end_idx))
|
|
1836
1825
|
|
|
1837
1826
|
start = end_idx
|
|
1838
|
-
|
|
1839
|
-
print(f"[HIGHLIGHT DEBUG] Applied formatting to {found_count} term occurrences in text")
|
|
1840
1827
|
|
|
1841
1828
|
def highlight_non_translatables(self, nt_matches: list, highlighted_ranges: list = None):
|
|
1842
1829
|
"""
|
|
@@ -6224,6 +6211,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
6224
6211
|
self.termbase_cache_lock = threading.Lock() # Thread-safe cache access
|
|
6225
6212
|
self.termbase_batch_worker_thread = None # Background worker thread
|
|
6226
6213
|
self.termbase_batch_stop_event = threading.Event() # Signal to stop background worker
|
|
6214
|
+
|
|
6215
|
+
# In-memory termbase index for instant lookups (v1.9.182)
|
|
6216
|
+
# Loaded once on project load, contains ALL terms from activated termbases
|
|
6217
|
+
# Structure: list of term dicts with pre-compiled regex patterns
|
|
6218
|
+
self.termbase_index = []
|
|
6219
|
+
self.termbase_index_lock = threading.Lock()
|
|
6227
6220
|
|
|
6228
6221
|
# TM/MT/LLM prefetch cache for instant segment switching (like memoQ)
|
|
6229
6222
|
# Maps segment ID → {"TM": [...], "MT": [...], "LLM": [...]}
|
|
@@ -6237,9 +6230,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
6237
6230
|
self.idle_prefetch_timer = None # QTimer for triggering prefetch after typing pause
|
|
6238
6231
|
self.idle_prefetch_delay_ms = 1500 # Start prefetch 1.5s after user stops typing
|
|
6239
6232
|
|
|
6240
|
-
#
|
|
6233
|
+
# Cache kill switch for performance testing
|
|
6241
6234
|
# When True, all caches are bypassed - direct lookups every time
|
|
6242
|
-
self.disable_all_caches =
|
|
6235
|
+
self.disable_all_caches = False # v1.9.183: Default to False (caches ENABLED)
|
|
6243
6236
|
|
|
6244
6237
|
# Undo/Redo stack for grid edits
|
|
6245
6238
|
self.undo_stack = [] # List of (segment_id, old_target, new_target, old_status, new_status)
|
|
@@ -10335,12 +10328,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
10335
10328
|
|
|
10336
10329
|
# Superdocs removed (online GitBook will be used instead)
|
|
10337
10330
|
|
|
10338
|
-
print("[DEBUG] About to create SuperlookupTab...")
|
|
10339
10331
|
lookup_tab = SuperlookupTab(self, user_data_path=self.user_data_path)
|
|
10340
|
-
print("[DEBUG] SuperlookupTab created successfully")
|
|
10341
10332
|
self.lookup_tab = lookup_tab # Store reference for later use
|
|
10342
10333
|
modules_tabs.addTab(lookup_tab, "🔍 Superlookup")
|
|
10343
|
-
print("[DEBUG] Superlookup tab added to modules_tabs")
|
|
10344
10334
|
|
|
10345
10335
|
# Supervoice - Voice Commands & Dictation
|
|
10346
10336
|
supervoice_tab = self._create_voice_dictation_settings_tab()
|
|
@@ -12202,27 +12192,130 @@ class SupervertalerQt(QMainWindow):
|
|
|
12202
12192
|
f"Error testing segmentation:\n\n{e}"
|
|
12203
12193
|
)
|
|
12204
12194
|
|
|
12205
|
-
def _update_both_termviews(self, source_text, termbase_list, nt_matches):
|
|
12195
|
+
def _update_both_termviews(self, source_text, termbase_list, nt_matches, status_hint=None):
|
|
12206
12196
|
"""Update all three Termview instances with the same data.
|
|
12207
|
-
|
|
12197
|
+
|
|
12208
12198
|
Termview locations:
|
|
12209
12199
|
1. Under grid (collapsible via View menu)
|
|
12210
12200
|
2. Match Panel tab (top section)
|
|
12201
|
+
|
|
12202
|
+
Args:
|
|
12203
|
+
source_text: The source text for the current segment
|
|
12204
|
+
termbase_list: List of termbase match dictionaries
|
|
12205
|
+
nt_matches: List of NT (Never Translate) matches
|
|
12206
|
+
status_hint: Optional hint for display when no matches:
|
|
12207
|
+
'no_termbases_activated' - no glossaries activated for project
|
|
12208
|
+
'wrong_language' - activated glossaries don't match project language
|
|
12211
12209
|
"""
|
|
12212
12210
|
# Update left Termview (under grid)
|
|
12213
12211
|
if hasattr(self, 'termview_widget') and self.termview_widget:
|
|
12214
12212
|
try:
|
|
12215
|
-
self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches)
|
|
12213
|
+
self.termview_widget.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
|
|
12216
12214
|
except Exception as e:
|
|
12217
12215
|
self.log(f"Error updating left termview: {e}")
|
|
12218
|
-
|
|
12216
|
+
|
|
12219
12217
|
# Update Match Panel Termview
|
|
12220
12218
|
if hasattr(self, 'termview_widget_match') and self.termview_widget_match:
|
|
12221
12219
|
try:
|
|
12222
|
-
self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches)
|
|
12220
|
+
self.termview_widget_match.update_with_matches(source_text, termbase_list, nt_matches, status_hint)
|
|
12223
12221
|
except Exception as e:
|
|
12224
12222
|
self.log(f"Error updating Match Panel termview: {e}")
|
|
12225
|
-
|
|
12223
|
+
|
|
12224
|
+
def _update_termview_for_segment(self, segment):
|
|
12225
|
+
"""Explicitly update termview for a segment (v1.9.182).
|
|
12226
|
+
|
|
12227
|
+
This is called directly from Ctrl+Enter navigation to ensure
|
|
12228
|
+
the termview updates immediately, bypassing the deferred timer approach.
|
|
12229
|
+
"""
|
|
12230
|
+
if not segment or not hasattr(self, 'termview_widget'):
|
|
12231
|
+
return
|
|
12232
|
+
|
|
12233
|
+
try:
|
|
12234
|
+
# Use in-memory index for fast lookup
|
|
12235
|
+
stored_matches = self.find_termbase_matches_in_source(segment.source)
|
|
12236
|
+
|
|
12237
|
+
# Convert dict format to list format for termview
|
|
12238
|
+
termbase_matches = [
|
|
12239
|
+
{
|
|
12240
|
+
'source_term': match_data.get('source', ''),
|
|
12241
|
+
'target_term': match_data.get('translation', ''),
|
|
12242
|
+
'termbase_name': match_data.get('termbase_name', ''),
|
|
12243
|
+
'ranking': match_data.get('ranking', 99),
|
|
12244
|
+
'is_project_termbase': match_data.get('is_project_termbase', False),
|
|
12245
|
+
'term_id': match_data.get('term_id'),
|
|
12246
|
+
'termbase_id': match_data.get('termbase_id'),
|
|
12247
|
+
'notes': match_data.get('notes', '')
|
|
12248
|
+
}
|
|
12249
|
+
for match_data in stored_matches.values()
|
|
12250
|
+
] if stored_matches else []
|
|
12251
|
+
|
|
12252
|
+
# Get NT matches
|
|
12253
|
+
nt_matches = self.find_nt_matches_in_source(segment.source)
|
|
12254
|
+
|
|
12255
|
+
# Get status hint
|
|
12256
|
+
status_hint = self._get_termbase_status_hint()
|
|
12257
|
+
|
|
12258
|
+
# Update both Termview widgets
|
|
12259
|
+
self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
|
|
12260
|
+
|
|
12261
|
+
except Exception as e:
|
|
12262
|
+
self.log(f"Error in _update_termview_for_segment: {e}")
|
|
12263
|
+
|
|
12264
|
+
def _get_termbase_status_hint(self) -> str:
|
|
12265
|
+
"""Check termbase activation status and return appropriate hint.
|
|
12266
|
+
|
|
12267
|
+
Returns:
|
|
12268
|
+
'no_termbases_activated' - if no glossaries are activated for this project
|
|
12269
|
+
'wrong_language' - if activated glossaries don't match project language pair
|
|
12270
|
+
None - if everything is correctly configured
|
|
12271
|
+
"""
|
|
12272
|
+
if not self.current_project:
|
|
12273
|
+
return None
|
|
12274
|
+
|
|
12275
|
+
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
12276
|
+
if not project_id:
|
|
12277
|
+
return None
|
|
12278
|
+
|
|
12279
|
+
# Check if termbase manager is available
|
|
12280
|
+
if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
|
|
12281
|
+
return None
|
|
12282
|
+
|
|
12283
|
+
try:
|
|
12284
|
+
# Get active termbase IDs for this project
|
|
12285
|
+
active_tb_ids = self.termbase_mgr.get_active_termbase_ids(project_id)
|
|
12286
|
+
|
|
12287
|
+
# Check if no termbases are activated
|
|
12288
|
+
if not active_tb_ids or len(active_tb_ids) == 0:
|
|
12289
|
+
return 'no_termbases_activated'
|
|
12290
|
+
|
|
12291
|
+
# Check if any activated termbases match the project's language pair
|
|
12292
|
+
project_source = (self.current_project.source_lang or '').lower()
|
|
12293
|
+
project_target = (self.current_project.target_lang or '').lower()
|
|
12294
|
+
|
|
12295
|
+
# Get all termbases and check language pairs
|
|
12296
|
+
all_termbases = self.termbase_mgr.get_all_termbases()
|
|
12297
|
+
has_matching_language = False
|
|
12298
|
+
|
|
12299
|
+
for tb in all_termbases:
|
|
12300
|
+
if tb['id'] in active_tb_ids:
|
|
12301
|
+
tb_source = (tb.get('source_lang') or '').lower()
|
|
12302
|
+
tb_target = (tb.get('target_lang') or '').lower()
|
|
12303
|
+
# Match if: no language set, or languages match (bidirectional)
|
|
12304
|
+
if (not tb_source and not tb_target) or \
|
|
12305
|
+
(tb_source == project_source and tb_target == project_target) or \
|
|
12306
|
+
(tb_source == project_target and tb_target == project_source):
|
|
12307
|
+
has_matching_language = True
|
|
12308
|
+
break
|
|
12309
|
+
|
|
12310
|
+
if not has_matching_language:
|
|
12311
|
+
return 'wrong_language'
|
|
12312
|
+
|
|
12313
|
+
return None # All good
|
|
12314
|
+
|
|
12315
|
+
except Exception as e:
|
|
12316
|
+
self.log(f"Error checking termbase status: {e}")
|
|
12317
|
+
return None
|
|
12318
|
+
|
|
12226
12319
|
def _refresh_termbase_display_for_current_segment(self):
|
|
12227
12320
|
"""Refresh only termbase/glossary display for the current segment.
|
|
12228
12321
|
|
|
@@ -12282,9 +12375,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
12282
12375
|
|
|
12283
12376
|
# Get NT matches
|
|
12284
12377
|
nt_matches = self.find_nt_matches_in_source(segment.source)
|
|
12285
|
-
|
|
12378
|
+
|
|
12379
|
+
# Get status hint for termbase activation
|
|
12380
|
+
status_hint = self._get_termbase_status_hint()
|
|
12381
|
+
|
|
12286
12382
|
# Update both Termview widgets (left and right)
|
|
12287
|
-
self._update_both_termviews(segment.source, termbase_list, nt_matches)
|
|
12383
|
+
self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
|
|
12288
12384
|
except Exception as e:
|
|
12289
12385
|
self.log(f"Error updating termview: {e}")
|
|
12290
12386
|
|
|
@@ -12752,6 +12848,39 @@ class SupervertalerQt(QMainWindow):
|
|
|
12752
12848
|
# Use term_id as key to avoid duplicates
|
|
12753
12849
|
self.termbase_cache[segment_id][term_id] = new_match
|
|
12754
12850
|
self.log(f"⚡ Added term directly to cache (instant update)")
|
|
12851
|
+
|
|
12852
|
+
# v1.9.182: Also add to in-memory termbase index for future lookups
|
|
12853
|
+
import re
|
|
12854
|
+
source_lower = source_text.lower().strip()
|
|
12855
|
+
try:
|
|
12856
|
+
if any(c in source_lower for c in '.%,/-'):
|
|
12857
|
+
pattern = re.compile(r'(?<!\w)' + re.escape(source_lower) + r'(?!\w)')
|
|
12858
|
+
else:
|
|
12859
|
+
pattern = re.compile(r'\b' + re.escape(source_lower) + r'\b')
|
|
12860
|
+
except re.error:
|
|
12861
|
+
pattern = None
|
|
12862
|
+
|
|
12863
|
+
index_entry = {
|
|
12864
|
+
'term_id': term_id,
|
|
12865
|
+
'source_term': source_text,
|
|
12866
|
+
'source_term_lower': source_lower,
|
|
12867
|
+
'target_term': target_text,
|
|
12868
|
+
'termbase_id': target_termbase['id'],
|
|
12869
|
+
'priority': 99,
|
|
12870
|
+
'domain': '',
|
|
12871
|
+
'notes': '',
|
|
12872
|
+
'project': '',
|
|
12873
|
+
'client': '',
|
|
12874
|
+
'forbidden': False,
|
|
12875
|
+
'is_project_termbase': False,
|
|
12876
|
+
'termbase_name': target_termbase['name'],
|
|
12877
|
+
'ranking': glossary_rank,
|
|
12878
|
+
'pattern': pattern,
|
|
12879
|
+
}
|
|
12880
|
+
with self.termbase_index_lock:
|
|
12881
|
+
self.termbase_index.append(index_entry)
|
|
12882
|
+
# Re-sort by length (longest first) for proper phrase matching
|
|
12883
|
+
self.termbase_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
|
|
12755
12884
|
|
|
12756
12885
|
# Update TermView widget with the new term
|
|
12757
12886
|
if hasattr(self, 'termview_widget') and self.termview_widget:
|
|
@@ -12776,9 +12905,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
12776
12905
|
|
|
12777
12906
|
# Get NT matches
|
|
12778
12907
|
nt_matches = self.find_nt_matches_in_source(segment.source)
|
|
12779
|
-
|
|
12908
|
+
|
|
12909
|
+
# Get status hint (although after adding a term, it should be fine)
|
|
12910
|
+
status_hint = self._get_termbase_status_hint()
|
|
12911
|
+
|
|
12780
12912
|
# Update both Termview widgets (left and right)
|
|
12781
|
-
self._update_both_termviews(segment.source, termbase_list, nt_matches)
|
|
12913
|
+
self._update_both_termviews(segment.source, termbase_list, nt_matches, status_hint)
|
|
12782
12914
|
self.log(f"✅ Both TermView widgets updated instantly with new term")
|
|
12783
12915
|
|
|
12784
12916
|
# Update source cell highlighting with updated cache
|
|
@@ -13634,15 +13766,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
13634
13766
|
# Use 0 (global) when no project is loaded - allows Superlookup to work
|
|
13635
13767
|
curr_proj = self.current_project if hasattr(self, 'current_project') else None
|
|
13636
13768
|
curr_proj_id = curr_proj.id if (curr_proj and hasattr(curr_proj, 'id')) else 0 # 0 = global
|
|
13637
|
-
|
|
13769
|
+
|
|
13638
13770
|
if checked:
|
|
13639
13771
|
termbase_mgr.activate_termbase(tb_id, curr_proj_id)
|
|
13640
13772
|
else:
|
|
13641
13773
|
termbase_mgr.deactivate_termbase(tb_id, curr_proj_id)
|
|
13642
|
-
|
|
13643
|
-
# Clear cache and
|
|
13774
|
+
|
|
13775
|
+
# Clear cache and rebuild in-memory index (v1.9.182)
|
|
13644
13776
|
with self.termbase_cache_lock:
|
|
13645
13777
|
self.termbase_cache.clear()
|
|
13778
|
+
self._build_termbase_index() # Rebuild index with new activation state
|
|
13646
13779
|
refresh_termbase_list()
|
|
13647
13780
|
|
|
13648
13781
|
read_checkbox.toggled.connect(on_read_toggle)
|
|
@@ -16971,7 +17104,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
16971
17104
|
|
|
16972
17105
|
# Cache kill switch
|
|
16973
17106
|
disable_cache_cb = CheckmarkCheckBox("Disable ALL caches (direct lookups every time)")
|
|
16974
|
-
disable_cache_cb.setChecked(general_settings.get('disable_all_caches',
|
|
17107
|
+
disable_cache_cb.setChecked(general_settings.get('disable_all_caches', False))
|
|
16975
17108
|
disable_cache_cb.setToolTip(
|
|
16976
17109
|
"When enabled, ALL caching is bypassed:\n"
|
|
16977
17110
|
"• Termbase cache\n"
|
|
@@ -19615,7 +19748,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
19615
19748
|
'results_compare_font_size': 9,
|
|
19616
19749
|
'autohotkey_path': ahk_path_edit.text().strip() if ahk_path_edit is not None else existing_settings.get('autohotkey_path', ''),
|
|
19617
19750
|
'enable_sound_effects': sound_effects_cb.isChecked() if sound_effects_cb is not None else existing_settings.get('enable_sound_effects', False),
|
|
19618
|
-
'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches',
|
|
19751
|
+
'disable_all_caches': disable_cache_cb.isChecked() if disable_cache_cb is not None else existing_settings.get('disable_all_caches', False)
|
|
19619
19752
|
}
|
|
19620
19753
|
|
|
19621
19754
|
# Keep a fast-access instance value
|
|
@@ -21558,36 +21691,35 @@ class SupervertalerQt(QMainWindow):
|
|
|
21558
21691
|
|
|
21559
21692
|
# Source language
|
|
21560
21693
|
source_lang_combo = QComboBox()
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
("
|
|
21564
|
-
("
|
|
21565
|
-
("
|
|
21566
|
-
("
|
|
21567
|
-
("
|
|
21568
|
-
("
|
|
21569
|
-
("
|
|
21570
|
-
("
|
|
21571
|
-
("
|
|
21694
|
+
# Full language list matching Settings → Language Pair (with ISO 639-1 codes)
|
|
21695
|
+
available_langs = [
|
|
21696
|
+
("Afrikaans", "af"), ("Albanian", "sq"), ("Arabic", "ar"), ("Armenian", "hy"),
|
|
21697
|
+
("Basque", "eu"), ("Bengali", "bn"), ("Bulgarian", "bg"), ("Catalan", "ca"),
|
|
21698
|
+
("Chinese (Simplified)", "zh-CN"), ("Chinese (Traditional)", "zh-TW"),
|
|
21699
|
+
("Croatian", "hr"), ("Czech", "cs"), ("Danish", "da"), ("Dutch", "nl"),
|
|
21700
|
+
("English", "en"), ("Estonian", "et"), ("Finnish", "fi"), ("French", "fr"),
|
|
21701
|
+
("Galician", "gl"), ("Georgian", "ka"), ("German", "de"), ("Greek", "el"),
|
|
21702
|
+
("Hebrew", "he"), ("Hindi", "hi"), ("Hungarian", "hu"), ("Icelandic", "is"),
|
|
21703
|
+
("Indonesian", "id"), ("Irish", "ga"), ("Italian", "it"), ("Japanese", "ja"),
|
|
21704
|
+
("Korean", "ko"), ("Latvian", "lv"), ("Lithuanian", "lt"), ("Macedonian", "mk"),
|
|
21705
|
+
("Malay", "ms"), ("Norwegian", "no"), ("Persian", "fa"), ("Polish", "pl"),
|
|
21706
|
+
("Portuguese", "pt"), ("Romanian", "ro"), ("Russian", "ru"), ("Serbian", "sr"),
|
|
21707
|
+
("Slovak", "sk"), ("Slovenian", "sl"), ("Spanish", "es"), ("Swahili", "sw"),
|
|
21708
|
+
("Swedish", "sv"), ("Thai", "th"), ("Turkish", "tr"), ("Ukrainian", "uk"),
|
|
21709
|
+
("Urdu", "ur"), ("Vietnamese", "vi"), ("Welsh", "cy"),
|
|
21572
21710
|
]
|
|
21573
|
-
for lang_name, lang_code in
|
|
21711
|
+
for lang_name, lang_code in available_langs:
|
|
21574
21712
|
source_lang_combo.addItem(lang_name, lang_code)
|
|
21575
21713
|
settings_layout.addRow("Source Language:", source_lang_combo)
|
|
21576
|
-
|
|
21714
|
+
|
|
21577
21715
|
# Target language
|
|
21578
21716
|
target_lang_combo = QComboBox()
|
|
21579
|
-
for lang_name, lang_code in
|
|
21717
|
+
for lang_name, lang_code in available_langs:
|
|
21580
21718
|
target_lang_combo.addItem(lang_name, lang_code)
|
|
21581
|
-
|
|
21582
|
-
# Set defaults based on global language settings
|
|
21583
|
-
|
|
21584
|
-
|
|
21585
|
-
if lang_name == self.source_language:
|
|
21586
|
-
source_lang_combo.setCurrentText(lang_name)
|
|
21587
|
-
if lang_name == self.target_language:
|
|
21588
|
-
target_lang_combo.setCurrentText(lang_name)
|
|
21589
|
-
except:
|
|
21590
|
-
target_lang_combo.setCurrentIndex(1) # Fallback to Dutch
|
|
21719
|
+
|
|
21720
|
+
# Set defaults based on global language settings
|
|
21721
|
+
source_lang_combo.setCurrentText(self.source_language)
|
|
21722
|
+
target_lang_combo.setCurrentText(self.target_language)
|
|
21591
21723
|
|
|
21592
21724
|
settings_layout.addRow("Target Language:", target_lang_combo)
|
|
21593
21725
|
|
|
@@ -21709,7 +21841,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
21709
21841
|
target_lang=target_lang,
|
|
21710
21842
|
segments=[]
|
|
21711
21843
|
)
|
|
21712
|
-
|
|
21844
|
+
|
|
21845
|
+
# Sync global language settings with new project languages
|
|
21846
|
+
self.source_language = source_lang
|
|
21847
|
+
self.target_language = target_lang
|
|
21848
|
+
|
|
21713
21849
|
# Process source text if provided
|
|
21714
21850
|
source_text = text_input.toPlainText().strip()
|
|
21715
21851
|
if source_text:
|
|
@@ -21819,7 +21955,13 @@ class SupervertalerQt(QMainWindow):
|
|
|
21819
21955
|
self.current_project = Project.from_dict(data)
|
|
21820
21956
|
self.project_file_path = file_path
|
|
21821
21957
|
self.project_modified = False
|
|
21822
|
-
|
|
21958
|
+
|
|
21959
|
+
# Sync global language settings with project languages
|
|
21960
|
+
if self.current_project.source_lang:
|
|
21961
|
+
self.source_language = self.current_project.source_lang
|
|
21962
|
+
if self.current_project.target_lang:
|
|
21963
|
+
self.target_language = self.current_project.target_lang
|
|
21964
|
+
|
|
21823
21965
|
# Restore prompt settings if they exist (unified library)
|
|
21824
21966
|
if hasattr(self.current_project, 'prompt_settings') and self.current_project.prompt_settings:
|
|
21825
21967
|
prompt_settings = self.current_project.prompt_settings
|
|
@@ -22174,7 +22316,154 @@ class SupervertalerQt(QMainWindow):
|
|
|
22174
22316
|
except Exception as e:
|
|
22175
22317
|
QMessageBox.critical(self, "Error", f"Failed to load project:\n{str(e)}")
|
|
22176
22318
|
self.log(f"✗ Error loading project: {e}")
|
|
22177
|
-
|
|
22319
|
+
|
|
22320
|
+
def _build_termbase_index(self):
|
|
22321
|
+
"""
|
|
22322
|
+
Build in-memory index of ALL terms from activated termbases (v1.9.182).
|
|
22323
|
+
|
|
22324
|
+
This is called ONCE on project load and replaces thousands of per-word
|
|
22325
|
+
database queries with a single bulk load + fast in-memory lookups.
|
|
22326
|
+
|
|
22327
|
+
Performance: Reduces 349-segment termbase search from 365 seconds to <1 second.
|
|
22328
|
+
"""
|
|
22329
|
+
import re
|
|
22330
|
+
import time
|
|
22331
|
+
start_time = time.time()
|
|
22332
|
+
|
|
22333
|
+
if not self.current_project or not hasattr(self, 'db_manager') or not self.db_manager:
|
|
22334
|
+
return
|
|
22335
|
+
|
|
22336
|
+
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
22337
|
+
|
|
22338
|
+
# Query ALL terms from activated termbases in ONE query
|
|
22339
|
+
# This replaces ~17,500 individual queries (349 segments × 50 words each)
|
|
22340
|
+
query = """
|
|
22341
|
+
SELECT
|
|
22342
|
+
t.id, t.source_term, t.target_term, t.termbase_id, t.priority,
|
|
22343
|
+
t.domain, t.notes, t.project, t.client, t.forbidden,
|
|
22344
|
+
tb.is_project_termbase, tb.name as termbase_name,
|
|
22345
|
+
COALESCE(ta.priority, tb.ranking) as ranking
|
|
22346
|
+
FROM termbase_terms t
|
|
22347
|
+
LEFT JOIN termbases tb ON CAST(t.termbase_id AS INTEGER) = tb.id
|
|
22348
|
+
LEFT JOIN termbase_activation ta ON ta.termbase_id = tb.id
|
|
22349
|
+
AND ta.project_id = ? AND ta.is_active = 1
|
|
22350
|
+
WHERE (ta.is_active = 1 OR tb.is_project_termbase = 1)
|
|
22351
|
+
"""
|
|
22352
|
+
|
|
22353
|
+
new_index = []
|
|
22354
|
+
try:
|
|
22355
|
+
self.db_manager.cursor.execute(query, [project_id or 0])
|
|
22356
|
+
rows = self.db_manager.cursor.fetchall()
|
|
22357
|
+
|
|
22358
|
+
for row in rows:
|
|
22359
|
+
source_term = row[1] # source_term
|
|
22360
|
+
if not source_term:
|
|
22361
|
+
continue
|
|
22362
|
+
|
|
22363
|
+
source_term_lower = source_term.lower().strip()
|
|
22364
|
+
if len(source_term_lower) < 2:
|
|
22365
|
+
continue
|
|
22366
|
+
|
|
22367
|
+
# Pre-compile regex pattern for word-boundary matching
|
|
22368
|
+
# This avoids recompiling the same pattern thousands of times
|
|
22369
|
+
try:
|
|
22370
|
+
# Handle terms with punctuation differently
|
|
22371
|
+
if any(c in source_term_lower for c in '.%,/-'):
|
|
22372
|
+
pattern = re.compile(r'(?<!\w)' + re.escape(source_term_lower) + r'(?!\w)')
|
|
22373
|
+
else:
|
|
22374
|
+
pattern = re.compile(r'\b' + re.escape(source_term_lower) + r'\b')
|
|
22375
|
+
except re.error:
|
|
22376
|
+
# If regex fails, use simple substring matching
|
|
22377
|
+
pattern = None
|
|
22378
|
+
|
|
22379
|
+
new_index.append({
|
|
22380
|
+
'term_id': row[0],
|
|
22381
|
+
'source_term': source_term,
|
|
22382
|
+
'source_term_lower': source_term_lower,
|
|
22383
|
+
'target_term': row[2],
|
|
22384
|
+
'termbase_id': row[3],
|
|
22385
|
+
'priority': row[4],
|
|
22386
|
+
'domain': row[5],
|
|
22387
|
+
'notes': row[6],
|
|
22388
|
+
'project': row[7],
|
|
22389
|
+
'client': row[8],
|
|
22390
|
+
'forbidden': row[9],
|
|
22391
|
+
'is_project_termbase': row[10],
|
|
22392
|
+
'termbase_name': row[11],
|
|
22393
|
+
'ranking': row[12],
|
|
22394
|
+
'pattern': pattern, # Pre-compiled regex
|
|
22395
|
+
})
|
|
22396
|
+
|
|
22397
|
+
# Sort by term length (longest first) for better phrase matching
|
|
22398
|
+
new_index.sort(key=lambda x: len(x['source_term_lower']), reverse=True)
|
|
22399
|
+
|
|
22400
|
+
# Thread-safe update of the index
|
|
22401
|
+
with self.termbase_index_lock:
|
|
22402
|
+
self.termbase_index = new_index
|
|
22403
|
+
|
|
22404
|
+
elapsed = time.time() - start_time
|
|
22405
|
+
self.log(f"✅ Built termbase index: {len(new_index)} terms in {elapsed:.2f}s")
|
|
22406
|
+
|
|
22407
|
+
except Exception as e:
|
|
22408
|
+
self.log(f"❌ Failed to build termbase index: {e}")
|
|
22409
|
+
import traceback
|
|
22410
|
+
self.log(traceback.format_exc())
|
|
22411
|
+
|
|
22412
|
+
def _search_termbase_in_memory(self, source_text: str) -> dict:
|
|
22413
|
+
"""
|
|
22414
|
+
Search termbase using in-memory index (v1.9.182).
|
|
22415
|
+
|
|
22416
|
+
This replaces _search_termbases_thread_safe() for batch operations.
|
|
22417
|
+
Instead of N database queries (one per word), we do:
|
|
22418
|
+
- 1 pass through the index (typically ~1000 terms)
|
|
22419
|
+
- Fast string 'in' check + pre-compiled regex validation
|
|
22420
|
+
|
|
22421
|
+
Performance: <1ms per segment vs 1+ second per segment.
|
|
22422
|
+
"""
|
|
22423
|
+
if not source_text:
|
|
22424
|
+
return {}
|
|
22425
|
+
|
|
22426
|
+
with self.termbase_index_lock:
|
|
22427
|
+
if not self.termbase_index:
|
|
22428
|
+
return {}
|
|
22429
|
+
index = self.termbase_index # Local reference for thread safety
|
|
22430
|
+
|
|
22431
|
+
source_lower = source_text.lower()
|
|
22432
|
+
matches = {}
|
|
22433
|
+
|
|
22434
|
+
for term in index:
|
|
22435
|
+
term_lower = term['source_term_lower']
|
|
22436
|
+
|
|
22437
|
+
# Quick substring check first (very fast, implemented in C)
|
|
22438
|
+
if term_lower not in source_lower:
|
|
22439
|
+
continue
|
|
22440
|
+
|
|
22441
|
+
# Word boundary validation using pre-compiled pattern
|
|
22442
|
+
pattern = term.get('pattern')
|
|
22443
|
+
if pattern:
|
|
22444
|
+
if not pattern.search(source_lower):
|
|
22445
|
+
continue
|
|
22446
|
+
|
|
22447
|
+
# Term matches! Add to results
|
|
22448
|
+
term_id = term['term_id']
|
|
22449
|
+
matches[term_id] = {
|
|
22450
|
+
'source': term['source_term'],
|
|
22451
|
+
'translation': term['target_term'],
|
|
22452
|
+
'term_id': term_id,
|
|
22453
|
+
'termbase_id': term['termbase_id'],
|
|
22454
|
+
'termbase_name': term['termbase_name'],
|
|
22455
|
+
'priority': term['priority'],
|
|
22456
|
+
'ranking': term['ranking'],
|
|
22457
|
+
'is_project_termbase': term['is_project_termbase'],
|
|
22458
|
+
'forbidden': term['forbidden'],
|
|
22459
|
+
'domain': term['domain'],
|
|
22460
|
+
'notes': term['notes'],
|
|
22461
|
+
'project': term['project'],
|
|
22462
|
+
'client': term['client'],
|
|
22463
|
+
}
|
|
22464
|
+
|
|
22465
|
+
return matches
|
|
22466
|
+
|
|
22178
22467
|
def _start_termbase_batch_worker(self):
|
|
22179
22468
|
"""
|
|
22180
22469
|
Start background thread to batch-process termbase matches for all segments.
|
|
@@ -22182,21 +22471,25 @@ class SupervertalerQt(QMainWindow):
|
|
|
22182
22471
|
"""
|
|
22183
22472
|
if not self.current_project or len(self.current_project.segments) == 0:
|
|
22184
22473
|
return
|
|
22185
|
-
|
|
22474
|
+
|
|
22475
|
+
# Build in-memory termbase index FIRST (v1.9.182)
|
|
22476
|
+
# This is the key optimization: load all terms once, then do fast in-memory lookups
|
|
22477
|
+
self._build_termbase_index()
|
|
22478
|
+
|
|
22186
22479
|
# 🧪 EXPERIMENTAL: Skip batch worker if cache kill switch is enabled
|
|
22187
22480
|
if getattr(self, 'disable_all_caches', False):
|
|
22188
22481
|
self.log("🧪 Termbase batch worker SKIPPED (caches disabled)")
|
|
22189
22482
|
return
|
|
22190
|
-
|
|
22483
|
+
|
|
22191
22484
|
# Stop any existing worker thread
|
|
22192
22485
|
self.termbase_batch_stop_event.set()
|
|
22193
22486
|
if self.termbase_batch_worker_thread and self.termbase_batch_worker_thread.is_alive():
|
|
22194
22487
|
self.log("⏹️ Stopping existing termbase batch worker...")
|
|
22195
22488
|
self.termbase_batch_worker_thread.join(timeout=2)
|
|
22196
|
-
|
|
22489
|
+
|
|
22197
22490
|
# Reset stop event for new worker
|
|
22198
22491
|
self.termbase_batch_stop_event.clear()
|
|
22199
|
-
|
|
22492
|
+
|
|
22200
22493
|
# Start new background worker thread
|
|
22201
22494
|
segment_count = len(self.current_project.segments)
|
|
22202
22495
|
self.log(f"🔄 Starting background termbase batch processor for {segment_count} segments...")
|
|
@@ -22212,96 +22505,60 @@ class SupervertalerQt(QMainWindow):
|
|
|
22212
22505
|
"""
|
|
22213
22506
|
Background worker thread: process all segments and populate termbase cache.
|
|
22214
22507
|
Runs in separate thread to not block UI.
|
|
22215
|
-
|
|
22216
|
-
|
|
22508
|
+
|
|
22509
|
+
v1.9.182: Now uses in-memory termbase index for 1000x faster lookups.
|
|
22510
|
+
Old approach: 365 seconds for 349 segments (1 second/segment)
|
|
22511
|
+
New approach: <1 second for 349 segments (<3ms/segment)
|
|
22217
22512
|
"""
|
|
22218
22513
|
if not segments:
|
|
22219
22514
|
return
|
|
22220
|
-
|
|
22221
|
-
# Create a separate database connection for this thread
|
|
22222
|
-
# SQLite connections are thread-local and cannot be shared across threads
|
|
22223
|
-
import sqlite3
|
|
22224
|
-
try:
|
|
22225
|
-
thread_db_connection = sqlite3.connect(self.db_manager.db_path)
|
|
22226
|
-
thread_db_connection.row_factory = sqlite3.Row
|
|
22227
|
-
thread_db_cursor = thread_db_connection.cursor()
|
|
22228
|
-
except Exception as e:
|
|
22229
|
-
self.log(f"❌ Failed to create database connection in batch worker: {e}")
|
|
22230
|
-
return
|
|
22231
|
-
|
|
22515
|
+
|
|
22232
22516
|
try:
|
|
22233
22517
|
processed = 0
|
|
22234
22518
|
cached = 0
|
|
22519
|
+
with_matches = 0
|
|
22235
22520
|
start_time = time.time()
|
|
22236
|
-
|
|
22521
|
+
|
|
22237
22522
|
for segment in segments:
|
|
22238
22523
|
# Check if stop event was signaled (user closed project or started new one)
|
|
22239
22524
|
if self.termbase_batch_stop_event.is_set():
|
|
22240
22525
|
self.log(f"⏹️ Termbase batch worker stopped by user (processed {processed} segments)")
|
|
22241
22526
|
break
|
|
22242
|
-
|
|
22527
|
+
|
|
22243
22528
|
segment_id = segment.id
|
|
22244
|
-
|
|
22529
|
+
|
|
22245
22530
|
# Skip if already in cache (thread-safe check)
|
|
22246
22531
|
with self.termbase_cache_lock:
|
|
22247
22532
|
if segment_id in self.termbase_cache:
|
|
22248
22533
|
cached += 1
|
|
22249
22534
|
continue
|
|
22250
|
-
|
|
22251
|
-
#
|
|
22535
|
+
|
|
22536
|
+
# v1.9.182: Use in-memory index for instant lookup (no database queries!)
|
|
22252
22537
|
try:
|
|
22253
|
-
|
|
22254
|
-
|
|
22255
|
-
|
|
22256
|
-
|
|
22257
|
-
|
|
22258
|
-
|
|
22259
|
-
|
|
22260
|
-
target_lang=self.current_project.target_lang if self.current_project else None,
|
|
22261
|
-
project_id=current_project_id
|
|
22262
|
-
)
|
|
22263
|
-
|
|
22538
|
+
matches = self._search_termbase_in_memory(segment.source)
|
|
22539
|
+
|
|
22540
|
+
# Store in cache (thread-safe) - even empty results to avoid re-lookup
|
|
22541
|
+
with self.termbase_cache_lock:
|
|
22542
|
+
self.termbase_cache[segment_id] = matches
|
|
22543
|
+
|
|
22544
|
+
processed += 1
|
|
22264
22545
|
if matches:
|
|
22265
|
-
|
|
22266
|
-
|
|
22267
|
-
self.termbase_cache[segment_id] = matches
|
|
22268
|
-
|
|
22269
|
-
processed += 1
|
|
22270
|
-
|
|
22271
|
-
# Log progress every 100 segments
|
|
22272
|
-
if processed % 100 == 0:
|
|
22273
|
-
elapsed = time.time() - start_time
|
|
22274
|
-
rate = processed / elapsed if elapsed > 0 else 0
|
|
22275
|
-
remaining = len(segments) - processed
|
|
22276
|
-
eta_seconds = remaining / rate if rate > 0 else 0
|
|
22277
|
-
self.log(f"📊 Batch progress: {processed}/{len(segments)} cached " +
|
|
22278
|
-
f"({rate:.1f} seg/sec, ETA: {int(eta_seconds)}s)")
|
|
22279
|
-
|
|
22546
|
+
with_matches += 1
|
|
22547
|
+
|
|
22280
22548
|
except Exception as e:
|
|
22281
22549
|
self.log(f"❌ Error processing segment {segment_id} in batch worker: {e}")
|
|
22282
22550
|
continue
|
|
22283
|
-
|
|
22284
|
-
# Small delay to prevent CPU saturation (let UI thread work)
|
|
22285
|
-
time.sleep(0.001) # 1ms delay between segments
|
|
22286
|
-
|
|
22551
|
+
|
|
22287
22552
|
elapsed = time.time() - start_time
|
|
22288
22553
|
total_cached = len(self.termbase_cache)
|
|
22289
|
-
|
|
22290
|
-
|
|
22291
|
-
|
|
22554
|
+
rate = processed / elapsed if elapsed > 0 else 0
|
|
22555
|
+
self.log(f"✅ Termbase batch worker complete: {processed} segments in {elapsed:.2f}s " +
|
|
22556
|
+
f"({rate:.0f} seg/sec, {with_matches} with matches)")
|
|
22557
|
+
|
|
22292
22558
|
except Exception as e:
|
|
22293
22559
|
self.log(f"❌ Termbase batch worker error: {e}")
|
|
22294
22560
|
import traceback
|
|
22295
22561
|
self.log(traceback.format_exc())
|
|
22296
|
-
|
|
22297
|
-
finally:
|
|
22298
|
-
# Close thread-local database connection
|
|
22299
|
-
try:
|
|
22300
|
-
thread_db_cursor.close()
|
|
22301
|
-
thread_db_connection.close()
|
|
22302
|
-
self.log("✓ Closed thread-local database connection in batch worker")
|
|
22303
|
-
except:
|
|
22304
|
-
pass
|
|
22305
22562
|
|
|
22306
22563
|
def _search_termbases_thread_safe(self, source_text: str, cursor, source_lang: str = None, target_lang: str = None, project_id: int = None) -> Dict[str, str]:
|
|
22307
22564
|
"""
|
|
@@ -22501,11 +22758,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
22501
22758
|
Also triggers PROACTIVE HIGHLIGHTING for upcoming segments with glossary matches.
|
|
22502
22759
|
"""
|
|
22503
22760
|
import json
|
|
22504
|
-
|
|
22505
|
-
print(f"[PROACTIVE DEBUG] _trigger_idle_prefetch called for row {current_row}")
|
|
22506
|
-
|
|
22761
|
+
|
|
22507
22762
|
if not self.current_project or current_row < 0:
|
|
22508
|
-
print(f"[PROACTIVE DEBUG] Early exit: no project or invalid row")
|
|
22509
22763
|
return
|
|
22510
22764
|
|
|
22511
22765
|
try:
|
|
@@ -22514,9 +22768,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
22514
22768
|
already_cached_ids = []
|
|
22515
22769
|
start_idx = current_row + 1
|
|
22516
22770
|
end_idx = min(start_idx + 5, len(self.current_project.segments))
|
|
22517
|
-
|
|
22518
|
-
print(f"[PROACTIVE DEBUG] Checking segments {start_idx} to {end_idx}")
|
|
22519
|
-
|
|
22771
|
+
|
|
22520
22772
|
for seg in self.current_project.segments[start_idx:end_idx]:
|
|
22521
22773
|
# Check if already cached
|
|
22522
22774
|
with self.translation_matches_cache_lock:
|
|
@@ -22524,23 +22776,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
22524
22776
|
next_segment_ids.append(seg.id)
|
|
22525
22777
|
else:
|
|
22526
22778
|
already_cached_ids.append(seg.id)
|
|
22527
|
-
|
|
22528
|
-
print(f"[PROACTIVE DEBUG] Already cached IDs: {already_cached_ids}, Need prefetch: {next_segment_ids}")
|
|
22529
|
-
|
|
22779
|
+
|
|
22530
22780
|
# For already-cached segments, trigger proactive highlighting immediately
|
|
22531
22781
|
# This handles the case where segments were cached earlier but not highlighted
|
|
22532
22782
|
for seg_id in already_cached_ids:
|
|
22533
22783
|
try:
|
|
22534
22784
|
with self.termbase_cache_lock:
|
|
22535
22785
|
termbase_raw = self.termbase_cache.get(seg_id, {})
|
|
22536
|
-
print(f"[PROACTIVE DEBUG] Segment {seg_id} termbase cache: {len(termbase_raw) if termbase_raw else 0} matches")
|
|
22537
22786
|
if termbase_raw:
|
|
22538
22787
|
termbase_json = json.dumps(termbase_raw)
|
|
22539
22788
|
# Apply highlighting on main thread (we're already on main thread here)
|
|
22540
|
-
print(f"[PROACTIVE DEBUG] Calling _apply_proactive_highlighting for seg {seg_id}")
|
|
22541
22789
|
self._apply_proactive_highlighting(seg_id, termbase_json)
|
|
22542
|
-
except Exception
|
|
22543
|
-
|
|
22790
|
+
except Exception:
|
|
22791
|
+
pass # Silent failure for proactive highlighting
|
|
22544
22792
|
|
|
22545
22793
|
if next_segment_ids:
|
|
22546
22794
|
# Start prefetch in background (silent, no logging)
|
|
@@ -22622,43 +22870,35 @@ class SupervertalerQt(QMainWindow):
|
|
|
22622
22870
|
|
|
22623
22871
|
# Fetch TM/termbase matches (pass cursor for thread-safe termbase lookups)
|
|
22624
22872
|
matches = self._fetch_all_matches_for_segment(segment, thread_db_cursor)
|
|
22625
|
-
|
|
22626
|
-
#
|
|
22627
|
-
# This prevents "empty cache hits" when TM database is still empty
|
|
22873
|
+
|
|
22874
|
+
# Count matches for logging and proactive highlighting
|
|
22628
22875
|
tm_count = len(matches.get("TM", []))
|
|
22629
22876
|
tb_count = len(matches.get("Termbases", []))
|
|
22630
22877
|
mt_count = len(matches.get("MT", []))
|
|
22631
22878
|
llm_count = len(matches.get("LLM", []))
|
|
22632
22879
|
total_matches = tm_count + tb_count + mt_count + llm_count
|
|
22633
22880
|
|
|
22634
|
-
|
|
22635
|
-
|
|
22881
|
+
# Only cache results if we found something
|
|
22882
|
+
# Don't cache empty results - let main thread do fresh lookup
|
|
22636
22883
|
if total_matches > 0:
|
|
22637
|
-
# Store in cache only if we have results
|
|
22638
22884
|
with self.translation_matches_cache_lock:
|
|
22639
22885
|
self.translation_matches_cache[segment_id] = matches
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
22654
|
-
|
|
22655
|
-
|
|
22656
|
-
self._proactive_highlight_signal.emit(segment_id, termbase_json)
|
|
22657
|
-
else:
|
|
22658
|
-
print(f"[PREFETCH DEBUG] WARNING: tb_count={tb_count} but termbase_raw is empty!")
|
|
22659
|
-
except Exception as e:
|
|
22660
|
-
print(f"[PREFETCH DEBUG] ERROR emitting signal: {e}")
|
|
22661
|
-
# else: Don't cache empty results - let it fall through to slow lookup next time
|
|
22886
|
+
|
|
22887
|
+
# PROACTIVE HIGHLIGHTING: Emit signal to apply highlighting on main thread
|
|
22888
|
+
# This makes upcoming segments show their glossary matches immediately
|
|
22889
|
+
if tb_count > 0:
|
|
22890
|
+
try:
|
|
22891
|
+
# Extract raw termbase matches from cache for highlighting
|
|
22892
|
+
with self.termbase_cache_lock:
|
|
22893
|
+
termbase_raw = self.termbase_cache.get(segment_id, {})
|
|
22894
|
+
|
|
22895
|
+
if termbase_raw:
|
|
22896
|
+
# Convert to JSON for thread-safe signal transfer
|
|
22897
|
+
termbase_json = json.dumps(termbase_raw)
|
|
22898
|
+
# Emit signal - will be handled on main thread
|
|
22899
|
+
self._proactive_highlight_signal.emit(segment_id, termbase_json)
|
|
22900
|
+
except Exception:
|
|
22901
|
+
pass # Silent fail for proactive highlighting
|
|
22662
22902
|
|
|
22663
22903
|
except Exception as e:
|
|
22664
22904
|
self.log(f"Error in prefetch worker: {e}")
|
|
@@ -22708,31 +22948,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
22708
22948
|
source_lang_code = self._convert_language_to_code(source_lang)
|
|
22709
22949
|
target_lang_code = self._convert_language_to_code(target_lang)
|
|
22710
22950
|
|
|
22711
|
-
# 1. TM matches (
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
try:
|
|
22715
|
-
tm_results = self.db_manager.search_translation_memory(
|
|
22716
|
-
segment.source,
|
|
22717
|
-
source_lang,
|
|
22718
|
-
target_lang,
|
|
22719
|
-
limit=5
|
|
22720
|
-
)
|
|
22721
|
-
|
|
22722
|
-
if tm_results: # Only add if we got results
|
|
22723
|
-
for tm_match in tm_results:
|
|
22724
|
-
match_obj = TranslationMatch(
|
|
22725
|
-
source=tm_match.get('source', ''),
|
|
22726
|
-
target=tm_match.get('target', ''),
|
|
22727
|
-
relevance=tm_match.get('similarity', 0),
|
|
22728
|
-
metadata={'tm_name': tm_match.get('tm_id', 'project')},
|
|
22729
|
-
match_type='TM',
|
|
22730
|
-
compare_source=tm_match.get('source', ''),
|
|
22731
|
-
provider_code='TM'
|
|
22732
|
-
)
|
|
22733
|
-
matches_dict["TM"].append(match_obj)
|
|
22734
|
-
except Exception as e:
|
|
22735
|
-
pass # Silently continue
|
|
22951
|
+
# 1. TM matches - SKIP in prefetch worker (TM search not thread-safe)
|
|
22952
|
+
# TM will be fetched on-demand when user navigates to segment
|
|
22953
|
+
pass
|
|
22736
22954
|
|
|
22737
22955
|
# 2. MT matches (if enabled)
|
|
22738
22956
|
if self.enable_mt_matching:
|
|
@@ -22907,8 +23125,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
22907
23125
|
mode_note = " (overwrite)" if overwrite_mode else ""
|
|
22908
23126
|
msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
|
|
22909
23127
|
self._queue_tm_save_log(msg)
|
|
22910
|
-
#
|
|
22911
|
-
|
|
23128
|
+
# NOTE: Removed cache invalidation here - it was destroying batch worker's cache
|
|
23129
|
+
# on every Ctrl+Enter, making navigation extremely slow. The small chance of
|
|
23130
|
+
# seeing stale TM matches is far less important than responsive navigation.
|
|
22912
23131
|
|
|
22913
23132
|
def invalidate_translation_cache(self, smart_invalidation=True):
|
|
22914
23133
|
"""
|
|
@@ -23734,9 +23953,9 @@ class SupervertalerQt(QMainWindow):
|
|
|
23734
23953
|
# Initialize TM for this project
|
|
23735
23954
|
self.initialize_tm_database()
|
|
23736
23955
|
|
|
23737
|
-
# Deactivate all resources for new project
|
|
23956
|
+
# Deactivate all resources for new project, then auto-activate language-matching ones
|
|
23738
23957
|
self._deactivate_all_resources_for_new_project()
|
|
23739
|
-
|
|
23958
|
+
|
|
23740
23959
|
# Auto-resize rows for better initial display
|
|
23741
23960
|
self.auto_resize_rows()
|
|
23742
23961
|
|
|
@@ -26210,7 +26429,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
26210
26429
|
|
|
26211
26430
|
# Store memoQ source path in project for persistence across saves
|
|
26212
26431
|
self.current_project.memoq_source_path = file_path
|
|
26213
|
-
|
|
26432
|
+
|
|
26433
|
+
# Sync global language settings with imported project languages
|
|
26434
|
+
self.source_language = source_lang
|
|
26435
|
+
self.target_language = target_lang
|
|
26436
|
+
|
|
26214
26437
|
# Create segments with simple sequential IDs
|
|
26215
26438
|
for idx, source_text in enumerate(source_segments):
|
|
26216
26439
|
existing_target = target_segments[idx] if idx < len(target_segments) else ""
|
|
@@ -26234,15 +26457,15 @@ class SupervertalerQt(QMainWindow):
|
|
|
26234
26457
|
self.load_segments_to_grid()
|
|
26235
26458
|
self.initialize_tm_database()
|
|
26236
26459
|
|
|
26237
|
-
# Deactivate all resources for new project
|
|
26460
|
+
# Deactivate all resources for new project, then auto-activate language-matching ones
|
|
26238
26461
|
self._deactivate_all_resources_for_new_project()
|
|
26239
|
-
|
|
26462
|
+
|
|
26240
26463
|
# Auto-resize rows for better initial display
|
|
26241
26464
|
self.auto_resize_rows()
|
|
26242
|
-
|
|
26465
|
+
|
|
26243
26466
|
# Initialize spellcheck for target language
|
|
26244
26467
|
self._initialize_spellcheck_for_target_language(target_lang)
|
|
26245
|
-
|
|
26468
|
+
|
|
26246
26469
|
# If smart formatting was used, auto-enable Tags view so user sees the tags
|
|
26247
26470
|
if self.memoq_smart_formatting:
|
|
26248
26471
|
self._enable_tag_view_after_import()
|
|
@@ -26731,7 +26954,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
26731
26954
|
|
|
26732
26955
|
# Store memoQ XLIFF source path in project for persistence across saves
|
|
26733
26956
|
self.current_project.mqxliff_source_path = file_path
|
|
26734
|
-
|
|
26957
|
+
|
|
26958
|
+
# Sync global language settings with imported project languages
|
|
26959
|
+
self.source_language = source_lang
|
|
26960
|
+
self.target_language = target_lang
|
|
26961
|
+
|
|
26735
26962
|
# Update UI
|
|
26736
26963
|
self.project_file_path = None
|
|
26737
26964
|
self.project_modified = True
|
|
@@ -26739,15 +26966,15 @@ class SupervertalerQt(QMainWindow):
|
|
|
26739
26966
|
self.load_segments_to_grid()
|
|
26740
26967
|
self.initialize_tm_database()
|
|
26741
26968
|
|
|
26742
|
-
# Deactivate all resources for new project
|
|
26969
|
+
# Deactivate all resources for new project, then auto-activate language-matching ones
|
|
26743
26970
|
self._deactivate_all_resources_for_new_project()
|
|
26744
|
-
|
|
26971
|
+
|
|
26745
26972
|
# Auto-resize rows for better initial display
|
|
26746
26973
|
self.auto_resize_rows()
|
|
26747
|
-
|
|
26974
|
+
|
|
26748
26975
|
# Initialize spellcheck for target language
|
|
26749
26976
|
self._initialize_spellcheck_for_target_language(target_lang)
|
|
26750
|
-
|
|
26977
|
+
|
|
26751
26978
|
# Log success
|
|
26752
26979
|
self.log(f"✓ Imported {len(segments)} segments from memoQ XLIFF: {Path(file_path).name}")
|
|
26753
26980
|
self.log(f" Source: {source_lang}, Target: {target_lang}")
|
|
@@ -27006,7 +27233,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
27006
27233
|
|
|
27007
27234
|
# Store CafeTran source path in project for persistence across saves
|
|
27008
27235
|
self.current_project.cafetran_source_path = file_path
|
|
27009
|
-
|
|
27236
|
+
|
|
27237
|
+
# Sync global language settings with imported project languages
|
|
27238
|
+
self.source_language = self.current_project.source_lang
|
|
27239
|
+
self.target_language = self.current_project.target_lang
|
|
27240
|
+
|
|
27010
27241
|
# Update UI
|
|
27011
27242
|
self.project_file_path = None
|
|
27012
27243
|
self.project_modified = True
|
|
@@ -27014,16 +27245,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
27014
27245
|
self.load_segments_to_grid()
|
|
27015
27246
|
self.initialize_tm_database()
|
|
27016
27247
|
|
|
27017
|
-
# Deactivate all resources for new project
|
|
27248
|
+
# Deactivate all resources for new project, then auto-activate language-matching ones
|
|
27018
27249
|
self._deactivate_all_resources_for_new_project()
|
|
27019
|
-
|
|
27250
|
+
|
|
27020
27251
|
# Auto-resize rows for better initial display
|
|
27021
27252
|
self.auto_resize_rows()
|
|
27022
|
-
|
|
27253
|
+
|
|
27023
27254
|
# Initialize spellcheck for target language
|
|
27024
27255
|
target_lang = self.current_project.target_lang if self.current_project else 'nl'
|
|
27025
27256
|
self._initialize_spellcheck_for_target_language(target_lang)
|
|
27026
|
-
|
|
27257
|
+
|
|
27027
27258
|
# Log success
|
|
27028
27259
|
self.log(f"✓ Imported {len(segments)} segments from CafeTran bilingual DOCX: {Path(file_path).name}")
|
|
27029
27260
|
|
|
@@ -27230,7 +27461,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
27230
27461
|
|
|
27231
27462
|
# Store Trados source path in project for persistence across saves
|
|
27232
27463
|
self.current_project.trados_source_path = file_path
|
|
27233
|
-
|
|
27464
|
+
|
|
27465
|
+
# Sync global language settings with imported project languages
|
|
27466
|
+
self.source_language = source_lang
|
|
27467
|
+
self.target_language = target_lang
|
|
27468
|
+
|
|
27234
27469
|
# Update UI
|
|
27235
27470
|
self.project_file_path = None
|
|
27236
27471
|
self.project_modified = True
|
|
@@ -27238,15 +27473,15 @@ class SupervertalerQt(QMainWindow):
|
|
|
27238
27473
|
self.load_segments_to_grid()
|
|
27239
27474
|
self.initialize_tm_database()
|
|
27240
27475
|
|
|
27241
|
-
# Deactivate all resources for new project
|
|
27476
|
+
# Deactivate all resources for new project, then auto-activate language-matching ones
|
|
27242
27477
|
self._deactivate_all_resources_for_new_project()
|
|
27243
|
-
|
|
27478
|
+
|
|
27244
27479
|
# Auto-resize rows for better initial display
|
|
27245
27480
|
self.auto_resize_rows()
|
|
27246
|
-
|
|
27481
|
+
|
|
27247
27482
|
# Initialize spellcheck for target language
|
|
27248
27483
|
self._initialize_spellcheck_for_target_language(target_lang)
|
|
27249
|
-
|
|
27484
|
+
|
|
27250
27485
|
# Count segments with tags
|
|
27251
27486
|
tagged_count = sum(1 for s in trados_segments if s.source_tags)
|
|
27252
27487
|
|
|
@@ -27590,7 +27825,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
27590
27825
|
self.sdlppx_handler = handler
|
|
27591
27826
|
self.sdlppx_source_file = file_path
|
|
27592
27827
|
self.current_project.sdlppx_source_path = file_path
|
|
27593
|
-
|
|
27828
|
+
|
|
27829
|
+
# Sync global language settings with imported project languages
|
|
27830
|
+
self.source_language = source_lang
|
|
27831
|
+
self.target_language = target_lang
|
|
27832
|
+
|
|
27594
27833
|
# Update UI
|
|
27595
27834
|
self.project_file_path = None
|
|
27596
27835
|
self.project_modified = True
|
|
@@ -27923,6 +28162,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
27923
28162
|
# Store Phrase source path in project for persistence across saves
|
|
27924
28163
|
self.current_project.phrase_source_path = file_path
|
|
27925
28164
|
|
|
28165
|
+
# Sync global language settings with imported project languages
|
|
28166
|
+
self.source_language = source_lang
|
|
28167
|
+
self.target_language = target_lang
|
|
28168
|
+
|
|
27926
28169
|
# Update UI
|
|
27927
28170
|
self.project_file_path = None
|
|
27928
28171
|
self.project_modified = True
|
|
@@ -28203,7 +28446,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
28203
28446
|
|
|
28204
28447
|
# Store Déjà Vu source path in project for persistence
|
|
28205
28448
|
self.current_project.dejavu_source_path = file_path
|
|
28206
|
-
|
|
28449
|
+
|
|
28450
|
+
# Sync global language settings with imported project languages
|
|
28451
|
+
self.source_language = source_lang
|
|
28452
|
+
self.target_language = target_lang
|
|
28453
|
+
|
|
28207
28454
|
# Create segments
|
|
28208
28455
|
for idx, seg_data in enumerate(segments_data):
|
|
28209
28456
|
segment = Segment(
|
|
@@ -30864,8 +31111,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
30864
31111
|
self.show_translation_results_pane = settings.get('show_translation_results_pane', False)
|
|
30865
31112
|
self.show_compare_panel = settings.get('show_compare_panel', True)
|
|
30866
31113
|
|
|
30867
|
-
#
|
|
30868
|
-
self.disable_all_caches = settings.get('disable_all_caches',
|
|
31114
|
+
# Load cache kill switch setting (default: False = caches ENABLED for performance)
|
|
31115
|
+
self.disable_all_caches = settings.get('disable_all_caches', False)
|
|
30869
31116
|
|
|
30870
31117
|
# Load LLM provider settings for AI Assistant
|
|
30871
31118
|
llm_settings = self.load_llm_settings()
|
|
@@ -31278,7 +31525,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
31278
31525
|
"""Handle cell selection change"""
|
|
31279
31526
|
if self.debug_mode_enabled:
|
|
31280
31527
|
self.log(f"🎯 on_cell_selected called: row {current_row}, col {current_col}")
|
|
31281
|
-
|
|
31528
|
+
|
|
31282
31529
|
# 🚫 GUARD: Don't re-run lookups if we're staying on the same row
|
|
31283
31530
|
# This prevents lookups when user edits text (focus changes within same row)
|
|
31284
31531
|
if hasattr(self, '_last_selected_row') and self._last_selected_row == current_row:
|
|
@@ -31286,34 +31533,35 @@ class SupervertalerQt(QMainWindow):
|
|
|
31286
31533
|
self.log(f"⏭️ Skipping lookup - already on row {current_row}")
|
|
31287
31534
|
return
|
|
31288
31535
|
self._last_selected_row = current_row
|
|
31289
|
-
|
|
31290
|
-
# ⚡
|
|
31291
|
-
#
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
if is_arrow_nav or is_ctrl_enter_nav:
|
|
31296
|
-
self._arrow_key_navigation = False # Reset flags
|
|
31297
|
-
self._ctrl_enter_navigation = False
|
|
31298
|
-
|
|
31299
|
-
# Schedule deferred lookup with short delay (150ms) for rapid navigation
|
|
31300
|
-
if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
|
|
31301
|
-
self._deferred_lookup_timer.stop()
|
|
31302
|
-
from PyQt6.QtCore import QTimer
|
|
31303
|
-
self._deferred_lookup_timer = QTimer()
|
|
31304
|
-
self._deferred_lookup_timer.setSingleShot(True)
|
|
31305
|
-
self._deferred_lookup_timer.timeout.connect(
|
|
31306
|
-
lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
|
|
31307
|
-
self._on_cell_selected_full(r, c, pr, pc)
|
|
31308
|
-
)
|
|
31309
|
-
self._deferred_lookup_timer.start(150) # 150ms debounce
|
|
31310
|
-
|
|
31311
|
-
# Do minimal UI update immediately (orange highlight, scroll)
|
|
31536
|
+
|
|
31537
|
+
# ⚡ FILTER MODE: Skip ALL heavy lookups when text filters are active
|
|
31538
|
+
# User is quickly navigating through filtered results - don't slow them down
|
|
31539
|
+
is_filtering = getattr(self, 'filtering_active', False)
|
|
31540
|
+
if is_filtering:
|
|
31541
|
+
# Only do minimal UI update (orange highlight) - no TM/termbase lookups
|
|
31312
31542
|
self._on_cell_selected_minimal(current_row, previous_row)
|
|
31313
31543
|
return
|
|
31314
|
-
|
|
31315
|
-
#
|
|
31316
|
-
|
|
31544
|
+
|
|
31545
|
+
# ⚡ FAST PATH: Defer heavy lookups for ALL navigation (arrow keys, Ctrl+Enter, AND mouse clicks)
|
|
31546
|
+
# This makes segment navigation feel INSTANT - cursor moves first, lookups happen after
|
|
31547
|
+
# Reset any navigation flags
|
|
31548
|
+
self._arrow_key_navigation = False
|
|
31549
|
+
self._ctrl_enter_navigation = False
|
|
31550
|
+
|
|
31551
|
+
# Schedule deferred lookup with short delay - debounce prevents hammering during rapid navigation
|
|
31552
|
+
if hasattr(self, '_deferred_lookup_timer') and self._deferred_lookup_timer:
|
|
31553
|
+
self._deferred_lookup_timer.stop()
|
|
31554
|
+
from PyQt6.QtCore import QTimer
|
|
31555
|
+
self._deferred_lookup_timer = QTimer()
|
|
31556
|
+
self._deferred_lookup_timer.setSingleShot(True)
|
|
31557
|
+
self._deferred_lookup_timer.timeout.connect(
|
|
31558
|
+
lambda r=current_row, c=current_col, pr=previous_row, pc=previous_col:
|
|
31559
|
+
self._on_cell_selected_full(r, c, pr, pc)
|
|
31560
|
+
)
|
|
31561
|
+
self._deferred_lookup_timer.start(10) # 10ms - just enough to batch rapid arrow key holding
|
|
31562
|
+
|
|
31563
|
+
# Do minimal UI update immediately (orange highlight, scroll)
|
|
31564
|
+
self._on_cell_selected_minimal(current_row, previous_row)
|
|
31317
31565
|
|
|
31318
31566
|
def _center_row_in_viewport(self, row: int):
|
|
31319
31567
|
"""Center the given row vertically in the visible table viewport.
|
|
@@ -31535,9 +31783,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
31535
31783
|
]
|
|
31536
31784
|
# Also get NT matches (fresh, not cached - they may have changed)
|
|
31537
31785
|
nt_matches = self.find_nt_matches_in_source(segment.source)
|
|
31538
|
-
|
|
31786
|
+
|
|
31787
|
+
# Get status hint for termbase activation
|
|
31788
|
+
status_hint = self._get_termbase_status_hint()
|
|
31789
|
+
|
|
31539
31790
|
# Update both Termview widgets (left and right)
|
|
31540
|
-
self._update_both_termviews(segment.source, termbase_matches, nt_matches)
|
|
31791
|
+
self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
|
|
31541
31792
|
except Exception as e:
|
|
31542
31793
|
self.log(f"Error updating termview from cache: {e}")
|
|
31543
31794
|
|
|
@@ -31570,9 +31821,25 @@ class SupervertalerQt(QMainWindow):
|
|
|
31570
31821
|
if has_fuzzy_match and not has_100_match:
|
|
31571
31822
|
self._play_sound_effect('tm_fuzzy_match')
|
|
31572
31823
|
|
|
31573
|
-
# Skip the slow lookup below, we already have
|
|
31574
|
-
#
|
|
31824
|
+
# Skip the slow TERMBASE lookup below, we already have termbase matches cached
|
|
31825
|
+
# But TM lookup was skipped in prefetch (not thread-safe), so schedule it now
|
|
31575
31826
|
matches_dict = cached_matches # Set for later use
|
|
31827
|
+
|
|
31828
|
+
# v1.9.182: Schedule TM lookup even on cache hit (prefetch skips TM - not thread-safe)
|
|
31829
|
+
tm_count = len(cached_matches.get("TM", []))
|
|
31830
|
+
if tm_count == 0 and self.enable_tm_matching:
|
|
31831
|
+
find_replace_active = getattr(self, 'find_replace_active', False)
|
|
31832
|
+
if not find_replace_active:
|
|
31833
|
+
# Get termbase matches for the lookup
|
|
31834
|
+
termbase_matches_for_tm = [
|
|
31835
|
+
{
|
|
31836
|
+
'source_term': match.source,
|
|
31837
|
+
'target_term': match.target,
|
|
31838
|
+
'termbase_name': match.metadata.get('termbase_name', '') if match.metadata else '',
|
|
31839
|
+
}
|
|
31840
|
+
for match in cached_matches.get("Termbases", [])
|
|
31841
|
+
]
|
|
31842
|
+
self._schedule_mt_and_llm_matches(segment, termbase_matches_for_tm)
|
|
31576
31843
|
|
|
31577
31844
|
# Check if TM/Termbase matching is enabled
|
|
31578
31845
|
if not matches_dict and (not self.enable_tm_matching and not self.enable_termbase_matching):
|
|
@@ -31621,9 +31888,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
31621
31888
|
] if stored_matches else []
|
|
31622
31889
|
# Also get NT matches
|
|
31623
31890
|
nt_matches = self.find_nt_matches_in_source(segment.source)
|
|
31624
|
-
|
|
31891
|
+
|
|
31892
|
+
# Get status hint for termbase activation
|
|
31893
|
+
status_hint = self._get_termbase_status_hint()
|
|
31894
|
+
|
|
31625
31895
|
# Update both Termview widgets (left and right)
|
|
31626
|
-
self._update_both_termviews(segment.source, termbase_matches, nt_matches)
|
|
31896
|
+
self._update_both_termviews(segment.source, termbase_matches, nt_matches, status_hint)
|
|
31627
31897
|
except Exception as e:
|
|
31628
31898
|
self.log(f"Error refreshing termview: {e}")
|
|
31629
31899
|
|
|
@@ -31790,15 +32060,19 @@ class SupervertalerQt(QMainWindow):
|
|
|
31790
32060
|
|
|
31791
32061
|
# Schedule expensive searches (TM, MT, LLM) with debouncing to prevent UI blocking
|
|
31792
32062
|
# ONLY schedule if:
|
|
31793
|
-
# 1. Cache miss
|
|
32063
|
+
# 1. Cache miss OR cache hit with no TM matches (prefetch doesn't include TM - not thread-safe)
|
|
31794
32064
|
# 2. TM matching is enabled
|
|
31795
32065
|
# 3. Find/Replace is not active (to avoid slowdowns during navigation)
|
|
32066
|
+
needs_tm_lookup = True
|
|
31796
32067
|
with self.translation_matches_cache_lock:
|
|
31797
|
-
|
|
31798
|
-
|
|
32068
|
+
if segment_id in self.translation_matches_cache:
|
|
32069
|
+
cached = self.translation_matches_cache[segment_id]
|
|
32070
|
+
# v1.9.182: Check if TM matches exist - prefetch worker skips TM lookups
|
|
32071
|
+
needs_tm_lookup = len(cached.get("TM", [])) == 0
|
|
32072
|
+
|
|
31799
32073
|
find_replace_active = getattr(self, 'find_replace_active', False)
|
|
31800
|
-
|
|
31801
|
-
if
|
|
32074
|
+
|
|
32075
|
+
if needs_tm_lookup and self.enable_tm_matching and not find_replace_active:
|
|
31802
32076
|
# Get termbase matches if they exist (could be None or empty)
|
|
31803
32077
|
termbase_matches = matches_dict.get('Termbases', []) if matches_dict else []
|
|
31804
32078
|
self._schedule_mt_and_llm_matches(segment, termbase_matches)
|
|
@@ -31810,9 +32084,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
31810
32084
|
next_segment_ids = []
|
|
31811
32085
|
start_idx = current_row + 1
|
|
31812
32086
|
end_idx = min(start_idx + 20, len(self.current_project.segments))
|
|
31813
|
-
|
|
31814
|
-
print(f"[PROACTIVE NAV DEBUG] Navigation to row {current_row}, checking segments {start_idx} to {end_idx}")
|
|
31815
|
-
|
|
32087
|
+
|
|
31816
32088
|
for seg in self.current_project.segments[start_idx:end_idx]:
|
|
31817
32089
|
# Check if already cached
|
|
31818
32090
|
with self.translation_matches_cache_lock:
|
|
@@ -31826,15 +32098,12 @@ class SupervertalerQt(QMainWindow):
|
|
|
31826
32098
|
try:
|
|
31827
32099
|
with self.termbase_cache_lock:
|
|
31828
32100
|
termbase_raw = self.termbase_cache.get(seg.id, {})
|
|
31829
|
-
print(f"[PROACTIVE NAV DEBUG] Seg {seg.id}: cached, termbase_raw has {len(termbase_raw) if termbase_raw else 0} matches")
|
|
31830
32101
|
if termbase_raw:
|
|
31831
32102
|
termbase_json = json.dumps(termbase_raw)
|
|
31832
|
-
print(f"[PROACTIVE NAV DEBUG] Calling _apply_proactive_highlighting for seg {seg.id}")
|
|
31833
32103
|
self._apply_proactive_highlighting(seg.id, termbase_json)
|
|
31834
|
-
except Exception
|
|
31835
|
-
|
|
31836
|
-
|
|
31837
|
-
print(f"[PROACTIVE NAV DEBUG] Need to prefetch: {len(next_segment_ids)} segments")
|
|
32104
|
+
except Exception:
|
|
32105
|
+
pass # Silent failure for proactive highlighting
|
|
32106
|
+
|
|
31838
32107
|
if next_segment_ids:
|
|
31839
32108
|
self._start_prefetch_worker(next_segment_ids)
|
|
31840
32109
|
|
|
@@ -33765,7 +34034,8 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
33765
34034
|
self.nt_manager.set_list_active(list_name, False)
|
|
33766
34035
|
|
|
33767
34036
|
self.log("📋 New project: All TMs, glossaries, and NT lists deactivated (start clean)")
|
|
33768
|
-
|
|
34037
|
+
self.log("💡 Tip: Go to Resources tab to activate TMs and glossaries for this project")
|
|
34038
|
+
|
|
33769
34039
|
def search_and_display_tm_matches(self, source_text: str):
|
|
33770
34040
|
"""Search TM and Termbases and display matches with visual diff for fuzzy matches"""
|
|
33771
34041
|
self.log(f"🚨 search_and_display_tm_matches called with source_text: '{source_text[:50]}...'")
|
|
@@ -33788,11 +34058,34 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
33788
34058
|
try:
|
|
33789
34059
|
# Get activated TM IDs for current project
|
|
33790
34060
|
tm_ids = None
|
|
34061
|
+
no_tms_activated = False
|
|
34062
|
+
tms_wrong_language = False
|
|
33791
34063
|
if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr and self.current_project:
|
|
33792
34064
|
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
33793
34065
|
if project_id:
|
|
33794
34066
|
tm_ids = self.tm_metadata_mgr.get_active_tm_ids(project_id)
|
|
33795
|
-
|
|
34067
|
+
# Check if no TMs are activated for this project
|
|
34068
|
+
if tm_ids is not None and len(tm_ids) == 0:
|
|
34069
|
+
no_tms_activated = True
|
|
34070
|
+
elif tm_ids and len(tm_ids) > 0:
|
|
34071
|
+
# Check if any activated TMs match the project's language pair
|
|
34072
|
+
project_source = (self.current_project.source_lang or '').lower()
|
|
34073
|
+
project_target = (self.current_project.target_lang or '').lower()
|
|
34074
|
+
all_tms = self.tm_metadata_mgr.get_all_tms()
|
|
34075
|
+
has_matching_language = False
|
|
34076
|
+
for tm in all_tms:
|
|
34077
|
+
if tm['id'] in tm_ids:
|
|
34078
|
+
tm_source = (tm.get('source_lang') or '').lower()
|
|
34079
|
+
tm_target = (tm.get('target_lang') or '').lower()
|
|
34080
|
+
# Match if languages align (bidirectional) or TM has no language set
|
|
34081
|
+
if (not tm_source and not tm_target) or \
|
|
34082
|
+
(tm_source == project_source and tm_target == project_target) or \
|
|
34083
|
+
(tm_source == project_target and tm_target == project_source):
|
|
34084
|
+
has_matching_language = True
|
|
34085
|
+
break
|
|
34086
|
+
if not has_matching_language:
|
|
34087
|
+
tms_wrong_language = True
|
|
34088
|
+
|
|
33796
34089
|
# Search for matches (using activated TMs if available)
|
|
33797
34090
|
matches = self.tm_database.search_all(source_text, tm_ids=tm_ids, max_matches=5)
|
|
33798
34091
|
|
|
@@ -33839,10 +34132,26 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
33839
34132
|
|
|
33840
34133
|
if not matches:
|
|
33841
34134
|
if hasattr(self, 'tm_display'):
|
|
33842
|
-
|
|
33843
|
-
|
|
33844
|
-
|
|
33845
|
-
|
|
34135
|
+
if no_tms_activated:
|
|
34136
|
+
# Show helpful message when no TMs are activated
|
|
34137
|
+
self.tm_display.setHtml(
|
|
34138
|
+
f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
|
|
34139
|
+
f"<p style='color: #E65100;'><i>No TMs activated for this project.</i></p>"
|
|
34140
|
+
f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate translation memories.</p>"
|
|
34141
|
+
)
|
|
34142
|
+
elif tms_wrong_language:
|
|
34143
|
+
# Show message when TMs are activated but don't match project language pair
|
|
34144
|
+
project_lang_pair = f"{self.current_project.source_lang} → {self.current_project.target_lang}" if self.current_project else ""
|
|
34145
|
+
self.tm_display.setHtml(
|
|
34146
|
+
f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
|
|
34147
|
+
f"<p style='color: #E65100;'><i>Activated TMs don't match project language ({project_lang_pair}).</i></p>"
|
|
34148
|
+
f"<p style='color: #999; font-size: 9pt;'>Go to <b>Resources → TM</b> to activate TMs for this language pair.</p>"
|
|
34149
|
+
)
|
|
34150
|
+
else:
|
|
34151
|
+
self.tm_display.setHtml(
|
|
34152
|
+
f"<p style='color: #666;'><b>Source:</b> {source_text}</p>"
|
|
34153
|
+
f"<p style='color: #999;'><i>No translation memory matches found</i></p>"
|
|
34154
|
+
)
|
|
33846
34155
|
return
|
|
33847
34156
|
|
|
33848
34157
|
# If using TranslationResultsPanel, populate it with TM and Termbase results
|
|
@@ -34197,18 +34506,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
34197
34506
|
"""
|
|
34198
34507
|
Find all termbase matches in source text
|
|
34199
34508
|
Returns dict of {term: translation} for all matches found
|
|
34509
|
+
|
|
34510
|
+
v1.9.182: Uses in-memory index for instant lookup when available.
|
|
34511
|
+
Falls back to per-word database queries if index not built.
|
|
34200
34512
|
"""
|
|
34201
34513
|
if not source_text or not hasattr(self, 'db_manager') or not self.db_manager:
|
|
34202
34514
|
return {}
|
|
34203
34515
|
|
|
34204
34516
|
try:
|
|
34517
|
+
# v1.9.182: Use in-memory index for instant lookup (1000x faster)
|
|
34518
|
+
# The index is built on project load by _build_termbase_index()
|
|
34519
|
+
with self.termbase_index_lock:
|
|
34520
|
+
has_index = bool(self.termbase_index)
|
|
34521
|
+
|
|
34522
|
+
if has_index:
|
|
34523
|
+
# Fast path: use pre-built in-memory index
|
|
34524
|
+
return self._search_termbase_in_memory(source_text)
|
|
34525
|
+
|
|
34526
|
+
# Fallback: original per-word database query approach
|
|
34527
|
+
# (only used if index not yet built, e.g., during startup)
|
|
34205
34528
|
source_lang = self.current_project.source_lang if self.current_project else None
|
|
34206
34529
|
target_lang = self.current_project.target_lang if self.current_project else None
|
|
34207
|
-
|
|
34530
|
+
|
|
34208
34531
|
# Convert language names to codes for termbase search
|
|
34209
34532
|
source_lang_code = self._convert_language_to_code(source_lang) if source_lang else None
|
|
34210
34533
|
target_lang_code = self._convert_language_to_code(target_lang) if target_lang else None
|
|
34211
|
-
|
|
34534
|
+
|
|
34212
34535
|
# Strip HTML/XML/CAT tool tags from source text before word splitting
|
|
34213
34536
|
# This handles <b>, </b>, <i>, memoQ {1}, [2}, Trados <1>, Déjà Vu {00001}, etc.
|
|
34214
34537
|
import re
|
|
@@ -34218,7 +34541,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
34218
34541
|
# memoQ content tags: [uicontrol id="..."} or {uicontrol] or [tagname ...} or {tagname]
|
|
34219
34542
|
clean_source_text = re.sub(r'\[[^\[\]]*\}', '', clean_source_text) # Opening: [anything}
|
|
34220
34543
|
clean_source_text = re.sub(r'\{[^\{\}]*\]', '', clean_source_text) # Closing: {anything]
|
|
34221
|
-
|
|
34544
|
+
|
|
34222
34545
|
# Search termbases for all terms that appear in the source text
|
|
34223
34546
|
# Split source text into words and search for each one
|
|
34224
34547
|
words = clean_source_text.split()
|
|
@@ -34440,23 +34763,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
34440
34763
|
termbase_matches_json: JSON-encoded termbase matches dict (thread-safe transfer)
|
|
34441
34764
|
"""
|
|
34442
34765
|
import json
|
|
34443
|
-
|
|
34444
|
-
print(f"[PROACTIVE DEBUG] _apply_proactive_highlighting called for segment {segment_id}")
|
|
34445
|
-
|
|
34766
|
+
|
|
34446
34767
|
if not self.current_project or not self.table:
|
|
34447
|
-
print(f"[PROACTIVE DEBUG] Early exit: no project or table")
|
|
34448
34768
|
return
|
|
34449
|
-
|
|
34769
|
+
|
|
34450
34770
|
try:
|
|
34451
34771
|
# Decode the matches from JSON
|
|
34452
34772
|
termbase_matches = json.loads(termbase_matches_json) if termbase_matches_json else {}
|
|
34453
|
-
|
|
34454
|
-
print(f"[PROACTIVE DEBUG] Decoded {len(termbase_matches)} termbase matches")
|
|
34455
|
-
|
|
34773
|
+
|
|
34456
34774
|
if not termbase_matches:
|
|
34457
|
-
print(f"[PROACTIVE DEBUG] No matches to highlight, returning")
|
|
34458
34775
|
return # Nothing to highlight
|
|
34459
|
-
|
|
34776
|
+
|
|
34460
34777
|
# Find the row for this segment ID
|
|
34461
34778
|
row = -1
|
|
34462
34779
|
for r in range(self.table.rowCount()):
|
|
@@ -34469,44 +34786,25 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
34469
34786
|
break
|
|
34470
34787
|
except ValueError:
|
|
34471
34788
|
continue
|
|
34472
|
-
|
|
34473
|
-
print(f"[PROACTIVE DEBUG] Found row {row} for segment {segment_id}")
|
|
34474
|
-
|
|
34789
|
+
|
|
34475
34790
|
if row < 0:
|
|
34476
|
-
print(f"[PROACTIVE DEBUG] Segment not visible in current page")
|
|
34477
34791
|
return # Segment not visible in current page
|
|
34478
|
-
|
|
34792
|
+
|
|
34479
34793
|
# Get segment source text
|
|
34480
34794
|
segment = None
|
|
34481
34795
|
for seg in self.current_project.segments:
|
|
34482
34796
|
if seg.id == segment_id:
|
|
34483
34797
|
segment = seg
|
|
34484
34798
|
break
|
|
34485
|
-
|
|
34799
|
+
|
|
34486
34800
|
if not segment:
|
|
34487
|
-
print(f"[PROACTIVE DEBUG] Segment object not found")
|
|
34488
34801
|
return
|
|
34489
|
-
|
|
34490
|
-
print(f"[PROACTIVE DEBUG] Applying highlight_source_with_termbase to row {row}")
|
|
34491
|
-
print(f"[PROACTIVE DEBUG] Source text: {segment.source[:80]}...")
|
|
34492
|
-
print(f"[PROACTIVE DEBUG] Matches keys: {list(termbase_matches.keys())[:5]}")
|
|
34493
|
-
if termbase_matches:
|
|
34494
|
-
first_key = list(termbase_matches.keys())[0]
|
|
34495
|
-
print(f"[PROACTIVE DEBUG] Sample match: {first_key} => {termbase_matches[first_key]}")
|
|
34496
|
-
|
|
34497
|
-
# Check if the source widget exists and is the right type
|
|
34498
|
-
source_widget = self.table.cellWidget(row, 2)
|
|
34499
|
-
print(f"[PROACTIVE DEBUG] Source widget type: {type(source_widget).__name__ if source_widget else 'None'}")
|
|
34500
|
-
print(f"[PROACTIVE DEBUG] Has highlight method: {hasattr(source_widget, 'highlight_termbase_matches') if source_widget else 'N/A'}")
|
|
34501
|
-
|
|
34802
|
+
|
|
34502
34803
|
# Apply highlighting (this updates the source cell widget)
|
|
34503
34804
|
self.highlight_source_with_termbase(row, segment.source, termbase_matches)
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
|
|
34507
|
-
print(f"[PROACTIVE DEBUG] ERROR: {e}")
|
|
34508
|
-
import traceback
|
|
34509
|
-
print(f"[PROACTIVE DEBUG] Traceback: {traceback.format_exc()}")
|
|
34805
|
+
|
|
34806
|
+
except Exception:
|
|
34807
|
+
pass # Silent failure for proactive highlighting
|
|
34510
34808
|
|
|
34511
34809
|
def insert_term_translation(self, row: int, translation: str):
|
|
34512
34810
|
"""
|
|
@@ -35494,8 +35792,11 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35494
35792
|
'termbase_id': match_info.get('termbase_id'),
|
|
35495
35793
|
'notes': match_info.get('notes', '')
|
|
35496
35794
|
})
|
|
35795
|
+
# Get status hint for termbase activation
|
|
35796
|
+
status_hint = self._get_termbase_status_hint()
|
|
35797
|
+
|
|
35497
35798
|
# Update both Termview widgets
|
|
35498
|
-
self._update_both_termviews(segment.source, tb_list, nt_matches)
|
|
35799
|
+
self._update_both_termviews(segment.source, tb_list, nt_matches, status_hint)
|
|
35499
35800
|
self.log(" ✓ TermView updated")
|
|
35500
35801
|
except Exception as e:
|
|
35501
35802
|
self.log(f" ⚠️ TermView update error: {e}")
|
|
@@ -38673,95 +38974,32 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38673
38974
|
|
|
38674
38975
|
self.table.clearSelection()
|
|
38675
38976
|
self.table.setCurrentCell(row, 3) # Column 3 = Target (widget column)
|
|
38977
|
+
self.table.selectRow(row) # v1.9.182: Ensure row is visually selected
|
|
38978
|
+
# Ensure the row is visible by scrolling to it
|
|
38979
|
+
self.table.scrollToItem(self.table.item(row, 0), QTableWidget.ScrollHint.PositionAtCenter)
|
|
38676
38980
|
self.log(f"⏭️ Moved to next unconfirmed segment {seg.id}")
|
|
38677
|
-
|
|
38678
|
-
#
|
|
38679
|
-
|
|
38680
|
-
|
|
38681
|
-
|
|
38682
|
-
|
|
38683
|
-
|
|
38684
|
-
|
|
38685
|
-
|
|
38686
|
-
|
|
38687
|
-
|
|
38688
|
-
|
|
38689
|
-
# Use get_exact_match for 100% matches instead of fuzzy search
|
|
38690
|
-
source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
|
|
38691
|
-
target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
|
|
38692
|
-
exact_match = self.db_manager.get_exact_match(
|
|
38693
|
-
seg.source,
|
|
38694
|
-
tm_ids=activated_tm_ids,
|
|
38695
|
-
source_lang=source_lang,
|
|
38696
|
-
target_lang=target_lang
|
|
38697
|
-
)
|
|
38698
|
-
|
|
38699
|
-
# Check if there's a 100% match and (target is empty OR overwrite is enabled)
|
|
38700
|
-
target_is_empty = not seg.target.strip()
|
|
38701
|
-
can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
|
|
38702
|
-
|
|
38703
|
-
if exact_match and can_auto_confirm:
|
|
38704
|
-
match_target = exact_match.get('target_text', '')
|
|
38705
|
-
overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
|
|
38706
|
-
self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
|
|
38707
|
-
|
|
38708
|
-
# Insert the match into the target cell
|
|
38709
|
-
target_widget = self.table.cellWidget(row, 3)
|
|
38710
|
-
if target_widget and match_target:
|
|
38711
|
-
target_widget.setPlainText(match_target)
|
|
38712
|
-
seg.target = match_target
|
|
38713
|
-
seg.status = 'confirmed'
|
|
38714
|
-
self.update_status_icon(row, 'confirmed')
|
|
38715
|
-
self.project_modified = True
|
|
38716
|
-
|
|
38717
|
-
# Save to TM
|
|
38718
|
-
try:
|
|
38719
|
-
self.save_segment_to_activated_tms(seg.source, seg.target)
|
|
38720
|
-
self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
|
|
38721
|
-
except Exception as e:
|
|
38722
|
-
self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
|
|
38723
|
-
|
|
38724
|
-
# Continue to the NEXT unconfirmed segment (skip this one)
|
|
38725
|
-
for next_row in range(row + 1, self.table.rowCount()):
|
|
38726
|
-
if next_row < len(self.current_project.segments):
|
|
38727
|
-
next_seg = self.current_project.segments[next_row]
|
|
38728
|
-
if next_seg.status not in ['confirmed', 'approved']:
|
|
38729
|
-
# Check pagination
|
|
38730
|
-
if self.table.isRowHidden(next_row):
|
|
38731
|
-
if hasattr(self, 'grid_page_size') and hasattr(self, 'grid_current_page'):
|
|
38732
|
-
target_page = next_row // self.grid_page_size
|
|
38733
|
-
if target_page != self.grid_current_page:
|
|
38734
|
-
self.grid_current_page = target_page
|
|
38735
|
-
self._update_pagination_ui()
|
|
38736
|
-
self._apply_pagination_to_grid()
|
|
38737
|
-
|
|
38738
|
-
# ⚡ INSTANT NAVIGATION
|
|
38739
|
-
self._ctrl_enter_navigation = True
|
|
38740
|
-
|
|
38741
|
-
self.table.clearSelection()
|
|
38742
|
-
self.table.setCurrentCell(next_row, 3)
|
|
38743
|
-
self.log(f"⏭️ Auto-skipped to next unconfirmed segment {next_seg.id}")
|
|
38744
|
-
next_target_widget = self.table.cellWidget(next_row, 3)
|
|
38745
|
-
if next_target_widget:
|
|
38746
|
-
next_target_widget.setFocus()
|
|
38747
|
-
next_target_widget.moveCursor(QTextCursor.MoveOperation.End)
|
|
38748
|
-
|
|
38749
|
-
# Recursively check if this next segment also has a 100% match
|
|
38750
|
-
self.confirm_and_next_unconfirmed()
|
|
38751
|
-
return
|
|
38752
|
-
|
|
38753
|
-
# No more unconfirmed segments after this one
|
|
38754
|
-
self.log("✅ No more unconfirmed segments after auto-confirm")
|
|
38755
|
-
# Update status bar after auto-confirming
|
|
38756
|
-
self.update_progress_stats()
|
|
38757
|
-
return
|
|
38758
|
-
|
|
38759
|
-
# Get the target cell widget and set focus to it (normal behavior without auto-confirm)
|
|
38981
|
+
|
|
38982
|
+
# v1.9.182: Explicitly update termview (don't rely on deferred signal)
|
|
38983
|
+
self._update_termview_for_segment(seg)
|
|
38984
|
+
|
|
38985
|
+
# v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
|
|
38986
|
+
if self.enable_tm_matching:
|
|
38987
|
+
find_replace_active = getattr(self, 'find_replace_active', False)
|
|
38988
|
+
if not find_replace_active:
|
|
38989
|
+
self._schedule_mt_and_llm_matches(seg, [])
|
|
38990
|
+
|
|
38991
|
+
# Get the target cell widget and set focus to it IMMEDIATELY
|
|
38992
|
+
# (moved BEFORE auto-confirm check for instant responsiveness)
|
|
38760
38993
|
target_widget = self.table.cellWidget(row, 3)
|
|
38761
38994
|
if target_widget:
|
|
38762
38995
|
target_widget.setFocus()
|
|
38763
38996
|
# Move cursor to end of text
|
|
38764
38997
|
target_widget.moveCursor(QTextCursor.MoveOperation.End)
|
|
38998
|
+
|
|
38999
|
+
# v1.9.182: Defer auto-confirm check to not block navigation
|
|
39000
|
+
# The TM lookup is slow - do it asynchronously after navigation completes
|
|
39001
|
+
if self.auto_confirm_100_percent_matches:
|
|
39002
|
+
QTimer.singleShot(50, lambda r=row, s=seg: self._check_auto_confirm_100_percent(r, s))
|
|
38765
39003
|
return
|
|
38766
39004
|
|
|
38767
39005
|
# No more unconfirmed segments, just go to next
|
|
@@ -38783,14 +39021,106 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38783
39021
|
|
|
38784
39022
|
self.table.clearSelection()
|
|
38785
39023
|
self.table.setCurrentCell(next_row, 3) # Column 3 = Target (widget column)
|
|
39024
|
+
self.table.selectRow(next_row) # v1.9.182: Ensure row is visually selected
|
|
39025
|
+
# Ensure the row is visible by scrolling to it
|
|
39026
|
+
self.table.scrollToItem(self.table.item(next_row, 0), QTableWidget.ScrollHint.PositionAtCenter)
|
|
38786
39027
|
self.log(f"⏭️ Moved to next segment (all remaining confirmed)")
|
|
39028
|
+
|
|
39029
|
+
# v1.9.182: Explicitly update termview (don't rely on deferred signal)
|
|
39030
|
+
if next_row < len(self.current_project.segments):
|
|
39031
|
+
next_seg = self.current_project.segments[next_row]
|
|
39032
|
+
self._update_termview_for_segment(next_seg)
|
|
39033
|
+
|
|
39034
|
+
# v1.9.182: Explicitly schedule TM lookup (don't rely on deferred signal)
|
|
39035
|
+
if self.enable_tm_matching:
|
|
39036
|
+
find_replace_active = getattr(self, 'find_replace_active', False)
|
|
39037
|
+
if not find_replace_active:
|
|
39038
|
+
self._schedule_mt_and_llm_matches(next_seg, [])
|
|
39039
|
+
|
|
38787
39040
|
# Get the target cell widget and set focus to it
|
|
38788
39041
|
target_widget = self.table.cellWidget(next_row, 3)
|
|
38789
39042
|
if target_widget:
|
|
38790
39043
|
target_widget.setFocus()
|
|
38791
39044
|
# Move cursor to end of text
|
|
38792
39045
|
target_widget.moveCursor(QTextCursor.MoveOperation.End)
|
|
38793
|
-
|
|
39046
|
+
|
|
39047
|
+
def _check_auto_confirm_100_percent(self, row: int, seg):
|
|
39048
|
+
"""
|
|
39049
|
+
v1.9.182: Deferred auto-confirm check for 100% TM matches.
|
|
39050
|
+
|
|
39051
|
+
This is called asynchronously after Ctrl+Enter navigation to avoid blocking
|
|
39052
|
+
the UI thread with slow TM database queries.
|
|
39053
|
+
"""
|
|
39054
|
+
try:
|
|
39055
|
+
# Verify we're still on the same segment (user may have navigated away)
|
|
39056
|
+
current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
|
|
39057
|
+
if current_row != row:
|
|
39058
|
+
return # User has moved - don't auto-confirm wrong segment
|
|
39059
|
+
|
|
39060
|
+
if not self.enable_tm_matching or not hasattr(self, 'db_manager') or not self.db_manager:
|
|
39061
|
+
return
|
|
39062
|
+
|
|
39063
|
+
# Get activated TM IDs from project settings
|
|
39064
|
+
activated_tm_ids = []
|
|
39065
|
+
if hasattr(self.current_project, 'tm_settings') and self.current_project.tm_settings:
|
|
39066
|
+
activated_tm_ids = self.current_project.tm_settings.get('activated_tm_ids', [])
|
|
39067
|
+
|
|
39068
|
+
if not activated_tm_ids:
|
|
39069
|
+
return
|
|
39070
|
+
|
|
39071
|
+
# Use get_exact_match for 100% matches
|
|
39072
|
+
source_lang = self.current_project.source_lang if hasattr(self.current_project, 'source_lang') else None
|
|
39073
|
+
target_lang = self.current_project.target_lang if hasattr(self.current_project, 'target_lang') else None
|
|
39074
|
+
exact_match = self.db_manager.get_exact_match(
|
|
39075
|
+
seg.source,
|
|
39076
|
+
tm_ids=activated_tm_ids,
|
|
39077
|
+
source_lang=source_lang,
|
|
39078
|
+
target_lang=target_lang
|
|
39079
|
+
)
|
|
39080
|
+
|
|
39081
|
+
if not exact_match:
|
|
39082
|
+
return
|
|
39083
|
+
|
|
39084
|
+
# Check if there's a 100% match and (target is empty OR overwrite is enabled)
|
|
39085
|
+
target_is_empty = not seg.target.strip()
|
|
39086
|
+
can_auto_confirm = target_is_empty or self.auto_confirm_overwrite_existing
|
|
39087
|
+
|
|
39088
|
+
if not can_auto_confirm:
|
|
39089
|
+
return
|
|
39090
|
+
|
|
39091
|
+
# Verify AGAIN that we're still on the same segment (TM query may have taken time)
|
|
39092
|
+
current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
|
|
39093
|
+
if current_row != row:
|
|
39094
|
+
return # User has moved during TM lookup
|
|
39095
|
+
|
|
39096
|
+
match_target = exact_match.get('target_text', '')
|
|
39097
|
+
if not match_target:
|
|
39098
|
+
return
|
|
39099
|
+
|
|
39100
|
+
overwrite_note = " (overwriting existing)" if not target_is_empty else " (empty target)"
|
|
39101
|
+
self.log(f"🎯 Auto-confirm: Found 100% TM match for segment {seg.id}{overwrite_note}")
|
|
39102
|
+
|
|
39103
|
+
# Insert the match into the target cell
|
|
39104
|
+
target_widget = self.table.cellWidget(row, 3)
|
|
39105
|
+
if target_widget:
|
|
39106
|
+
target_widget.setPlainText(match_target)
|
|
39107
|
+
seg.target = match_target
|
|
39108
|
+
seg.status = 'confirmed'
|
|
39109
|
+
self.update_status_icon(row, 'confirmed')
|
|
39110
|
+
self.project_modified = True
|
|
39111
|
+
|
|
39112
|
+
# Save to TM
|
|
39113
|
+
try:
|
|
39114
|
+
self.save_segment_to_activated_tms(seg.source, seg.target)
|
|
39115
|
+
self.log(f"💾 Auto-confirmed and saved segment {seg.id} to TM")
|
|
39116
|
+
except Exception as e:
|
|
39117
|
+
self.log(f"⚠️ Error saving auto-confirmed segment to TM: {e}")
|
|
39118
|
+
|
|
39119
|
+
# Continue to the NEXT unconfirmed segment (skip this one)
|
|
39120
|
+
self.confirm_and_next_unconfirmed()
|
|
39121
|
+
except Exception as e:
|
|
39122
|
+
self.log(f"⚠️ Error in auto-confirm check: {e}")
|
|
39123
|
+
|
|
38794
39124
|
def confirm_selected_or_next(self):
|
|
38795
39125
|
"""Smart confirm: if multiple segments selected, confirm all; otherwise confirm and go to next.
|
|
38796
39126
|
|
|
@@ -43941,12 +44271,12 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
43941
44271
|
self._pending_mt_llm_segment = segment
|
|
43942
44272
|
self._pending_termbase_matches = termbase_matches or []
|
|
43943
44273
|
|
|
43944
|
-
# Start debounced timer - only call APIs after user stops
|
|
44274
|
+
# Start debounced timer - only call APIs after user stops navigating
|
|
43945
44275
|
from PyQt6.QtCore import QTimer
|
|
43946
44276
|
self._mt_llm_timer = QTimer()
|
|
43947
44277
|
self._mt_llm_timer.setSingleShot(True)
|
|
43948
44278
|
self._mt_llm_timer.timeout.connect(lambda: self._execute_mt_llm_lookup())
|
|
43949
|
-
self._mt_llm_timer.start(
|
|
44279
|
+
self._mt_llm_timer.start(150) # Wait 150ms of inactivity before external API calls
|
|
43950
44280
|
|
|
43951
44281
|
except Exception as e:
|
|
43952
44282
|
self.log(f"Error scheduling MT/LLM search: {e}")
|
|
@@ -43971,7 +44301,21 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
43971
44301
|
"""Search for TM, MT and LLM matches - called only after debounce delay"""
|
|
43972
44302
|
try:
|
|
43973
44303
|
from modules.translation_results_panel import TranslationMatch
|
|
43974
|
-
|
|
44304
|
+
|
|
44305
|
+
# v1.9.182: Validate we're still on the same segment before displaying results
|
|
44306
|
+
# This prevents stale results from showing when user navigates quickly
|
|
44307
|
+
current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
|
|
44308
|
+
if current_row >= 0:
|
|
44309
|
+
id_item = self.table.item(current_row, 0)
|
|
44310
|
+
if id_item:
|
|
44311
|
+
try:
|
|
44312
|
+
current_segment_id = int(id_item.text())
|
|
44313
|
+
if current_segment_id != segment.id:
|
|
44314
|
+
# User has moved to a different segment - abort this lookup
|
|
44315
|
+
return
|
|
44316
|
+
except (ValueError, AttributeError):
|
|
44317
|
+
pass
|
|
44318
|
+
|
|
43975
44319
|
# Get current project languages for all translation services
|
|
43976
44320
|
source_lang = getattr(self.current_project, 'source_lang', None) if self.current_project else None
|
|
43977
44321
|
target_lang = getattr(self.current_project, 'target_lang', None) if self.current_project else None
|
|
@@ -44044,6 +44388,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
44044
44388
|
|
|
44045
44389
|
# Show TM matches immediately (progressive loading)
|
|
44046
44390
|
if matches_dict["TM"]:
|
|
44391
|
+
# v1.9.182: Re-validate we're still on same segment before displaying
|
|
44392
|
+
current_row = self.table.currentRow() if hasattr(self, 'table') and self.table else -1
|
|
44393
|
+
if current_row >= 0:
|
|
44394
|
+
id_item = self.table.item(current_row, 0)
|
|
44395
|
+
if id_item:
|
|
44396
|
+
try:
|
|
44397
|
+
current_segment_id = int(id_item.text())
|
|
44398
|
+
if current_segment_id != segment.id:
|
|
44399
|
+
# User moved - still cache results but don't display
|
|
44400
|
+
with self.translation_matches_cache_lock:
|
|
44401
|
+
if segment.id in self.translation_matches_cache:
|
|
44402
|
+
self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
|
|
44403
|
+
return # Don't display stale results
|
|
44404
|
+
except (ValueError, AttributeError):
|
|
44405
|
+
pass
|
|
44406
|
+
|
|
44047
44407
|
tm_only = {"TM": matches_dict["TM"]}
|
|
44048
44408
|
if hasattr(self, 'results_panels') and self.results_panels:
|
|
44049
44409
|
for panel in self.results_panels:
|
|
@@ -44097,6 +44457,16 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
44097
44457
|
has_fuzzy_match = any(float(tm.relevance) < 99.5 and float(tm.relevance) >= 50 for tm in matches_dict["TM"])
|
|
44098
44458
|
if has_fuzzy_match and not has_100_match:
|
|
44099
44459
|
self._play_sound_effect('tm_fuzzy_match')
|
|
44460
|
+
|
|
44461
|
+
# v1.9.182: Update cache with TM results so subsequent visits are instant
|
|
44462
|
+
if matches_dict["TM"]:
|
|
44463
|
+
with self.translation_matches_cache_lock:
|
|
44464
|
+
if segment.id in self.translation_matches_cache:
|
|
44465
|
+
# Merge TM results into existing cache entry
|
|
44466
|
+
self.translation_matches_cache[segment.id]["TM"] = matches_dict["TM"]
|
|
44467
|
+
else:
|
|
44468
|
+
# Create new cache entry with TM results
|
|
44469
|
+
self.translation_matches_cache[segment.id] = matches_dict
|
|
44100
44470
|
except Exception as e:
|
|
44101
44471
|
self.log(f"Error in delayed TM search: {e}")
|
|
44102
44472
|
|
|
@@ -46675,10 +47045,8 @@ class SuperlookupTab(QWidget):
|
|
|
46675
47045
|
for row in db_manager.cursor.fetchall():
|
|
46676
47046
|
if row[0]:
|
|
46677
47047
|
all_languages.add(row[0])
|
|
46678
|
-
except Exception
|
|
46679
|
-
|
|
46680
|
-
else:
|
|
46681
|
-
print(f"[DEBUG] No db_manager available for language population")
|
|
47048
|
+
except Exception:
|
|
47049
|
+
pass # Silent failure for language population
|
|
46682
47050
|
|
|
46683
47051
|
# Get languages from termbases
|
|
46684
47052
|
if termbase_mgr:
|
|
@@ -46689,8 +47057,8 @@ class SuperlookupTab(QWidget):
|
|
|
46689
47057
|
all_languages.add(tb['source_lang'])
|
|
46690
47058
|
if tb.get('target_lang'):
|
|
46691
47059
|
all_languages.add(tb['target_lang'])
|
|
46692
|
-
except Exception
|
|
46693
|
-
|
|
47060
|
+
except Exception:
|
|
47061
|
+
pass # Silent failure for language population
|
|
46694
47062
|
|
|
46695
47063
|
# Group languages by their base language name
|
|
46696
47064
|
# E.g., "en", "en-US", "en-GB", "English" all map to "English"
|
|
@@ -46720,8 +47088,6 @@ class SuperlookupTab(QWidget):
|
|
|
46720
47088
|
# Store variants list as the data for this item
|
|
46721
47089
|
self.lang_from_combo.addItem(base_name, variants)
|
|
46722
47090
|
self.lang_to_combo.addItem(base_name, variants)
|
|
46723
|
-
|
|
46724
|
-
print(f"[DEBUG] Populated language dropdowns with {len(sorted_base_langs)} base languages (from {len(all_languages)} variants)")
|
|
46725
47091
|
|
|
46726
47092
|
def _get_base_language_name(self, lang_code):
|
|
46727
47093
|
"""Extract the base language name from any language code or name.
|
|
@@ -46908,37 +47274,20 @@ class SuperlookupTab(QWidget):
|
|
|
46908
47274
|
selected_tm_ids = self.get_selected_tm_ids()
|
|
46909
47275
|
search_direction = self.get_search_direction()
|
|
46910
47276
|
from_lang, to_lang = self.get_language_filters()
|
|
46911
|
-
|
|
46912
|
-
# Write language info to debug file
|
|
46913
|
-
with open('superlookup_debug.txt', 'a') as f:
|
|
46914
|
-
f.write(f"Language filters: from_lang='{from_lang}', to_lang='{to_lang}'\\n")
|
|
46915
|
-
f.write(f"Search direction: {search_direction}\\n")
|
|
46916
|
-
|
|
46917
|
-
print(f"[DEBUG] Superlookup: Selected TM IDs: {selected_tm_ids}, direction: {search_direction}", flush=True)
|
|
46918
|
-
print(f"[DEBUG] Superlookup: Language filters: from={from_lang}, to={to_lang}", flush=True)
|
|
46919
|
-
print(f"[DEBUG] Superlookup: tm_database = {self.tm_database}", flush=True)
|
|
47277
|
+
|
|
46920
47278
|
if self.engine:
|
|
46921
47279
|
self.engine.set_enabled_tm_ids(selected_tm_ids if selected_tm_ids else None)
|
|
46922
47280
|
|
|
46923
47281
|
# Perform TM lookup with direction and language filters
|
|
46924
47282
|
tm_results = []
|
|
46925
47283
|
if self.tm_database:
|
|
46926
|
-
|
|
46927
|
-
tm_results = self.engine.search_tm(text, direction=search_direction,
|
|
47284
|
+
tm_results = self.engine.search_tm(text, direction=search_direction,
|
|
46928
47285
|
source_lang=from_lang, target_lang=to_lang)
|
|
46929
|
-
|
|
46930
|
-
else:
|
|
46931
|
-
print(f"[DEBUG] Superlookup: tm_database is None, skipping TM search!", flush=True)
|
|
46932
|
-
|
|
47286
|
+
|
|
46933
47287
|
# Perform termbase lookup (search Supervertaler termbases directly)
|
|
46934
|
-
print(f"[DEBUG] About to call search_termbases with from_lang='{from_lang}', to_lang='{to_lang}'", flush=True)
|
|
46935
47288
|
try:
|
|
46936
47289
|
termbase_results = self.search_termbases(text, source_lang=from_lang, target_lang=to_lang)
|
|
46937
|
-
|
|
46938
|
-
except Exception as e:
|
|
46939
|
-
print(f"[DEBUG] ERROR in search_termbases: {e}", flush=True)
|
|
46940
|
-
import traceback
|
|
46941
|
-
traceback.print_exc()
|
|
47290
|
+
except Exception:
|
|
46942
47291
|
termbase_results = []
|
|
46943
47292
|
|
|
46944
47293
|
# Perform Supermemory semantic search
|