GameSentenceMiner 2.10.9__tar.gz → 2.10.11__tar.gz

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 (76) hide show
  1. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/gsm.py +7 -0
  2. gamesentenceminer-2.10.11/GameSentenceMiner/ocr/owocr_area_selector.py +457 -0
  3. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/owocr_helper.py +2 -2
  4. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/configuration.py +1 -0
  5. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/service.py +10 -21
  6. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/texthooking_page.py +4 -4
  7. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  8. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/PKG-INFO +1 -1
  9. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/pyproject.toml +1 -1
  10. gamesentenceminer-2.10.9/GameSentenceMiner/ocr/owocr_area_selector.py +0 -905
  11. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/__init__.py +0 -0
  12. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ai/__init__.py +0 -0
  13. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  14. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/anki.py +0 -0
  15. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/__init__.py +0 -0
  16. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon.png +0 -0
  17. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon128.png +0 -0
  18. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon256.png +0 -0
  19. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon32.png +0 -0
  20. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon512.png +0 -0
  21. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon64.png +0 -0
  22. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/pickaxe.png +0 -0
  23. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/config_gui.py +0 -0
  24. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/gametext.py +0 -0
  25. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/obs.py +0 -0
  26. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/__init__.py +0 -0
  27. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  28. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  29. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  30. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  31. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  32. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  33. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  34. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  35. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  36. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  37. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/__init__.py +0 -0
  38. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  39. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/__init__.py +0 -0
  40. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/send.py +0 -0
  41. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/websocket.py +0 -0
  42. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  43. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  44. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  45. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  46. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/electron_config.py +0 -0
  47. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/ffmpeg.py +0 -0
  48. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/gsm_utils.py +0 -0
  49. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/model.py +0 -0
  50. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/notification.py +0 -0
  51. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/package.py +0 -0
  52. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/ss_selector.py +0 -0
  53. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/text_log.py +0 -0
  54. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/vad.py +0 -0
  55. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/__init__.py +0 -0
  56. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/__init__.py +0 -0
  57. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  58. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  59. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon.ico +0 -0
  60. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon.svg +0 -0
  61. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  62. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/style.css +0 -0
  63. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  64. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  65. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/__init__.py +0 -0
  66. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/index.html +0 -0
  67. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  68. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/utility.html +0 -0
  69. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  70. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  71. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  72. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/requires.txt +0 -0
  73. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  74. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/LICENSE +0 -0
  75. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/README.md +0 -0
  76. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/setup.cfg +0 -0
@@ -452,6 +452,13 @@ def cleanup():
452
452
  if icon:
453
453
  icon.stop()
454
454
 
455
+ for video in gsm_state.videos_to_remove:
456
+ try:
457
+ if os.path.exists(video):
458
+ os.remove(video)
459
+ except Exception as e:
460
+ logger.error(f"Error removing temporary video file {video}: {e}")
461
+
455
462
  settings_window.window.destroy()
456
463
  time.sleep(5)
457
464
  logger.info("Cleanup complete.")
