supervertaler 1.9.195__py3-none-any.whl → 1.9.197__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 CHANGED
@@ -32,7 +32,7 @@ License: MIT
32
32
  """
33
33
 
34
34
  # Version Information.
35
- __version__ = "1.9.195"
35
+ __version__ = "1.9.197"
36
36
  __phase__ = "0.9"
37
37
  __release_date__ = "2026-02-02"
38
38
  __edition__ = "Qt"
@@ -391,10 +391,10 @@ def runs_to_tagged_text(paragraphs) -> str:
391
391
  def strip_formatting_tags(text: str) -> str:
392
392
  """
393
393
  Remove HTML formatting tags from text, leaving plain text.
394
-
394
+
395
395
  Args:
396
396
  text: Text with HTML tags like <b>, </b>, <i>, </i>, <u>, </u>
397
-
397
+
398
398
  Returns:
399
399
  Plain text without tags
400
400
  """
@@ -403,6 +403,77 @@ def strip_formatting_tags(text: str) -> str:
403
403
  return re.sub(r'</?[biu]>', '', text)
404
404
 
405
405
 
406
+ def strip_outer_wrapping_tags(text: str) -> tuple:
407
+ """
408
+ Strip outer wrapping tags from text if the entire segment is wrapped in a single tag pair.
409
+
410
+ This handles structural tags like <li-o>...</li-o>, <p>...</p>, <td>...</td>, etc.
411
+ Inner formatting tags like <b>...</b> are preserved.
412
+
413
+ Args:
414
+ text: Text that may be wrapped in outer structural tags
415
+
416
+ Returns:
417
+ Tuple of (stripped_text, tag_name) where tag_name is the outer tag that was stripped,
418
+ or (original_text, None) if no outer wrapping tag was found
419
+
420
+ Examples:
421
+ "<li-o>Hello <b>world</b></li-o>" -> ("Hello <b>world</b>", "li-o")
422
+ "<p>Simple text</p>" -> ("Simple text", "p")
423
+ "No tags here" -> ("No tags here", None)
424
+ "<b>Bold text</b>" -> ("<b>Bold text</b>", None) # <b> is formatting, not structural
425
+ """
426
+ import re
427
+
428
+ if not text or not text.strip():
429
+ return (text, None)
430
+
431
+ text = text.strip()
432
+
433
+ # Structural tags that wrap entire segments (not inline formatting)
434
+ structural_tags = {
435
+ 'li-o', 'li-b', 'li', 'p', 'td', 'th', 'tr', 'div', 'span',
436
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'title', 'caption',
437
+ 'blockquote', 'pre', 'code', 'dt', 'dd', 'header', 'footer',
438
+ 'article', 'section', 'aside', 'nav', 'main', 'figure', 'figcaption'
439
+ }
440
+
441
+ # Pattern to match opening tag at start: <tag> or <tag attr="...">
442
+ opening_pattern = r'^<([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[^>]*)?>(.*)$'
443
+ opening_match = re.match(opening_pattern, text, re.DOTALL)
444
+
445
+ if not opening_match:
446
+ return (text, None)
447
+
448
+ tag_name = opening_match.group(1).lower()
449
+ rest = opening_match.group(2)
450
+
451
+ # Only strip structural tags, not inline formatting like <b>, <i>, <u>, <em>, <strong>
452
+ if tag_name not in structural_tags:
453
+ return (text, None)
454
+
455
+ # Check if text ends with matching closing tag
456
+ closing_pattern = rf'^(.*)</{re.escape(tag_name)}>$'
457
+ closing_match = re.match(closing_pattern, rest, re.DOTALL | re.IGNORECASE)
458
+
459
+ if not closing_match:
460
+ return (text, None)
461
+
462
+ inner_content = closing_match.group(1)
463
+
464
+ # Verify this is truly a wrapping pair (no other occurrences of this tag inside)
465
+ # Count opening and closing tags of this type in the inner content
466
+ inner_opening_count = len(re.findall(rf'<{re.escape(tag_name)}(?:\s+[^>]*)?>',
467
+ inner_content, re.IGNORECASE))
468
+ inner_closing_count = len(re.findall(rf'</{re.escape(tag_name)}>', inner_content, re.IGNORECASE))
469
+
470
+ # If there are nested tags of the same type, don't strip
471
+ if inner_opening_count > 0 or inner_closing_count > 0:
472
+ return (text, None)
473
+
474
+ return (inner_content, tag_name)
475
+
476
+
406
477
  def has_formatting_tags(text: str) -> bool:
407
478
  """
408
479
  Check if text contains any formatting tags.
@@ -6222,7 +6293,10 @@ class SupervertalerQt(QMainWindow):
6222
6293
  self.enable_alternating_row_colors = True # Enable alternating row colors by default
6223
6294
  self.even_row_color = '#FFFFFF' # White for even rows
6224
6295
  self.odd_row_color = '#F0F0F0' # Light gray for odd rows
6225
-
6296
+
6297
+ # Hide outer wrapping tags in grid display (e.g. <li-o>...</li-o>)
6298
+ self.hide_outer_wrapping_tags = False # Disabled by default
6299
+
6226
6300
  # Termbase highlight style settings
6227
6301
  self.termbase_highlight_style = 'semibold' # 'background', 'dotted', or 'semibold'
6228
6302
  self.termbase_dotted_color = '#808080' # Medium gray for dotted underline (more visible)
