supervertaler 1.9.163__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.
Files changed (85) hide show
  1. Supervertaler.py +48473 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1911 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +351 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1176 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.163.dist-info/METADATA +906 -0
  81. supervertaler-1.9.163.dist-info/RECORD +85 -0
  82. supervertaler-1.9.163.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.163.dist-info/top_level.txt +2 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ AI Assistant Attachment Manager
3
+
4
+ Manages persistent storage of attached files for the AI Assistant.
5
+ Files are converted to markdown and stored with metadata.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import hashlib
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+ from typing import Dict, List, Optional, Tuple
14
+
15
+
16
+ class AttachmentManager:
17
+ """
18
+ Manages file attachments for AI Assistant conversations.
19
+
20
+ Features:
21
+ - Persistent storage of converted markdown files
22
+ - Metadata tracking (original name, path, type, size, date)
23
+ - Session-based organization
24
+ - Master index for quick lookup
25
+ """
26
+
27
+ def __init__(self, base_dir: str = None, log_callback=None):
28
+ """
29
+ Initialize the AttachmentManager.
30
+
31
+ Args:
32
+ base_dir: Base directory for attachments (default: user_data_private/ai_assistant)
33
+ log_callback: Function to call for logging messages
34
+ """
35
+ self.log = log_callback if log_callback else print
36
+
37
+ # Set base directory
38
+ if base_dir is None:
39
+ # Default to user_data_private/ai_assistant
40
+ base_dir = Path("user_data_private") / "ai_assistant"
41
+
42
+ self.base_dir = Path(base_dir)
43
+ self.attachments_dir = self.base_dir / "attachments"
44
+ self.conversations_dir = self.base_dir / "conversations"
45
+ self.index_file = self.base_dir / "index.json"
46
+
47
+ # Create directory structure
48
+ self._init_directories()
49
+
50
+ # Load index
51
+ self.index = self._load_index()
52
+
53
+ # Current session ID
54
+ self.current_session_id = None
55
+
56
+ def _init_directories(self):
57
+ """Create necessary directories if they don't exist"""
58
+ self.base_dir.mkdir(parents=True, exist_ok=True)
59
+ self.attachments_dir.mkdir(parents=True, exist_ok=True)
60
+ self.conversations_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ self.log(f"✓ Attachment directories initialized: {self.base_dir}")
63
+
64
+ def _load_index(self) -> Dict:
65
+ """Load the master index of all attachments"""
66
+ if not self.index_file.exists():
67
+ return {
68
+ "version": "1.0",
69
+ "created": datetime.now().isoformat(),
70
+ "attachments": {}, # file_id -> metadata
71
+ "sessions": {} # session_id -> [file_ids]
72
+ }
73
+
74
+ try:
75
+ with open(self.index_file, 'r', encoding='utf-8') as f:
76
+ return json.load(f)
77
+ except Exception as e:
78
+ self.log(f"⚠ Failed to load index: {e}")
79
+ return {
80
+ "version": "1.0",
81
+ "created": datetime.now().isoformat(),
82
+ "attachments": {},
83
+ "sessions": {}
84
+ }
85
+
86
+ def _save_index(self):
87
+ """Save the master index"""
88
+ try:
89
+ self.index["updated"] = datetime.now().isoformat()
90
+ with open(self.index_file, 'w', encoding='utf-8') as f:
91
+ json.dump(self.index, f, indent=2, ensure_ascii=False)
92
+ except Exception as e:
93
+ self.log(f"✗ Failed to save index: {e}")
94
+
95
+ def _generate_file_id(self, original_path: str, content: str) -> str:
96
+ """Generate unique file ID based on path and content hash"""
97
+ content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()[:16]
98
+ path_hash = hashlib.sha256(original_path.encode('utf-8')).hexdigest()[:8]
99
+ return f"{path_hash}_{content_hash}"
100
+
101
+ def set_session(self, session_id: str):
102
+ """Set the current session ID"""
103
+ self.current_session_id = session_id
104
+
105
+ # Create session directory
106
+ session_dir = self.attachments_dir / session_id
107
+ session_dir.mkdir(parents=True, exist_ok=True)
108
+
109
+ # Initialize session in index
110
+ if session_id not in self.index["sessions"]:
111
+ self.index["sessions"][session_id] = []
112
+ self._save_index()
113
+
114
+ def attach_file(
115
+ self,
116
+ original_path: str,
117
+ markdown_content: str,
118
+ original_name: str = None,
119
+ conversation_id: str = None
120
+ ) -> Optional[str]:
121
+ """
122
+ Save an attached file with metadata.
123
+
124
+ Args:
125
+ original_path: Full path to original file
126
+ markdown_content: Converted markdown content
127
+ original_name: Original filename (optional, extracted from path if not provided)
128
+ conversation_id: ID of conversation this file belongs to
129
+
130
+ Returns:
131
+ file_id if successful, None otherwise
132
+ """
133
+ try:
134
+ # Ensure session is set
135
+ if self.current_session_id is None:
136
+ self.set_session(datetime.now().strftime("%Y%m%d_%H%M%S"))
137
+
138
+ # Extract filename if not provided
139
+ if original_name is None:
140
+ original_name = Path(original_path).name
141
+
142
+ # Generate file ID
143
+ file_id = self._generate_file_id(original_path, markdown_content)
144
+
145
+ # Check if already attached
146
+ if file_id in self.index["attachments"]:
147
+ self.log(f"⚠ File already attached: {original_name}")
148
+ return file_id
149
+
150
+ # Session directory
151
+ session_dir = self.attachments_dir / self.current_session_id
152
+
153
+ # Save markdown content
154
+ md_file = session_dir / f"{file_id}.md"
155
+ md_file.write_text(markdown_content, encoding='utf-8')
156
+
157
+ # Create metadata
158
+ file_type = Path(original_path).suffix
159
+ metadata = {
160
+ "file_id": file_id,
161
+ "original_name": original_name,
162
+ "original_path": str(original_path),
163
+ "file_type": file_type,
164
+ "size_bytes": len(markdown_content.encode('utf-8')),
165
+ "size_chars": len(markdown_content),
166
+ "attached_at": datetime.now().isoformat(),
167
+ "session_id": self.current_session_id,
168
+ "conversation_id": conversation_id,
169
+ "markdown_path": str(md_file.relative_to(self.base_dir))
170
+ }
171
+
172
+ # Save metadata
173
+ meta_file = session_dir / f"{file_id}.meta.json"
174
+ with open(meta_file, 'w', encoding='utf-8') as f:
175
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
176
+
177
+ # Update index
178
+ self.index["attachments"][file_id] = metadata
179
+ self.index["sessions"][self.current_session_id].append(file_id)
180
+ self._save_index()
181
+
182
+ self.log(f"✓ Attached file: {original_name} ({file_id})")
183
+ return file_id
184
+
185
+ except Exception as e:
186
+ self.log(f"✗ Failed to attach file: {e}")
187
+ return None
188
+
189
+ def get_file(self, file_id: str) -> Optional[Dict]:
190
+ """
191
+ Get file metadata and content.
192
+
193
+ Args:
194
+ file_id: File ID
195
+
196
+ Returns:
197
+ Dictionary with metadata and content, or None if not found
198
+ """
199
+ if file_id not in self.index["attachments"]:
200
+ return None
201
+
202
+ metadata = self.index["attachments"][file_id].copy()
203
+
204
+ # Load markdown content
205
+ md_path = self.base_dir / metadata["markdown_path"]
206
+ if md_path.exists():
207
+ metadata["content"] = md_path.read_text(encoding='utf-8')
208
+ else:
209
+ metadata["content"] = None
210
+
211
+ return metadata
212
+
213
+ def remove_file(self, file_id: str) -> bool:
214
+ """
215
+ Remove an attached file.
216
+
217
+ Args:
218
+ file_id: File ID to remove
219
+
220
+ Returns:
221
+ True if successful, False otherwise
222
+ """
223
+ if file_id not in self.index["attachments"]:
224
+ self.log(f"⚠ File not found: {file_id}")
225
+ return False
226
+
227
+ try:
228
+ metadata = self.index["attachments"][file_id]
229
+ session_id = metadata["session_id"]
230
+
231
+ # Delete files
232
+ session_dir = self.attachments_dir / session_id
233
+ md_file = session_dir / f"{file_id}.md"
234
+ meta_file = session_dir / f"{file_id}.meta.json"
235
+
236
+ if md_file.exists():
237
+ md_file.unlink()
238
+ if meta_file.exists():
239
+ meta_file.unlink()
240
+
241
+ # Update index
242
+ del self.index["attachments"][file_id]
243
+ if session_id in self.index["sessions"]:
244
+ if file_id in self.index["sessions"][session_id]:
245
+ self.index["sessions"][session_id].remove(file_id)
246
+
247
+ self._save_index()
248
+
249
+ self.log(f"✓ Removed file: {file_id}")
250
+ return True
251
+
252
+ except Exception as e:
253
+ self.log(f"✗ Failed to remove file: {e}")
254
+ return False
255
+
256
+ def list_session_files(self, session_id: str = None) -> List[Dict]:
257
+ """
258
+ List all files in a session.
259
+
260
+ Args:
261
+ session_id: Session ID (uses current session if None)
262
+
263
+ Returns:
264
+ List of file metadata dictionaries
265
+ """
266
+ if session_id is None:
267
+ session_id = self.current_session_id
268
+
269
+ if session_id is None or session_id not in self.index["sessions"]:
270
+ return []
271
+
272
+ file_ids = self.index["sessions"][session_id]
273
+ files = []
274
+
275
+ for file_id in file_ids:
276
+ if file_id in self.index["attachments"]:
277
+ files.append(self.index["attachments"][file_id].copy())
278
+
279
+ # Sort by attached_at (most recent first)
280
+ files.sort(key=lambda x: x.get("attached_at", ""), reverse=True)
281
+
282
+ return files
283
+
284
+ def list_all_files(self) -> List[Dict]:
285
+ """
286
+ List all attached files across all sessions.
287
+
288
+ Returns:
289
+ List of file metadata dictionaries
290
+ """
291
+ files = [
292
+ metadata.copy()
293
+ for metadata in self.index["attachments"].values()
294
+ ]
295
+
296
+ # Sort by attached_at (most recent first)
297
+ files.sort(key=lambda x: x.get("attached_at", ""), reverse=True)
298
+
299
+ return files
300
+
301
+ def get_stats(self) -> Dict:
302
+ """
303
+ Get statistics about attachments.
304
+
305
+ Returns:
306
+ Dictionary with stats (total_files, total_size, sessions, etc.)
307
+ """
308
+ total_files = len(self.index["attachments"])
309
+ total_size = sum(
310
+ meta.get("size_bytes", 0)
311
+ for meta in self.index["attachments"].values()
312
+ )
313
+ total_sessions = len(self.index["sessions"])
314
+
315
+ return {
316
+ "total_files": total_files,
317
+ "total_size_bytes": total_size,
318
+ "total_size_mb": total_size / (1024 * 1024),
319
+ "total_sessions": total_sessions,
320
+ "current_session": self.current_session_id
321
+ }
322
+
323
+ def cleanup_empty_sessions(self):
324
+ """Remove sessions with no files"""
325
+ empty_sessions = [
326
+ session_id
327
+ for session_id, file_ids in self.index["sessions"].items()
328
+ if len(file_ids) == 0
329
+ ]
330
+
331
+ for session_id in empty_sessions:
332
+ del self.index["sessions"][session_id]
333
+
334
+ # Remove session directory if empty
335
+ session_dir = self.attachments_dir / session_id
336
+ if session_dir.exists() and not any(session_dir.iterdir()):
337
+ session_dir.rmdir()
338
+
339
+ if empty_sessions:
340
+ self._save_index()
341
+ self.log(f"✓ Cleaned up {len(empty_sessions)} empty sessions")
342
+
343
+ return len(empty_sessions)
@@ -0,0 +1,210 @@
1
+ """
2
+ AI Assistant File Viewer Dialog
3
+
4
+ Dialog for viewing attached file content in markdown format.
5
+ """
6
+
7
+ from PyQt6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit,
9
+ QPushButton, QGroupBox, QMessageBox, QApplication
10
+ )
11
+ from PyQt6.QtCore import Qt
12
+ from PyQt6.QtGui import QFont
13
+ from datetime import datetime
14
+
15
+
16
+ class FileViewerDialog(QDialog):
17
+ """
18
+ Dialog for viewing attached file content.
19
+
20
+ Shows:
21
+ - Original filename
22
+ - File type and size
23
+ - Attached date
24
+ - Converted markdown content (read-only)
25
+ - Copy to clipboard button
26
+ """
27
+
28
+ def __init__(self, file_data: dict, parent=None):
29
+ """
30
+ Initialize the file viewer dialog.
31
+
32
+ Args:
33
+ file_data: Dictionary with file metadata and content
34
+ parent: Parent widget
35
+ """
36
+ super().__init__(parent)
37
+
38
+ self.file_data = file_data
39
+ self.setup_ui()
40
+
41
+ def setup_ui(self):
42
+ """Setup the dialog UI"""
43
+ self.setWindowTitle("View Attached File")
44
+ self.setModal(True)
45
+ self.resize(800, 600)
46
+
47
+ layout = QVBoxLayout(self)
48
+ layout.setSpacing(10)
49
+ layout.setContentsMargins(15, 15, 15, 15)
50
+
51
+ # File info group
52
+ info_group = QGroupBox("File Information")
53
+ info_layout = QVBoxLayout(info_group)
54
+
55
+ # Original filename
56
+ name_label = QLabel(f"<b>Filename:</b> {self.file_data.get('original_name', 'Unknown')}")
57
+ name_label.setWordWrap(True)
58
+ info_layout.addWidget(name_label)
59
+
60
+ # File type and size
61
+ file_type = self.file_data.get('file_type', 'Unknown')
62
+ size_bytes = self.file_data.get('size_bytes', 0)
63
+ size_kb = size_bytes / 1024
64
+
65
+ if size_kb < 1024:
66
+ size_str = f"{size_kb:.1f} KB"
67
+ else:
68
+ size_str = f"{size_kb / 1024:.1f} MB"
69
+
70
+ type_size_label = QLabel(f"<b>Type:</b> {file_type} &nbsp;&nbsp; <b>Size:</b> {size_str}")
71
+ info_layout.addWidget(type_size_label)
72
+
73
+ # Attached date
74
+ attached_at = self.file_data.get('attached_at', '')
75
+ if attached_at:
76
+ try:
77
+ # Parse ISO format date
78
+ dt = datetime.fromisoformat(attached_at)
79
+ date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
80
+ except:
81
+ date_str = attached_at
82
+ else:
83
+ date_str = "Unknown"
84
+
85
+ date_label = QLabel(f"<b>Attached:</b> {date_str}")
86
+ info_layout.addWidget(date_label)
87
+
88
+ layout.addWidget(info_group)
89
+
90
+ # Content group
91
+ content_group = QGroupBox("Converted Content (Markdown)")
92
+ content_layout = QVBoxLayout(content_group)
93
+
94
+ # Content viewer (read-only text editor)
95
+ self.content_viewer = QTextEdit()
96
+ self.content_viewer.setReadOnly(True)
97
+ self.content_viewer.setFont(QFont("Consolas", 9))
98
+
99
+ # Set content
100
+ content = self.file_data.get('content', '')
101
+ if content:
102
+ self.content_viewer.setPlainText(content)
103
+ else:
104
+ self.content_viewer.setPlainText("(No content available)")
105
+
106
+ # Move cursor to top
107
+ cursor = self.content_viewer.textCursor()
108
+ cursor.movePosition(cursor.MoveOperation.Start)
109
+ self.content_viewer.setTextCursor(cursor)
110
+
111
+ content_layout.addWidget(self.content_viewer)
112
+
113
+ layout.addWidget(content_group)
114
+
115
+ # Button bar
116
+ button_layout = QHBoxLayout()
117
+ button_layout.setSpacing(8)
118
+
119
+ # Copy button
120
+ copy_btn = QPushButton("📋 Copy to Clipboard")
121
+ copy_btn.setToolTip("Copy content to clipboard")
122
+ copy_btn.clicked.connect(self.copy_to_clipboard)
123
+ button_layout.addWidget(copy_btn)
124
+
125
+ button_layout.addStretch()
126
+
127
+ # Close button
128
+ close_btn = QPushButton("Close")
129
+ close_btn.setDefault(True)
130
+ close_btn.clicked.connect(self.accept)
131
+ button_layout.addWidget(close_btn)
132
+
133
+ layout.addLayout(button_layout)
134
+
135
+ def copy_to_clipboard(self):
136
+ """Copy content to clipboard"""
137
+ content = self.file_data.get('content', '')
138
+ if content:
139
+ clipboard = QApplication.clipboard()
140
+ clipboard.setText(content)
141
+
142
+ QMessageBox.information(
143
+ self,
144
+ "Copied",
145
+ "Content copied to clipboard.",
146
+ QMessageBox.StandardButton.Ok
147
+ )
148
+ else:
149
+ QMessageBox.warning(
150
+ self,
151
+ "No Content",
152
+ "No content available to copy.",
153
+ QMessageBox.StandardButton.Ok
154
+ )
155
+
156
+
157
+ class FileRemoveConfirmDialog(QDialog):
158
+ """
159
+ Confirmation dialog for removing attached files.
160
+ """
161
+
162
+ def __init__(self, filename: str, parent=None):
163
+ """
164
+ Initialize the confirmation dialog.
165
+
166
+ Args:
167
+ filename: Name of file to remove
168
+ parent: Parent widget
169
+ """
170
+ super().__init__(parent)
171
+
172
+ self.filename = filename
173
+ self.setup_ui()
174
+
175
+ def setup_ui(self):
176
+ """Setup the dialog UI"""
177
+ self.setWindowTitle("Confirm Remove File")
178
+ self.setModal(True)
179
+
180
+ layout = QVBoxLayout(self)
181
+ layout.setSpacing(15)
182
+ layout.setContentsMargins(20, 20, 20, 20)
183
+
184
+ # Warning icon and message
185
+ msg_label = QLabel(
186
+ f"⚠️ Are you sure you want to remove this file?\n\n"
187
+ f"<b>{self.filename}</b>\n\n"
188
+ f"The file will be permanently deleted from disk."
189
+ )
190
+ msg_label.setWordWrap(True)
191
+ msg_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
192
+ layout.addWidget(msg_label)
193
+
194
+ # Button bar
195
+ button_layout = QHBoxLayout()
196
+ button_layout.setSpacing(8)
197
+
198
+ # Cancel button
199
+ cancel_btn = QPushButton("Cancel")
200
+ cancel_btn.setDefault(True)
201
+ cancel_btn.clicked.connect(self.reject)
202
+ button_layout.addWidget(cancel_btn)
203
+
204
+ # Remove button
205
+ remove_btn = QPushButton("Remove File")
206
+ remove_btn.setStyleSheet("background-color: #d32f2f; color: white;")
207
+ remove_btn.clicked.connect(self.accept)
208
+ button_layout.addWidget(remove_btn)
209
+
210
+ layout.addLayout(button_layout)