@@ -0,0 +1,457 @@
1
+ import ctypes
2
+ import json
3
+ import sys
4
+ from multiprocessing import Process, Manager
5
+ from pathlib import Path
6
+
7
+ import mss
8
+ from PIL import Image, ImageTk
9
+
10
+ # Assuming a mock or real obs module exists in this path
11
+ from GameSentenceMiner import obs
12
+ from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window
13
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
14
+
15
+ try:
16
+ import pygetwindow as gw
17
+ except ImportError:
18
+ print("Error: pygetwindow library not found. Please install it: pip install pygetwindow")
19
+ gw = None
20
+
21
+ try:
22
+ import tkinter as tk
23
+ from tkinter import font as tkfont # NEW: Import for better font control
24
+
25
+ selector_available = True
26
+ except ImportError:
27
+ print("Error: tkinter library not found. GUI selection is unavailable.")
28
+ selector_available = False
29
+
30
+ MIN_RECT_WIDTH = 25
31
+ MIN_RECT_HEIGHT = 25
32
+
33
+ COORD_SYSTEM_PERCENTAGE = "percentage"
34
+
35
+
36
+ class ScreenSelector:
37
+ def __init__(self, result, window_name):
38
+ if not selector_available or not gw:
39
+ raise RuntimeError("tkinter or pygetwindow is not available.")
40
+ if not window_name:
41
+ raise ValueError("A target window name is required for percentage-based coordinates.")
42
+
43
+ obs.connect_to_obs_sync()
44
+ self.window_name = window_name
45
+ print(f"Targeting window: '{window_name}'")
46
+
47
+ self.sct = mss.mss()
48
+ self.monitors = self.sct.monitors[1:]
49
+ if not self.monitors:
50
+ raise RuntimeError("No monitors found by mss.")
51
+ for i, monitor in enumerate(self.monitors):
52
+ monitor['index'] = i
53
+
54
+ # --- Window Awareness is now critical ---
55
+ self.target_window = self._find_target_window()
56
+ self.target_window_geometry = self._get_window_geometry(self.target_window)
57
+ if not self.target_window_geometry:
58
+ raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
59
+ print(f"Found target window at: {self.target_window_geometry}")
60
+ # ---
61
+
62
+ self.root = None
63
+ self.result = result
64
+ self.rectangles = [] # Internal storage is ALWAYS absolute pixels for drawing
65
+ self.drawn_rect_ids = []
66
+ self.current_rect_id = None
67
+ self.start_x = self.start_y = None
68
+ self.image_mode = True
69
+ self.redo_stack = []
70
+ self.bounding_box = {} # Geometry of the single large canvas window
71
+
72
+ self.load_existing_rectangles()
73
+
74
+ def _find_target_window(self):
75
+ try:
76
+ return get_window(self.window_name)
77
+ except Exception as e:
78
+ print(f"Error finding window '{self.window_name}': {e}")
79
+ return None
80
+
81
+ def _get_window_geometry(self, window):
82
+ if window:
83
+ try:
84
+ # Ensure width/height are positive and non-zero
85
+ width = max(1, window.width)
86
+ height = max(1, window.height)
87
+ return {"left": window.left, "top": window.top, "width": width, "height": height}
88
+ except Exception:
89
+ return None
90
+ return None
91
+
92
+ def get_scene_ocr_config(self):
93
+ app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
94
+ ocr_config_dir = app_dir / "ocr_config"
95
+ ocr_config_dir.mkdir(parents=True, exist_ok=True)
96
+ try:
97
+ scene = sanitize_filename(obs.get_current_scene() or "default_scene")
98
+ except Exception as e:
99
+ print(f"Error getting OBS scene: {e}. Using default config name.")
100
+ scene = "default_scene"
101
+ return ocr_config_dir / f"{scene}.json"
102
+
103
+ def load_existing_rectangles(self):
104
+ """Loads rectangles from config, converting from percentage to absolute pixels for use."""
105
+ config_path = self.get_scene_ocr_config()
106
+ win_geom = self.target_window_geometry # Use current geometry for conversion
107
+ win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
108
+
109
+ try:
110
+ with open(config_path, 'r', encoding='utf-8') as f:
111
+ config_data = json.load(f)
112
+
113
+ if config_data.get("coordinate_system") != COORD_SYSTEM_PERCENTAGE:
114
+ print(
115
+ f"Warning: Config file '{config_path}' does not use '{COORD_SYSTEM_PERCENTAGE}' system. Please re-create selections.")
116
+ return
117
+
118
+ print(f"Loading rectangles from {config_path}...")
119
+ self.rectangles = []
120
+ loaded_count = 0
121
+
122
+ for rect_data in config_data.get("rectangles", []):
123
+ try:
124
+ coords_pct = rect_data["coordinates"]
125
+ x_pct, y_pct, w_pct, h_pct = map(float, coords_pct)
126
+
127
+ # Convert from percentage to absolute pixel coordinates
128
+ x_abs = (x_pct * win_w) + win_l
129
+ y_abs = (y_pct * win_h) + win_t
130
+ w_abs = w_pct * win_w
131
+ h_abs = h_pct * win_h
132
+ abs_coords = (int(x_abs), int(y_abs), int(w_abs), int(h_abs))
133
+
134
+ monitor_index = rect_data["monitor"]['index']
135
+ target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
136
+ if target_monitor:
137
+ self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"]))
138
+ loaded_count += 1
139
+ except (KeyError, ValueError, TypeError) as e:
140
+ print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
141
+
142
+ print(f"Loaded {loaded_count} valid rectangles.")
143
+ except FileNotFoundError:
144
+ print(f"No config found at {config_path}. Starting fresh.")
145
+ except Exception as e:
146
+ print(f"Error loading config: {e}. Starting fresh.")
147
+
148
+ def save_rects(self, event=None):
149
+ """Saves rectangles to config, converting from absolute pixels to percentages."""
150
+ config_path = self.get_scene_ocr_config()
151
+ win_geom = self.target_window_geometry
152
+ win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
153
+ print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
154
+
155
+ serializable_rects = []
156
+ for monitor_dict, abs_coords, is_excluded in self.rectangles:
157
+ x_abs, y_abs, w_abs, h_abs = abs_coords
158
+
159
+ # Convert absolute pixel coordinates to percentages
160
+ x_pct = (x_abs - win_l) / win_w
161
+ y_pct = (y_abs - win_t) / win_h
162
+ w_pct = w_abs / win_w
163
+ h_pct = h_abs / win_h
164
+ coords_to_save = [x_pct, y_pct, w_pct, h_pct]
165
+
166
+ serializable_rects.append({
167
+ "monitor": {'index': monitor_dict['index']},
168
+ "coordinates": coords_to_save,
169
+ "is_excluded": is_excluded
170
+ })
171
+
172
+ save_data = {
173
+ "scene": obs.get_current_scene() or "default_scene",
174
+ "window": self.window_name,
175
+ "coordinate_system": COORD_SYSTEM_PERCENTAGE, # Always save as percentage
176
+ "window_geometry": win_geom, # Save the geometry used for conversion
177
+ "rectangles": serializable_rects
178
+ }
179
+
180
+ with open(config_path, 'w', encoding="utf-8") as f:
181
+ json.dump(save_data, f, indent=4, ensure_ascii=False)
182
+
183
+ print(f"Successfully saved {len(serializable_rects)} rectangles.")
184
+ # Pass back the internal absolute coords for any immediate post-processing
185
+ self.result['rectangles'] = [(r[0], list(r[1]), r[2]) for r in self.rectangles]
186
+ self.result['window_geometry'] = win_geom
187
+ self.result['coordinate_system'] = COORD_SYSTEM_PERCENTAGE
188
+ self.quit_app()
189
+
190
+ def undo_last_rect(self, event=None):
191
+ if self.rectangles and self.drawn_rect_ids:
192
+ last_rect_tuple = self.rectangles.pop()
193
+ last_rect_id = self.drawn_rect_ids.pop()
194
+ self.redo_stack.append((*last_rect_tuple, last_rect_id))
195
+ event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
196
+ print("Undo: Removed last rectangle.")
197
+
198
+ def redo_last_rect(self, event=None):
199
+ if not self.redo_stack: return
200
+ monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
201
+ canvas = event.widget.winfo_toplevel().winfo_children()[0]
202
+ x_abs, y_abs, w_abs, h_abs = abs_coords
203
+ canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
204
+ new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
205
+ outline='orange' if is_excluded else 'green', width=2)
206
+ self.rectangles.append((monitor, abs_coords, is_excluded))
207
+ self.drawn_rect_ids.append(new_rect_id)
208
+ print("Redo: Restored rectangle.")
209
+
210
+ # --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
211
+ def _create_instructions_widget(self, canvas):
212
+ """Creates a text box with usage instructions on the canvas."""
213
+ instructions_text = (
214
+ "How to Use:\n"
215
+ " • Left Click + Drag: Create a capture area (green).\n"
216
+ " • Shift + Left Click + Drag: Create an exclusion area (orange).\n"
217
+ " • Right-Click on a box: Delete it.\n\n"
218
+ "Hotkeys:\n"
219
+ " • Ctrl + S: Save and Quit\n"
220
+ " • Ctrl + Z / Ctrl + Y: Undo / Redo\n"
221
+ " • M: Toggle background visibility\n"
222
+ " • I: Toggle these instructions\n"
223
+ " • Esc: Quit without saving"
224
+ " "
225
+ )
226
+
227
+ # Use a common, readable font
228
+ instruction_font = tkfont.Font(family="Segoe UI", size=10, weight="normal")
229
+
230
+ # Create the text item first to get its size
231
+ text_id = canvas.create_text(
232
+ 20, 20, # Position with a small margin
233
+ text=instructions_text,
234
+ anchor=tk.NW,
235
+ fill='white',
236
+ font=instruction_font,
237
+ justify=tk.LEFT
238
+ )
239
+
240
+ # Get the bounding box of the text to draw a background
241
+ text_bbox = canvas.bbox(text_id)
242
+
243
+ # Create a background rectangle with padding
244
+ rect_id = canvas.create_rectangle(
245
+ text_bbox[0] - 10, # left
246
+ text_bbox[1] - 10, # top
247
+ text_bbox[2] + 10, # right
248
+ text_bbox[3] + 10, # bottom
249
+ fill='#2B2B2B', # Dark, semi-opaque background
250
+ outline='white',
251
+ width=1
252
+ )
253
+
254
+ # Lower the rectangle so it's behind the text
255
+ canvas.tag_lower(rect_id, text_id)
256
+
257
+ def toggle_instructions(self, event=None):
258
+ canvas = event.widget.winfo_toplevel().winfo_children()[0]
259
+ # Find all text and rectangle items (assuming only one of each for instructions)
260
+ text_items = [item for item in canvas.find_all() if canvas.type(item) == 'text']
261
+ rect_items = [item for item in canvas.find_all() if canvas.type(item) == 'rectangle']
262
+
263
+ if text_items and rect_items:
264
+ current_state = canvas.itemcget(text_items[0], 'state')
265
+ new_state = tk.NORMAL if current_state == tk.HIDDEN else tk.HIDDEN
266
+ for item in text_items + rect_items:
267
+ canvas.itemconfigure(item, state=new_state)
268
+ print("Toggled instructions visibility.")
269
+
270
+ def start(self):
271
+ self.root = tk.Tk()
272
+ self.root.withdraw()
273
+
274
+ # Calculate bounding box of all monitors
275
+ left = min(m['left'] for m in self.monitors)
276
+ top = min(m['top'] for m in self.monitors)
277
+ right = max(m['left'] + m['width'] for m in self.monitors)
278
+ bottom = max(m['top'] + m['height'] for m in self.monitors)
279
+ self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
280
+
281
+ sct_img = self.sct.grab(self.sct.monitors[0])
282
+ img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
283
+
284
+ window = tk.Toplevel(self.root)
285
+ window.geometry(f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}")
286
+ window.overrideredirect(1)
287
+ window.attributes('-topmost', 1)
288
+
289
+ self.photo_image = ImageTk.PhotoImage(img)
290
+ canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
291
+ canvas.pack(fill=tk.BOTH, expand=True)
292
+ canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
293
+
294
+ # --- MODIFIED: CALL THE INSTRUCTION WIDGET CREATOR ---
295
+ self._create_instructions_widget(canvas)
296
+ # --- END MODIFICATION ---
297
+
298
+ # Draw existing rectangles (which were converted to absolute pixels on load)
299
+ for _, abs_coords, is_excluded in self.rectangles:
300
+ x_abs, y_abs, w_abs, h_abs = abs_coords
301
+ canvas_x = x_abs - self.bounding_box['left']
302
+ canvas_y = y_abs - self.bounding_box['top']
303
+ rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
304
+ outline='orange' if is_excluded else 'green', width=2)
305
+ self.drawn_rect_ids.append(rect_id)
306
+
307
+ def on_click(event):
308
+ self.start_x, self.start_y = event.x, event.y
309
+ outline = 'purple' if bool(event.state & 0x0001) else 'red'
310
+ self.current_rect_id = canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
311
+ outline=outline, width=2)
312
+
313
+ def on_drag(event):
314
+ if self.current_rect_id: canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
315
+
316
+ def on_release(event):
317
+ if not self.current_rect_id: return
318
+ coords = canvas.coords(self.current_rect_id)
319
+ x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
320
+ y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
321
+ w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
322
+
323
+ if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
324
+ is_excl = bool(event.state & 0x0001)
325
+ canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
326
+
327
+ center_x, center_y = x_abs + w / 2, y_abs + h / 2
328
+ target_mon = self.monitors[0]
329
+ for mon in self.monitors:
330
+ if mon['left'] <= center_x < mon['left'] + mon['width'] and mon['top'] <= center_y < mon['top'] + \
331
+ mon['height']:
332
+ target_mon = mon
333
+ break
334
+
335
+ self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl))
336
+ self.drawn_rect_ids.append(self.current_rect_id)
337
+ self.redo_stack.clear()
338
+ else:
339
+ canvas.delete(self.current_rect_id)
340
+ self.current_rect_id = self.start_x = self.start_y = None
341
+
342
+ def on_right_click(event):
343
+ # Iterate through our rectangles in reverse to find the topmost one.
344
+ for i in range(len(self.rectangles) - 1, -1, -1):
345
+ _monitor, abs_coords, _is_excluded = self.rectangles[i]
346
+ x_abs, y_abs, w_abs, h_abs = abs_coords
347
+ canvas_x1 = x_abs - self.bounding_box['left']
348
+ canvas_y1 = y_abs - self.bounding_box['top']
349
+ canvas_x2 = canvas_x1 + w_abs
350
+ canvas_y2 = canvas_y1 + h_abs
351
+
352
+ if canvas_x1 <= event.x <= canvas_x2 and canvas_y1 <= event.y <= canvas_y2:
353
+ # --- UNDO/REDO CHANGE ---
354
+ # We found the rectangle. Prepare the 'remove' action.
355
+ # We need to save the data AND its original index to restore it correctly.
356
+ rect_tuple_to_del = self.rectangles[i]
357
+ item_id_to_del = self.drawn_rect_ids[i]
358
+
359
+ self.redo_stack.append((*rect_tuple_to_del, i))
360
+
361
+ # Now, perform the deletion
362
+ del self.rectangles[i]
363
+ del self.drawn_rect_ids[i]
364
+ canvas.delete(item_id_to_del)
365
+ print("Deleted rectangle.")
366
+
367
+ break # Stop after deleting the topmost one
368
+
369
+ def toggle_image_mode(e=None):
370
+ self.image_mode = not self.image_mode
371
+ # Only change alpha of the main window, not the text widget
372
+ window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
373
+ print("Toggled background visibility.")
374
+
375
+ def on_enter(e=None):
376
+ canvas.focus_set()
377
+
378
+ canvas.bind('<Enter>', on_enter)
379
+ canvas.bind('<ButtonPress-1>', on_click)
380
+ canvas.bind('<B1-Motion>', on_drag)
381
+ canvas.bind('<ButtonRelease-1>', on_release)
382
+ canvas.bind('<Button-3>', on_right_click)
383
+ canvas.bind('<Control-s>', self.save_rects)
384
+ canvas.bind('<Control-y>', self.redo_last_rect)
385
+ canvas.bind('<Control-z>', self.undo_last_rect)
386
+ canvas.bind("<Escape>", self.quit_app)
387
+ canvas.bind("<m>", toggle_image_mode)
388
+ canvas.bind("<i>", self.toggle_instructions)
389
+
390
+ canvas.focus_set()
391
+ # The print message is now redundant but kept for console feedback
392
+ print("Starting UI. See on-screen instructions. Press Esc to quit, Ctrl+S to save.")
393
+ self.root.mainloop()
394
+
395
+ def quit_app(self, event=None):
396
+ if self.root and self.root.winfo_exists(): self.root.destroy()
397
+ self.root = None
398
+
399
+
400
+ def run_screen_selector(result_dict, window_name):
401
+ try:
402
+ selector = ScreenSelector(result_dict, window_name)
403
+ selector.start()
404
+ except Exception as e:
405
+ print(f"Error in selector process: {e}", file=sys.stderr)
406
+ import traceback
407
+ traceback.print_exc()
408
+ result_dict['error'] = str(e)
409
+
410
+
411
+ def get_screen_selection(window_name):
412
+ if not selector_available or not gw: return None
413
+ if not window_name:
414
+ print("Error: A target window name must be provided.", file=sys.stderr)
415
+ return None
416
+
417
+ with Manager() as manager:
418
+ result_data = manager.dict()
419
+ process = Process(target=run_screen_selector, args=(result_data, window_name))
420
+ print(f"Starting ScreenSelector process...")
421
+ process.start()
422
+ process.join()
423
+
424
+ if 'error' in result_data:
425
+ print(f"Selector process failed: {result_data['error']}", file=sys.stderr)
426
+ return None
427
+ elif 'rectangles' in result_data:
428
+ print("Screen selection successful.")
429
+ return dict(result_data)
430
+ else:
431
+ print("Selection was cancelled by the user.")
432
+ return {}
433
+
434
+
435
+ if __name__ == "__main__":
436
+ set_dpi_awareness()
437
+ target_window_title = "Windowed Projector (Preview)" # Default
438
+ if len(sys.argv) > 1:
439
+ target_window_title = sys.argv[1]
440
+
441
+ selection_result = get_screen_selection(target_window_title)
442
+
443
+ if selection_result is None:
444
+ print("\n--- Screen selection failed. ---")
445
+ elif not selection_result:
446
+ print("\n--- Screen selection cancelled. ---")
447
+ elif 'rectangles' in selection_result:
448
+ print("\n--- Selection Result ---")
449
+ rects = selection_result.get('rectangles', [])
450
+ win_geom = selection_result.get('window_geometry')
451
+ print(f"Saved relative to window: {win_geom}")
452
+ print(f"Selected rectangles ({len(rects)}):")
453
+ # The returned coordinates are absolute pixels for immediate use
454
+ for i, (monitor, coords, is_excluded) in enumerate(rects):
455
+ coord_str = f"(X:{coords[0]}, Y:{coords[1]}, W:{coords[2]}, H:{coords[3]})"
456
+ print(
457
+ f" Rect {i + 1}: On Monitor Idx:{monitor.get('index', 'N/A')}, Coords={coord_str}, Excluded={is_excluded}")
@@ -241,12 +241,12 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
241
241
 
