supervertaler 1.9.153__py3-none-any.whl → 1.9.185__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of supervertaler might be problematic. Click here for more details.
- Supervertaler.py +3450 -1135
- modules/database_manager.py +313 -120
- modules/database_migrations.py +54 -7
- modules/extract_tm.py +518 -0
- modules/keyboard_shortcuts_widget.py +7 -0
- modules/mqxliff_handler.py +71 -2
- modules/project_tm.py +320 -0
- modules/superlookup.py +12 -8
- modules/tag_manager.py +20 -2
- modules/termbase_manager.py +105 -2
- modules/termview_widget.py +82 -42
- modules/theme_manager.py +41 -4
- modules/tm_metadata_manager.py +59 -13
- modules/translation_memory.py +4 -13
- modules/translation_results_panel.py +0 -7
- modules/unified_prompt_library.py +2 -2
- modules/unified_prompt_manager_qt.py +47 -18
- supervertaler-1.9.185.dist-info/METADATA +151 -0
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.185.dist-info}/RECORD +23 -21
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.185.dist-info}/WHEEL +1 -1
- supervertaler-1.9.153.dist-info/METADATA +0 -896
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.185.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.185.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.153.dist-info → supervertaler-1.9.185.dist-info}/top_level.txt +0 -0
modules/termview_widget.py
CHANGED
|
@@ -172,7 +172,7 @@ class TermBlock(QWidget):
|
|
|
172
172
|
# Get theme colors
|
|
173
173
|
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
174
174
|
separator_color = "#555555" if is_dark else "#CCCCCC"
|
|
175
|
-
source_text_color = "#
|
|
175
|
+
source_text_color = "#FFFFFF" if is_dark else "#333"
|
|
176
176
|
no_match_color = "#666666" if is_dark else "#ddd"
|
|
177
177
|
no_match_bg = "#2A2A2A" if is_dark else "#F5F5F5"
|
|
178
178
|
|
|
@@ -224,10 +224,17 @@ class TermBlock(QWidget):
|
|
|
224
224
|
if self.translations:
|
|
225
225
|
target_text = primary_translation.get('target_term', primary_translation.get('target', ''))
|
|
226
226
|
termbase_name = primary_translation.get('termbase_name', '')
|
|
227
|
-
|
|
228
|
-
# Background color based on termbase type
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
|
|
228
|
+
# Background color based on termbase type (theme-aware)
|
|
229
|
+
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
230
|
+
if is_dark:
|
|
231
|
+
# Dark mode: darker backgrounds
|
|
232
|
+
bg_color = "#4A2D3A" if self.is_effective_project else "#2D3E4A" # Dark pink/blue
|
|
233
|
+
hover_color = "#5A3D4A" if self.is_effective_project else "#3D4E5A" # Lighter on hover
|
|
234
|
+
else:
|
|
235
|
+
# Light mode: original colors
|
|
236
|
+
bg_color = "#FFE5F0" if self.is_effective_project else "#D6EBFF" # Pink for project, light blue for regular
|
|
237
|
+
hover_color = "#FFD0E8" if self.is_effective_project else "#BBDEFB" # Slightly darker on hover
|
|
231
238
|
|
|
232
239
|
# Create horizontal layout for target + shortcut badge
|
|
233
240
|
# Apply background to container so it covers both text and badge
|
|
@@ -251,9 +258,11 @@ class TermBlock(QWidget):
|
|
|
251
258
|
target_font.setBold(self.font_bold)
|
|
252
259
|
target_label.setFont(target_font)
|
|
253
260
|
target_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
261
|
+
# Theme-aware text color
|
|
262
|
+
target_text_color = "#B0C4DE" if is_dark else "#0052A3" # Light blue in dark mode
|
|
254
263
|
target_label.setStyleSheet(f"""
|
|
255
264
|
QLabel {{
|
|
256
|
-
color:
|
|
265
|
+
color: {target_text_color};
|
|
257
266
|
padding: 0px;
|
|
258
267
|
background-color: transparent;
|
|
259
268
|
border: none;
|
|
@@ -312,11 +321,12 @@ class TermBlock(QWidget):
|
|
|
312
321
|
if len(self.translations) > 1:
|
|
313
322
|
count_label = QLabel(f"+{len(self.translations) - 1}")
|
|
314
323
|
count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
|
|
325
|
+
count_label.setStyleSheet(f"""
|
|
326
|
+
QLabel {{
|
|
327
|
+
color: {count_color};
|
|
318
328
|
font-size: 7px;
|
|
319
|
-
}
|
|
329
|
+
}}
|
|
320
330
|
""")
|
|
321
331
|
layout.addWidget(count_label)
|
|
322
332
|
return
|
|
@@ -336,10 +346,13 @@ class TermBlock(QWidget):
|
|
|
336
346
|
badge_label = QLabel(badge_text)
|
|
337
347
|
badge_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
338
348
|
badge_label.setFixedSize(badge_width, 14)
|
|
349
|
+
# Theme-aware badge colors
|
|
350
|
+
badge_bg = "#4A90E2" if is_dark else "#1976D2" # Lighter blue in dark mode
|
|
351
|
+
badge_text_color = "#FFFFFF" if is_dark else "white"
|
|
339
352
|
badge_label.setStyleSheet(f"""
|
|
340
353
|
QLabel {{
|
|
341
|
-
background-color:
|
|
342
|
-
color:
|
|
354
|
+
background-color: {badge_bg};
|
|
355
|
+
color: {badge_text_color};
|
|
343
356
|
font-size: 9px;
|
|
344
357
|
font-weight: bold;
|
|
345
358
|
border-radius: 7px;
|
|
@@ -352,16 +365,17 @@ class TermBlock(QWidget):
|
|
|
352
365
|
target_layout.addWidget(badge_label)
|
|
353
366
|
|
|
354
367
|
layout.addWidget(target_container)
|
|
355
|
-
|
|
368
|
+
|
|
356
369
|
# Show count if multiple translations - very compact
|
|
357
370
|
if len(self.translations) > 1:
|
|
358
371
|
count_label = QLabel(f"+{len(self.translations) - 1}")
|
|
359
372
|
count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
count_color = "#AAA" if is_dark else "#999" # Lighter in dark mode
|
|
374
|
+
count_label.setStyleSheet(f"""
|
|
375
|
+
QLabel {{
|
|
376
|
+
color: {count_color};
|
|
363
377
|
font-size: 7px;
|
|
364
|
-
}
|
|
378
|
+
}}
|
|
365
379
|
""")
|
|
366
380
|
layout.addWidget(count_label)
|
|
367
381
|
else:
|
|
@@ -439,7 +453,7 @@ class NTBlock(QWidget):
|
|
|
439
453
|
|
|
440
454
|
# Get theme colors
|
|
441
455
|
is_dark = self.theme_manager and self.theme_manager.current_theme.name == "Dark"
|
|
442
|
-
source_text_color = "#
|
|
456
|
+
source_text_color = "#FFFFFF" if is_dark else "#5D4E37"
|
|
443
457
|
|
|
444
458
|
# Pastel yellow border for non-translatables
|
|
445
459
|
border_color = "#E6C200" # Darker yellow for border
|
|
@@ -515,6 +529,9 @@ class TermviewWidget(QWidget):
|
|
|
515
529
|
self.current_target_lang = None
|
|
516
530
|
self.current_project_id = None # Store project ID for termbase priority lookup
|
|
517
531
|
|
|
532
|
+
# Debug mode - disable verbose tokenization logging by default (performance)
|
|
533
|
+
self.debug_tokenize = False
|
|
534
|
+
|
|
518
535
|
# Default font settings (will be updated from main app settings)
|
|
519
536
|
self.current_font_family = "Segoe UI"
|
|
520
537
|
self.current_font_size = 10
|
|
@@ -634,6 +651,17 @@ class TermviewWidget(QWidget):
|
|
|
634
651
|
is_dark = theme.name == "Dark"
|
|
635
652
|
info_label_color = "#909090" if is_dark else info_text
|
|
636
653
|
self.info_label.setStyleSheet(f"color: {info_label_color}; font-size: 10px; padding: 5px;")
|
|
654
|
+
|
|
655
|
+
# Refresh term blocks to pick up new theme colors
|
|
656
|
+
if hasattr(self, '_last_termbase_matches') and hasattr(self, '_last_nt_matches') and hasattr(self, 'current_source'):
|
|
657
|
+
# Re-render with stored matches to apply new theme colors
|
|
658
|
+
if self.current_source:
|
|
659
|
+
self.update_with_matches(
|
|
660
|
+
self.current_source,
|
|
661
|
+
self._last_termbase_matches or [],
|
|
662
|
+
self._last_nt_matches,
|
|
663
|
+
self._status_hint if hasattr(self, '_status_hint') else None
|
|
664
|
+
)
|
|
637
665
|
|
|
638
666
|
def set_font_settings(self, font_family: str = "Segoe UI", font_size: int = 10, bold: bool = False):
|
|
639
667
|
"""Update font settings for Termview
|
|
@@ -691,27 +719,31 @@ class TermviewWidget(QWidget):
|
|
|
691
719
|
font.setBold(self.current_font_bold)
|
|
692
720
|
block.source_label.setFont(font)
|
|
693
721
|
|
|
694
|
-
def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None):
|
|
722
|
+
def update_with_matches(self, source_text: str, termbase_matches: List[Dict], nt_matches: List[Dict] = None, status_hint: str = None):
|
|
695
723
|
"""
|
|
696
724
|
Update the termview display with pre-computed termbase and NT matches
|
|
697
|
-
|
|
725
|
+
|
|
698
726
|
RYS-STYLE DISPLAY: Show source text as tokens with translations underneath
|
|
699
|
-
|
|
727
|
+
|
|
700
728
|
Args:
|
|
701
729
|
source_text: Source segment text
|
|
702
730
|
termbase_matches: List of termbase match dicts from Translation Results
|
|
703
731
|
nt_matches: Optional list of NT match dicts with 'text', 'start', 'end', 'list_name' keys
|
|
732
|
+
status_hint: Optional hint about why there might be no matches (e.g., 'no_termbases_activated', 'wrong_language')
|
|
704
733
|
"""
|
|
705
734
|
self.current_source = source_text
|
|
706
|
-
|
|
735
|
+
# Store matches for theme refresh
|
|
736
|
+
self._last_termbase_matches = termbase_matches
|
|
737
|
+
self._last_nt_matches = nt_matches
|
|
738
|
+
|
|
707
739
|
# Clear existing blocks and shortcut mappings
|
|
708
740
|
self.clear_terms()
|
|
709
741
|
self.shortcut_terms = {} # Reset shortcut mappings
|
|
710
|
-
|
|
742
|
+
|
|
711
743
|
if not source_text or not source_text.strip():
|
|
712
744
|
self.info_label.setText("No segment selected")
|
|
713
745
|
return
|
|
714
|
-
|
|
746
|
+
|
|
715
747
|
# Strip HTML/XML tags from source text for display in TermView
|
|
716
748
|
# This handles CAT tool tags like <b>, </b>, <i>, </i>, <u>, </u>, <bi>, <sub>, <sup>, <li-o>, <li-b>
|
|
717
749
|
# as well as memoQ tags {1}, [2}, {3], Trados tags <1>, </1>, and Déjà Vu tags {00001}
|
|
@@ -722,17 +754,17 @@ class TermviewWidget(QWidget):
|
|
|
722
754
|
display_text = re.sub(r'\[[^\[\]]*\}', '', display_text) # Opening: [anything}
|
|
723
755
|
display_text = re.sub(r'\{[^\{\}]*\]', '', display_text) # Closing: {anything]
|
|
724
756
|
display_text = display_text.strip()
|
|
725
|
-
|
|
757
|
+
|
|
726
758
|
# If stripping tags leaves nothing, fall back to original
|
|
727
759
|
if not display_text:
|
|
728
760
|
display_text = source_text
|
|
729
|
-
|
|
761
|
+
|
|
730
762
|
has_termbase = termbase_matches and len(termbase_matches) > 0
|
|
731
763
|
has_nt = nt_matches and len(nt_matches) > 0
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
764
|
+
|
|
765
|
+
# Store status hint for info label (will be set at the end)
|
|
766
|
+
self._status_hint = status_hint
|
|
767
|
+
self._has_any_matches = has_termbase or has_nt
|
|
736
768
|
|
|
737
769
|
# Convert termbase matches to dict for easy lookup: {source_term.lower(): [translations]}
|
|
738
770
|
matches_dict = {}
|
|
@@ -820,7 +852,6 @@ class TermviewWidget(QWidget):
|
|
|
820
852
|
|
|
821
853
|
# Check if this is a non-translatable
|
|
822
854
|
if lookup_key in nt_dict:
|
|
823
|
-
# Create NT block
|
|
824
855
|
nt_block = NTBlock(token, nt_dict[lookup_key], self, theme_manager=self.theme_manager,
|
|
825
856
|
font_size=self.current_font_size, font_family=self.current_font_family,
|
|
826
857
|
font_bold=self.current_font_bold)
|
|
@@ -865,11 +896,18 @@ class TermviewWidget(QWidget):
|
|
|
865
896
|
info_parts.append(f"{blocks_with_translations} terms")
|
|
866
897
|
if blocks_with_nt > 0:
|
|
867
898
|
info_parts.append(f"{blocks_with_nt} NTs")
|
|
868
|
-
|
|
899
|
+
|
|
869
900
|
if info_parts:
|
|
870
901
|
self.info_label.setText(f"✓ Found {', '.join(info_parts)} in {len(tokens)} words")
|
|
871
902
|
else:
|
|
872
|
-
|
|
903
|
+
# Show appropriate message based on status hint when no matches
|
|
904
|
+
status_hint = getattr(self, '_status_hint', None)
|
|
905
|
+
if status_hint == 'no_termbases_activated':
|
|
906
|
+
self.info_label.setText(f"No glossaries activated ({len(tokens)} words)")
|
|
907
|
+
elif status_hint == 'wrong_language':
|
|
908
|
+
self.info_label.setText(f"Glossaries don't match language pair ({len(tokens)} words)")
|
|
909
|
+
else:
|
|
910
|
+
self.info_label.setText(f"No matches in {len(tokens)} words")
|
|
873
911
|
|
|
874
912
|
def get_all_termbase_matches(self, text: str) -> Dict[str, List[Dict]]:
|
|
875
913
|
"""
|
|
@@ -996,9 +1034,9 @@ class TermviewWidget(QWidget):
|
|
|
996
1034
|
Returns:
|
|
997
1035
|
List of tokens (words/phrases/numbers), with multi-word terms kept together
|
|
998
1036
|
"""
|
|
999
|
-
# DEBUG: Log multi-word terms we're looking for
|
|
1037
|
+
# DEBUG: Log multi-word terms we're looking for (only if debug_tokenize enabled)
|
|
1000
1038
|
multi_word_terms = [k for k in matches.keys() if ' ' in k]
|
|
1001
|
-
if multi_word_terms:
|
|
1039
|
+
if multi_word_terms and self.debug_tokenize:
|
|
1002
1040
|
self.log(f"🔍 Tokenize: Looking for {len(multi_word_terms)} multi-word terms:")
|
|
1003
1041
|
for term in sorted(multi_word_terms, key=len, reverse=True)[:3]:
|
|
1004
1042
|
self.log(f" - '{term}'")
|
|
@@ -1023,11 +1061,12 @@ class TermviewWidget(QWidget):
|
|
|
1023
1061
|
else:
|
|
1024
1062
|
pattern = r'\b' + term_escaped + r'\b'
|
|
1025
1063
|
|
|
1026
|
-
# DEBUG: Check if multi-word term is found
|
|
1064
|
+
# DEBUG: Check if multi-word term is found (only if debug_tokenize enabled)
|
|
1027
1065
|
found = re.search(pattern, text_lower)
|
|
1028
|
-
self.
|
|
1029
|
-
|
|
1030
|
-
|
|
1066
|
+
if self.debug_tokenize:
|
|
1067
|
+
self.log(f"🔍 Tokenize: Pattern '{pattern}' for '{term}' → {'FOUND' if found else 'NOT FOUND'}")
|
|
1068
|
+
if found:
|
|
1069
|
+
self.log(f" Match at position {found.span()}: '{text[found.start():found.end()]}'")
|
|
1031
1070
|
|
|
1032
1071
|
# Find all matches using regex
|
|
1033
1072
|
for match in re.finditer(pattern, text_lower):
|
|
@@ -1040,10 +1079,11 @@ class TermviewWidget(QWidget):
|
|
|
1040
1079
|
original_term = text[pos:pos + len(term)]
|
|
1041
1080
|
tokens_with_positions.append((pos, len(term), original_term))
|
|
1042
1081
|
used_positions.update(term_positions)
|
|
1043
|
-
self.
|
|
1082
|
+
if self.debug_tokenize:
|
|
1083
|
+
self.log(f" ✅ Added multi-word token: '{original_term}' covering positions {pos}-{pos+len(term)}")
|
|
1044
1084
|
|
|
1045
|
-
# DEBUG: Log used_positions after first pass
|
|
1046
|
-
if ' ' in sorted(matches.keys(), key=len, reverse=True)[0]:
|
|
1085
|
+
# DEBUG: Log used_positions after first pass (only if debug_tokenize enabled)
|
|
1086
|
+
if matches and ' ' in sorted(matches.keys(), key=len, reverse=True)[0] and self.debug_tokenize:
|
|
1047
1087
|
self.log(f"🔍 After first pass: {len(used_positions)} positions marked as used")
|
|
1048
1088
|
self.log(f" Used positions: {sorted(list(used_positions))[:20]}...")
|
|
1049
1089
|
|
modules/theme_manager.py
CHANGED
|
@@ -212,7 +212,10 @@ class ThemeManager:
|
|
|
212
212
|
self.themes_file = user_data_path / "themes.json"
|
|
213
213
|
self.current_theme: Theme = self.PREDEFINED_THEMES["Light (Default)"]
|
|
214
214
|
self.custom_themes: Dict[str, Theme] = {}
|
|
215
|
-
|
|
215
|
+
|
|
216
|
+
# Global UI font scale (50-200%, default 100%)
|
|
217
|
+
self.font_scale: int = 100
|
|
218
|
+
|
|
216
219
|
# Load custom themes
|
|
217
220
|
self.load_custom_themes()
|
|
218
221
|
|
|
@@ -289,14 +292,48 @@ class ThemeManager:
|
|
|
289
292
|
def apply_theme(self, app: QApplication):
|
|
290
293
|
"""
|
|
291
294
|
Apply current theme to application
|
|
292
|
-
|
|
295
|
+
|
|
293
296
|
Args:
|
|
294
297
|
app: QApplication instance
|
|
295
298
|
"""
|
|
296
299
|
theme = self.current_theme
|
|
297
|
-
|
|
300
|
+
|
|
301
|
+
# Calculate scaled font sizes based on font_scale (default 100%)
|
|
302
|
+
base_font_size = int(10 * self.font_scale / 100) # Base: 10pt at 100%
|
|
303
|
+
small_font_size = max(7, int(9 * self.font_scale / 100)) # Small text (status bar)
|
|
304
|
+
|
|
305
|
+
# Font scaling rules (only applied if scale != 100%)
|
|
306
|
+
font_rules = ""
|
|
307
|
+
if self.font_scale != 100:
|
|
308
|
+
font_rules = f"""
|
|
309
|
+
/* Global font scaling ({self.font_scale}%) */
|
|
310
|
+
QWidget {{ font-size: {base_font_size}pt; }}
|
|
311
|
+
QMenuBar {{ font-size: {base_font_size}pt; }}
|
|
312
|
+
QMenuBar::item {{ font-size: {base_font_size}pt; }}
|
|
313
|
+
QMenu {{ font-size: {base_font_size}pt; }}
|
|
314
|
+
QMenu::item {{ font-size: {base_font_size}pt; }}
|
|
315
|
+
QStatusBar {{ font-size: {small_font_size}pt; }}
|
|
316
|
+
QTabBar::tab {{ font-size: {base_font_size}pt; }}
|
|
317
|
+
QToolBar {{ font-size: {base_font_size}pt; }}
|
|
318
|
+
QLabel {{ font-size: {base_font_size}pt; }}
|
|
319
|
+
QCheckBox {{ font-size: {base_font_size}pt; }}
|
|
320
|
+
QRadioButton {{ font-size: {base_font_size}pt; }}
|
|
321
|
+
QComboBox {{ font-size: {base_font_size}pt; }}
|
|
322
|
+
QSpinBox {{ font-size: {base_font_size}pt; }}
|
|
323
|
+
QDoubleSpinBox {{ font-size: {base_font_size}pt; }}
|
|
324
|
+
QLineEdit {{ font-size: {base_font_size}pt; }}
|
|
325
|
+
QPushButton {{ font-size: {base_font_size}pt; }}
|
|
326
|
+
QGroupBox {{ font-size: {base_font_size}pt; }}
|
|
327
|
+
QGroupBox::title {{ font-size: {base_font_size}pt; }}
|
|
328
|
+
QTextEdit {{ font-size: {base_font_size}pt; }}
|
|
329
|
+
QPlainTextEdit {{ font-size: {base_font_size}pt; }}
|
|
330
|
+
QListWidget {{ font-size: {base_font_size}pt; }}
|
|
331
|
+
QTreeWidget {{ font-size: {base_font_size}pt; }}
|
|
332
|
+
QHeaderView::section {{ font-size: {base_font_size}pt; }}
|
|
333
|
+
"""
|
|
334
|
+
|
|
298
335
|
# Create and apply stylesheet - COLORS ONLY, preserves native sizes/spacing
|
|
299
|
-
stylesheet = f"""
|
|
336
|
+
stylesheet = font_rules + f"""
|
|
300
337
|
/* Main window background */
|
|
301
338
|
QMainWindow, QWidget {{
|
|
302
339
|
background-color: {theme.window_bg};
|
modules/tm_metadata_manager.py
CHANGED
|
@@ -344,19 +344,22 @@ class TMMetadataManager:
|
|
|
344
344
|
"""Check if a TM is active for a project (or global when project_id=0)"""
|
|
345
345
|
if project_id is None:
|
|
346
346
|
return False # If None (not 0), default to inactive
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
try:
|
|
349
349
|
cursor = self.db_manager.cursor
|
|
350
|
-
|
|
350
|
+
|
|
351
|
+
# Check if TM is active for this project OR globally (project_id=0)
|
|
351
352
|
cursor.execute("""
|
|
352
|
-
SELECT is_active FROM tm_activation
|
|
353
|
-
WHERE tm_id = ? AND project_id = ?
|
|
353
|
+
SELECT is_active FROM tm_activation
|
|
354
|
+
WHERE tm_id = ? AND (project_id = ? OR project_id = 0)
|
|
355
|
+
ORDER BY project_id DESC
|
|
354
356
|
""", (tm_db_id, project_id))
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
|
|
358
|
+
# Return True if any activation is active (project-specific takes priority due to ORDER BY)
|
|
359
|
+
for row in cursor.fetchall():
|
|
360
|
+
if bool(row[0]):
|
|
361
|
+
return True
|
|
362
|
+
|
|
360
363
|
# If no activation record exists, TM is inactive by default
|
|
361
364
|
return False
|
|
362
365
|
except Exception as e:
|
|
@@ -382,20 +385,63 @@ class TMMetadataManager:
|
|
|
382
385
|
|
|
383
386
|
try:
|
|
384
387
|
cursor = self.db_manager.cursor
|
|
385
|
-
|
|
388
|
+
|
|
386
389
|
# Only return TMs that have been explicitly activated (is_active = 1)
|
|
390
|
+
# Include both project-specific activations AND global activations (project_id=0)
|
|
387
391
|
cursor.execute("""
|
|
388
|
-
SELECT tm.tm_id
|
|
392
|
+
SELECT DISTINCT tm.tm_id
|
|
389
393
|
FROM translation_memories tm
|
|
390
394
|
INNER JOIN tm_activation ta ON tm.id = ta.tm_id
|
|
391
|
-
WHERE ta.project_id = ? AND ta.is_active = 1
|
|
395
|
+
WHERE (ta.project_id = ? OR ta.project_id = 0) AND ta.is_active = 1
|
|
392
396
|
""", (project_id,))
|
|
393
|
-
|
|
397
|
+
|
|
394
398
|
return [row[0] for row in cursor.fetchall()]
|
|
395
399
|
except Exception as e:
|
|
396
400
|
self.log(f"✗ Error fetching active tm_ids: {e}")
|
|
397
401
|
return []
|
|
398
402
|
|
|
403
|
+
def get_writable_tm_ids(self, project_id: Optional[int]) -> List[str]:
|
|
404
|
+
"""
|
|
405
|
+
Get list of writable tm_id strings for a project.
|
|
406
|
+
|
|
407
|
+
Returns TMs where:
|
|
408
|
+
- The TM has an activation record for this project AND
|
|
409
|
+
- read_only = 0 (Write checkbox is enabled)
|
|
410
|
+
|
|
411
|
+
This is used for SAVING segments to TM, separate from get_active_tm_ids()
|
|
412
|
+
which is used for READING/matching from TM.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
List of tm_id strings that are writable for the project
|
|
416
|
+
"""
|
|
417
|
+
if project_id is None:
|
|
418
|
+
# No project - return all writable TMs
|
|
419
|
+
try:
|
|
420
|
+
cursor = self.db_manager.cursor
|
|
421
|
+
cursor.execute("SELECT tm_id FROM translation_memories WHERE read_only = 0")
|
|
422
|
+
return [row[0] for row in cursor.fetchall()]
|
|
423
|
+
except Exception as e:
|
|
424
|
+
self.log(f"✗ Error fetching all writable tm_ids: {e}")
|
|
425
|
+
return []
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
cursor = self.db_manager.cursor
|
|
429
|
+
|
|
430
|
+
# Return TMs where Write checkbox is enabled (read_only = 0)
|
|
431
|
+
# AND the TM has an activation record for this project OR for global (project_id=0)
|
|
432
|
+
# This ensures TMs created when no project was loaded still work
|
|
433
|
+
cursor.execute("""
|
|
434
|
+
SELECT DISTINCT tm.tm_id
|
|
435
|
+
FROM translation_memories tm
|
|
436
|
+
INNER JOIN tm_activation ta ON tm.id = ta.tm_id
|
|
437
|
+
WHERE (ta.project_id = ? OR ta.project_id = 0) AND tm.read_only = 0
|
|
438
|
+
""", (project_id,))
|
|
439
|
+
|
|
440
|
+
return [row[0] for row in cursor.fetchall()]
|
|
441
|
+
except Exception as e:
|
|
442
|
+
self.log(f"✗ Error fetching writable tm_ids: {e}")
|
|
443
|
+
return []
|
|
444
|
+
|
|
399
445
|
# ========================================================================
|
|
400
446
|
# PROJECT TM MANAGEMENT (similar to termbases)
|
|
401
447
|
# ========================================================================
|
modules/translation_memory.py
CHANGED
|
@@ -123,7 +123,7 @@ class TMDatabase:
|
|
|
123
123
|
if source_lang and target_lang:
|
|
124
124
|
self.set_tm_languages(source_lang, target_lang)
|
|
125
125
|
|
|
126
|
-
# Global fuzzy threshold
|
|
126
|
+
# Global fuzzy threshold (75% minimum similarity for fuzzy matches)
|
|
127
127
|
self.fuzzy_threshold = 0.75
|
|
128
128
|
|
|
129
129
|
# TM metadata cache (populated from database as needed)
|
|
@@ -205,20 +205,14 @@ class TMDatabase:
|
|
|
205
205
|
Returns:
|
|
206
206
|
List of match dictionaries sorted by similarity
|
|
207
207
|
"""
|
|
208
|
-
print(f"[DEBUG] TMDatabase.search_all: source='{source[:50]}...', tm_ids={tm_ids}")
|
|
209
|
-
|
|
210
208
|
# Determine which TMs to search
|
|
211
209
|
# If tm_ids is None or empty, search ALL TMs (don't filter by tm_id)
|
|
212
210
|
if tm_ids is None and enabled_only:
|
|
213
211
|
tm_ids = [tm_id for tm_id, meta in self.tm_metadata.items() if meta.get('enabled', True)]
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
|
|
216
213
|
# If tm_ids is still empty, set to None to search ALL TMs
|
|
217
214
|
if tm_ids is not None and len(tm_ids) == 0:
|
|
218
215
|
tm_ids = None
|
|
219
|
-
print(f"[DEBUG] TMDatabase.search_all: Empty tm_ids, setting to None to search ALL")
|
|
220
|
-
|
|
221
|
-
print(f"[DEBUG] TMDatabase.search_all: Final tm_ids to search: {tm_ids}")
|
|
222
216
|
|
|
223
217
|
# First try exact match
|
|
224
218
|
exact_match = self.db.get_exact_match(
|
|
@@ -227,8 +221,7 @@ class TMDatabase:
|
|
|
227
221
|
source_lang=self.source_lang,
|
|
228
222
|
target_lang=self.target_lang
|
|
229
223
|
)
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
|
|
232
225
|
if exact_match:
|
|
233
226
|
# Format as match dictionary
|
|
234
227
|
return [{
|
|
@@ -241,7 +234,6 @@ class TMDatabase:
|
|
|
241
234
|
}]
|
|
242
235
|
|
|
243
236
|
# Try fuzzy matches
|
|
244
|
-
print(f"[DEBUG] TMDatabase.search_all: Calling fuzzy search with source_lang={self.source_lang}, target_lang={self.target_lang}")
|
|
245
237
|
fuzzy_matches = self.db.search_fuzzy_matches(
|
|
246
238
|
source=source,
|
|
247
239
|
tm_ids=tm_ids,
|
|
@@ -250,8 +242,7 @@ class TMDatabase:
|
|
|
250
242
|
source_lang=self.source_lang,
|
|
251
243
|
target_lang=self.target_lang
|
|
252
244
|
)
|
|
253
|
-
|
|
254
|
-
|
|
245
|
+
|
|
255
246
|
# Format matches for UI
|
|
256
247
|
formatted_matches = []
|
|
257
248
|
for match in fuzzy_matches:
|
|
@@ -1676,13 +1676,6 @@ class TranslationResultsPanel(QWidget):
|
|
|
1676
1676
|
Args:
|
|
1677
1677
|
matches_dict: Dict with keys like "NT", "MT", "TM", "Termbases"
|
|
1678
1678
|
"""
|
|
1679
|
-
print(f"🎯 TranslationResultsPanel.set_matches() called with matches_dict keys: {list(matches_dict.keys())}")
|
|
1680
|
-
for match_type, matches in matches_dict.items():
|
|
1681
|
-
print(f" {match_type}: {len(matches)} matches")
|
|
1682
|
-
if match_type == "Termbases" and matches:
|
|
1683
|
-
for i, match in enumerate(matches[:2]): # Show first 2 for debugging
|
|
1684
|
-
print(f" [{i}] {match.source} → {match.target}")
|
|
1685
|
-
|
|
1686
1679
|
# Ensure CompactMatchItem has current theme_manager
|
|
1687
1680
|
if self.theme_manager:
|
|
1688
1681
|
CompactMatchItem.theme_manager = self.theme_manager
|
|
@@ -367,7 +367,7 @@ class UnifiedPromptLibrary:
|
|
|
367
367
|
|
|
368
368
|
self.active_primary_prompt = self.prompts[relative_path]['content']
|
|
369
369
|
self.active_primary_prompt_path = relative_path
|
|
370
|
-
self.log(f"✓ Set
|
|
370
|
+
self.log(f"✓ Set custom prompt: {self.prompts[relative_path].get('name', relative_path)}")
|
|
371
371
|
return True
|
|
372
372
|
|
|
373
373
|
def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
|
|
@@ -399,7 +399,7 @@ class UnifiedPromptLibrary:
|
|
|
399
399
|
self.active_primary_prompt = content
|
|
400
400
|
self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
|
|
401
401
|
|
|
402
|
-
self.log(f"✓ Set external
|
|
402
|
+
self.log(f"✓ Set external custom prompt: {display_name}")
|
|
403
403
|
return True, display_name
|
|
404
404
|
|
|
405
405
|
def attach_prompt(self, relative_path: str) -> bool:
|
|
@@ -1566,9 +1566,9 @@ class UnifiedPromptManagerQt:
|
|
|
1566
1566
|
|
|
1567
1567
|
layout.addWidget(mode_frame)
|
|
1568
1568
|
|
|
1569
|
-
#
|
|
1569
|
+
# Custom Prompt
|
|
1570
1570
|
primary_layout = QHBoxLayout()
|
|
1571
|
-
primary_label = QLabel("
|
|
1571
|
+
primary_label = QLabel("Custom Prompt ⭐:")
|
|
1572
1572
|
primary_label.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold))
|
|
1573
1573
|
primary_layout.addWidget(primary_label)
|
|
1574
1574
|
|
|
@@ -2210,8 +2210,8 @@ class UnifiedPromptManagerQt:
|
|
|
2210
2210
|
if data['type'] == 'prompt':
|
|
2211
2211
|
path = data['path']
|
|
2212
2212
|
|
|
2213
|
-
# Set as
|
|
2214
|
-
action_primary = menu.addAction("⭐ Set as
|
|
2213
|
+
# Set as custom prompt
|
|
2214
|
+
action_primary = menu.addAction("⭐ Set as Custom Prompt")
|
|
2215
2215
|
action_primary.triggered.connect(lambda: self._set_primary_prompt(path))
|
|
2216
2216
|
|
|
2217
2217
|
# Attach/detach
|
|
@@ -2372,8 +2372,20 @@ class UnifiedPromptManagerQt:
|
|
|
2372
2372
|
else:
|
|
2373
2373
|
# Name unchanged, just update in place
|
|
2374
2374
|
if self.library.save_prompt(path, prompt_data):
|
|
2375
|
+
# Refresh active prompts if this prompt is currently active or attached
|
|
2376
|
+
# This ensures "Preview Combined" shows the updated content immediately
|
|
2377
|
+
if self.library.active_primary_prompt_path == path:
|
|
2378
|
+
# Update cached primary prompt content
|
|
2379
|
+
self.library.active_primary_prompt = self.library.prompts[path]['content']
|
|
2380
|
+
|
|
2381
|
+
if path in self.library.attached_prompt_paths:
|
|
2382
|
+
# Update cached attached prompt content
|
|
2383
|
+
idx = self.library.attached_prompt_paths.index(path)
|
|
2384
|
+
self.library.attached_prompts[idx] = self.library.prompts[path]['content']
|
|
2385
|
+
|
|
2375
2386
|
QMessageBox.information(self.main_widget, "Saved", "Prompt updated successfully!")
|
|
2376
2387
|
self._refresh_tree()
|
|
2388
|
+
self._update_attached_list() # Refresh attached list to show updated names
|
|
2377
2389
|
else:
|
|
2378
2390
|
QMessageBox.warning(self.main_widget, "Error", "Failed to save prompt")
|
|
2379
2391
|
else:
|
|
@@ -2493,7 +2505,7 @@ class UnifiedPromptManagerQt:
|
|
|
2493
2505
|
self.library.active_primary_prompt_path = None
|
|
2494
2506
|
self.primary_prompt_label.setText("[None selected]")
|
|
2495
2507
|
self.primary_prompt_label.setStyleSheet("color: #999;")
|
|
2496
|
-
self.log_message("✓ Cleared
|
|
2508
|
+
self.log_message("✓ Cleared custom prompt")
|
|
2497
2509
|
|
|
2498
2510
|
def _load_external_primary_prompt(self):
|
|
2499
2511
|
"""Load an external prompt file (not in library) as primary"""
|
|
@@ -2787,7 +2799,7 @@ class UnifiedPromptManagerQt:
|
|
|
2787
2799
|
composition_parts.append(f"📏 Total prompt length: {len(combined):,} characters")
|
|
2788
2800
|
|
|
2789
2801
|
if self.library.active_primary_prompt:
|
|
2790
|
-
composition_parts.append(f"✓
|
|
2802
|
+
composition_parts.append(f"✓ Custom prompt attached")
|
|
2791
2803
|
|
|
2792
2804
|
if self.library.attached_prompts:
|
|
2793
2805
|
composition_parts.append(f"✓ {len(self.library.attached_prompts)} additional prompt(s) attached")
|
|
@@ -2993,49 +3005,66 @@ If the text refers to figures (e.g., 'Figure 1A'), relevant images may be provid
|
|
|
2993
3005
|
|
|
2994
3006
|
# === Prompt Composition (for translation) ===
|
|
2995
3007
|
|
|
2996
|
-
def build_final_prompt(self, source_text: str, source_lang: str, target_lang: str,
|
|
3008
|
+
def build_final_prompt(self, source_text: str, source_lang: str, target_lang: str,
|
|
3009
|
+
mode: str = None, glossary_terms: list = None) -> str:
|
|
2997
3010
|
"""
|
|
2998
3011
|
Build final prompt for translation using 2-layer architecture:
|
|
2999
3012
|
1. System Prompt (auto-selected by mode)
|
|
3000
3013
|
2. Combined prompts from library (primary + attached)
|
|
3001
|
-
|
|
3014
|
+
3. Glossary terms (optional, injected before translation delimiter)
|
|
3015
|
+
|
|
3002
3016
|
Args:
|
|
3003
3017
|
source_text: Text to translate
|
|
3004
3018
|
source_lang: Source language
|
|
3005
3019
|
target_lang: Target language
|
|
3006
3020
|
mode: Override mode (if None, uses self.current_mode)
|
|
3007
|
-
|
|
3021
|
+
glossary_terms: Optional list of term dicts with 'source_term' and 'target_term' keys
|
|
3022
|
+
|
|
3008
3023
|
Returns:
|
|
3009
3024
|
Complete prompt ready for LLM
|
|
3010
3025
|
"""
|
|
3011
3026
|
if mode is None:
|
|
3012
3027
|
mode = self.current_mode
|
|
3013
|
-
|
|
3028
|
+
|
|
3014
3029
|
# Layer 1: System Prompt
|
|
3015
3030
|
system_template = self.get_system_template(mode)
|
|
3016
|
-
|
|
3031
|
+
|
|
3017
3032
|
# Replace placeholders in system prompt
|
|
3018
3033
|
system_template = system_template.replace("{{SOURCE_LANGUAGE}}", source_lang)
|
|
3019
3034
|
system_template = system_template.replace("{{TARGET_LANGUAGE}}", target_lang)
|
|
3020
3035
|
system_template = system_template.replace("{{SOURCE_TEXT}}", source_text)
|
|
3021
|
-
|
|
3036
|
+
|
|
3022
3037
|
# Layer 2: Library prompts (primary + attached)
|
|
3023
3038
|
library_prompts = ""
|
|
3024
|
-
|
|
3039
|
+
|
|
3025
3040
|
if self.library.active_primary_prompt:
|
|
3026
|
-
library_prompts += "\n\n#
|
|
3041
|
+
library_prompts += "\n\n# CUSTOM PROMPT\n\n"
|
|
3027
3042
|
library_prompts += self.library.active_primary_prompt
|
|
3028
|
-
|
|
3043
|
+
|
|
3029
3044
|
for attached_content in self.library.attached_prompts:
|
|
3030
3045
|
library_prompts += "\n\n# ADDITIONAL INSTRUCTIONS\n\n"
|
|
3031
3046
|
library_prompts += attached_content
|
|
3032
|
-
|
|
3047
|
+
|
|
3033
3048
|
# Combine
|
|
3034
3049
|
final_prompt = system_template + library_prompts
|
|
3035
|
-
|
|
3050
|
+
|
|
3051
|
+
# Glossary injection (if terms provided)
|
|
3052
|
+
if glossary_terms:
|
|
3053
|
+
final_prompt += "\n\n# GLOSSARY\n\n"
|
|
3054
|
+
final_prompt += "Use these approved terms in your translation:\n\n"
|
|
3055
|
+
for term in glossary_terms:
|
|
3056
|
+
source_term = term.get('source_term', '')
|
|
3057
|
+
target_term = term.get('target_term', '')
|
|
3058
|
+
if source_term and target_term:
|
|
3059
|
+
# Mark forbidden terms
|
|
3060
|
+
if term.get('forbidden'):
|
|
3061
|
+
final_prompt += f"- {source_term} → ⚠️ DO NOT USE: {target_term}\n"
|
|
3062
|
+
else:
|
|
3063
|
+
final_prompt += f"- {source_term} → {target_term}\n"
|
|
3064
|
+
|
|
3036
3065
|
# Add translation delimiter
|
|
3037
3066
|
final_prompt += "\n\n**YOUR TRANSLATION (provide ONLY the translated text, no numbering or labels):**\n"
|
|
3038
|
-
|
|
3067
|
+
|
|
3039
3068
|
return final_prompt
|
|
3040
3069
|
|
|
3041
3070
|
# ============================================================================
|