supervertaler 1.9.173__py3-none-any.whl → 1.9.175__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 +635 -171
- modules/database_manager.py +28 -6
- modules/database_migrations.py +54 -7
- modules/termbase_manager.py +105 -1
- modules/unified_prompt_library.py +2 -2
- modules/unified_prompt_manager_qt.py +35 -18
- supervertaler-1.9.175.dist-info/METADATA +151 -0
- {supervertaler-1.9.173.dist-info → supervertaler-1.9.175.dist-info}/RECORD +12 -12
- supervertaler-1.9.173.dist-info/METADATA +0 -936
- {supervertaler-1.9.173.dist-info → supervertaler-1.9.175.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.173.dist-info → supervertaler-1.9.175.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.173.dist-info → supervertaler-1.9.175.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.173.dist-info → supervertaler-1.9.175.dist-info}/top_level.txt +0 -0
Supervertaler.py
CHANGED
|
@@ -34,7 +34,7 @@ License: MIT
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
# Version Information.
|
|
37
|
-
__version__ = "1.9.
|
|
37
|
+
__version__ = "1.9.175"
|
|
38
38
|
__phase__ = "0.9"
|
|
39
39
|
__release_date__ = "2026-01-28"
|
|
40
40
|
__edition__ = "Qt"
|
|
@@ -753,7 +753,83 @@ def get_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
|
|
|
753
753
|
opening, closing = get_tag_pair(num)
|
|
754
754
|
if opening not in target_tags or closing not in target_tags:
|
|
755
755
|
return (opening, closing)
|
|
756
|
-
|
|
756
|
+
|
|
757
|
+
return (None, None)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def get_html_wrapping_tag_pair(source_text: str, target_text: str) -> tuple:
|
|
761
|
+
"""
|
|
762
|
+
Get the next available HTML tag pair for wrapping selected text.
|
|
763
|
+
|
|
764
|
+
Finds paired HTML tags (opening + closing) from source that are not yet
|
|
765
|
+
complete in target. Supports common formatting tags: b, i, u, em, strong,
|
|
766
|
+
span, li, p, a, sub, sup, etc.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
source_text: Source segment text with HTML tags
|
|
770
|
+
target_text: Current target text
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
Tuple of (opening_tag, closing_tag) or (None, None) if no pairs available
|
|
774
|
+
"""
|
|
775
|
+
import re
|
|
776
|
+
|
|
777
|
+
# Find all HTML tags in source
|
|
778
|
+
# Match: <tag>, <tag attr="...">, </tag>
|
|
779
|
+
tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?>'
|
|
780
|
+
source_matches = re.findall(tag_pattern, source_text)
|
|
781
|
+
target_matches = re.findall(tag_pattern, target_text)
|
|
782
|
+
|
|
783
|
+
if not source_matches:
|
|
784
|
+
return (None, None)
|
|
785
|
+
|
|
786
|
+
# Build lists of opening and closing tags in source
|
|
787
|
+
source_opening = [] # [(full_tag, tag_name), ...]
|
|
788
|
+
source_closing = []
|
|
789
|
+
|
|
790
|
+
for match in re.finditer(tag_pattern, source_text):
|
|
791
|
+
is_closing = match.group(1) == '/'
|
|
792
|
+
tag_name = match.group(2).lower()
|
|
793
|
+
full_tag = match.group(0)
|
|
794
|
+
|
|
795
|
+
if is_closing:
|
|
796
|
+
source_closing.append((full_tag, tag_name))
|
|
797
|
+
else:
|
|
798
|
+
source_opening.append((full_tag, tag_name))
|
|
799
|
+
|
|
800
|
+
# Build sets of tag names already in target
|
|
801
|
+
target_opening_names = set()
|
|
802
|
+
target_closing_names = set()
|
|
803
|
+
|
|
804
|
+
for is_closing, tag_name in target_matches:
|
|
805
|
+
if is_closing == '/':
|
|
806
|
+
target_closing_names.add(tag_name.lower())
|
|
807
|
+
else:
|
|
808
|
+
target_opening_names.add(tag_name.lower())
|
|
809
|
+
|
|
810
|
+
# Find first tag pair where both opening and closing exist in source
|
|
811
|
+
# but at least one is missing from target
|
|
812
|
+
seen_tags = set()
|
|
813
|
+
for full_tag, tag_name in source_opening:
|
|
814
|
+
if tag_name in seen_tags:
|
|
815
|
+
continue
|
|
816
|
+
seen_tags.add(tag_name)
|
|
817
|
+
|
|
818
|
+
# Check if there's a matching closing tag in source
|
|
819
|
+
has_closing = any(name == tag_name for _, name in source_closing)
|
|
820
|
+
if not has_closing:
|
|
821
|
+
continue
|
|
822
|
+
|
|
823
|
+
# Check if pair is incomplete in target
|
|
824
|
+
opening_in_target = tag_name in target_opening_names
|
|
825
|
+
closing_in_target = tag_name in target_closing_names
|
|
826
|
+
|
|
827
|
+
if not opening_in_target or not closing_in_target:
|
|
828
|
+
# Find the actual opening and closing tags from source
|
|
829
|
+
opening_tag = full_tag
|
|
830
|
+
closing_tag = f"</{tag_name}>"
|
|
831
|
+
return (opening_tag, closing_tag)
|
|
832
|
+
|
|
757
833
|
return (None, None)
|
|
758
834
|
|
|
759
835
|
|
|
@@ -1148,48 +1224,48 @@ class GridTextEditor(QTextEdit):
|
|
|
1148
1224
|
def _insert_next_tag_or_wrap_selection(self):
|
|
1149
1225
|
"""
|
|
1150
1226
|
Insert the next memoQ tag, HTML tag, or CafeTran pipe symbol from source, or wrap selection.
|
|
1151
|
-
|
|
1227
|
+
|
|
1152
1228
|
Behavior:
|
|
1153
|
-
- If text is selected: Wrap it with the next available tag pair [N}selection{N] or |selection|
|
|
1229
|
+
- If text is selected: Wrap it with the next available tag pair [N}selection{N] or <tag>selection</tag> or |selection|
|
|
1154
1230
|
- If no selection: Insert the next unused tag/pipe from source at cursor position
|
|
1155
|
-
|
|
1231
|
+
|
|
1156
1232
|
Supports:
|
|
1157
1233
|
- memoQ tags: [1}, {1], [2}, {2], etc.
|
|
1158
1234
|
- HTML/XML tags: <li>, </li>, <b>, </b>, <i>, </i>, etc.
|
|
1159
1235
|
- CafeTran pipe symbols: |
|
|
1160
|
-
|
|
1236
|
+
|
|
1161
1237
|
Shortcut: Ctrl+, (comma)
|
|
1162
1238
|
"""
|
|
1163
1239
|
# Get the main window and current segment
|
|
1164
1240
|
if not self.table_widget or self.current_row is None:
|
|
1165
1241
|
return
|
|
1166
|
-
|
|
1242
|
+
|
|
1167
1243
|
# Navigate up to find main window
|
|
1168
1244
|
main_window = self.table_widget.parent()
|
|
1169
1245
|
while main_window and not hasattr(main_window, 'current_project'):
|
|
1170
1246
|
main_window = main_window.parent()
|
|
1171
|
-
|
|
1247
|
+
|
|
1172
1248
|
if not main_window or not hasattr(main_window, 'current_project'):
|
|
1173
1249
|
return
|
|
1174
|
-
|
|
1250
|
+
|
|
1175
1251
|
if not main_window.current_project or self.current_row >= len(main_window.current_project.segments):
|
|
1176
1252
|
return
|
|
1177
|
-
|
|
1253
|
+
|
|
1178
1254
|
segment = main_window.current_project.segments[self.current_row]
|
|
1179
1255
|
source_text = segment.source
|
|
1180
1256
|
current_target = self.toPlainText()
|
|
1181
|
-
|
|
1257
|
+
|
|
1182
1258
|
# Check what type of tags are in the source
|
|
1183
1259
|
has_memoq_tags = bool(extract_memoq_tags(source_text))
|
|
1184
1260
|
has_html_tags = bool(extract_html_tags(source_text))
|
|
1185
1261
|
has_any_tags = has_memoq_tags or has_html_tags
|
|
1186
1262
|
has_pipe_symbols = '|' in source_text
|
|
1187
|
-
|
|
1263
|
+
|
|
1188
1264
|
# Check if there's a selection
|
|
1189
1265
|
cursor = self.textCursor()
|
|
1190
1266
|
if cursor.hasSelection():
|
|
1191
1267
|
selected_text = cursor.selectedText()
|
|
1192
|
-
|
|
1268
|
+
|
|
1193
1269
|
# Try memoQ tag pair first
|
|
1194
1270
|
if has_memoq_tags:
|
|
1195
1271
|
opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
|
|
@@ -1199,7 +1275,17 @@ class GridTextEditor(QTextEdit):
|
|
|
1199
1275
|
if hasattr(main_window, 'log'):
|
|
1200
1276
|
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
1201
1277
|
return
|
|
1202
|
-
|
|
1278
|
+
|
|
1279
|
+
# Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
|
|
1280
|
+
if has_html_tags:
|
|
1281
|
+
opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
|
|
1282
|
+
if opening_tag and closing_tag:
|
|
1283
|
+
wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
|
|
1284
|
+
cursor.insertText(wrapped_text)
|
|
1285
|
+
if hasattr(main_window, 'log'):
|
|
1286
|
+
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
1287
|
+
return
|
|
1288
|
+
|
|
1203
1289
|
# Try CafeTran pipe symbols
|
|
1204
1290
|
if has_pipe_symbols:
|
|
1205
1291
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -1210,12 +1296,12 @@ class GridTextEditor(QTextEdit):
|
|
|
1210
1296
|
if hasattr(main_window, 'log'):
|
|
1211
1297
|
main_window.log(f"🏷️ Wrapped selection with |...|")
|
|
1212
1298
|
return
|
|
1213
|
-
|
|
1299
|
+
|
|
1214
1300
|
if hasattr(main_window, 'log'):
|
|
1215
1301
|
main_window.log("⚠️ No tag pairs available from source")
|
|
1216
1302
|
else:
|
|
1217
1303
|
# No selection - insert next unused tag or pipe at cursor
|
|
1218
|
-
|
|
1304
|
+
|
|
1219
1305
|
# Try memoQ tags and HTML tags (find_next_unused_tag handles both)
|
|
1220
1306
|
if has_any_tags:
|
|
1221
1307
|
next_tag = find_next_unused_tag(source_text, current_target)
|
|
@@ -1224,7 +1310,7 @@ class GridTextEditor(QTextEdit):
|
|
|
1224
1310
|
if hasattr(main_window, 'log'):
|
|
1225
1311
|
main_window.log(f"🏷️ Inserted tag: {next_tag}")
|
|
1226
1312
|
return
|
|
1227
|
-
|
|
1313
|
+
|
|
1228
1314
|
# Try CafeTran pipe symbols
|
|
1229
1315
|
if has_pipe_symbols:
|
|
1230
1316
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -1233,7 +1319,7 @@ class GridTextEditor(QTextEdit):
|
|
|
1233
1319
|
if hasattr(main_window, 'log'):
|
|
1234
1320
|
main_window.log(f"🏷️ Inserted pipe symbol (|)")
|
|
1235
1321
|
return
|
|
1236
|
-
|
|
1322
|
+
|
|
1237
1323
|
if hasattr(main_window, 'log'):
|
|
1238
1324
|
main_window.log("✓ All tags from source already in target")
|
|
1239
1325
|
|
|
@@ -2080,16 +2166,27 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
2080
2166
|
# Use stored table reference and row number
|
|
2081
2167
|
if self.table_ref and self.row >= 0:
|
|
2082
2168
|
try:
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2169
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
2170
|
+
modifiers = event.modifiers()
|
|
2171
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
2172
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
2173
|
+
|
|
2174
|
+
if is_shift or is_ctrl:
|
|
2175
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
2176
|
+
# but don't call selectRow() which would clear the selection
|
|
2177
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2178
|
+
else:
|
|
2179
|
+
# Normal click - select just this row
|
|
2180
|
+
self.table_ref.selectRow(self.row)
|
|
2181
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2182
|
+
|
|
2183
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
2184
|
+
# Find the main window and call the method directly
|
|
2185
|
+
main_window = self.table_ref.parent()
|
|
2186
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
2187
|
+
main_window = main_window.parent()
|
|
2188
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
2189
|
+
main_window.on_cell_selected(self.row, 2, -1, -1)
|
|
2093
2190
|
except Exception as e:
|
|
2094
2191
|
print(f"Error triggering manual cell selection: {e}")
|
|
2095
2192
|
|
|
@@ -2166,20 +2263,32 @@ class ReadOnlyGridTextEditor(QTextEdit):
|
|
|
2166
2263
|
"""Select text when focused for easy copying and trigger row selection"""
|
|
2167
2264
|
super().focusInEvent(event)
|
|
2168
2265
|
# Don't auto-select - let user select manually
|
|
2169
|
-
|
|
2266
|
+
|
|
2170
2267
|
# Use stored table reference and row number
|
|
2171
2268
|
if self.table_ref and self.row >= 0:
|
|
2172
2269
|
try:
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2270
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
2271
|
+
from PyQt6.QtWidgets import QApplication
|
|
2272
|
+
modifiers = QApplication.keyboardModifiers()
|
|
2273
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
2274
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
2275
|
+
|
|
2276
|
+
if is_shift or is_ctrl:
|
|
2277
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
2278
|
+
# but don't call selectRow() which would clear the selection
|
|
2279
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2280
|
+
else:
|
|
2281
|
+
# Normal focus - select just this row
|
|
2282
|
+
self.table_ref.selectRow(self.row)
|
|
2283
|
+
self.table_ref.setCurrentCell(self.row, 2)
|
|
2284
|
+
|
|
2285
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
2286
|
+
# Find the main window and call the method directly
|
|
2287
|
+
main_window = self.table_ref.parent()
|
|
2288
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
2289
|
+
main_window = main_window.parent()
|
|
2290
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
2291
|
+
main_window.on_cell_selected(self.row, 2, -1, -1)
|
|
2183
2292
|
except Exception as e:
|
|
2184
2293
|
print(f"Error triggering manual cell selection: {e}")
|
|
2185
2294
|
|
|
@@ -3026,19 +3135,30 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3026
3135
|
super().mousePressEvent(event)
|
|
3027
3136
|
# Auto-select the row when clicking in the target cell
|
|
3028
3137
|
if self.table and self.row >= 0:
|
|
3029
|
-
|
|
3030
|
-
|
|
3138
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
3139
|
+
modifiers = event.modifiers()
|
|
3140
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
3141
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
3142
|
+
|
|
3143
|
+
if is_shift or is_ctrl:
|
|
3144
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
3145
|
+
# but don't call selectRow() which would clear the selection
|
|
3146
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3147
|
+
else:
|
|
3148
|
+
# Normal click - select just this row
|
|
3149
|
+
self.table.selectRow(self.row)
|
|
3150
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3031
3151
|
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3152
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
3153
|
+
# Find the main window and call the method directly
|
|
3154
|
+
try:
|
|
3155
|
+
main_window = self.table.parent()
|
|
3156
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
3157
|
+
main_window = main_window.parent()
|
|
3158
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
3159
|
+
main_window.on_cell_selected(self.row, 3, -1, -1)
|
|
3160
|
+
except Exception as e:
|
|
3161
|
+
print(f"Error triggering manual cell selection: {e}")
|
|
3042
3162
|
|
|
3043
3163
|
def mouseReleaseEvent(self, event):
|
|
3044
3164
|
"""Smart word selection - expand partial selections to full words
|
|
@@ -3108,19 +3228,31 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3108
3228
|
self.show()
|
|
3109
3229
|
# Auto-select the row when focusing the target cell
|
|
3110
3230
|
if self.table and self.row >= 0:
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3231
|
+
# Check for Shift or Ctrl modifier - let Qt handle native multi-selection
|
|
3232
|
+
from PyQt6.QtWidgets import QApplication
|
|
3233
|
+
modifiers = QApplication.keyboardModifiers()
|
|
3234
|
+
is_shift = modifiers & Qt.KeyboardModifier.ShiftModifier
|
|
3235
|
+
is_ctrl = modifiers & Qt.KeyboardModifier.ControlModifier
|
|
3236
|
+
|
|
3237
|
+
if is_shift or is_ctrl:
|
|
3238
|
+
# For Shift+click (range) or Ctrl+click (toggle), just set current cell
|
|
3239
|
+
# but don't call selectRow() which would clear the selection
|
|
3240
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3241
|
+
else:
|
|
3242
|
+
# Normal focus - select just this row
|
|
3243
|
+
self.table.selectRow(self.row)
|
|
3244
|
+
self.table.setCurrentCell(self.row, 3) # Column 3 is Target
|
|
3245
|
+
|
|
3246
|
+
# CRITICAL: Manually trigger on_cell_selected since signals aren't firing
|
|
3247
|
+
# Find the main window and call the method directly
|
|
3248
|
+
try:
|
|
3249
|
+
main_window = self.table.parent()
|
|
3250
|
+
while main_window and not hasattr(main_window, 'on_cell_selected'):
|
|
3251
|
+
main_window = main_window.parent()
|
|
3252
|
+
if main_window and hasattr(main_window, 'on_cell_selected'):
|
|
3253
|
+
main_window.on_cell_selected(self.row, 3, -1, -1)
|
|
3254
|
+
except Exception as e:
|
|
3255
|
+
print(f"Error triggering manual cell selection: {e}")
|
|
3124
3256
|
|
|
3125
3257
|
def keyPressEvent(self, event):
|
|
3126
3258
|
"""Handle Tab and Ctrl+E keys to cycle between source and target cells"""
|
|
@@ -3423,7 +3555,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3423
3555
|
cursor = self.textCursor()
|
|
3424
3556
|
if cursor.hasSelection():
|
|
3425
3557
|
selected_text = cursor.selectedText()
|
|
3426
|
-
|
|
3558
|
+
|
|
3427
3559
|
# Try memoQ tag pair first
|
|
3428
3560
|
if has_memoq_tags:
|
|
3429
3561
|
opening_tag, closing_tag = get_wrapping_tag_pair(source_text, current_target)
|
|
@@ -3433,7 +3565,17 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3433
3565
|
if hasattr(main_window, 'log'):
|
|
3434
3566
|
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
3435
3567
|
return
|
|
3436
|
-
|
|
3568
|
+
|
|
3569
|
+
# Try HTML tag pairs (e.g., <b>...</b>, <i>...</i>)
|
|
3570
|
+
if has_html_tags:
|
|
3571
|
+
opening_tag, closing_tag = get_html_wrapping_tag_pair(source_text, current_target)
|
|
3572
|
+
if opening_tag and closing_tag:
|
|
3573
|
+
wrapped_text = f"{opening_tag}{selected_text}{closing_tag}"
|
|
3574
|
+
cursor.insertText(wrapped_text)
|
|
3575
|
+
if hasattr(main_window, 'log'):
|
|
3576
|
+
main_window.log(f"🏷️ Wrapped selection with {opening_tag}...{closing_tag}")
|
|
3577
|
+
return
|
|
3578
|
+
|
|
3437
3579
|
# Try CafeTran pipe symbols
|
|
3438
3580
|
if has_pipe_symbols:
|
|
3439
3581
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -3444,12 +3586,12 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3444
3586
|
if hasattr(main_window, 'log'):
|
|
3445
3587
|
main_window.log(f"🏷️ Wrapped selection with |...|")
|
|
3446
3588
|
return
|
|
3447
|
-
|
|
3589
|
+
|
|
3448
3590
|
if hasattr(main_window, 'log'):
|
|
3449
3591
|
main_window.log("⚠️ No tag pairs available from source")
|
|
3450
3592
|
else:
|
|
3451
3593
|
# No selection - insert next unused tag or pipe at cursor
|
|
3452
|
-
|
|
3594
|
+
|
|
3453
3595
|
# Try memoQ tags and HTML tags (find_next_unused_tag handles both)
|
|
3454
3596
|
if has_any_tags:
|
|
3455
3597
|
next_tag = find_next_unused_tag(source_text, current_target)
|
|
@@ -3458,7 +3600,7 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3458
3600
|
if hasattr(main_window, 'log'):
|
|
3459
3601
|
main_window.log(f"🏷️ Inserted tag: {next_tag}")
|
|
3460
3602
|
return
|
|
3461
|
-
|
|
3603
|
+
|
|
3462
3604
|
# Try CafeTran pipe symbols
|
|
3463
3605
|
if has_pipe_symbols:
|
|
3464
3606
|
pipes_needed = get_next_pipe_count_needed(source_text, current_target)
|
|
@@ -3467,10 +3609,10 @@ class EditableGridTextEditor(QTextEdit):
|
|
|
3467
3609
|
if hasattr(main_window, 'log'):
|
|
3468
3610
|
main_window.log(f"🏷️ Inserted pipe symbol (|)")
|
|
3469
3611
|
return
|
|
3470
|
-
|
|
3612
|
+
|
|
3471
3613
|
if hasattr(main_window, 'log'):
|
|
3472
3614
|
main_window.log("✓ All tags from source already in target")
|
|
3473
|
-
|
|
3615
|
+
|
|
3474
3616
|
def _copy_source_to_target(self):
|
|
3475
3617
|
"""
|
|
3476
3618
|
Copy source text to target cell.
|
|
@@ -5179,12 +5321,14 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5179
5321
|
|
|
5180
5322
|
self.status_not_started = CheckmarkCheckBox("Not started")
|
|
5181
5323
|
self.status_edited = CheckmarkCheckBox("Edited")
|
|
5324
|
+
self.status_pretranslated = CheckmarkCheckBox("Pre-translated")
|
|
5182
5325
|
self.status_translated = CheckmarkCheckBox("Translated")
|
|
5183
5326
|
self.status_confirmed = CheckmarkCheckBox("Confirmed")
|
|
5184
5327
|
self.status_draft = CheckmarkCheckBox("Draft")
|
|
5185
|
-
|
|
5328
|
+
|
|
5186
5329
|
status_layout.addWidget(self.status_not_started)
|
|
5187
5330
|
status_layout.addWidget(self.status_edited)
|
|
5331
|
+
status_layout.addWidget(self.status_pretranslated)
|
|
5188
5332
|
status_layout.addWidget(self.status_translated)
|
|
5189
5333
|
status_layout.addWidget(self.status_confirmed)
|
|
5190
5334
|
status_layout.addWidget(self.status_draft)
|
|
@@ -5257,6 +5401,7 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5257
5401
|
|
|
5258
5402
|
self.status_not_started.setChecked(False)
|
|
5259
5403
|
self.status_edited.setChecked(False)
|
|
5404
|
+
self.status_pretranslated.setChecked(False)
|
|
5260
5405
|
self.status_translated.setChecked(False)
|
|
5261
5406
|
self.status_confirmed.setChecked(False)
|
|
5262
5407
|
self.status_draft.setChecked(False)
|
|
@@ -5283,6 +5428,8 @@ class AdvancedFiltersDialog(QDialog):
|
|
|
5283
5428
|
row_status.append('not_started')
|
|
5284
5429
|
if self.status_edited.isChecked():
|
|
5285
5430
|
row_status.append('edited')
|
|
5431
|
+
if self.status_pretranslated.isChecked():
|
|
5432
|
+
row_status.append('pretranslated')
|
|
5286
5433
|
if self.status_translated.isChecked():
|
|
5287
5434
|
row_status.append('translated')
|
|
5288
5435
|
if self.status_confirmed.isChecked():
|
|
@@ -5647,11 +5794,14 @@ class PreTranslationWorker(QThread):
|
|
|
5647
5794
|
custom_prompt = None
|
|
5648
5795
|
if self.prompt_manager:
|
|
5649
5796
|
try:
|
|
5797
|
+
# Get glossary terms for AI injection
|
|
5798
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5650
5799
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5651
5800
|
source_text=segment.source,
|
|
5652
5801
|
source_lang=source_lang,
|
|
5653
5802
|
target_lang=target_lang,
|
|
5654
|
-
mode="single"
|
|
5803
|
+
mode="single",
|
|
5804
|
+
glossary_terms=glossary_terms
|
|
5655
5805
|
)
|
|
5656
5806
|
# Extract just the instruction part (without the source text section)
|
|
5657
5807
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -5710,11 +5860,14 @@ class PreTranslationWorker(QThread):
|
|
|
5710
5860
|
if self.prompt_manager and batch_segments:
|
|
5711
5861
|
try:
|
|
5712
5862
|
first_segment = batch_segments[0][1]
|
|
5863
|
+
# Get glossary terms for AI injection
|
|
5864
|
+
glossary_terms = self.parent_app.get_ai_inject_glossary_terms() if hasattr(self.parent_app, 'get_ai_inject_glossary_terms') else []
|
|
5713
5865
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5714
5866
|
source_text=first_segment.source,
|
|
5715
5867
|
source_lang=source_lang,
|
|
5716
5868
|
target_lang=target_lang,
|
|
5717
|
-
mode="single"
|
|
5869
|
+
mode="single",
|
|
5870
|
+
glossary_terms=glossary_terms
|
|
5718
5871
|
)
|
|
5719
5872
|
# Extract just the instruction part
|
|
5720
5873
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -6161,6 +6314,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6161
6314
|
# TM Metadata Manager - needed for TM list in Superlookup
|
|
6162
6315
|
from modules.tm_metadata_manager import TMMetadataManager
|
|
6163
6316
|
self.tm_metadata_mgr = TMMetadataManager(self.db_manager, self.log)
|
|
6317
|
+
|
|
6318
|
+
# Termbase Manager - needed for glossary AI injection
|
|
6319
|
+
from modules.termbase_manager import TermbaseManager
|
|
6320
|
+
self.termbase_mgr = TermbaseManager(self.db_manager, self.log)
|
|
6164
6321
|
|
|
6165
6322
|
# Spellcheck Manager for target language spell checking
|
|
6166
6323
|
self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
|
|
@@ -7499,13 +7656,30 @@ class SupervertalerQt(QMainWindow):
|
|
|
7499
7656
|
|
|
7500
7657
|
# Bulk Operations submenu
|
|
7501
7658
|
bulk_menu = edit_menu.addMenu("Bulk &Operations")
|
|
7502
|
-
|
|
7659
|
+
|
|
7503
7660
|
confirm_selected_action = QAction("✅ &Confirm Selected Segments", self)
|
|
7504
7661
|
confirm_selected_action.setShortcut("Ctrl+Shift+Return")
|
|
7505
7662
|
confirm_selected_action.setToolTip("Confirm all selected segments (Ctrl+Shift+Enter)")
|
|
7506
7663
|
confirm_selected_action.triggered.connect(self.confirm_selected_segments_from_menu)
|
|
7507
7664
|
bulk_menu.addAction(confirm_selected_action)
|
|
7508
|
-
|
|
7665
|
+
|
|
7666
|
+
# Change Status submenu
|
|
7667
|
+
status_submenu = bulk_menu.addMenu("🏷️ Change &Status")
|
|
7668
|
+
user_statuses = [
|
|
7669
|
+
("not_started", "❌ &Not started"),
|
|
7670
|
+
("pretranslated", "🤖 &Pre-translated"),
|
|
7671
|
+
("translated", "✏️ &Translated"),
|
|
7672
|
+
("confirmed", "✔ &Confirmed"),
|
|
7673
|
+
("tr_confirmed", "🌟 T&R confirmed"),
|
|
7674
|
+
("proofread", "🟪 Proo&fread"),
|
|
7675
|
+
("approved", "⭐ &Approved"),
|
|
7676
|
+
("rejected", "🚫 Re&jected"),
|
|
7677
|
+
]
|
|
7678
|
+
for status_key, label in user_statuses:
|
|
7679
|
+
action = QAction(label, self)
|
|
7680
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s, from_menu=True))
|
|
7681
|
+
status_submenu.addAction(action)
|
|
7682
|
+
|
|
7509
7683
|
clear_translations_action = QAction("🗑️ &Clear Translations", self)
|
|
7510
7684
|
clear_translations_action.setToolTip("Clear translations for selected segments")
|
|
7511
7685
|
clear_translations_action.triggered.connect(self.clear_selected_translations_from_menu)
|
|
@@ -12586,7 +12760,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12586
12760
|
"💡 <b>Glossaries</b><br>"
|
|
12587
12761
|
"• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
|
|
12588
12762
|
"• <b>Write</b> (blue ✓): Glossary is updated with new terms<br>"
|
|
12589
|
-
"• <b>Priority</b>: Manually set 1-N (lower = higher priority).
|
|
12763
|
+
"• <b>Priority</b>: Manually set 1-N (lower = higher priority). Priority #1 = Project Glossary.<br>"
|
|
12764
|
+
"• <b>AI</b> (orange ✓): Send glossary terms to LLM with every translation (increases prompt size)"
|
|
12590
12765
|
)
|
|
12591
12766
|
help_msg.setWordWrap(True)
|
|
12592
12767
|
help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
|
|
@@ -12608,8 +12783,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12608
12783
|
# Termbase list with table
|
|
12609
12784
|
termbase_table = QTableWidget()
|
|
12610
12785
|
self.termbase_table = termbase_table # Store for external access (Superlookup navigation)
|
|
12611
|
-
termbase_table.setColumnCount(
|
|
12612
|
-
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority"])
|
|
12786
|
+
termbase_table.setColumnCount(8)
|
|
12787
|
+
termbase_table.setHorizontalHeaderLabels(["Type", "Name", "Languages", "Terms", "Read", "Write", "Priority", "AI"])
|
|
12613
12788
|
termbase_table.horizontalHeader().setStretchLastSection(False)
|
|
12614
12789
|
termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
|
|
12615
12790
|
termbase_table.setColumnWidth(1, 180) # Name
|
|
@@ -12618,6 +12793,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
12618
12793
|
termbase_table.setColumnWidth(4, 50) # Read checkbox
|
|
12619
12794
|
termbase_table.setColumnWidth(5, 50) # Write checkbox
|
|
12620
12795
|
termbase_table.setColumnWidth(6, 60) # Priority
|
|
12796
|
+
termbase_table.setColumnWidth(7, 40) # AI checkbox
|
|
12621
12797
|
termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
12622
12798
|
termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
|
12623
12799
|
termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
|
|
@@ -13311,7 +13487,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
13311
13487
|
priority_item.setToolTip("No priority - glossary not readable")
|
|
13312
13488
|
priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
13313
13489
|
termbase_table.setItem(row, 6, priority_item)
|
|
13314
|
-
|
|
13490
|
+
|
|
13491
|
+
# AI checkbox (purple/orange) - whether to inject terms into LLM prompts
|
|
13492
|
+
ai_enabled = termbase_mgr.get_termbase_ai_inject(tb['id'])
|
|
13493
|
+
ai_checkbox = OrangeCheckmarkCheckBox()
|
|
13494
|
+
ai_checkbox.setChecked(ai_enabled)
|
|
13495
|
+
ai_checkbox.setToolTip("AI: Send glossary terms to LLM with translation prompts")
|
|
13496
|
+
|
|
13497
|
+
def on_ai_toggle(checked, tb_id=tb['id'], tb_name=tb['name']):
|
|
13498
|
+
if checked:
|
|
13499
|
+
# Show warning when enabling
|
|
13500
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
13501
|
+
msg = QMessageBox()
|
|
13502
|
+
msg.setWindowTitle("Enable AI Injection")
|
|
13503
|
+
msg.setText(f"Enable AI injection for '{tb_name}'?")
|
|
13504
|
+
msg.setInformativeText(
|
|
13505
|
+
"When enabled, ALL terms from this glossary will be sent to the LLM "
|
|
13506
|
+
"with every translation request.\n\n"
|
|
13507
|
+
"This helps the AI consistently use your preferred terminology "
|
|
13508
|
+
"throughout the translation.\n\n"
|
|
13509
|
+
"Recommended for small, curated glossaries (< 500 terms)."
|
|
13510
|
+
)
|
|
13511
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
13512
|
+
msg.setDefaultButton(QMessageBox.StandardButton.Yes)
|
|
13513
|
+
if msg.exec() != QMessageBox.StandardButton.Yes:
|
|
13514
|
+
# User cancelled - revert checkbox
|
|
13515
|
+
sender = termbase_table.cellWidget(termbase_table.currentRow(), 7)
|
|
13516
|
+
if sender:
|
|
13517
|
+
sender.blockSignals(True)
|
|
13518
|
+
sender.setChecked(False)
|
|
13519
|
+
sender.blockSignals(False)
|
|
13520
|
+
return
|
|
13521
|
+
termbase_mgr.set_termbase_ai_inject(tb_id, checked)
|
|
13522
|
+
self.log(f"{'✅ Enabled' if checked else '❌ Disabled'} AI injection for glossary: {tb_name}")
|
|
13523
|
+
|
|
13524
|
+
ai_checkbox.toggled.connect(on_ai_toggle)
|
|
13525
|
+
termbase_table.setCellWidget(row, 7, ai_checkbox)
|
|
13526
|
+
|
|
13315
13527
|
# Update header checkbox states based on current selection
|
|
13316
13528
|
tb_read_header_checkbox.blockSignals(True)
|
|
13317
13529
|
tb_write_header_checkbox.blockSignals(True)
|
|
@@ -15408,7 +15620,57 @@ class SupervertalerQt(QMainWindow):
|
|
|
15408
15620
|
|
|
15409
15621
|
model_group.setLayout(model_layout)
|
|
15410
15622
|
layout.addWidget(model_group)
|
|
15411
|
-
|
|
15623
|
+
|
|
15624
|
+
# ========== SECTION 2b: Model Version Checker ==========
|
|
15625
|
+
version_check_group = QGroupBox("🔄 Model Version Checker")
|
|
15626
|
+
version_check_layout = QVBoxLayout()
|
|
15627
|
+
|
|
15628
|
+
version_check_info = QLabel(
|
|
15629
|
+
"Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
|
|
15630
|
+
"Get notified when new models are available and easily add them to Supervertaler."
|
|
15631
|
+
)
|
|
15632
|
+
version_check_info.setWordWrap(True)
|
|
15633
|
+
version_check_layout.addWidget(version_check_info)
|
|
15634
|
+
|
|
15635
|
+
# Auto-check setting
|
|
15636
|
+
auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
|
|
15637
|
+
auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
|
|
15638
|
+
auto_check_models_cb.setToolTip(
|
|
15639
|
+
"When enabled, Supervertaler will check for new models once per day when you start the application.\n"
|
|
15640
|
+
"You'll see a popup if new models are detected."
|
|
15641
|
+
)
|
|
15642
|
+
version_check_layout.addWidget(auto_check_models_cb)
|
|
15643
|
+
|
|
15644
|
+
# Manual check button
|
|
15645
|
+
manual_check_btn = QPushButton("🔍 Check for New Models Now")
|
|
15646
|
+
manual_check_btn.setToolTip("Manually check for new models from all providers")
|
|
15647
|
+
manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
|
|
15648
|
+
version_check_layout.addWidget(manual_check_btn)
|
|
15649
|
+
|
|
15650
|
+
# Store reference for saving
|
|
15651
|
+
self.auto_check_models_cb = auto_check_models_cb
|
|
15652
|
+
|
|
15653
|
+
version_check_group.setLayout(version_check_layout)
|
|
15654
|
+
layout.addWidget(version_check_group)
|
|
15655
|
+
|
|
15656
|
+
# ========== SECTION 2c: API Keys ==========
|
|
15657
|
+
api_keys_group = QGroupBox("🔑 API Keys")
|
|
15658
|
+
api_keys_layout = QVBoxLayout()
|
|
15659
|
+
|
|
15660
|
+
api_keys_info = QLabel(
|
|
15661
|
+
f"Configure your API keys in:<br>"
|
|
15662
|
+
f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
|
|
15663
|
+
)
|
|
15664
|
+
api_keys_info.setWordWrap(True)
|
|
15665
|
+
api_keys_layout.addWidget(api_keys_info)
|
|
15666
|
+
|
|
15667
|
+
open_keys_btn = QPushButton("📝 Open API Keys File")
|
|
15668
|
+
open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
|
|
15669
|
+
api_keys_layout.addWidget(open_keys_btn)
|
|
15670
|
+
|
|
15671
|
+
api_keys_group.setLayout(api_keys_layout)
|
|
15672
|
+
layout.addWidget(api_keys_group)
|
|
15673
|
+
|
|
15412
15674
|
# ========== SECTION 3: Enable/Disable LLM Providers ==========
|
|
15413
15675
|
provider_enable_group = QGroupBox("✅ Enable/Disable LLM Providers")
|
|
15414
15676
|
provider_enable_layout = QVBoxLayout()
|
|
@@ -15688,58 +15950,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
15688
15950
|
|
|
15689
15951
|
behavior_group.setLayout(behavior_layout)
|
|
15690
15952
|
layout.addWidget(behavior_group)
|
|
15691
|
-
|
|
15692
|
-
# ========== SECTION 7: API Keys ==========
|
|
15693
|
-
# ========== MODEL VERSION CHECKER ==========
|
|
15694
|
-
version_check_group = QGroupBox("🔄 Model Version Checker")
|
|
15695
|
-
version_check_layout = QVBoxLayout()
|
|
15696
|
-
|
|
15697
|
-
version_check_info = QLabel(
|
|
15698
|
-
"Automatically check for new LLM models from OpenAI, Anthropic, and Google.\n"
|
|
15699
|
-
"Get notified when new models are available and easily add them to Supervertaler."
|
|
15700
|
-
)
|
|
15701
|
-
version_check_info.setWordWrap(True)
|
|
15702
|
-
version_check_layout.addWidget(version_check_info)
|
|
15703
|
-
|
|
15704
|
-
# Auto-check setting
|
|
15705
|
-
auto_check_models_cb = CheckmarkCheckBox("Enable automatic model checking (once per day on startup)")
|
|
15706
|
-
auto_check_models_cb.setChecked(general_settings.get('auto_check_models', True))
|
|
15707
|
-
auto_check_models_cb.setToolTip(
|
|
15708
|
-
"When enabled, Supervertaler will check for new models once per day when you start the application.\n"
|
|
15709
|
-
"You'll see a popup if new models are detected."
|
|
15710
|
-
)
|
|
15711
|
-
version_check_layout.addWidget(auto_check_models_cb)
|
|
15712
|
-
|
|
15713
|
-
# Manual check button
|
|
15714
|
-
manual_check_btn = QPushButton("🔍 Check for New Models Now")
|
|
15715
|
-
manual_check_btn.setToolTip("Manually check for new models from all providers")
|
|
15716
|
-
manual_check_btn.clicked.connect(lambda: self._check_for_new_models(force=True))
|
|
15717
|
-
version_check_layout.addWidget(manual_check_btn)
|
|
15718
|
-
|
|
15719
|
-
# Store reference for saving
|
|
15720
|
-
self.auto_check_models_cb = auto_check_models_cb
|
|
15721
|
-
|
|
15722
|
-
version_check_group.setLayout(version_check_layout)
|
|
15723
|
-
layout.addWidget(version_check_group)
|
|
15724
|
-
|
|
15725
|
-
# ========== API KEYS ==========
|
|
15726
|
-
api_keys_group = QGroupBox("🔑 API Keys")
|
|
15727
|
-
api_keys_layout = QVBoxLayout()
|
|
15728
|
-
|
|
15729
|
-
api_keys_info = QLabel(
|
|
15730
|
-
f"Configure your API keys in:<br>"
|
|
15731
|
-
f"<code>{self.user_data_path / 'api_keys.txt'}</code>"
|
|
15732
|
-
)
|
|
15733
|
-
api_keys_info.setWordWrap(True)
|
|
15734
|
-
api_keys_layout.addWidget(api_keys_info)
|
|
15735
15953
|
|
|
15736
|
-
open_keys_btn = QPushButton("📝 Open API Keys File")
|
|
15737
|
-
open_keys_btn.clicked.connect(lambda: self.open_api_keys_file())
|
|
15738
|
-
api_keys_layout.addWidget(open_keys_btn)
|
|
15739
|
-
|
|
15740
|
-
api_keys_group.setLayout(api_keys_layout)
|
|
15741
|
-
layout.addWidget(api_keys_group)
|
|
15742
|
-
|
|
15743
15954
|
# ========== SAVE BUTTON ==========
|
|
15744
15955
|
save_btn = QPushButton("💾 Save AI Settings")
|
|
15745
15956
|
save_btn.setStyleSheet("font-weight: bold; padding: 8px; outline: none;")
|
|
@@ -19844,7 +20055,13 @@ class SupervertalerQt(QMainWindow):
|
|
|
19844
20055
|
self.pagination_label = QLabel("Segments 1-50 of 0")
|
|
19845
20056
|
self.pagination_label.setStyleSheet("color: #555;")
|
|
19846
20057
|
pagination_layout.addWidget(self.pagination_label)
|
|
19847
|
-
|
|
20058
|
+
|
|
20059
|
+
# Tip label for Ctrl+, shortcut (subtle, helpful for new users)
|
|
20060
|
+
tip_label = QLabel("💡 Tip: Ctrl+, inserts the next tag from source")
|
|
20061
|
+
tip_label.setStyleSheet("color: #888; font-size: 9pt; margin-left: 20px;")
|
|
20062
|
+
tip_label.setToolTip("Select text first to wrap it with a tag pair (e.g., <b>selection</b>)")
|
|
20063
|
+
pagination_layout.addWidget(tip_label)
|
|
20064
|
+
|
|
19848
20065
|
pagination_layout.addStretch()
|
|
19849
20066
|
|
|
19850
20067
|
# Pagination controls (right side)
|
|
@@ -19972,6 +20189,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
19972
20189
|
from modules.statuses import get_status, STATUSES
|
|
19973
20190
|
status_label = QLabel("Status:")
|
|
19974
20191
|
tab_status_combo = QComboBox()
|
|
20192
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
19975
20193
|
for status_key in STATUSES.keys():
|
|
19976
20194
|
definition = get_status(status_key)
|
|
19977
20195
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -20870,6 +21088,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
20870
21088
|
from modules.statuses import STATUSES
|
|
20871
21089
|
status_label = QLabel("Status:")
|
|
20872
21090
|
tab_status_combo = QComboBox()
|
|
21091
|
+
tab_status_combo.setMinimumWidth(130) # Ensure full status text is visible
|
|
20873
21092
|
for status_key in STATUSES.keys():
|
|
20874
21093
|
definition = get_status(status_key)
|
|
20875
21094
|
tab_status_combo.addItem(definition.label, status_key)
|
|
@@ -22319,27 +22538,31 @@ class SupervertalerQt(QMainWindow):
|
|
|
22319
22538
|
def save_segment_to_activated_tms(self, source: str, target: str):
|
|
22320
22539
|
"""
|
|
22321
22540
|
Save segment to all writable TMs for current project.
|
|
22322
|
-
|
|
22541
|
+
|
|
22323
22542
|
Note: Uses get_writable_tm_ids() which checks the Write checkbox (read_only=0),
|
|
22324
22543
|
NOT get_active_tm_ids() which checks the Read checkbox (is_active=1).
|
|
22325
|
-
|
|
22544
|
+
|
|
22545
|
+
Respects tm_save_mode setting:
|
|
22546
|
+
- 'latest': Overwrites existing entries with same source (keeps only newest translation)
|
|
22547
|
+
- 'all': Keeps all translations with different targets (default SQLite behavior)
|
|
22548
|
+
|
|
22326
22549
|
Args:
|
|
22327
22550
|
source: Source text
|
|
22328
22551
|
target: Target text
|
|
22329
22552
|
"""
|
|
22330
22553
|
if not self.current_project:
|
|
22331
22554
|
return
|
|
22332
|
-
|
|
22555
|
+
|
|
22333
22556
|
if not hasattr(self.current_project, 'source_lang') or not hasattr(self.current_project, 'target_lang'):
|
|
22334
22557
|
return
|
|
22335
|
-
|
|
22558
|
+
|
|
22336
22559
|
# Get WRITABLE TM IDs for this project (Write checkbox enabled)
|
|
22337
22560
|
tm_ids = []
|
|
22338
|
-
|
|
22561
|
+
|
|
22339
22562
|
if hasattr(self, 'tm_metadata_mgr') and self.tm_metadata_mgr:
|
|
22340
22563
|
if hasattr(self, 'current_project') and self.current_project:
|
|
22341
22564
|
project_id = self.current_project.id if hasattr(self.current_project, 'id') else None
|
|
22342
|
-
|
|
22565
|
+
|
|
22343
22566
|
if project_id:
|
|
22344
22567
|
# Use get_writable_tm_ids() to find TMs with Write enabled
|
|
22345
22568
|
tm_ids = self.tm_metadata_mgr.get_writable_tm_ids(project_id)
|
|
@@ -22349,13 +22572,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22349
22572
|
self.log(f"⚠️ Cannot save to TM: No current project loaded!")
|
|
22350
22573
|
else:
|
|
22351
22574
|
self.log(f"⚠️ Cannot save to TM: TM metadata manager not available!")
|
|
22352
|
-
|
|
22575
|
+
|
|
22353
22576
|
# If no TMs have Write enabled, skip saving
|
|
22354
22577
|
if not tm_ids:
|
|
22355
22578
|
self.log("⚠️ No TMs with Write enabled - segment not saved to TM.")
|
|
22356
22579
|
self.log(f" - To fix: Go to Resources > Translation Memories > TM List and enable the Write checkbox")
|
|
22357
22580
|
return
|
|
22358
|
-
|
|
22581
|
+
|
|
22582
|
+
# Check TM save mode: 'latest' = overwrite, 'all' = keep all variants
|
|
22583
|
+
overwrite_mode = getattr(self, 'tm_save_mode', 'latest') == 'latest'
|
|
22584
|
+
|
|
22359
22585
|
# Save to each writable TM
|
|
22360
22586
|
saved_count = 0
|
|
22361
22587
|
for tm_id in tm_ids:
|
|
@@ -22365,14 +22591,16 @@ class SupervertalerQt(QMainWindow):
|
|
|
22365
22591
|
target=target,
|
|
22366
22592
|
source_lang=self.current_project.source_lang,
|
|
22367
22593
|
target_lang=self.current_project.target_lang,
|
|
22368
|
-
tm_id=tm_id
|
|
22594
|
+
tm_id=tm_id,
|
|
22595
|
+
overwrite=overwrite_mode
|
|
22369
22596
|
)
|
|
22370
22597
|
saved_count += 1
|
|
22371
22598
|
except Exception as e:
|
|
22372
22599
|
self.log(f"⚠️ Could not save to TM '{tm_id}': {e}")
|
|
22373
|
-
|
|
22600
|
+
|
|
22374
22601
|
if saved_count > 0:
|
|
22375
|
-
|
|
22602
|
+
mode_note = " (overwrite)" if overwrite_mode else ""
|
|
22603
|
+
msg = f"💾 Saved segment to {saved_count} TM(s){mode_note}"
|
|
22376
22604
|
self._queue_tm_save_log(msg)
|
|
22377
22605
|
# Invalidate cache so prefetched segments get fresh TM matches
|
|
22378
22606
|
self.invalidate_translation_cache()
|
|
@@ -31385,9 +31613,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31385
31613
|
|
|
31386
31614
|
# Get source text
|
|
31387
31615
|
source_text = current_segment.source
|
|
31388
|
-
|
|
31616
|
+
|
|
31617
|
+
# Get glossary terms for AI injection
|
|
31618
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
31619
|
+
|
|
31389
31620
|
# Build combined prompt
|
|
31390
|
-
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31621
|
+
combined = self.prompt_manager_qt.build_final_prompt(
|
|
31622
|
+
source_text, source_lang, target_lang, glossary_terms=glossary_terms
|
|
31623
|
+
)
|
|
31391
31624
|
|
|
31392
31625
|
# Check for figure/image context
|
|
31393
31626
|
figure_info = ""
|
|
@@ -31414,11 +31647,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31414
31647
|
composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
|
|
31415
31648
|
|
|
31416
31649
|
if self.prompt_manager_qt.library.active_primary_prompt:
|
|
31417
|
-
composition_parts.append(f"✓
|
|
31650
|
+
composition_parts.append(f"✓ Custom prompt attached")
|
|
31418
31651
|
|
|
31419
31652
|
if self.prompt_manager_qt.library.attached_prompts:
|
|
31420
31653
|
composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
|
|
31421
|
-
|
|
31654
|
+
|
|
31655
|
+
if glossary_terms:
|
|
31656
|
+
composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
|
|
31657
|
+
|
|
31422
31658
|
if figure_info:
|
|
31423
31659
|
composition_parts.append(figure_info)
|
|
31424
31660
|
|
|
@@ -31453,12 +31689,59 @@ class SupervertalerQt(QMainWindow):
|
|
|
31453
31689
|
image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
|
|
31454
31690
|
layout.addWidget(image_notice)
|
|
31455
31691
|
|
|
31456
|
-
# Text editor for preview
|
|
31692
|
+
# Text editor for preview with syntax highlighting
|
|
31457
31693
|
text_edit = QTextEdit()
|
|
31458
|
-
text_edit.setPlainText(combined)
|
|
31459
31694
|
text_edit.setReadOnly(True)
|
|
31460
31695
|
text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
|
|
31461
31696
|
text_edit.setStyleSheet("font-family: 'Consolas', 'Courier New', monospace; font-size: 9pt;")
|
|
31697
|
+
|
|
31698
|
+
# Format the prompt with color highlighting
|
|
31699
|
+
import html
|
|
31700
|
+
import re
|
|
31701
|
+
|
|
31702
|
+
# Escape HTML entities first
|
|
31703
|
+
formatted_html = html.escape(combined)
|
|
31704
|
+
|
|
31705
|
+
# Replace newlines with <br> for HTML
|
|
31706
|
+
formatted_html = formatted_html.replace('\n', '<br>')
|
|
31707
|
+
|
|
31708
|
+
# Make "# SYSTEM PROMPT" bold and red
|
|
31709
|
+
formatted_html = re.sub(
|
|
31710
|
+
r'(#\s*SYSTEM\s*PROMPT)',
|
|
31711
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31712
|
+
formatted_html,
|
|
31713
|
+
flags=re.IGNORECASE
|
|
31714
|
+
)
|
|
31715
|
+
|
|
31716
|
+
# Make "# CUSTOM PROMPT" bold and red
|
|
31717
|
+
formatted_html = re.sub(
|
|
31718
|
+
r'(#\s*CUSTOM\s*PROMPT)',
|
|
31719
|
+
r'<span style="color: #d32f2f; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31720
|
+
formatted_html,
|
|
31721
|
+
flags=re.IGNORECASE
|
|
31722
|
+
)
|
|
31723
|
+
|
|
31724
|
+
# Make "# GLOSSARY" bold and orange
|
|
31725
|
+
formatted_html = re.sub(
|
|
31726
|
+
r'(#\s*GLOSSARY)',
|
|
31727
|
+
r'<span style="color: #FF9800; font-weight: bold; font-size: 11pt;">\1</span>',
|
|
31728
|
+
formatted_html,
|
|
31729
|
+
flags=re.IGNORECASE
|
|
31730
|
+
)
|
|
31731
|
+
|
|
31732
|
+
# Make source text section blue (pattern: "XX text:<br>..." until double line break or # header)
|
|
31733
|
+
# Match language code + " text:" followed by content until "# " or end
|
|
31734
|
+
formatted_html = re.sub(
|
|
31735
|
+
r'(\w{2,5}\s+text:)(<br>)(.*?)(<br><br>(?:#|\*\*YOUR TRANSLATION)|$)',
|
|
31736
|
+
r'<span style="color: #1565c0; font-weight: bold;">\1</span>\2<span style="color: #1565c0;">\3</span>\4',
|
|
31737
|
+
formatted_html,
|
|
31738
|
+
flags=re.DOTALL
|
|
31739
|
+
)
|
|
31740
|
+
|
|
31741
|
+
# Wrap in pre-like styling div
|
|
31742
|
+
formatted_html = f'<div style="font-family: Consolas, Courier New, monospace; white-space: pre-wrap;">{formatted_html}</div>'
|
|
31743
|
+
|
|
31744
|
+
text_edit.setHtml(formatted_html)
|
|
31462
31745
|
layout.addWidget(text_edit, 1)
|
|
31463
31746
|
|
|
31464
31747
|
# Close button
|
|
@@ -31476,25 +31759,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
31476
31759
|
def show_grid_context_menu(self, position):
|
|
31477
31760
|
"""Show context menu for grid view with bulk operations"""
|
|
31478
31761
|
selected_segments = self.get_selected_segments_from_grid()
|
|
31479
|
-
|
|
31762
|
+
|
|
31480
31763
|
if not selected_segments:
|
|
31481
31764
|
return
|
|
31482
|
-
|
|
31765
|
+
|
|
31483
31766
|
menu = QMenu(self)
|
|
31484
|
-
|
|
31767
|
+
|
|
31485
31768
|
# Confirm selected segments action
|
|
31486
31769
|
if len(selected_segments) >= 1:
|
|
31487
31770
|
confirm_action = menu.addAction(f"✅ Confirm {len(selected_segments)} Segment(s)")
|
|
31488
31771
|
confirm_action.setToolTip(f"Confirm {len(selected_segments)} selected segment(s)")
|
|
31489
31772
|
confirm_action.triggered.connect(self.confirm_selected_segments)
|
|
31490
|
-
|
|
31773
|
+
|
|
31774
|
+
# Change Status submenu
|
|
31775
|
+
from modules.statuses import get_status
|
|
31776
|
+
status_menu = menu.addMenu(f"🏷️ Change Status ({len(selected_segments)})")
|
|
31777
|
+
# Common user-settable statuses (excluding TM-specific ones like pm, cm, tm_100, etc.)
|
|
31778
|
+
user_statuses = [
|
|
31779
|
+
("not_started", "❌ Not started"),
|
|
31780
|
+
("pretranslated", "🤖 Pre-translated"),
|
|
31781
|
+
("translated", "✏️ Translated"),
|
|
31782
|
+
("confirmed", "✔ Confirmed"),
|
|
31783
|
+
("tr_confirmed", "🌟 TR confirmed"),
|
|
31784
|
+
("proofread", "🟪 Proofread"),
|
|
31785
|
+
("approved", "⭐ Approved"),
|
|
31786
|
+
("rejected", "🚫 Rejected"),
|
|
31787
|
+
]
|
|
31788
|
+
for status_key, label in user_statuses:
|
|
31789
|
+
action = status_menu.addAction(label)
|
|
31790
|
+
action.triggered.connect(lambda checked, s=status_key: self.change_status_selected(s))
|
|
31791
|
+
|
|
31491
31792
|
# Clear translations action
|
|
31492
31793
|
clear_action = menu.addAction("🗑️ Clear Translations")
|
|
31493
31794
|
clear_action.setToolTip(f"Clear translations for {len(selected_segments)} selected segment(s)")
|
|
31494
31795
|
clear_action.triggered.connect(lambda: self.clear_selected_translations(selected_segments, 'grid'))
|
|
31495
|
-
|
|
31796
|
+
|
|
31496
31797
|
menu.addSeparator()
|
|
31497
|
-
|
|
31798
|
+
|
|
31498
31799
|
# Clear proofreading notes (if any selected segment has proofreading notes)
|
|
31499
31800
|
has_proofreading = any(seg.notes and "⚠️ PROOFREAD:" in seg.notes for seg in selected_segments)
|
|
31500
31801
|
if has_proofreading:
|
|
@@ -31502,11 +31803,11 @@ class SupervertalerQt(QMainWindow):
|
|
|
31502
31803
|
clear_proofread_action.setToolTip("Remove proofreading issues from selected segment(s)")
|
|
31503
31804
|
clear_proofread_action.triggered.connect(lambda: self._clear_proofreading_from_selected(selected_segments))
|
|
31504
31805
|
menu.addSeparator()
|
|
31505
|
-
|
|
31806
|
+
|
|
31506
31807
|
# Select all action
|
|
31507
31808
|
select_all_action = menu.addAction("📋 Select All (Ctrl+A)")
|
|
31508
31809
|
select_all_action.triggered.connect(lambda: self.table.selectAll())
|
|
31509
|
-
|
|
31810
|
+
|
|
31510
31811
|
menu.exec(self.table.viewport().mapToGlobal(position))
|
|
31511
31812
|
|
|
31512
31813
|
def _clear_proofreading_from_selected(self, segments):
|
|
@@ -35182,7 +35483,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35182
35483
|
primary_prompt_text = f"✅ {prompt_path}"
|
|
35183
35484
|
attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
|
|
35184
35485
|
|
|
35185
|
-
ai_layout.addWidget(QLabel(f"<b>
|
|
35486
|
+
ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
|
|
35186
35487
|
if attached_count > 0:
|
|
35187
35488
|
ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
|
|
35188
35489
|
|
|
@@ -38331,17 +38632,75 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
38331
38632
|
if not self.current_project:
|
|
38332
38633
|
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38333
38634
|
return
|
|
38334
|
-
|
|
38635
|
+
|
|
38335
38636
|
if not hasattr(self, 'table') or not self.table:
|
|
38336
38637
|
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
38337
38638
|
return
|
|
38338
|
-
|
|
38639
|
+
|
|
38339
38640
|
selected_segments = self.get_selected_segments_from_grid()
|
|
38340
38641
|
if selected_segments:
|
|
38341
38642
|
self.confirm_selected_segments()
|
|
38342
38643
|
else:
|
|
38343
38644
|
QMessageBox.information(self, "No Selection", "Please select one or more segments to confirm.")
|
|
38344
|
-
|
|
38645
|
+
|
|
38646
|
+
def change_status_selected(self, new_status: str, from_menu: bool = False):
|
|
38647
|
+
"""Change status of all selected segments to the specified status.
|
|
38648
|
+
|
|
38649
|
+
Args:
|
|
38650
|
+
new_status: The status key to set (e.g., 'translated', 'pretranslated')
|
|
38651
|
+
from_menu: If True, show message boxes for errors (called from menu)
|
|
38652
|
+
"""
|
|
38653
|
+
if not self.current_project:
|
|
38654
|
+
if from_menu:
|
|
38655
|
+
QMessageBox.information(self, "Not Available", "Please load a project first.")
|
|
38656
|
+
else:
|
|
38657
|
+
self.log("⚠️ No project loaded")
|
|
38658
|
+
return
|
|
38659
|
+
|
|
38660
|
+
if not hasattr(self, 'table') or not self.table:
|
|
38661
|
+
if from_menu:
|
|
38662
|
+
QMessageBox.information(self, "Not Available", "Grid view is not available.")
|
|
38663
|
+
return
|
|
38664
|
+
|
|
38665
|
+
selected_segments = self.get_selected_segments_from_grid()
|
|
38666
|
+
|
|
38667
|
+
if not selected_segments:
|
|
38668
|
+
if from_menu:
|
|
38669
|
+
QMessageBox.information(self, "No Selection", "Please select one or more segments to change.")
|
|
38670
|
+
else:
|
|
38671
|
+
self.log("⚠️ No segments selected")
|
|
38672
|
+
return
|
|
38673
|
+
|
|
38674
|
+
# Sync all target text from grid widgets first
|
|
38675
|
+
self._sync_grid_targets_to_segments(selected_segments)
|
|
38676
|
+
|
|
38677
|
+
# Get status definition for logging
|
|
38678
|
+
from modules.statuses import get_status
|
|
38679
|
+
status_def = get_status(new_status)
|
|
38680
|
+
|
|
38681
|
+
changed_count = 0
|
|
38682
|
+
for segment in selected_segments:
|
|
38683
|
+
# Skip if already has this status
|
|
38684
|
+
if segment.status == new_status:
|
|
38685
|
+
continue
|
|
38686
|
+
|
|
38687
|
+
segment.status = new_status
|
|
38688
|
+
changed_count += 1
|
|
38689
|
+
|
|
38690
|
+
# Update grid status icon
|
|
38691
|
+
row = self._find_row_for_segment(segment.id)
|
|
38692
|
+
if row >= 0:
|
|
38693
|
+
self.update_status_icon(row, new_status)
|
|
38694
|
+
|
|
38695
|
+
if changed_count > 0:
|
|
38696
|
+
self.project_modified = True
|
|
38697
|
+
self.update_window_title()
|
|
38698
|
+
self.log(f"✅ Changed {changed_count} segment(s) to '{status_def.label}'")
|
|
38699
|
+
# Update status bar progress stats
|
|
38700
|
+
self.update_progress_stats()
|
|
38701
|
+
else:
|
|
38702
|
+
self.log(f"ℹ️ All {len(selected_segments)} selected segment(s) already have status '{status_def.label}'")
|
|
38703
|
+
|
|
38345
38704
|
def _sync_grid_targets_to_segments(self, segments):
|
|
38346
38705
|
"""Sync target text from grid widgets to segment objects.
|
|
38347
38706
|
|
|
@@ -40210,13 +40569,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40210
40569
|
except Exception as e:
|
|
40211
40570
|
self.log(f"⚠ Could not add surrounding segments: {e}")
|
|
40212
40571
|
|
|
40572
|
+
# Get glossary terms for AI injection
|
|
40573
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
40574
|
+
|
|
40213
40575
|
custom_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
40214
40576
|
source_text=segment.source,
|
|
40215
40577
|
source_lang=self.current_project.source_lang,
|
|
40216
40578
|
target_lang=self.current_project.target_lang,
|
|
40217
|
-
mode="single"
|
|
40579
|
+
mode="single",
|
|
40580
|
+
glossary_terms=glossary_terms
|
|
40218
40581
|
)
|
|
40219
|
-
|
|
40582
|
+
|
|
40220
40583
|
# Add surrounding context before the translation delimiter
|
|
40221
40584
|
if surrounding_context:
|
|
40222
40585
|
# Insert before the "YOUR TRANSLATION" delimiter
|
|
@@ -41580,7 +41943,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41580
41943
|
|
|
41581
41944
|
if match_pct >= 75: # Accept matches 75% and above
|
|
41582
41945
|
segment.target = tm_match
|
|
41583
|
-
segment.status = "
|
|
41946
|
+
segment.status = "pretranslated"
|
|
41584
41947
|
translated_count += 1
|
|
41585
41948
|
|
|
41586
41949
|
# Update grid immediately
|
|
@@ -41694,7 +42057,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41694
42057
|
if tm_match and len(tm_match) > 0:
|
|
41695
42058
|
translation = tm_match[0]['target']
|
|
41696
42059
|
segment.target = translation
|
|
41697
|
-
segment.status = '
|
|
42060
|
+
segment.status = 'pretranslated'
|
|
41698
42061
|
translated_count += 1
|
|
41699
42062
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}... (TM 100%)")
|
|
41700
42063
|
else:
|
|
@@ -41741,7 +42104,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41741
42104
|
|
|
41742
42105
|
if translation and not translation.startswith('['): # Skip error messages
|
|
41743
42106
|
segment.target = translation
|
|
41744
|
-
segment.status = '
|
|
42107
|
+
segment.status = 'pretranslated'
|
|
41745
42108
|
translated_count += 1
|
|
41746
42109
|
self.log(f" ✓ Segment {segment.id}: {segment.source[:40]}... → {translation[:40]}...")
|
|
41747
42110
|
else:
|
|
@@ -41767,11 +42130,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
41767
42130
|
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
|
|
41768
42131
|
try:
|
|
41769
42132
|
first_segment = batch_segments[0][1]
|
|
42133
|
+
# Get glossary terms for AI injection
|
|
42134
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
41770
42135
|
base_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
41771
42136
|
source_text=first_segment.source,
|
|
41772
42137
|
source_lang=source_lang,
|
|
41773
42138
|
target_lang=target_lang,
|
|
41774
|
-
mode="single"
|
|
42139
|
+
mode="single",
|
|
42140
|
+
glossary_terms=glossary_terms
|
|
41775
42141
|
)
|
|
41776
42142
|
if "**SOURCE TEXT:**" in base_prompt:
|
|
41777
42143
|
base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
|
|
@@ -42199,11 +42565,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42199
42565
|
# Access parent through closure
|
|
42200
42566
|
parent = self
|
|
42201
42567
|
if hasattr(parent, 'prompt_manager_qt') and parent.prompt_manager_qt:
|
|
42568
|
+
# Get glossary terms for AI injection
|
|
42569
|
+
glossary_terms = parent.get_ai_inject_glossary_terms() if hasattr(parent, 'get_ai_inject_glossary_terms') else []
|
|
42202
42570
|
custom_prompt = parent.prompt_manager_qt.build_final_prompt(
|
|
42203
42571
|
source_text=source_text,
|
|
42204
42572
|
source_lang=source_lang,
|
|
42205
42573
|
target_lang=target_lang,
|
|
42206
|
-
mode="single"
|
|
42574
|
+
mode="single",
|
|
42575
|
+
glossary_terms=glossary_terms
|
|
42207
42576
|
)
|
|
42208
42577
|
except Exception as e:
|
|
42209
42578
|
self.log(f"⚠ Could not build LLM prompt from manager: {e}")
|
|
@@ -42686,7 +43055,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42686
43055
|
api_keys['google'] = api_keys['gemini']
|
|
42687
43056
|
|
|
42688
43057
|
return api_keys
|
|
42689
|
-
|
|
43058
|
+
|
|
43059
|
+
def get_ai_inject_glossary_terms(self) -> list:
|
|
43060
|
+
"""Get glossary terms from AI-inject-enabled termbases for the current project.
|
|
43061
|
+
|
|
43062
|
+
Returns:
|
|
43063
|
+
List of term dictionaries with source_term, target_term, forbidden keys
|
|
43064
|
+
"""
|
|
43065
|
+
if not hasattr(self, 'termbase_mgr') or not self.termbase_mgr:
|
|
43066
|
+
return []
|
|
43067
|
+
|
|
43068
|
+
project_id = None
|
|
43069
|
+
if hasattr(self, 'current_project') and self.current_project:
|
|
43070
|
+
project_id = getattr(self.current_project, 'id', None)
|
|
43071
|
+
|
|
43072
|
+
return self.termbase_mgr.get_ai_inject_terms(project_id)
|
|
43073
|
+
|
|
42690
43074
|
def ensure_example_api_keys(self):
|
|
42691
43075
|
"""Create example API keys file on first launch for new users"""
|
|
42692
43076
|
example_file = self.user_data_path / "api_keys.example.txt"
|
|
@@ -48001,7 +48385,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
|
|
|
48001
48385
|
|
|
48002
48386
|
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48003
48387
|
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48004
|
-
|
|
48388
|
+
|
|
48389
|
+
painter.end()
|
|
48390
|
+
|
|
48391
|
+
|
|
48392
|
+
class OrangeCheckmarkCheckBox(QCheckBox):
|
|
48393
|
+
"""Custom checkbox with orange background and white checkmark when checked (for AI injection)"""
|
|
48394
|
+
|
|
48395
|
+
def __init__(self, text="", parent=None):
|
|
48396
|
+
super().__init__(text, parent)
|
|
48397
|
+
self.setCheckable(True)
|
|
48398
|
+
self.setEnabled(True)
|
|
48399
|
+
self.setStyleSheet("""
|
|
48400
|
+
QCheckBox {
|
|
48401
|
+
font-size: 9pt;
|
|
48402
|
+
spacing: 6px;
|
|
48403
|
+
}
|
|
48404
|
+
QCheckBox::indicator {
|
|
48405
|
+
width: 16px;
|
|
48406
|
+
height: 16px;
|
|
48407
|
+
border: 2px solid #999;
|
|
48408
|
+
border-radius: 3px;
|
|
48409
|
+
background-color: white;
|
|
48410
|
+
}
|
|
48411
|
+
QCheckBox::indicator:checked {
|
|
48412
|
+
background-color: #FF9800;
|
|
48413
|
+
border-color: #FF9800;
|
|
48414
|
+
}
|
|
48415
|
+
QCheckBox::indicator:hover {
|
|
48416
|
+
border-color: #666;
|
|
48417
|
+
}
|
|
48418
|
+
QCheckBox::indicator:checked:hover {
|
|
48419
|
+
background-color: #F57C00;
|
|
48420
|
+
border-color: #F57C00;
|
|
48421
|
+
}
|
|
48422
|
+
""")
|
|
48423
|
+
|
|
48424
|
+
def paintEvent(self, event):
|
|
48425
|
+
"""Override paint event to draw white checkmark when checked"""
|
|
48426
|
+
super().paintEvent(event)
|
|
48427
|
+
|
|
48428
|
+
if self.isChecked():
|
|
48429
|
+
from PyQt6.QtWidgets import QStyleOptionButton
|
|
48430
|
+
from PyQt6.QtGui import QPainter, QPen, QColor
|
|
48431
|
+
from PyQt6.QtCore import QPointF
|
|
48432
|
+
|
|
48433
|
+
opt = QStyleOptionButton()
|
|
48434
|
+
self.initStyleOption(opt)
|
|
48435
|
+
indicator_rect = self.style().subElementRect(
|
|
48436
|
+
self.style().SubElement.SE_CheckBoxIndicator,
|
|
48437
|
+
opt,
|
|
48438
|
+
self
|
|
48439
|
+
)
|
|
48440
|
+
|
|
48441
|
+
if indicator_rect.isValid():
|
|
48442
|
+
painter = QPainter(self)
|
|
48443
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
48444
|
+
pen_width = max(2.0, min(indicator_rect.width(), indicator_rect.height()) * 0.12)
|
|
48445
|
+
painter.setPen(QPen(QColor(255, 255, 255), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
|
|
48446
|
+
painter.setBrush(QColor(255, 255, 255))
|
|
48447
|
+
|
|
48448
|
+
x = indicator_rect.x()
|
|
48449
|
+
y = indicator_rect.y()
|
|
48450
|
+
w = indicator_rect.width()
|
|
48451
|
+
h = indicator_rect.height()
|
|
48452
|
+
|
|
48453
|
+
padding = min(w, h) * 0.15
|
|
48454
|
+
x += padding
|
|
48455
|
+
y += padding
|
|
48456
|
+
w -= padding * 2
|
|
48457
|
+
h -= padding * 2
|
|
48458
|
+
|
|
48459
|
+
check_x1 = x + w * 0.10
|
|
48460
|
+
check_y1 = y + h * 0.50
|
|
48461
|
+
check_x2 = x + w * 0.35
|
|
48462
|
+
check_y2 = y + h * 0.70
|
|
48463
|
+
check_x3 = x + w * 0.90
|
|
48464
|
+
check_y3 = y + h * 0.25
|
|
48465
|
+
|
|
48466
|
+
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48467
|
+
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48468
|
+
|
|
48005
48469
|
painter.end()
|
|
48006
48470
|
|
|
48007
48471
|
|