@@ -17690,14 +17764,41 @@ class SupervertalerQt(QMainWindow):
17690
17764
 
17691
17765
  grid_group.setLayout(grid_layout)
17692
17766
  layout.addWidget(grid_group)
17693
-
17694
- # Translation Results Pane & Tag Coloring section
17695
- results_group = QGroupBox("📋 Translation Results Pane && Tag Colors")
17767
+
17768
+ # Grid Display Options section
17769
+ grid_display_group = QGroupBox("📊 Grid Display Options")
17770
+ grid_display_layout = QVBoxLayout()
17771
+
17772
+ grid_display_info = QLabel(
17773
+ "Configure how content is displayed in the translation grid."
17774
+ )
17775
+ grid_display_info.setStyleSheet("font-size: 8pt; padding: 8px; border-radius: 2px;")
17776
+ grid_display_info.setWordWrap(True)
17777
+ grid_display_layout.addWidget(grid_display_info)
17778
+
17779
+ # Hide wrapping tags checkbox
17780
+ hide_wrapping_tags_layout = QHBoxLayout()
17781
+ hide_wrapping_tags_check = CheckmarkCheckBox("Hide outer wrapping tags in grid (e.g. <li-o>...</li-o>)")
17782
+ hide_wrapping_tags_check.setChecked(font_settings.get('hide_outer_wrapping_tags', False))
17783
+ hide_wrapping_tags_check.setToolTip(
17784
+ "When enabled, structural tags that wrap the entire segment (like <li-o>, <p>, <td>) are hidden in the grid.\n"
17785
+ "The segment type is already shown in the Type column, so this reduces visual clutter.\n"
17786
+ "Inner formatting tags like <b>bold</b> are still shown.\n"
17787
+ "This affects the Source column display only - the Target column keeps tags for editing."
17788
+ )
17789
+ hide_wrapping_tags_layout.addWidget(hide_wrapping_tags_check)
17790
+ hide_wrapping_tags_layout.addStretch()
17791
+ grid_display_layout.addLayout(hide_wrapping_tags_layout)
17792
+
17793
+ grid_display_group.setLayout(grid_display_layout)
17794
+ layout.addWidget(grid_display_group)
17795
+
17796
+ # Match Panel & Tag Colors section
17797
+ results_group = QGroupBox("📋 Match Panel && Tag Colors")
17696
17798
  results_layout = QVBoxLayout()
17697
17799
 
17698
17800
  results_size_info = QLabel(
17699
- "Set the default font sizes for the translation results pane.\n"
17700
- "You can also adjust these using View menu → Translation Results Pane."
17801
+ "Set font sizes for the Match Panel (TM/termbase matches) and tag colors."
17701
17802
  )
17702
17803
  results_size_info.setStyleSheet("font-size: 8pt; padding: 8px; border-radius: 2px;")
17703
17804
  results_size_info.setWordWrap(True)
@@ -17737,7 +17838,7 @@ class SupervertalerQt(QMainWindow):
17737
17838
  show_tags_layout.addWidget(show_tags_check)
17738
17839
  show_tags_layout.addStretch()
17739
17840
  results_layout.addLayout(show_tags_layout)
17740
-
17841
+
17741
17842
  # Tag highlight color picker
17742
17843
  tag_color_layout = QHBoxLayout()
17743
17844
  tag_color_layout.addWidget(QLabel("Tag Highlight Color:"))
@@ -18295,7 +18396,8 @@ class SupervertalerQt(QMainWindow):
18295
18396
  grid_font_spin, match_font_spin, compare_font_spin, show_tags_check, tag_color_btn,
18296
18397
  alt_colors_check, even_color_btn, odd_color_btn, invisible_char_color_btn, grid_font_family_combo,
18297
18398
  termview_font_family_combo, termview_font_spin, termview_bold_check,
18298
- border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check
18399
+ border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check,
18400
+ hide_wrapping_tags_check
18299
18401
  )
18300
18402
 
18301
18403
  save_btn.clicked.connect(save_view_settings_with_scale)
@@ -18307,14 +18409,15 @@ class SupervertalerQt(QMainWindow):
18307
18409
 
18308
18410
  def _create_voice_dictation_settings_tab(self):
18309
18411
  """Create Supervoice Settings tab content with Voice Commands"""
18310
- from PyQt6.QtWidgets import (QGroupBox, QPushButton, QComboBox, QSpinBox,
18412
+ from PyQt6.QtWidgets import (QGroupBox, QPushButton, QComboBox, QSpinBox,
18311
18413
  QTableWidget, QTableWidgetItem, QHeaderView,
18312
- QAbstractItemView, QCheckBox)
18414
+ QAbstractItemView, QCheckBox, QSplitter)
18415
+ from modules.keyboard_shortcuts_widget import CheckmarkCheckBox
18313
18416
 
18314
18417
  tab = QWidget()
18315
- layout = QVBoxLayout(tab)
18316
- layout.setContentsMargins(20, 20, 20, 20)
18317
- layout.setSpacing(15)
18418
+ main_layout = QVBoxLayout(tab)
18419
+ main_layout.setContentsMargins(20, 20, 20, 20)
18420
+ main_layout.setSpacing(15)
18318
18421
 
18319
18422
  # Load current dictation settings
18320
18423
  dictation_settings = self.load_dictation_settings()