242
242
  line_start_time = time if time else datetime.now()
243
243
 
244
- if not twopassocr:
244
+ if manual or not twopassocr:
245
245
  if previous_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 90:
246
246
  logger.info("Seems like Text we already sent, not doing anything.")
247
247
  return
248
248
  save_result_image(img)
249
- asyncio.run(send_result(text, time))
249
+ asyncio.run(send_result(text, line_start_time))
250
250
  previous_orig_text = orig_text_string
251
251
  previous_text = None
252
252
  previous_img = None
@@ -683,6 +683,7 @@ class GsmAppState:
683
683
  self.last_mined_line = None
684
684
  self.keep_running = True
685
685
  self.current_game = ''
686
+ self.videos_to_remove = set()
686
687
 
687
688
  @dataclass_json
688
689
  @dataclass
@@ -21,7 +21,7 @@ def handle_texthooker_button(video_path='', get_audio_from_video=None):
21
21
  if get_config().advanced.audio_player_path:
22
22
  play_audio_in_external(gsm_state.previous_audio)
23
23
  elif get_config().advanced.video_player_path:
24
- play_video_in_external(line, gsm_state.previous_audio)
24
+ play_video_in_external(line, video_path)
25
25
  else:
26
26
  import sounddevice as sd
27
27
  data, samplerate = gsm_state.previous_audio
