supervertaler 1.9.153__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.

Files changed (85) hide show
  1. Supervertaler.py +47886 -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 +1878 -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 +333 -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 +1172 -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.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
@@ -0,0 +1,350 @@
1
+ """
2
+ Supervertaler Feature Manager
3
+ =============================
4
+
5
+ Manages feature availability and user enable/disable toggles. Features can be
6
+ toggled in Settings → Features.
7
+
8
+ Each feature module has:
9
+ - Required pip packages (some are optional extras)
10
+ - Size estimate for user information
11
+ - Availability check (are dependencies installed?)
12
+ - Enable/disable toggle (user preference)
13
+
14
+ Installation examples:
15
+ pip install supervertaler # Recommended core install
16
+ pip install supervertaler[supermemory] # Optional: Supermemory semantic search (heavy)
17
+ pip install supervertaler[local-whisper] # Optional: Local Whisper (offline, heavy)
18
+ pip install supervertaler[all] # Legacy alias (no-op; kept for compatibility)
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import importlib.util
24
+ import importlib
25
+ from dataclasses import dataclass, field
26
+ from typing import Dict, List, Optional, Callable
27
+ from pathlib import Path
28
+
29
+
30
+ @dataclass
31
+ class FeatureModule:
32
+ """Definition of an optional feature module."""
33
+ id: str
34
+ name: str
35
+ description: str
36
+ pip_extra: str # Name used in pip install supervertaler[extra]
37
+ packages: List[str] # Required pip packages
38
+ size_mb: int # Approximate installed size in MB
39
+ check_import: str # Module to import to check availability
40
+ icon: str = "📦"
41
+ category: str = "Optional"
42
+ enabled_by_default: bool = True
43
+
44
+ def is_available(self) -> bool:
45
+ """Check if required packages are installed."""
46
+ # IMPORTANT: do NOT import the module here.
47
+ # Some optional features (e.g., sentence-transformers/torch) can be slow to import
48
+ # and may even crash in frozen/PyInstaller builds depending on native deps.
49
+ # Availability checks should be cheap and side-effect free.
50
+ try:
51
+ return importlib.util.find_spec(self.check_import) is not None
52
+ except (ImportError, ValueError):
53
+ return False
54
+
55
+
56
+ # Define all optional feature modules
57
+ FEATURE_MODULES: Dict[str, FeatureModule] = {
58
+ "voice": FeatureModule(
59
+ id="voice",
60
+ name="Supervoice (Voice Commands)",
61
+ description=(
62
+ "Voice dictation and hands-free commands. Works via the OpenAI Whisper API (recommended). "
63
+ "Optional offline Local Whisper is available via the 'local-whisper' extra."
64
+ ),
65
+ pip_extra="voice",
66
+ packages=["sounddevice", "numpy", "openai"],
67
+ size_mb=150,
68
+ check_import="sounddevice",
69
+ icon="🎤",
70
+ category="AI Features",
71
+ enabled_by_default=False,
72
+ ),
73
+ "local_whisper": FeatureModule(
74
+ id="local_whisper",
75
+ name="Local Whisper (Offline Speech Recognition)",
76
+ description=(
77
+ "Offline Whisper speech recognition (no API key required). This is a heavy dependency (PyTorch) and "
78
+ "may increase install size significantly. Requires FFmpeg for best results."
79
+ ),
80
+ pip_extra="local-whisper",
81
+ packages=["openai-whisper"],
82
+ size_mb=1500,
83
+ check_import="whisper",
84
+ icon="🤖",
85
+ category="AI Features",
86
+ enabled_by_default=False,
87
+ ),
88
+ "webengine": FeatureModule(
89
+ id="webengine",
90
+ name="Web Browser (Superlookup)",
91
+ description="Built-in web browser for research resources in Superlookup. Access IATE, ProZ, Linguee, Wikipedia directly from the app.",
92
+ pip_extra="web",
93
+ packages=["PyQt6-WebEngine"],
94
+ size_mb=100,
95
+ check_import="PyQt6.QtWebEngineWidgets",
96
+ icon="🌐",
97
+ category="UI Features",
98
+ ),
99
+ "pdf": FeatureModule(
100
+ id="pdf",
101
+ name="PDF Rescue (OCR)",
102
+ description="Extract text from scanned PDFs using AI OCR. Converts locked PDFs into editable DOCX files.",
103
+ pip_extra="pdf",
104
+ packages=["PyMuPDF"],
105
+ size_mb=30,
106
+ check_import="fitz",
107
+ icon="📄",
108
+ category="Document Processing",
109
+ ),
110
+ "mt_providers": FeatureModule(
111
+ id="mt_providers",
112
+ name="MT Providers (DeepL, Amazon)",
113
+ description="Additional machine translation providers: DeepL API and Amazon Translate. Requires API keys.",
114
+ pip_extra="mt",
115
+ packages=["boto3", "deepl"],
116
+ size_mb=30,
117
+ check_import="deepl",
118
+ icon="🔄",
119
+ category="Translation",
120
+ ),
121
+ "hunspell": FeatureModule(
122
+ id="hunspell",
123
+ name="Hunspell Spellcheck",
124
+ description="Advanced spellcheck using Hunspell dictionaries. Supports regional variants (en-US, en-GB, etc). Uses spylls (pure Python) on Windows.",
125
+ pip_extra="hunspell",
126
+ packages=["spylls"], # Pure Python Hunspell - works on all platforms
127
+ size_mb=20,
128
+ check_import="spylls",
129
+ icon="📝",
130
+ category="Quality Assurance",
131
+ enabled_by_default=True, # spylls works everywhere
132
+ ),
133
+ "automation": FeatureModule(
134
+ id="automation",
135
+ name="AutoFingers (Windows)",
136
+ description="Automated keyboard/mouse control for memoQ integration. Windows only - uses AutoHotkey.",
137
+ pip_extra="windows",
138
+ packages=["keyboard", "ahk"],
139
+ size_mb=10,
140
+ check_import="keyboard",
141
+ icon="🤖",
142
+ category="Automation",
143
+ enabled_by_default=False, # Windows-only
144
+ ),
145
+ }
146
+
147
+
148
+ class FeatureManager:
149
+ """
150
+ Manages feature availability and user preferences.
151
+
152
+ Usage:
153
+ fm = FeatureManager(user_data_path)
154
+
155
+ # Check if a feature can be used
156
+ if fm.is_feature_usable("supermemory"):
157
+ from modules.supermemory import SupermemoryEngine
158
+
159
+ # Get all features for Settings UI
160
+ for feature in fm.get_all_features():
161
+ print(f"{feature.name}: {'✅' if fm.is_feature_usable(feature.id) else '❌'}")
162
+ """
163
+
164
+ def __init__(self, user_data_path: str = "user_data"):
165
+ self.user_data_path = Path(user_data_path)
166
+ self.settings_file = self.user_data_path / "feature_settings.json"
167
+ self._preferences: Dict[str, bool] = {}
168
+ self._load_preferences()
169
+
170
+ def _load_preferences(self):
171
+ """Load user feature preferences from disk."""
172
+ if self.settings_file.exists():
173
+ try:
174
+ with open(self.settings_file, "r", encoding="utf-8") as f:
175
+ self._preferences = json.load(f)
176
+ except (json.JSONDecodeError, IOError):
177
+ self._preferences = {}
178
+
179
+ # Apply defaults for any missing features
180
+ for feature_id, feature in FEATURE_MODULES.items():
181
+ if feature_id not in self._preferences:
182
+ self._preferences[feature_id] = feature.enabled_by_default
183
+
184
+ def _save_preferences(self):
185
+ """Save user feature preferences to disk."""
186
+ self.user_data_path.mkdir(parents=True, exist_ok=True)
187
+ with open(self.settings_file, "w", encoding="utf-8") as f:
188
+ json.dump(self._preferences, f, indent=2)
189
+
190
+ def get_feature(self, feature_id: str) -> Optional[FeatureModule]:
191
+ """Get a feature module definition by ID."""
192
+ return FEATURE_MODULES.get(feature_id)
193
+
194
+ def get_all_features(self) -> List[FeatureModule]:
195
+ """Get all feature module definitions."""
196
+ return list(FEATURE_MODULES.values())
197
+
198
+ def get_features_by_category(self) -> Dict[str, List[FeatureModule]]:
199
+ """Get features grouped by category."""
200
+ categories: Dict[str, List[FeatureModule]] = {}
201
+ for feature in FEATURE_MODULES.values():
202
+ if feature.category not in categories:
203
+ categories[feature.category] = []
204
+ categories[feature.category].append(feature)
205
+ return categories
206
+
207
+ def is_feature_available(self, feature_id: str) -> bool:
208
+ """Check if a feature's dependencies are installed."""
209
+ feature = FEATURE_MODULES.get(feature_id)
210
+ if not feature:
211
+ return False
212
+ return feature.is_available()
213
+
214
+ def is_feature_enabled(self, feature_id: str) -> bool:
215
+ """Check if a feature is enabled by user preference."""
216
+ return self._preferences.get(feature_id, True)
217
+
218
+ def is_feature_usable(self, feature_id: str) -> bool:
219
+ """Check if a feature can be used (available AND enabled)."""
220
+ return self.is_feature_available(feature_id) and self.is_feature_enabled(feature_id)
221
+
222
+ def set_feature_enabled(self, feature_id: str, enabled: bool):
223
+ """Enable or disable a feature."""
224
+ self._preferences[feature_id] = enabled
225
+ self._save_preferences()
226
+
227
+ def get_total_size_mb(self, only_enabled: bool = False) -> int:
228
+ """Get total size of installed/enabled features."""
229
+ total = 0
230
+ for feature_id, feature in FEATURE_MODULES.items():
231
+ if only_enabled and not self.is_feature_enabled(feature_id):
232
+ continue
233
+ if feature.is_available():
234
+ total += feature.size_mb
235
+ return total
236
+
237
+ def get_install_command(self, feature_id: str) -> str:
238
+ """Get the pip install command for a feature."""
239
+ feature = FEATURE_MODULES.get(feature_id)
240
+ if not feature:
241
+ return ""
242
+ return f"pip install supervertaler[{feature.pip_extra}]"
243
+
244
+ def get_missing_features(self) -> List[FeatureModule]:
245
+ """Get features that are enabled but not installed."""
246
+ missing = []
247
+ for feature_id, feature in FEATURE_MODULES.items():
248
+ if self.is_feature_enabled(feature_id) and not feature.is_available():
249
+ missing.append(feature)
250
+ return missing
251
+
252
+
253
+ # Lazy import helpers - use these instead of direct imports
254
+ def lazy_import_supermemory():
255
+ """Lazily import Supermemory module."""
256
+ try:
257
+ from modules.supermemory import SupermemoryEngine
258
+ return SupermemoryEngine
259
+ except ImportError:
260
+ return None
261
+
262
+
263
+ def lazy_import_whisper():
264
+ """Lazily import Whisper for voice commands."""
265
+ try:
266
+ import whisper
267
+ return whisper
268
+ except ImportError:
269
+ return None
270
+
271
+
272
+ def lazy_import_chromadb():
273
+ """Lazily import ChromaDB for vector storage."""
274
+ try:
275
+ import chromadb
276
+ return chromadb
277
+ except ImportError:
278
+ return None
279
+
280
+
281
+ def lazy_import_sentence_transformers():
282
+ """Lazily import sentence-transformers for embeddings."""
283
+ try:
284
+ from sentence_transformers import SentenceTransformer
285
+ return SentenceTransformer
286
+ except ImportError:
287
+ return None
288
+
289
+
290
+ def lazy_import_deepl():
291
+ """Lazily import DeepL API client."""
292
+ try:
293
+ import deepl
294
+ return deepl
295
+ except ImportError:
296
+ return None
297
+
298
+
299
+ def lazy_import_boto3():
300
+ """Lazily import boto3 for Amazon Translate."""
301
+ try:
302
+ import boto3
303
+ return boto3
304
+ except ImportError:
305
+ return None
306
+
307
+
308
+ def lazy_import_hunspell():
309
+ """Lazily import Hunspell spellchecker."""
310
+ try:
311
+ import hunspell
312
+ return hunspell
313
+ except ImportError:
314
+ return None
315
+
316
+
317
+ def lazy_import_webengine():
318
+ """Lazily import PyQt6 WebEngine."""
319
+ try:
320
+ from PyQt6.QtWebEngineWidgets import QWebEngineView
321
+ return QWebEngineView
322
+ except ImportError:
323
+ return None
324
+
325
+
326
+ def lazy_import_fitz():
327
+ """Lazily import PyMuPDF (fitz) for PDF processing."""
328
+ try:
329
+ import fitz
330
+ return fitz
331
+ except ImportError:
332
+ return None
333
+
334
+
335
+ # Global feature manager instance (initialized on first use)
336
+ _feature_manager: Optional[FeatureManager] = None
337
+
338
+
339
+ def get_feature_manager(user_data_path: str = "user_data") -> FeatureManager:
340
+ """Get or create the global feature manager instance."""
341
+ global _feature_manager
342
+ if _feature_manager is None:
343
+ _feature_manager = FeatureManager(user_data_path)
344
+ return _feature_manager
345
+
346
+
347
+ def check_feature(feature_id: str) -> bool:
348
+ """Quick check if a feature is usable. Use at import time."""
349
+ fm = get_feature_manager()
350
+ return fm.is_feature_usable(feature_id)
@@ -0,0 +1,340 @@
1
+ """
2
+ Figure Context Manager
3
+ Handles loading, displaying, and providing visual context for technical translations.
4
+
5
+ This module manages figure images that can be automatically included with translation
6
+ requests when the source text references figures (e.g., "Figure 1A", "see fig 2").
7
+
8
+ Author: Michael Beijer + AI Assistant
9
+ Date: October 13, 2025
10
+ """
11
+
12
+ from typing import Dict, List, Tuple, Any, Optional
13
+ import os
14
+ import re
15
+ import base64
16
+ import io
17
+
18
+ try:
19
+ from PIL import Image, ImageTk
20
+ PIL_AVAILABLE = True
21
+ except ImportError:
22
+ PIL_AVAILABLE = False
23
+ Image = None
24
+ ImageTk = None
25
+
26
+
27
+ class FigureContextManager:
28
+ """Manages figure context images for multimodal AI translation."""
29
+
30
+ def __init__(self, app):
31
+ """
32
+ Initialize the Figure Context Manager.
33
+
34
+ Args:
35
+ app: Reference to main application (for logging and UI updates)
36
+ """
37
+ self.app = app
38
+ self.figure_context_map: Dict[str, Any] = {} # ref -> PIL Image
39
+ self.figure_context_folder: Optional[str] = None
40
+ self._photo_references = [] # Store PhotoImage references to prevent GC
41
+
42
+ def detect_figure_references(self, text: str) -> List[str]:
43
+ """
44
+ Detect figure references in text and return normalized list.
45
+
46
+ Examples:
47
+ "As shown in Figure 1A" -> ['1a']
48
+ "See Figures 2 and 3B" -> ['2', '3b']
49
+ "refer to fig. 4" -> ['4']
50
+
51
+ Args:
52
+ text: Source text to scan for figure references
53
+
54
+ Returns:
55
+ List of normalized figure references (lowercase, no spaces)
56
+ """
57
+ if not text:
58
+ return []
59
+
60
+ # Pattern matches: figure/figuur/fig + optional period + space + identifier
61
+ # Match: digit(s) optionally followed by letter(s) (e.g., 1, 2A, 3b, 10C)
62
+ pattern = r"(?:figure|figuur|fig\.?)\s+(\d+[a-zA-Z]?)"
63
+ matches = re.findall(pattern, text, re.IGNORECASE)
64
+
65
+ normalized_refs = []
66
+ for match in matches:
67
+ normalized = normalize_figure_ref(f"fig {match}")
68
+ if normalized and normalized not in normalized_refs:
69
+ normalized_refs.append(normalized)
70
+
71
+ return normalized_refs
72
+
73
+ def load_from_folder(self, folder_path: str) -> int:
74
+ """
75
+ Load all figure images from a folder.
76
+
77
+ Supported formats: .png, .jpg, .jpeg, .gif, .bmp, .tiff
78
+
79
+ Filename examples:
80
+ - "Figure 1.png" -> ref '1'
81
+ - "Figure 2A.jpg" -> ref '2a'
82
+ - "fig3b.png" -> ref '3b'
83
+
84
+ Args:
85
+ folder_path: Path to folder containing figure images
86
+
87
+ Returns:
88
+ Number of successfully loaded images
89
+
90
+ Raises:
91
+ Exception: If folder doesn't exist or PIL is not available
92
+ """
93
+ if not PIL_AVAILABLE:
94
+ raise Exception("PIL/Pillow library is not installed")
95
+
96
+ if not os.path.exists(folder_path):
97
+ raise Exception(f"Folder not found: {folder_path}")
98
+
99
+ self.figure_context_folder = folder_path
100
+ self.figure_context_map.clear()
101
+
102
+ image_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff')
103
+ loaded_count = 0
104
+
105
+ for filename in os.listdir(folder_path):
106
+ if filename.lower().endswith(image_extensions):
107
+ img_path = os.path.join(folder_path, filename)
108
+ try:
109
+ img = Image.open(img_path)
110
+ # Normalize the filename to match figure references
111
+ normalized_name = normalize_figure_ref(filename)
112
+ if normalized_name:
113
+ self.figure_context_map[normalized_name] = img
114
+ loaded_count += 1
115
+ if hasattr(self.app, 'log'):
116
+ self.app.log(f"[Figure Context] Loaded: {filename} as '{normalized_name}'")
117
+ except Exception as e:
118
+ if hasattr(self.app, 'log'):
119
+ self.app.log(f"[Figure Context] Failed to load {filename}: {e}")
120
+
121
+ return loaded_count
122
+
123
+ def clear(self):
124
+ """Clear all loaded figure context images."""
125
+ self.figure_context_map.clear()
126
+ self.figure_context_folder = None
127
+ self._photo_references.clear()
128
+
129
+ def get_images_for_text(self, text: str) -> List[Tuple[str, Any]]:
130
+ """
131
+ Get figure images relevant to the given text.
132
+
133
+ Args:
134
+ text: Source text that may contain figure references
135
+
136
+ Returns:
137
+ List of tuples (ref, PIL.Image) for detected and available figures
138
+ """
139
+ figure_refs = self.detect_figure_references(text)
140
+ figure_images = []
141
+
142
+ for ref in figure_refs:
143
+ if ref in self.figure_context_map:
144
+ figure_images.append((ref, self.figure_context_map[ref]))
145
+
146
+ return figure_images
147
+
148
+ def has_images(self) -> bool:
149
+ """Check if any images are loaded."""
150
+ return len(self.figure_context_map) > 0
151
+
152
+ def get_image_count(self) -> int:
153
+ """Get the number of loaded images."""
154
+ return len(self.figure_context_map)
155
+
156
+ def get_folder_name(self) -> Optional[str]:
157
+ """Get the basename of the loaded folder, or None."""
158
+ if self.figure_context_folder:
159
+ return os.path.basename(self.figure_context_folder)
160
+ return None
161
+
162
+ def update_ui_display(self, image_folder_label=None, image_folder_var=None,
163
+ thumbnails_frame=None, figure_canvas=None):
164
+ """
165
+ Update UI elements to reflect current figure context state.
166
+
167
+ Args:
168
+ image_folder_label: tk.Label widget for main Images tab
169
+ image_folder_var: tk.StringVar for Context notebook
170
+ thumbnails_frame: tk.Frame for displaying thumbnails
171
+ figure_canvas: tk.Canvas for scrollable thumbnail area
172
+ """
173
+ # Update folder label
174
+ if image_folder_label:
175
+ if self.has_images():
176
+ count = self.get_image_count()
177
+ folder_name = self.get_folder_name() or "Unknown"
178
+ image_folder_label.config(
179
+ text=f"✓ {count} image{'s' if count != 1 else ''} loaded from: {folder_name}",
180
+ fg='#4CAF50'
181
+ )
182
+ else:
183
+ image_folder_label.config(
184
+ text="No figure context loaded",
185
+ fg='#999'
186
+ )
187
+
188
+ # Update image_folder_var for Context notebook
189
+ if image_folder_var:
190
+ if self.has_images():
191
+ count = self.get_image_count()
192
+ folder_name = self.get_folder_name() or "Unknown"
193
+ image_folder_var.set(f"✓ {count} figure{'s' if count != 1 else ''} loaded from: {folder_name}")
194
+ else:
195
+ image_folder_var.set("No figure context loaded")
196
+
197
+ # Update thumbnails display
198
+ if thumbnails_frame:
199
+ self._update_thumbnails(thumbnails_frame)
200
+
201
+ def _update_thumbnails(self, thumbnails_frame):
202
+ """
203
+ Update the thumbnail display in the Images tab.
204
+
205
+ Args:
206
+ thumbnails_frame: tk.Frame to populate with thumbnails
207
+ """
208
+ import tkinter as tk
209
+
210
+ # Clear existing thumbnails
211
+ for widget in thumbnails_frame.winfo_children():
212
+ widget.destroy()
213
+
214
+ # Clear photo references
215
+ self._photo_references.clear()
216
+
217
+ if self.has_images() and PIL_AVAILABLE:
218
+ # Display thumbnails
219
+ for ref, img in sorted(self.figure_context_map.items()):
220
+ # Create frame for each thumbnail
221
+ thumb_frame = tk.Frame(thumbnails_frame, bg='white', relief='solid', borderwidth=1)
222
+ thumb_frame.pack(fill='x', padx=5, pady=5)
223
+
224
+ # Figure name label
225
+ tk.Label(thumb_frame, text=f"Figure {ref.upper()}",
226
+ font=('Segoe UI', 9, 'bold'), bg='white').pack(anchor='w', padx=5, pady=2)
227
+
228
+ try:
229
+ # Create thumbnail (max 200px wide)
230
+ img_copy = img.copy()
231
+ img_copy.thumbnail((200, 200), Image.Resampling.LANCZOS)
232
+ photo = ImageTk.PhotoImage(img_copy)
233
+
234
+ # Store reference to prevent garbage collection
235
+ self._photo_references.append(photo)
236
+
237
+ # Display thumbnail
238
+ img_label = tk.Label(thumb_frame, image=photo, bg='white')
239
+ img_label.pack(padx=5, pady=5)
240
+
241
+ # Image info
242
+ info_text = f"{img.width} × {img.height} px"
243
+ tk.Label(thumb_frame, text=info_text,
244
+ font=('Segoe UI', 8), fg='#999', bg='white').pack(anchor='w', padx=5, pady=2)
245
+
246
+ except Exception as e:
247
+ tk.Label(thumb_frame, text=f"Error displaying: {e}",
248
+ font=('Segoe UI', 8), fg='red', bg='white').pack(padx=5, pady=2)
249
+ else:
250
+ # No images loaded - show placeholder
251
+ tk.Label(thumbnails_frame,
252
+ text="No images loaded\n\nUse 'Load figure context...' to add visual context for technical translations.",
253
+ font=('Segoe UI', 9), fg='#999', bg='white',
254
+ justify='center').pack(expand=True, pady=20)
255
+
256
+ def save_state(self) -> Dict[str, Any]:
257
+ """
258
+ Save current state for project persistence.
259
+
260
+ Returns:
261
+ Dictionary with folder_path and image_count
262
+ """
263
+ return {
264
+ 'folder_path': self.figure_context_folder,
265
+ 'image_count': self.get_image_count()
266
+ }
267
+
268
+ def restore_state(self, state: Dict[str, Any]) -> bool:
269
+ """
270
+ Restore state from saved project data.
271
+
272
+ Args:
273
+ state: Dictionary with figure context state
274
+
275
+ Returns:
276
+ True if images were successfully loaded, False otherwise
277
+ """
278
+ folder_path = state.get('folder_path')
279
+ if folder_path and os.path.exists(folder_path):
280
+ try:
281
+ loaded_count = self.load_from_folder(folder_path)
282
+ if loaded_count > 0:
283
+ if hasattr(self.app, 'log'):
284
+ self.app.log(f"✓ Loaded figure context: {loaded_count} images from {os.path.basename(folder_path)}")
285
+ return True
286
+ except Exception as e:
287
+ if hasattr(self.app, 'log'):
288
+ self.app.log(f"⚠ Failed to restore figure context: {e}")
289
+ return False
290
+
291
+
292
+ # === Helper Functions ===
293
+
294
+ def normalize_figure_ref(text: str) -> Optional[str]:
295
+ """
296
+ Normalize a figure reference to a standard format.
297
+
298
+ Examples:
299
+ "Figure 1" -> "1"
300
+ "fig. 2A" -> "2a"
301
+ "Figure3-B.png" -> "3b"
302
+
303
+ Args:
304
+ text: Text containing figure reference or filename
305
+
306
+ Returns:
307
+ Normalized reference (lowercase, alphanumeric only) or None
308
+ """
309
+ if not text:
310
+ return None
311
+
312
+ # Remove common prefixes and file extensions
313
+ text = text.lower()
314
+ text = re.sub(r'\.(png|jpg|jpeg|gif|bmp|tiff)$', '', text)
315
+
316
+ # Extract figure reference (letters and numbers)
317
+ match = re.search(r'(?:figure|figuur|fig\.?)\s*([\w\d]+(?:[\s\.\-]*[\w\d]+)?)', text, re.IGNORECASE)
318
+ if match:
319
+ ref = match.group(1)
320
+ # Remove spaces, dots, dashes
321
+ ref = re.sub(r'[\s\.\-]', '', ref)
322
+ return ref.lower()
323
+
324
+ return None
325
+
326
+
327
+ def pil_image_to_base64_png(img: Any) -> str:
328
+ """
329
+ Convert PIL Image to base64-encoded PNG string.
330
+
331
+ Args:
332
+ img: PIL.Image object
333
+
334
+ Returns:
335
+ Base64-encoded PNG data string
336
+ """
337
+ buffer = io.BytesIO()
338
+ img.save(buffer, format='PNG')
339
+ buffer.seek(0)
340
+ return base64.b64encode(buffer.read()).decode('utf-8')