@@ -18327,19 +18430,33 @@ class SupervertalerQt(QMainWindow):
18327
18430
  header_info.setTextFormat(Qt.TextFormat.RichText)
18328
18431
  header_info.setStyleSheet("font-size: 9pt; color: #444; padding: 10px; background-color: #E3F2FD; border-radius: 4px;")
18329
18432
  header_info.setWordWrap(True)
18330
- layout.addWidget(header_info)
18433
+ main_layout.addWidget(header_info)
18331
18434
 
18332
- # ===== Voice Commands Section =====
18333
- commands_group = QGroupBox("🗣️ Voice Commands (Talon-style)")
18334
- commands_layout = QVBoxLayout()
18435
+ # ===== Two-column layout: Left = Settings, Right = Voice Commands Table =====
18436
+ columns_layout = QHBoxLayout()
18437
+ columns_layout.setSpacing(15)
18335
18438
 
18336
- # Enable voice commands checkbox
18337
- voice_cmd_enabled = QCheckBox("Enable voice commands (spoken phrases trigger actions)")
18439
+ # --- LEFT COLUMN: Settings ---
18440
+ left_column = QVBoxLayout()
18441
+ left_column.setSpacing(15)
18442
+
18443
+ # Enable voice commands checkbox (green checkmark style)
18444
+ voice_cmd_enabled = CheckmarkCheckBox("Enable voice commands (spoken phrases trigger actions)")
18338
18445
  voice_cmd_enabled.setChecked(dictation_settings.get('voice_commands_enabled', True))
18339
18446
  voice_cmd_enabled.setToolTip("When enabled, spoken phrases like 'confirm' or 'next segment' will execute commands instead of being inserted as text")
18340
- commands_layout.addWidget(voice_cmd_enabled)
18447
+ left_column.addWidget(voice_cmd_enabled)
18341
18448
  self.voice_commands_enabled_checkbox = voice_cmd_enabled
18342
18449
 
18450
+ # Create a layout variable for settings sections to be added below
18451
+ layout = left_column
18452
+
18453
+ # --- RIGHT COLUMN: Voice Commands Table ---
18454
+ right_column = QVBoxLayout()
18455
+ right_column.setSpacing(10)
18456
+
18457
+ commands_group = QGroupBox("🗣️ Voice Commands (Talon-style)")
18458
+ commands_layout = QVBoxLayout()
18459
+
18343
18460
  commands_info = QLabel(
18344
18461
  "Voice commands let you control Supervertaler by voice. Say a phrase to execute an action.\n"
18345
18462
  "If no command matches, the spoken text is inserted as dictation."
@@ -18358,38 +18475,47 @@ class SupervertalerQt(QMainWindow):
18358
18475
  self.voice_commands_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
18359
18476
  self.voice_commands_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
18360
18477
  self.voice_commands_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
18361
- self.voice_commands_table.setMinimumHeight(200)
18362
- self.voice_commands_table.setMaximumHeight(300)
18363
-
18478
+ self.voice_commands_table.setMinimumHeight(350)
18479
+
18364
18480
  # Populate table with current commands
18365
18481
  self._populate_voice_commands_table()
18366
18482
  commands_layout.addWidget(self.voice_commands_table)
18367
18483
 
18368
18484
  # Command buttons
18369
18485
  cmd_btn_layout = QHBoxLayout()
18370
-
18371
- add_cmd_btn = QPushButton("➕ Add Command")
18486
+
18487
+ add_cmd_btn = QPushButton("➕ Add")
18372
18488
  add_cmd_btn.clicked.connect(self._add_voice_command)
18373
18489
  cmd_btn_layout.addWidget(add_cmd_btn)
18374
-
18375
- edit_cmd_btn = QPushButton("✏️ Edit Command")
18490
+
18491
+ edit_cmd_btn = QPushButton("✏️ Edit")
18376
18492
  edit_cmd_btn.clicked.connect(self._edit_voice_command)
18377
18493
  cmd_btn_layout.addWidget(edit_cmd_btn)
18378
-
18379
- remove_cmd_btn = QPushButton("🗑️ Remove Command")
18494
+
18495
+ remove_cmd_btn = QPushButton("🗑️ Remove")
18380
18496
  remove_cmd_btn.clicked.connect(self._remove_voice_command)
18381
18497
  cmd_btn_layout.addWidget(remove_cmd_btn)
18382
-
18498
+
18383
18499
  cmd_btn_layout.addStretch()
18384
-
18385
- reset_cmd_btn = QPushButton("🔄 Reset to Defaults")
18500
+
18501
+ reset_cmd_btn = QPushButton("🔄 Reset")
18386
18502
  reset_cmd_btn.clicked.connect(self._reset_voice_commands)
18387
18503
  cmd_btn_layout.addWidget(reset_cmd_btn)
18388
-
18504
+
18389
18505
  commands_layout.addLayout(cmd_btn_layout)
18390
18506
 
18391
18507
  commands_group.setLayout(commands_layout)
18392
- layout.addWidget(commands_group)
18508
+ right_column.addWidget(commands_group)
18509
+ right_column.addStretch()
18510
+
18511
+ # Add columns to the two-column layout
18512
+ left_widget = QWidget()
18513
+ left_widget.setLayout(left_column)
18514
+ right_widget = QWidget()
18515
+ right_widget.setLayout(right_column)
18516
+
18517
+ columns_layout.addWidget(left_widget, stretch=1)
18518
+ columns_layout.addWidget(right_widget, stretch=1)
18393
18519
 
18394
18520
  # ===== Always-On Mode Section =====
18395
18521
  alwayson_group = QGroupBox("🎧 Always-On Listening Mode")
@@ -18552,7 +18678,13 @@ class SupervertalerQt(QMainWindow):
18552
18678
  ahk_group.setLayout(ahk_layout)
18553
18679
  layout.addWidget(ahk_group)
18554
18680
 
18555
- # Save button
18681
+ # Add stretch to left column to push content up
18682
+ layout.addStretch()
18683
+
18684
+ # Add two-column layout to main layout
18685
+ main_layout.addLayout(columns_layout, stretch=1)
18686
+
18687
+ # Save button (full width, below the two columns)
18556
18688
  save_btn = QPushButton("💾 Save Supervoice Settings")
18557
18689
  save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 10px; border: none; outline: none;")
18558
18690
  save_btn.clicked.connect(lambda: self._save_voice_settings(
@@ -18561,15 +18693,13 @@ class SupervertalerQt(QMainWindow):
18561
18693
  lang_combo.currentText(),
18562
18694
  voice_cmd_enabled.isChecked()
18563
18695
  ))
18564
- layout.addWidget(save_btn)
18696
+ main_layout.addWidget(save_btn)
18565
18697
 
18566
18698
  # Store references
18567
18699
  self.dictation_model_combo = model_combo
18568
18700
  self.dictation_duration_spin = duration_spin
18569
18701
  self.dictation_lang_combo = lang_combo
18570
18702
 
18571
- layout.addStretch()
18572
-
18573
18703
  return tab
18574
18704
 
18575
18705
  def _populate_voice_commands_table(self):
@@ -20198,7 +20328,8 @@ class SupervertalerQt(QMainWindow):
20198
20328
  def _save_view_settings_from_ui(self, grid_spin, match_spin, compare_spin, show_tags_check=None, tag_color_btn=None,
20199
20329
  alt_colors_check=None, even_color_btn=None, odd_color_btn=None, invisible_char_color_btn=None,
20200
20330
  grid_font_family_combo=None, termview_font_family_combo=None, termview_font_spin=None, termview_bold_check=None,
20201
- border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None):
20331
+ border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None,
20332
+ hide_wrapping_tags_check=None):
20202
20333
  """Save view settings from UI"""