@@ -35,9 +35,7 @@ def handle_texthooker_button(video_path='', get_audio_from_video=None):
35
35
  play_audio_in_external(audio)
36
36
  gsm_state.previous_audio = audio
37
37
  elif get_config().advanced.video_player_path:
38
- new_video_path = play_video_in_external(line, video_path)
39
- gsm_state.previous_audio = new_video_path
40
- gsm_state.previous_replay = new_video_path
38
+ play_video_in_external(line, video_path)
41
39
  else:
42
40
  import sounddevice as sd
43
41
  import soundfile as sf
@@ -75,8 +73,8 @@ def handle_texthooker_button(video_path='', get_audio_from_video=None):
75
73
  logger.debug(f"Error Playing Audio/Video: {e}", exc_info=True)
76
74
  return
77
75
  finally:
78
- if video_path and get_config().paths.remove_video and os.path.exists(video_path):
79
- os.remove(video_path)
76
+ gsm_state.previous_replay = video_path
77
+ gsm_state.videos_to_remove.add(video_path)
80
78
 
81
79
 
82
80
  def play_audio_in_external(filepath):
@@ -94,37 +92,28 @@ def play_audio_in_external(filepath):
94
92
 
95
93
 
96
94
  def play_video_in_external(line, filepath):
97
- def move_video_when_closed(p, fp):
98
- p.wait()
99
- os.remove(fp)
100
-
101
- shutil.move(filepath, get_temporary_directory())
102
- new_filepath = os.path.join(get_temporary_directory(), os.path.basename(filepath))
103
-
104
95
  command = [get_config().advanced.video_player_path]
