supervertaler 1.9.174__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 +228 -20
- 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.174.dist-info → supervertaler-1.9.175.dist-info}/RECORD +11 -11
- supervertaler-1.9.174.dist-info/METADATA +0 -939
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.175.dist-info}/WHEEL +0 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.175.dist-info}/entry_points.txt +0 -0
- {supervertaler-1.9.174.dist-info → supervertaler-1.9.175.dist-info}/licenses/LICENSE +0 -0
- {supervertaler-1.9.174.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"
|
|
@@ -5794,11 +5794,14 @@ class PreTranslationWorker(QThread):
|
|
|
5794
5794
|
custom_prompt = None
|
|
5795
5795
|
if self.prompt_manager:
|
|
5796
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 []
|
|
5797
5799
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5798
5800
|
source_text=segment.source,
|
|
5799
5801
|
source_lang=source_lang,
|
|
5800
5802
|
target_lang=target_lang,
|
|
5801
|
-
mode="single"
|
|
5803
|
+
mode="single",
|
|
5804
|
+
glossary_terms=glossary_terms
|
|
5802
5805
|
)
|
|
5803
5806
|
# Extract just the instruction part (without the source text section)
|
|
5804
5807
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -5857,11 +5860,14 @@ class PreTranslationWorker(QThread):
|
|
|
5857
5860
|
if self.prompt_manager and batch_segments:
|
|
5858
5861
|
try:
|
|
5859
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 []
|
|
5860
5865
|
full_prompt = self.prompt_manager.build_final_prompt(
|
|
5861
5866
|
source_text=first_segment.source,
|
|
5862
5867
|
source_lang=source_lang,
|
|
5863
5868
|
target_lang=target_lang,
|
|
5864
|
-
mode="single"
|
|
5869
|
+
mode="single",
|
|
5870
|
+
glossary_terms=glossary_terms
|
|
5865
5871
|
)
|
|
5866
5872
|
# Extract just the instruction part
|
|
5867
5873
|
if "**SOURCE TEXT:**" in full_prompt:
|
|
@@ -6308,6 +6314,10 @@ class SupervertalerQt(QMainWindow):
|
|
|
6308
6314
|
# TM Metadata Manager - needed for TM list in Superlookup
|
|
6309
6315
|
from modules.tm_metadata_manager import TMMetadataManager
|
|
6310
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)
|
|
6311
6321
|
|
|
6312
6322
|
# Spellcheck Manager for target language spell checking
|
|
6313
6323
|
self.spellcheck_manager = get_spellcheck_manager(str(self.user_data_path))
|
|
@@ -12750,7 +12760,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12750
12760
|
"💡 <b>Glossaries</b><br>"
|
|
12751
12761
|
"• <b>Read</b> (green ✓): Glossary is used for terminology matching<br>"
|
|
12752
12762
|
"• <b>Write</b> (blue ✓): Glossary is updated with new terms<br>"
|
|
12753
|
-
"• <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)"
|
|
12754
12765
|
)
|
|
12755
12766
|
help_msg.setWordWrap(True)
|
|
12756
12767
|
help_msg.setStyleSheet("background-color: #e3f2fd; padding: 8px; border-radius: 4px; color: #1976d2;")
|
|
@@ -12772,8 +12783,8 @@ class SupervertalerQt(QMainWindow):
|
|
|
12772
12783
|
# Termbase list with table
|
|
12773
12784
|
termbase_table = QTableWidget()
|
|
12774
12785
|
self.termbase_table = termbase_table # Store for external access (Superlookup navigation)
|
|
12775
|
-
termbase_table.setColumnCount(
|
|
12776
|
-
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"])
|
|
12777
12788
|
termbase_table.horizontalHeader().setStretchLastSection(False)
|
|
12778
12789
|
termbase_table.setColumnWidth(0, 80) # Type (Project/Background)
|
|
12779
12790
|
termbase_table.setColumnWidth(1, 180) # Name
|
|
@@ -12782,6 +12793,7 @@ class SupervertalerQt(QMainWindow):
|
|
|
12782
12793
|
termbase_table.setColumnWidth(4, 50) # Read checkbox
|
|
12783
12794
|
termbase_table.setColumnWidth(5, 50) # Write checkbox
|
|
12784
12795
|
termbase_table.setColumnWidth(6, 60) # Priority
|
|
12796
|
+
termbase_table.setColumnWidth(7, 40) # AI checkbox
|
|
12785
12797
|
termbase_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
12786
12798
|
termbase_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
|
12787
12799
|
termbase_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Disable inline editing
|
|
@@ -13475,7 +13487,43 @@ class SupervertalerQt(QMainWindow):
|
|
|
13475
13487
|
priority_item.setToolTip("No priority - glossary not readable")
|
|
13476
13488
|
priority_item.setFlags(priority_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
13477
13489
|
termbase_table.setItem(row, 6, priority_item)
|
|
13478
|
-
|
|
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
|
+
|
|
13479
13527
|
# Update header checkbox states based on current selection
|
|
13480
13528
|
tb_read_header_checkbox.blockSignals(True)
|
|
13481
13529
|
tb_write_header_checkbox.blockSignals(True)
|
|
@@ -31565,9 +31613,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31565
31613
|
|
|
31566
31614
|
# Get source text
|
|
31567
31615
|
source_text = current_segment.source
|
|
31568
|
-
|
|
31616
|
+
|
|
31617
|
+
# Get glossary terms for AI injection
|
|
31618
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
31619
|
+
|
|
31569
31620
|
# Build combined prompt
|
|
31570
|
-
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
|
+
)
|
|
31571
31624
|
|
|
31572
31625
|
# Check for figure/image context
|
|
31573
31626
|
figure_info = ""
|
|
@@ -31594,11 +31647,14 @@ class SupervertalerQt(QMainWindow):
|
|
|
31594
31647
|
composition_parts.append(f"📏 Total prompt: {len(combined):,} characters")
|
|
31595
31648
|
|
|
31596
31649
|
if self.prompt_manager_qt.library.active_primary_prompt:
|
|
31597
|
-
composition_parts.append(f"✓
|
|
31650
|
+
composition_parts.append(f"✓ Custom prompt attached")
|
|
31598
31651
|
|
|
31599
31652
|
if self.prompt_manager_qt.library.attached_prompts:
|
|
31600
31653
|
composition_parts.append(f"✓ {len(self.prompt_manager_qt.library.attached_prompts)} additional prompt(s) attached")
|
|
31601
|
-
|
|
31654
|
+
|
|
31655
|
+
if glossary_terms:
|
|
31656
|
+
composition_parts.append(f"📚 {len(glossary_terms)} glossary term(s) injected")
|
|
31657
|
+
|
|
31602
31658
|
if figure_info:
|
|
31603
31659
|
composition_parts.append(figure_info)
|
|
31604
31660
|
|
|
@@ -31633,12 +31689,59 @@ class SupervertalerQt(QMainWindow):
|
|
|
31633
31689
|
image_notice.setStyleSheet("padding: 10px; border-radius: 4px; margin-bottom: 10px; border-left: 4px solid #ff9800;")
|
|
31634
31690
|
layout.addWidget(image_notice)
|
|
31635
31691
|
|
|
31636
|
-
# Text editor for preview
|
|
31692
|
+
# Text editor for preview with syntax highlighting
|
|
31637
31693
|
text_edit = QTextEdit()
|
|
31638
|
-
text_edit.setPlainText(combined)
|
|
31639
31694
|
text_edit.setReadOnly(True)
|
|
31640
31695
|
text_edit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
|
|
31641
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)
|
|
31642
31745
|
layout.addWidget(text_edit, 1)
|
|
31643
31746
|
|
|
31644
31747
|
# Close button
|
|
@@ -35380,7 +35483,7 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
35380
35483
|
primary_prompt_text = f"✅ {prompt_path}"
|
|
35381
35484
|
attached_count = len(library.attached_prompt_paths) if library.attached_prompt_paths else 0
|
|
35382
35485
|
|
|
35383
|
-
ai_layout.addWidget(QLabel(f"<b>
|
|
35486
|
+
ai_layout.addWidget(QLabel(f"<b>Custom Prompt:</b> {primary_prompt_text}"))
|
|
35384
35487
|
if attached_count > 0:
|
|
35385
35488
|
ai_layout.addWidget(QLabel(f"<b>Attached Prompts:</b> {attached_count}"))
|
|
35386
35489
|
|
|
@@ -40466,13 +40569,17 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
40466
40569
|
except Exception as e:
|
|
40467
40570
|
self.log(f"⚠ Could not add surrounding segments: {e}")
|
|
40468
40571
|
|
|
40572
|
+
# Get glossary terms for AI injection
|
|
40573
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
40574
|
+
|
|
40469
40575
|
custom_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
40470
40576
|
source_text=segment.source,
|
|
40471
40577
|
source_lang=self.current_project.source_lang,
|
|
40472
40578
|
target_lang=self.current_project.target_lang,
|
|
40473
|
-
mode="single"
|
|
40579
|
+
mode="single",
|
|
40580
|
+
glossary_terms=glossary_terms
|
|
40474
40581
|
)
|
|
40475
|
-
|
|
40582
|
+
|
|
40476
40583
|
# Add surrounding context before the translation delimiter
|
|
40477
40584
|
if surrounding_context:
|
|
40478
40585
|
# Insert before the "YOUR TRANSLATION" delimiter
|
|
@@ -42023,11 +42130,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42023
42130
|
if hasattr(self, 'prompt_manager_qt') and self.prompt_manager_qt and batch_segments:
|
|
42024
42131
|
try:
|
|
42025
42132
|
first_segment = batch_segments[0][1]
|
|
42133
|
+
# Get glossary terms for AI injection
|
|
42134
|
+
glossary_terms = self.get_ai_inject_glossary_terms()
|
|
42026
42135
|
base_prompt = self.prompt_manager_qt.build_final_prompt(
|
|
42027
42136
|
source_text=first_segment.source,
|
|
42028
42137
|
source_lang=source_lang,
|
|
42029
42138
|
target_lang=target_lang,
|
|
42030
|
-
mode="single"
|
|
42139
|
+
mode="single",
|
|
42140
|
+
glossary_terms=glossary_terms
|
|
42031
42141
|
)
|
|
42032
42142
|
if "**SOURCE TEXT:**" in base_prompt:
|
|
42033
42143
|
base_prompt = base_prompt.split("**SOURCE TEXT:**")[0].strip()
|
|
@@ -42455,11 +42565,14 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42455
42565
|
# Access parent through closure
|
|
42456
42566
|
parent = self
|
|
42457
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 []
|
|
42458
42570
|
custom_prompt = parent.prompt_manager_qt.build_final_prompt(
|
|
42459
42571
|
source_text=source_text,
|
|
42460
42572
|
source_lang=source_lang,
|
|
42461
42573
|
target_lang=target_lang,
|
|
42462
|
-
mode="single"
|
|
42574
|
+
mode="single",
|
|
42575
|
+
glossary_terms=glossary_terms
|
|
42463
42576
|
)
|
|
42464
42577
|
except Exception as e:
|
|
42465
42578
|
self.log(f"⚠ Could not build LLM prompt from manager: {e}")
|
|
@@ -42942,7 +43055,22 @@ OUTPUT ONLY THE SEGMENT MARKERS. DO NOT ADD EXPLANATIONS BEFORE OR AFTER."""
|
|
|
42942
43055
|
api_keys['google'] = api_keys['gemini']
|
|
42943
43056
|
|
|
42944
43057
|
return api_keys
|
|
42945
|
-
|
|
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
|
+
|
|
42946
43074
|
def ensure_example_api_keys(self):
|
|
42947
43075
|
"""Create example API keys file on first launch for new users"""
|
|
42948
43076
|
example_file = self.user_data_path / "api_keys.example.txt"
|
|
@@ -48257,7 +48385,87 @@ class BlueCheckmarkCheckBox(QCheckBox):
|
|
|
48257
48385
|
|
|
48258
48386
|
painter.drawLine(QPointF(check_x2, check_y2), QPointF(check_x3, check_y3))
|
|
48259
48387
|
painter.drawLine(QPointF(check_x1, check_y1), QPointF(check_x2, check_y2))
|
|
48260
|
-
|
|
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
|
+
|
|
48261
48469
|
painter.end()
|
|
48262
48470
|
|
|
48263
48471
|
|
modules/database_migrations.py
CHANGED
|
@@ -186,9 +186,13 @@ def run_all_migrations(db_manager) -> bool:
|
|
|
186
186
|
# Migration 3: Add display_order and forbidden fields to synonyms
|
|
187
187
|
if not migrate_synonym_fields(db_manager):
|
|
188
188
|
success = False
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
# Migration 4: Add ai_inject field to termbases
|
|
191
|
+
if not migrate_termbase_ai_inject(db_manager):
|
|
192
|
+
success = False
|
|
193
|
+
|
|
190
194
|
print("="*60)
|
|
191
|
-
|
|
195
|
+
|
|
192
196
|
return success
|
|
193
197
|
|
|
194
198
|
|
|
@@ -221,18 +225,26 @@ def check_and_migrate(db_manager) -> bool:
|
|
|
221
225
|
|
|
222
226
|
# Check if synonyms table exists
|
|
223
227
|
cursor.execute("""
|
|
224
|
-
SELECT name FROM sqlite_master
|
|
228
|
+
SELECT name FROM sqlite_master
|
|
225
229
|
WHERE type='table' AND name='termbase_synonyms'
|
|
226
230
|
""")
|
|
227
231
|
needs_synonyms_table = cursor.fetchone() is None
|
|
228
|
-
|
|
232
|
+
|
|
233
|
+
# Check if termbases table has ai_inject column
|
|
234
|
+
cursor.execute("PRAGMA table_info(termbases)")
|
|
235
|
+
termbase_columns = {row[1] for row in cursor.fetchall()}
|
|
236
|
+
needs_ai_inject = 'ai_inject' not in termbase_columns
|
|
237
|
+
|
|
229
238
|
if needs_migration:
|
|
230
239
|
print(f"⚠️ Migration needed - missing columns: {', '.join([c for c in ['project', 'client', 'term_uuid', 'note'] if c not in columns])}")
|
|
231
|
-
|
|
240
|
+
|
|
232
241
|
if needs_synonyms_table:
|
|
233
242
|
print("⚠️ Migration needed - termbase_synonyms table missing")
|
|
234
|
-
|
|
235
|
-
if
|
|
243
|
+
|
|
244
|
+
if needs_ai_inject:
|
|
245
|
+
print("⚠️ Migration needed - termbases.ai_inject column missing")
|
|
246
|
+
|
|
247
|
+
if needs_migration or needs_synonyms_table or needs_ai_inject:
|
|
236
248
|
success = run_all_migrations(db_manager)
|
|
237
249
|
if success:
|
|
238
250
|
# Generate UUIDs for terms that don't have them
|
|
@@ -316,6 +328,41 @@ def migrate_synonym_fields(db_manager) -> bool:
|
|
|
316
328
|
return False
|
|
317
329
|
|
|
318
330
|
|
|
331
|
+
def migrate_termbase_ai_inject(db_manager) -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Add ai_inject column to termbases table.
|
|
334
|
+
When enabled, the termbase's terms will be injected into LLM translation prompts.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
db_manager: DatabaseManager instance
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True if migration successful
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
cursor = db_manager.cursor
|
|
344
|
+
|
|
345
|
+
# Check which columns exist
|
|
346
|
+
cursor.execute("PRAGMA table_info(termbases)")
|
|
347
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
348
|
+
|
|
349
|
+
if 'ai_inject' not in columns:
|
|
350
|
+
print("📊 Adding 'ai_inject' column to termbases...")
|
|
351
|
+
cursor.execute("ALTER TABLE termbases ADD COLUMN ai_inject BOOLEAN DEFAULT 0")
|
|
352
|
+
db_manager.connection.commit()
|
|
353
|
+
print(" ✓ Column 'ai_inject' added successfully")
|
|
354
|
+
else:
|
|
355
|
+
print("✅ termbases.ai_inject column already exists")
|
|
356
|
+
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
print(f"❌ ai_inject migration failed: {e}")
|
|
361
|
+
import traceback
|
|
362
|
+
traceback.print_exc()
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
|
|
319
366
|
def generate_missing_uuids(db_manager) -> bool:
|
|
320
367
|
"""
|
|
321
368
|
Generate UUIDs for any termbase terms that don't have them.
|
modules/termbase_manager.py
CHANGED
|
@@ -409,7 +409,111 @@ class TermbaseManager:
|
|
|
409
409
|
except Exception as e:
|
|
410
410
|
self.log(f"✗ Error setting termbase read_only: {e}")
|
|
411
411
|
return False
|
|
412
|
-
|
|
412
|
+
|
|
413
|
+
def get_termbase_ai_inject(self, termbase_id: int) -> bool:
|
|
414
|
+
"""Get whether termbase terms should be injected into LLM prompts"""
|
|
415
|
+
try:
|
|
416
|
+
cursor = self.db_manager.cursor
|
|
417
|
+
cursor.execute("SELECT ai_inject FROM termbases WHERE id = ?", (termbase_id,))
|
|
418
|
+
result = cursor.fetchone()
|
|
419
|
+
return bool(result[0]) if result and result[0] else False
|
|
420
|
+
except Exception as e:
|
|
421
|
+
self.log(f"✗ Error getting termbase ai_inject: {e}")
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
def set_termbase_ai_inject(self, termbase_id: int, ai_inject: bool) -> bool:
|
|
425
|
+
"""Set whether termbase terms should be injected into LLM prompts"""
|
|
426
|
+
try:
|
|
427
|
+
cursor = self.db_manager.cursor
|
|
428
|
+
cursor.execute("""
|
|
429
|
+
UPDATE termbases SET ai_inject = ? WHERE id = ?
|
|
430
|
+
""", (1 if ai_inject else 0, termbase_id))
|
|
431
|
+
self.db_manager.connection.commit()
|
|
432
|
+
status = "enabled" if ai_inject else "disabled"
|
|
433
|
+
self.log(f"✓ AI injection {status} for termbase {termbase_id}")
|
|
434
|
+
return True
|
|
435
|
+
except Exception as e:
|
|
436
|
+
self.log(f"✗ Error setting termbase ai_inject: {e}")
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
def get_ai_inject_termbases(self, project_id: Optional[int] = None) -> List[Dict]:
|
|
440
|
+
"""
|
|
441
|
+
Get all termbases with ai_inject enabled that are active for the given project.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
project_id: Project ID (0 or None for global)
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
List of termbase dictionaries with all terms
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
cursor = self.db_manager.cursor
|
|
451
|
+
proj_id = project_id if project_id else 0
|
|
452
|
+
|
|
453
|
+
cursor.execute("""
|
|
454
|
+
SELECT t.id, t.name, t.source_lang, t.target_lang
|
|
455
|
+
FROM termbases t
|
|
456
|
+
LEFT JOIN termbase_activation ta ON t.id = ta.termbase_id AND ta.project_id = ?
|
|
457
|
+
WHERE t.ai_inject = 1
|
|
458
|
+
AND (ta.is_active = 1 OR (t.is_global = 1 AND ta.is_active IS NULL))
|
|
459
|
+
ORDER BY ta.priority ASC, t.name ASC
|
|
460
|
+
""", (proj_id,))
|
|
461
|
+
|
|
462
|
+
termbases = []
|
|
463
|
+
for row in cursor.fetchall():
|
|
464
|
+
termbases.append({
|
|
465
|
+
'id': row[0],
|
|
466
|
+
'name': row[1],
|
|
467
|
+
'source_lang': row[2],
|
|
468
|
+
'target_lang': row[3]
|
|
469
|
+
})
|
|
470
|
+
return termbases
|
|
471
|
+
except Exception as e:
|
|
472
|
+
self.log(f"✗ Error getting AI inject termbases: {e}")
|
|
473
|
+
return []
|
|
474
|
+
|
|
475
|
+
def get_ai_inject_terms(self, project_id: Optional[int] = None) -> List[Dict]:
|
|
476
|
+
"""
|
|
477
|
+
Get all terms from AI-inject-enabled termbases for the given project.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
project_id: Project ID (0 or None for global)
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
List of term dictionaries with source_term, target_term, forbidden, termbase_name
|
|
484
|
+
"""
|
|
485
|
+
try:
|
|
486
|
+
# First get all AI-inject termbases
|
|
487
|
+
ai_termbases = self.get_ai_inject_termbases(project_id)
|
|
488
|
+
if not ai_termbases:
|
|
489
|
+
return []
|
|
490
|
+
|
|
491
|
+
all_terms = []
|
|
492
|
+
cursor = self.db_manager.cursor
|
|
493
|
+
|
|
494
|
+
for tb in ai_termbases:
|
|
495
|
+
cursor.execute("""
|
|
496
|
+
SELECT source_term, target_term, forbidden, priority
|
|
497
|
+
FROM termbase_terms
|
|
498
|
+
WHERE termbase_id = ?
|
|
499
|
+
ORDER BY priority ASC, source_term ASC
|
|
500
|
+
""", (tb['id'],))
|
|
501
|
+
|
|
502
|
+
for row in cursor.fetchall():
|
|
503
|
+
all_terms.append({
|
|
504
|
+
'source_term': row[0],
|
|
505
|
+
'target_term': row[1],
|
|
506
|
+
'forbidden': bool(row[2]) if row[2] else False,
|
|
507
|
+
'priority': row[3] or 99,
|
|
508
|
+
'termbase_name': tb['name']
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
self.log(f"📚 Retrieved {len(all_terms)} terms from {len(ai_termbases)} AI-inject glossar{'y' if len(ai_termbases) == 1 else 'ies'}")
|
|
512
|
+
return all_terms
|
|
513
|
+
except Exception as e:
|
|
514
|
+
self.log(f"✗ Error getting AI inject terms: {e}")
|
|
515
|
+
return []
|
|
516
|
+
|
|
413
517
|
def set_termbase_priority(self, termbase_id: int, project_id: int, priority: int) -> bool:
|
|
414
518
|
"""
|
|
415
519
|
Set manual priority for a termbase in a specific project.
|
|
@@ -367,7 +367,7 @@ class UnifiedPromptLibrary:
|
|
|
367
367
|
|
|
368
368
|
self.active_primary_prompt = self.prompts[relative_path]['content']
|
|
369
369
|
self.active_primary_prompt_path = relative_path
|
|
370
|
-
self.log(f"✓ Set
|
|
370
|
+
self.log(f"✓ Set custom prompt: {self.prompts[relative_path].get('name', relative_path)}")
|
|
371
371
|
return True
|
|
372
372
|
|
|
373
373
|
def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
|
|
@@ -399,7 +399,7 @@ class UnifiedPromptLibrary:
|
|
|
399
399
|
self.active_primary_prompt = content
|
|
400
400
|
self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
|
|
401
401
|
|
|
402
|
-
self.log(f"✓ Set external
|
|
402
|
+
self.log(f"✓ Set external custom prompt: {display_name}")
|
|
403
403
|
return True, display_name
|
|
404
404
|
|
|
405
405
|
def attach_prompt(self, relative_path: str) -> bool:
|