20203
20334
  # CRITICAL: Suppress TM saves during view settings update
20204
20335
  # Grid operations (setStyleSheet, rehighlight, etc.) can trigger textChanged events
@@ -20211,7 +20342,8 @@ class SupervertalerQt(QMainWindow):
20211
20342
  grid_spin, match_spin, compare_spin, show_tags_check, tag_color_btn,
20212
20343
  alt_colors_check, even_color_btn, odd_color_btn, invisible_char_color_btn,
20213
20344
  grid_font_family_combo, termview_font_family_combo, termview_font_spin, termview_bold_check,
20214
- border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check
20345
+ border_color_btn, border_thickness_spin, badge_text_color_btn, tabs_above_check,
20346
+ hide_wrapping_tags_check
20215
20347
  )
20216
20348
  finally:
20217
20349
  self._suppress_target_change_handlers = previous_suppression
@@ -20219,16 +20351,18 @@ class SupervertalerQt(QMainWindow):
20219
20351
  def _save_view_settings_from_ui_impl(self, grid_spin, match_spin, compare_spin, show_tags_check=None, tag_color_btn=None,
20220
20352
  alt_colors_check=None, even_color_btn=None, odd_color_btn=None, invisible_char_color_btn=None,
20221
20353
  grid_font_family_combo=None, termview_font_family_combo=None, termview_font_spin=None, termview_bold_check=None,
20222
- border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None):
20354
+ border_color_btn=None, border_thickness_spin=None, badge_text_color_btn=None, tabs_above_check=None,
20355
+ hide_wrapping_tags_check=None):
20223
20356
  """Implementation of save view settings (called with TM saves suppressed)"""
20224
- general_settings = {
20225
- 'restore_last_project': self.load_general_settings().get('restore_last_project', False),
20226
- 'auto_propagate_exact_matches': self.auto_propagate_exact_matches, # Keep existing value
20357
+ # Load existing settings first to preserve all values, then update with new ones
20358
+ general_settings = self.load_general_settings()
20359
+ general_settings.update({
20360
+ 'auto_propagate_exact_matches': self.auto_propagate_exact_matches,
20227
20361
  'grid_font_size': grid_spin.value(),
20228
20362
  'results_match_font_size': match_spin.value(),
20229
20363
  'results_compare_font_size': compare_spin.value(),
20230
- 'enable_tm_termbase_matching': self.enable_tm_matching # Save TM/termbase matching state
20231
- }
20364
+ 'enable_tm_termbase_matching': self.enable_tm_matching
20365
+ })
20232
20366
 
20233
20367
  # Add tabs above grid setting if provided
20234
20368
  if tabs_above_check is not None:
@@ -20268,7 +20402,12 @@ class SupervertalerQt(QMainWindow):
20268
20402
  if invisible_char_color:
20269
20403
  general_settings['invisible_char_color'] = invisible_char_color
20270
20404
  self.invisible_char_color = invisible_char_color