105
96
 
106
- start, _, _, _ = get_video_timings(new_filepath, line)
97
+ start, _, _, _ = get_video_timings(filepath, line)
107
98
 
108
99
  if start:
109
100
  if "vlc" in get_config().advanced.video_player_path:
110
101
  command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
111
102
  else:
112
103
  command.extend(["--start", convert_to_vlc_seconds(start)])
113
- command.append(os.path.normpath(new_filepath))
104
+ command.append(os.path.normpath(filepath))
114
105
 
115
106
  logger.info(" ".join(command))
116
107
 
117
108
 
118
109
 
119
110
  try:
120
- proc = subprocess.Popen(command)
121
- print(f"Opened {filepath} in {get_config().advanced.video_player_path}.")
122
- threading.Thread(target=move_video_when_closed, args=(proc, filepath)).start()
111
+ subprocess.Popen(command)
112
+ logger.info(f"Opened {filepath} in {get_config().advanced.video_player_path}.")
123
113
  except FileNotFoundError:
124
- print("VLC not found. Make sure it's installed and in your PATH.")
114
+ logger.error("VLC not found. Make sure it's installed and in your PATH.")
125
115
  except Exception as e:
126
- print(f"An error occurred: {e}")
127
- return new_filepath
116
+ logger.error(f"An error occurred: {e}")
128
117
 