20271
-
20405
+
20406
+ # Add hide outer wrapping tags setting if provided
20407
+ if hide_wrapping_tags_check is not None:
20408
+ general_settings['hide_outer_wrapping_tags'] = hide_wrapping_tags_check.isChecked()
20409
+ self.hide_outer_wrapping_tags = hide_wrapping_tags_check.isChecked()
20410
+
20272
20411
  # Add focus border settings if provided
20273
20412
  if border_color_btn is not None:
20274
20413
  border_color = border_color_btn.property('selected_color')
@@ -20432,7 +20571,11 @@ class SupervertalerQt(QMainWindow):
20432
20571
  self.refresh_grid_tag_colors()
20433
20572
  # Also refresh row colors
20434
20573
  self.apply_alternating_row_colors()
20435
-
20574
+
20575
+ # Refresh source column if hide_outer_wrapping_tags setting changed
20576
+ if hide_wrapping_tags_check is not None and hasattr(self, 'table') and self.table is not None:
20577
+ self._refresh_source_column_display()
20578
+
20436
20579
  self.log("✓ View settings saved and applied")
20437
20580
  # Use explicit QMessageBox instance to ensure proper dialog closing
20438
20581
  msg = QMessageBox(self)
@@ -29773,8 +29916,13 @@ class SupervertalerQt(QMainWindow):
29773
29916
  self.table.setItem(row, 1, type_item)
29774
29917
 
29775
29918
  # Source - Use read-only QTextEdit widget for easy text selection
29919
+ # Strip outer wrapping tags if setting is enabled (display only, data unchanged)
29920
+ source_for_display = segment.source
29921
+ if self.hide_outer_wrapping_tags:
29922
+ stripped, _ = strip_outer_wrapping_tags(segment.source)
29923
+ source_for_display = stripped
29776
29924
  # Apply invisible character replacements for display only
29777
- source_display_text = self.apply_invisible_replacements(segment.source)
29925
+ source_display_text = self.apply_invisible_replacements(source_for_display)
29778
29926
  source_editor = ReadOnlyGridTextEditor(source_display_text, self.table, row)
29779
29927
 
29780
29928
  # Initialize empty termbase matches (will be populated lazily on segment selection or by background worker)
@@ -29793,6 +29941,7 @@ class SupervertalerQt(QMainWindow):
29793
29941
  self.table.setItem(row, 2, source_item)
29794
29942
 
29795
29943
  # Target - Use editable QTextEdit widget for easy text selection and editing
29944
+ # Note: We don't strip wrapping tags from target cells since users edit them directly
29796
29945
  # Apply invisible character replacements for display (will be reversed when saving)
29797
29946
  target_display_text = self.apply_invisible_replacements(segment.target)
29798
29947
  target_editor = EditableGridTextEditor(target_display_text, self.table, row, self.table)
@@ -31730,7 +31879,10 @@ class SupervertalerQt(QMainWindow):
31730
31879
  self.enable_alternating_row_colors = settings.get('enable_alternating_row_colors', True)
31731
31880
  self.even_row_color = settings.get('even_row_color', '#FFFFFF')
31732
31881
  self.odd_row_color = settings.get('odd_row_color', '#F0F0F0')
31733
-
31882
+
31883
+ # Load hide outer wrapping tags setting
31884
+ self.hide_outer_wrapping_tags = settings.get('hide_outer_wrapping_tags', False)
31885
+
31734
31886
  # Load termbase highlight style settings
31735
31887
  self.termbase_highlight_style = settings.get('termbase_highlight_style', 'semibold')
31736
31888
  self.termbase_dotted_color = settings.get('termbase_dotted_color', '#808080')
@@ -37377,6 +37529,34 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
37377
37529
  # during load_segments_to_grid when creating cell widgets
37378
37530
  self.load_segments_to_grid()
37379
37531
 
37532
+ def _refresh_source_column_display(self):
37533
+ """Refresh source column to reflect hide_outer_wrapping_tags setting"""
37534
+ if not hasattr(self, 'table') or not self.table:
37535
+ return
37536
+ if not hasattr(self, 'current_project') or not self.current_project:
37537
+ return
37538
+
37539
+ # Update each source cell with potentially stripped tags
37540
+ for row in range(self.table.rowCount()):
37541
+ if row >= len(self.current_project.segments):
37542
+ continue
37543
+
37544
+ segment = self.current_project.segments[row]
37545
+ source_widget = self.table.cellWidget(row, 2)
37546
+
37547
+ if source_widget and hasattr(source_widget, 'setPlainText'):
37548
+ # Calculate display text with or without outer wrapping tags
37549
+ source_for_display = segment.source
37550
+ if self.hide_outer_wrapping_tags:
37551
+ stripped, _ = strip_outer_wrapping_tags(segment.source)
37552
+ source_for_display = stripped
37553
+ source_display_text = self.apply_invisible_replacements(source_for_display)
37554
+ source_widget.setPlainText(source_display_text)
37555
+
37556
+ # Re-apply highlighting
37557
+ if hasattr(source_widget, 'highlighter'):
37558
+ source_widget.highlighter.rehighlight()
37559
+
37380
37560
  def apply_invisible_replacements(self, text):
37381
37561
  """Apply invisible character replacements to text based on settings"""
37382
37562
  if not hasattr(self, 'invisible_display_settings'):
modules/llm_clients.py CHANGED
@@ -594,18 +594,20 @@ class LLMClient:
594
594
  context: Optional[str] = None,
595
595
  custom_prompt: Optional[str] = None,
596
596
  max_tokens: Optional[int] = None,
597
- images: Optional[List] = None
597
+ images: Optional[List] = None,
598
+ system_prompt: Optional[str] = None
598
599
  ) -> str:
599
600
  """
600
601
  Translate text using configured LLM
601
-
602
+
602
603
  Args:
603
604
  text: Text to translate
604
605
  source_lang: Source language code
605
606
  target_lang: Target language code
606
607
  context: Optional context for translation
607
608
  custom_prompt: Optional custom prompt (overrides default simple prompt)
608
-
609
+ system_prompt: Optional system prompt for AI behavior context
610
+
609
611
  Returns:
610
612
  Translated text
611
613
  """
@@ -615,30 +617,30 @@ class LLMClient:
615
617
  else:
616
618
  # Build prompt
617
619
  prompt = f"Translate the following text from {source_lang} to {target_lang}:\n\n{text}"
618
-
620
+
619
621
  if context:
620
622
  prompt = f"Context: {context}\n\n{prompt}"
621
-
623
+
622
624
  # Log warning if images provided but model doesn't support vision
623
625
  if images and not self.model_supports_vision(self.provider, self.model):
624
626
  print(f"⚠️ Warning: Model {self.model} doesn't support vision. Images will be ignored.")
625
627
  images = None # Don't pass to API
626
-
628
+
627
629
  # Call appropriate provider
628
630
  if self.provider == "openai":
629
- return self._call_openai(prompt, max_tokens=max_tokens, images=images)
631
+ return self._call_openai(prompt, max_tokens=max_tokens, images=images, system_prompt=system_prompt)
630
632
  elif self.provider == "claude":
631
- return self._call_claude(prompt, max_tokens=max_tokens, images=images)
633
+ return self._call_claude(prompt, max_tokens=max_tokens, images=images, system_prompt=system_prompt)
632
634
  elif self.provider == "gemini":
633
- return self._call_gemini(prompt, max_tokens=max_tokens, images=images)
635
+ return self._call_gemini(prompt, max_tokens=max_tokens, images=images, system_prompt=system_prompt)
634
636
  elif self.provider == "ollama":
635
- return self._call_ollama(prompt, max_tokens=max_tokens)
637
+ return self._call_ollama(prompt, max_tokens=max_tokens, system_prompt=system_prompt)
636
638
  else:
637
639
  raise ValueError(f"Unsupported provider: {self.provider}")
638
640
 
639
- def _call_openai(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None) -> str:
641
+ def _call_openai(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None, system_prompt: Optional[str] = None) -> str:
640
642
  """Call OpenAI API with GPT-5/o1/o3 reasoning model support and vision capability"""
641
- print(f"🔵 _call_openai START: model={self.model}, prompt_len={len(prompt)}, max_tokens={max_tokens}, images={len(images) if images else 0}")
643
+ print(f"🔵 _call_openai START: model={self.model}, prompt_len={len(prompt)}, max_tokens={max_tokens}, images={len(images) if images else 0}, has_system={bool(system_prompt)}")
642
644
 
643
645
  try:
644
646
  from openai import OpenAI
@@ -686,10 +688,16 @@ class LLMClient:
686
688
  # Standard text-only format
687
689
  content = prompt
688
690
 
691
+ # Build messages list
692
+ messages = []
693
+ if system_prompt:
694
+ messages.append({"role": "system", "content": system_prompt})
695
+ messages.append({"role": "user", "content": content})
696
+
689
697
  # Build API call parameters
690
698
  api_params = {
691
699
  "model": self.model,
692
- "messages": [{"role": "user", "content": content}],
700
+ "messages": messages,
693
701
  "timeout": timeout_seconds
694
702
  }
695
703
 
@@ -742,7 +750,7 @@ class LLMClient:
742
750
  print(f" Response: {e.response}")
743
751
  raise # Re-raise to be caught by calling code
744
752
 
745
- def _call_claude(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None) -> str:
753
+ def _call_claude(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None, system_prompt: Optional[str] = None) -> str:
746
754
  """Call Anthropic Claude API with vision support"""
747
755
  try:
748
756
  import anthropic
@@ -786,12 +794,19 @@ class LLMClient:
786
794
  # Standard text-only format
787
795
  content = prompt
788
796
 
789
- response = client.messages.create(
790
- model=self.model,
791
- max_tokens=tokens_to_use,
792
- messages=[{"role": "user", "content": content}],
793
- timeout=timeout_seconds # Explicit timeout
794
- )
797
+ # Build API call parameters
798
+ api_params = {
799
+ "model": self.model,
800
+ "max_tokens": tokens_to_use,
801
+ "messages": [{"role": "user", "content": content}],
802
+ "timeout": timeout_seconds # Explicit timeout
803
+ }
804
+
805
+ # Add system prompt if provided (Claude uses 'system' parameter, not a message)
806
+ if system_prompt:
807
+ api_params["system"] = system_prompt
808
+
809
+ response = client.messages.create(**api_params)
795
810
 
796
811
  translation = response.content[0].text.strip()
797
812
 
@@ -800,7 +815,7 @@ class LLMClient:
800
815
 
801
816
  return translation
802
817
 
803
- def _call_gemini(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None) -> str:
818
+ def _call_gemini(self, prompt: str, max_tokens: Optional[int] = None, images: Optional[List] = None, system_prompt: Optional[str] = None) -> str:
804
819
  """Call Google Gemini API with vision support"""