129
118
 
130
119
  def convert_to_vlc_seconds(time_str):
@@ -287,8 +287,8 @@ def get_screenshot():
287
287
  if event_id is None:
288
288
  return jsonify({'error': 'Missing id'}), 400
289
289
  gsm_state.line_for_screenshot = get_line_by_id(event_id)
290
- if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot.id == gsm_state.previous_line_for_screenshot.id:
291
- handle_texthooker_button()
290
+ if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot.id == gsm_state.previous_line_for_screenshot.id or gsm_state.previous_line_for_audio:
291
+ handle_texthooker_button(gsm_state.previous_replay)
292
292
  else:
293
293
  obs.save_replay_buffer()
294
294
  return jsonify({}), 200
@@ -301,8 +301,8 @@ def play_audio():
301
301
  if event_id is None:
302
302
  return jsonify({'error': 'Missing id'}), 400
303
303
  gsm_state.line_for_audio = get_line_by_id(event_id)
304
- if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio:
305
- handle_texthooker_button()
304
+ if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio or gsm_state.previous_line_for_screenshot:
305
+ handle_texthooker_button(gsm_state.previous_replay)
306
306
  else:
307
307
  obs.save_replay_buffer()
308
308
  return jsonify({}), 200
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.9
3
+ Version: 2.10.11
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.9
3
+ Version: 2.10.11
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GameSentenceMiner"
10
- version = "2.10.9"
10
+ version = "2.10.11"
11
11
  description = "A tool for mining sentences from games. Update: Full UI Re-design"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"