805
820
  try:
806
821
  import google.generativeai as genai
@@ -809,10 +824,15 @@ class LLMClient:
809
824
  raise ImportError(
810
825
  "Google AI library not installed. Install with: pip install google-generativeai pillow"
811
826
  )
812
-
827
+
813
828
  genai.configure(api_key=self.api_key)
814
- model = genai.GenerativeModel(self.model)
815
-
829
+
830
+ # Gemini supports system instructions via GenerativeModel parameter
831
+ if system_prompt:
832
+ model = genai.GenerativeModel(self.model, system_instruction=system_prompt)
833
+ else:
834
+ model = genai.GenerativeModel(self.model)
835
+
816
836
  # Build content (text + optional images)
817
837
  if images:
818
838
  # Gemini format: list with prompt text followed by PIL Image objects
@@ -823,7 +843,7 @@ class LLMClient:
823
843
  else:
824
844
  # Standard text-only
825
845
  content = prompt
826
-
846
+
827
847
  response = model.generate_content(content)
828
848
  translation = response.text.strip()
829
849
 
@@ -832,20 +852,21 @@ class LLMClient:
832
852
 
833
853
  return translation
834
854
 
835
- def _call_ollama(self, prompt: str, max_tokens: Optional[int] = None) -> str:
855
+ def _call_ollama(self, prompt: str, max_tokens: Optional[int] = None, system_prompt: Optional[str] = None) -> str:
836
856
  """
837
857
  Call local Ollama server for translation.
838
-
858
+
839
859
  Ollama provides a simple REST API compatible with local LLM inference.
840
860
  Models run entirely on the user's computer - no API keys, no internet required.
841
-
861
+
842
862
  Args:
843
863
  prompt: The full prompt to send
844
864
  max_tokens: Maximum tokens to generate (default: 4096)
845
-
865
+ system_prompt: Optional system prompt for AI behavior context
866
+
846
867
  Returns:
847
868
  Translated text
848
-
869
+
849
870
  Raises:
850
871
  ConnectionError: If Ollama is not running
851
872
  ValueError: If model is not available
@@ -866,13 +887,17 @@ class LLMClient:
866
887
  print(f"🟠 _call_ollama START: model={self.model}, prompt_len={len(prompt)}, max_tokens={tokens_to_use}")
867
888
  print(f"🟠 Ollama endpoint: {endpoint}")
868
889
 
890
+ # Build messages list
891
+ messages = []
892
+ if system_prompt:
893
+ messages.append({"role": "system", "content": system_prompt})
894
+ messages.append({"role": "user", "content": prompt})
895
+
869
896
  # Build request payload
870
897
  # Using /api/chat for chat-style interaction (better for translation prompts)
871
898
  payload = {
872
899
  "model": self.model,
873
- "messages": [
874
- {"role": "user", "content": prompt}
875
- ],
900
+ "messages": messages,
876
901
  "stream": False, # Get complete response at once
877
902
  "options": {
878
903
  "temperature": 0.3, # Low temperature for consistent translations
@@ -3795,6 +3795,26 @@ Output complete ACTION."""
3795
3795
  from PyQt6.QtWidgets import QApplication
3796
3796
  QApplication.processEvents()
3797
3797
 
3798
+ # System prompt that explains the ACTION format to the AI
3799
+ ai_system_prompt = """You are an AI assistant for Supervertaler, a professional translation workbench.
3800
+
3801
+ You can execute actions using a special format. When you need to create, modify, or manage prompts, output ACTION blocks in this EXACT format:
3802
+
3803
+ ACTION:function_name PARAMS:{"param1": "value1", "param2": "value2"}
3804
+
3805
+ Available actions:
3806
+ - create_prompt: Create a new prompt. Required params: name, content. Optional: folder, description, activate
3807
+ - update_prompt: Update an existing prompt. Required params: name. Optional: content, folder, description
3808
+ - delete_prompt: Delete a prompt. Required params: name
3809
+ - list_prompts: List all prompts. Optional params: folder
3810
+ - activate_prompt: Set a prompt as active. Required params: name
3811
+
3812
+ IMPORTANT:
3813
+ 1. Output ONLY the ACTION block when asked to create/modify prompts - no explanatory text
3814
+ 2. The ACTION must be on a single line (PARAMS JSON can be multiline if needed)
3815
+ 3. Use valid JSON for PARAMS (double quotes for strings, escape special characters)
3816
+ 4. Do not wrap in code fences or add any markdown formatting"""
3817
+
3798
3818
  # Call LLM using translate method with custom prompt
3799
3819
  # The translate method accepts a custom_prompt parameter that we can use for any text generation
3800
3820
  self.log_message("[AI Assistant] Calling LLM translate method...")
@@ -3802,7 +3822,8 @@ Output complete ACTION."""
3802
3822
  text="", # Empty text since we're using custom_prompt
3803
3823
  source_lang="en",
3804
3824
  target_lang="en",
3805
- custom_prompt=prompt
3825
+ custom_prompt=prompt,
3826
+ system_prompt=ai_system_prompt
3806
3827
  )
3807
3828
 
3808
3829
  # Log the response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supervertaler
3
- Version: 1.9.195
3
+ Version: 1.9.197
4
4
  Summary: Professional AI-enhanced translation workbench with multi-LLM support, glossary system, TM, spellcheck, voice commands, and PyQt6 interface. Batteries included (core).
5
5
  Home-page: https://supervertaler.com
6
6
  Author: Michael Beijer
@@ -1,4 +1,4 @@
1
- Supervertaler.py,sha256=q0s4g5vj5OB-IFOhh6FBQVVzZhmpcPmTboqbSMQitfQ,2380037
1
+ Supervertaler.py,sha256=YdvLAgY__whrP8VBodeeVh70BXurnBlJ4PpFrFUkXdk,2387857
2
2
  modules/__init__.py,sha256=G58XleS-EJ2sX4Kehm-3N2m618_W2Es0Kg8CW_eBG7g,327
3
3
  modules/ai_actions.py,sha256=i5MJcM-7Y6CAvKUwxmxrVHeoZAVtAP7aRDdWM5KLkO0,33877
4
4
  modules/ai_attachment_manager.py,sha256=juZlrW3UPkIkcnj0SREgOQkQROLf0fcu3ShZcKXMxsI,11361
@@ -23,7 +23,7 @@ modules/find_replace_qt.py,sha256=z06yyjOCmpYBrCzZcCv68VK-2o6pjfFCPtbr9o95XH8,18
23
23
  modules/glossary_manager.py,sha256=JDxY9RAGcv-l6Nms4FH7CNDucZdY1TI4WTzyylAuj24,16437
24
24
  modules/image_extractor.py,sha256=HI6QHnpkjO35GHzTXbzazYdjoHZPFD44WAa4JGa9bAw,8332
25
25
  modules/keyboard_shortcuts_widget.py,sha256=H489eknMDJkYusri1hQYr5MT-NhkVoHWdx9k_NMTr68,26411
26
- modules/llm_clients.py,sha256=sdlc6CMFhPbAM5OEJow7LFsHCY8HOnU1jLXHVh5cJ50,48831
26
+ modules/llm_clients.py,sha256=q-E9PnmtAuN8LWs_EKlMV4sXN5Jx2L57kfKCTk9X-24,50012
27
27
  modules/llm_leaderboard.py,sha256=MQ-RbjlE10-CdgVHwoVXzXlCuUfulZ839FaF6dKlt1M,29139
28
28
  modules/llm_superbench_ui.py,sha256=lmzsL8lt0KzFw-z8De1zb49Emnv7f1dZv_DJmoQz0bQ,60212
29
29
  modules/local_llm_setup.py,sha256=33y-D_LKzkn2w8ejyjeKaovf_An6xQ98mKISoqe-Qjc,42661
@@ -76,13 +76,13 @@ modules/translation_memory.py,sha256=LnG8csZNL2GTHXT4zk0uecJEtvRc-MKwv7Pt7EX3s7s
76
76
  modules/translation_results_panel.py,sha256=OWqzV9xmJOi8NGCi3h42nq-qE7-v6WStjQWRsghCVbQ,92044
77
77
  modules/translation_services.py,sha256=lyVpWuZK1wtVtYZMDMdLoq1DHBoSaeAnp-Yejb0TlVQ,10530
78
78
  modules/unified_prompt_library.py,sha256=96u4WlMwnmmhD4uNJHZ-qVQj8v9_8dA2AVCWpBcwTrg,26006
79
- modules/unified_prompt_manager_qt.py,sha256=HkGUnH0wlfxt-hVe-nKCeWLyProYdefuuq2slqv8hI4,172160
79
+ modules/unified_prompt_manager_qt.py,sha256=y7xAIM9X7f1afXGE1tOcAc5EY7BKFy53Rgqi3fFzKmQ,173374
80
80
  modules/voice_commands.py,sha256=iBb-gjWxRMLhFH7-InSRjYJz1EIDBNA2Pog8V7TtJaY,38516
81
81
  modules/voice_dictation.py,sha256=QmitXfkG-vRt5hIQATjphHdhXfqmwhzcQcbXB6aRzIg,16386
82
82
  modules/voice_dictation_lite.py,sha256=jorY0BmWE-8VczbtGrWwt1zbnOctMoSlWOsQrcufBcc,9423
83
- supervertaler-1.9.195.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
84
- supervertaler-1.9.195.dist-info/METADATA,sha256=-VZa5KNc_rq-ItQkr9Xa01lp9lxdP_czPERVnrmwet4,5725
85
- supervertaler-1.9.195.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
86
- supervertaler-1.9.195.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
87
- supervertaler-1.9.195.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
88
- supervertaler-1.9.195.dist-info/RECORD,,
83
+ supervertaler-1.9.197.dist-info/licenses/LICENSE,sha256=m28u-4qL5nXIWnJ6xlQVw__H30rWFtRK3pCOais2OuY,1092
84
+ supervertaler-1.9.197.dist-info/METADATA,sha256=6_J_jdwGKZ-MCwIJODfkHTxXjhBHY7Wf5pK-xDMbQwU,5725
85
+ supervertaler-1.9.197.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
86
+ supervertaler-1.9.197.dist-info/entry_points.txt,sha256=NP4hiCvx-_30YYKqgr-jfJYQvHr1qTYBMfoVmKIXSM8,53
87
+ supervertaler-1.9.197.dist-info/top_level.txt,sha256=9tUHBYUSfaE4S2E4W3eavJsDyYymkwLfeWAHHAPT6Dk,22
88
+ supervertaler-1.9.197.dist-info/RECORD,,