GameSentenceMiner 2.10.9__tar.gz → 2.10.10__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.10/GameSentenceMiner/ocr/owocr_area_selector.py +374 -0
  2. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  3. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/PKG-INFO +1 -1
  4. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/pyproject.toml +1 -1
  5. gamesentenceminer-2.10.9/GameSentenceMiner/ocr/owocr_area_selector.py +0 -905
  6. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/__init__.py +0 -0
  7. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ai/__init__.py +0 -0
  8. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  9. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/anki.py +0 -0
  10. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/__init__.py +0 -0
  11. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon.png +0 -0
  12. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon128.png +0 -0
  13. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon256.png +0 -0
  14. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon32.png +0 -0
  15. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon512.png +0 -0
  16. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/icon64.png +0 -0
  17. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/assets/pickaxe.png +0 -0
  18. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/config_gui.py +0 -0
  19. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/gametext.py +0 -0
  20. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/gsm.py +0 -0
  21. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/obs.py +0 -0
  22. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ocr/__init__.py +0 -0
  23. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  24. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  25. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  26. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  27. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  28. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  29. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  30. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  31. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  32. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  33. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  34. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/__init__.py +0 -0
  35. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  36. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/communication/__init__.py +0 -0
  37. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/communication/send.py +0 -0
  38. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/communication/websocket.py +0 -0
  39. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/configuration.py +0 -0
  40. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  41. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  42. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  43. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  44. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/electron_config.py +0 -0
  45. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/ffmpeg.py +0 -0
  46. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/gsm_utils.py +0 -0
  47. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/model.py +0 -0
  48. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/notification.py +0 -0
  49. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/package.py +0 -0
  50. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/ss_selector.py +0 -0
  51. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/util/text_log.py +0 -0
  52. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/vad.py +0 -0
  53. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/__init__.py +0 -0
  54. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/service.py +0 -0
  55. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/__init__.py +0 -0
  56. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  57. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  58. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/favicon.ico +0 -0
  59. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/favicon.svg +0 -0
  60. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  61. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/style.css +0 -0
  62. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  63. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  64. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/templates/__init__.py +0 -0
  65. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/templates/index.html +0 -0
  66. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  67. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/templates/utility.html +0 -0
  68. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner/web/texthooking_page.py +0 -0
  69. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  70. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  71. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  72. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/requires.txt +0 -0
  73. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  74. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/LICENSE +0 -0
  75. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/README.md +0 -0
  76. {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.10}/setup.cfg +0 -0
@@ -0,0 +1,374 @@
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
+
24
+ selector_available = True
25
+ except ImportError:
26
+ print("Error: tkinter library not found. GUI selection is unavailable.")
27
+ selector_available = False
28
+
29
+ MIN_RECT_WIDTH = 25
30
+ MIN_RECT_HEIGHT = 25
31
+
32
+ COORD_SYSTEM_PERCENTAGE = "percentage"
33
+
34
+
35
+ class ScreenSelector:
36
+ def __init__(self, result, window_name):
37
+ if not selector_available or not gw:
38
+ raise RuntimeError("tkinter or pygetwindow is not available.")
39
+ if not window_name:
40
+ raise ValueError("A target window name is required for percentage-based coordinates.")
41
+
42
+ obs.connect_to_obs_sync()
43
+ self.window_name = window_name
44
+ print(f"Targeting window: '{window_name}'")
45
+
46
+ self.sct = mss.mss()
47
+ self.monitors = self.sct.monitors[1:]
48
+ if not self.monitors:
49
+ raise RuntimeError("No monitors found by mss.")
50
+ for i, monitor in enumerate(self.monitors):
51
+ monitor['index'] = i
52
+
53
+ # --- Window Awareness is now critical ---
54
+ self.target_window = self._find_target_window()
55
+ self.target_window_geometry = self._get_window_geometry(self.target_window)
56
+ if not self.target_window_geometry:
57
+ raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
58
+ print(f"Found target window at: {self.target_window_geometry}")
59
+ # ---
60
+
61
+ self.root = None
62
+ self.result = result
63
+ self.rectangles = [] # Internal storage is ALWAYS absolute pixels for drawing
64
+ self.drawn_rect_ids = []
65
+ self.current_rect_id = None
66
+ self.start_x = self.start_y = None
67
+ self.image_mode = True
68
+ self.redo_stack = []
69
+ self.bounding_box = {} # Geometry of the single large canvas window
70
+
71
+ self.load_existing_rectangles()
72
+
73
+ def _find_target_window(self):
74
+ try:
75
+ return get_window(self.window_name)
76
+ except Exception as e:
77
+ print(f"Error finding window '{self.window_name}': {e}")
78
+ return None
79
+
80
+ def _get_window_geometry(self, window):
81
+ if window:
82
+ try:
83
+ # Ensure width/height are positive and non-zero
84
+ width = max(1, window.width)
85
+ height = max(1, window.height)
86
+ return {"left": window.left, "top": window.top, "width": width, "height": height}
87
+ except Exception:
88
+ return None
89
+ return None
90
+
91
+ def get_scene_ocr_config(self):
92
+ app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
93
+ ocr_config_dir = app_dir / "ocr_config"
94
+ ocr_config_dir.mkdir(parents=True, exist_ok=True)
95
+ try:
96
+ scene = sanitize_filename(obs.get_current_scene() or "default_scene")
97
+ except Exception as e:
98
+ print(f"Error getting OBS scene: {e}. Using default config name.")
99
+ scene = "default_scene"
100
+ return ocr_config_dir / f"{scene}.json"
101
+
102
+ def load_existing_rectangles(self):
103
+ """Loads rectangles from config, converting from percentage to absolute pixels for use."""
104
+ config_path = self.get_scene_ocr_config()
105
+ win_geom = self.target_window_geometry # Use current geometry for conversion
106
+ win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
107
+
108
+ try:
109
+ with open(config_path, 'r', encoding='utf-8') as f:
110
+ config_data = json.load(f)
111
+
112
+ if config_data.get("coordinate_system") != COORD_SYSTEM_PERCENTAGE:
113
+ print(
114
+ f"Warning: Config file '{config_path}' does not use '{COORD_SYSTEM_PERCENTAGE}' system. Please re-create selections.")
115
+ return
116
+
117
+ print(f"Loading rectangles from {config_path}...")
118
+ self.rectangles = []
119
+ loaded_count = 0
120
+
121
+ for rect_data in config_data.get("rectangles", []):
122
+ try:
123
+ coords_pct = rect_data["coordinates"]
124
+ x_pct, y_pct, w_pct, h_pct = map(float, coords_pct)
125
+
126
+ # Convert from percentage to absolute pixel coordinates
127
+ x_abs = (x_pct * win_w) + win_l
128
+ y_abs = (y_pct * win_h) + win_t
129
+ w_abs = w_pct * win_w
130
+ h_abs = h_pct * win_h
131
+ abs_coords = (int(x_abs), int(y_abs), int(w_abs), int(h_abs))
132
+
133
+ monitor_index = rect_data["monitor"]['index']
134
+ target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
135
+ if target_monitor:
136
+ self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"]))
137
+ loaded_count += 1
138
+ except (KeyError, ValueError, TypeError) as e:
139
+ print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
140
+
141
+ print(f"Loaded {loaded_count} valid rectangles.")
142
+ except FileNotFoundError:
143
+ print(f"No config found at {config_path}. Starting fresh.")
144
+ except Exception as e:
145
+ print(f"Error loading config: {e}. Starting fresh.")
146
+
147
+ def save_rects(self, event=None):
148
+ """Saves rectangles to config, converting from absolute pixels to percentages."""
149
+ config_path = self.get_scene_ocr_config()
150
+ win_geom = self.target_window_geometry
151
+ win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
152
+ print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
153
+
154
+ serializable_rects = []
155
+ for monitor_dict, abs_coords, is_excluded in self.rectangles:
156
+ x_abs, y_abs, w_abs, h_abs = abs_coords
157
+
158
+ # Convert absolute pixel coordinates to percentages
159
+ x_pct = (x_abs - win_l) / win_w
160
+ y_pct = (y_abs - win_t) / win_h
161
+ w_pct = w_abs / win_w
162
+ h_pct = h_abs / win_h
163
+ coords_to_save = [x_pct, y_pct, w_pct, h_pct]
164
+
165
+ serializable_rects.append({
166
+ "monitor": {'index': monitor_dict['index']},
167
+ "coordinates": coords_to_save,
168
+ "is_excluded": is_excluded
169
+ })
170
+
171
+ save_data = {
172
+ "scene": obs.get_current_scene() or "default_scene",
173
+ "window": self.window_name,
174
+ "coordinate_system": COORD_SYSTEM_PERCENTAGE, # Always save as percentage
175
+ "window_geometry": win_geom, # Save the geometry used for conversion
176
+ "rectangles": serializable_rects
177
+ }
178
+
179
+ with open(config_path, 'w', encoding="utf-8") as f:
180
+ json.dump(save_data, f, indent=4, ensure_ascii=False)
181
+
182
+ print(f"Successfully saved {len(serializable_rects)} rectangles.")
183
+ # Pass back the internal absolute coords for any immediate post-processing
184
+ self.result['rectangles'] = [(r[0], list(r[1]), r[2]) for r in self.rectangles]
185
+ self.result['window_geometry'] = win_geom
186
+ self.result['coordinate_system'] = COORD_SYSTEM_PERCENTAGE
187
+ self.quit_app()
188
+
189
+ def undo_last_rect(self, event=None):
190
+ if self.rectangles and self.drawn_rect_ids:
191
+ last_rect_tuple = self.rectangles.pop()
192
+ last_rect_id = self.drawn_rect_ids.pop()
193
+ self.redo_stack.append((*last_rect_tuple, last_rect_id))
194
+ event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
195
+ print("Undo: Removed last rectangle.")
196
+
197
+ def redo_last_rect(self, event=None):
198
+ if not self.redo_stack: return
199
+ monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
200
+ canvas = event.widget.winfo_toplevel().winfo_children()[0]
201
+ x_abs, y_abs, w_abs, h_abs = abs_coords
202
+ canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
203
+ new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
204
+ outline='orange' if is_excluded else 'green', width=2)
205
+ self.rectangles.append((monitor, abs_coords, is_excluded))
206
+ self.drawn_rect_ids.append(new_rect_id)
207
+ print("Redo: Restored rectangle.")
208
+
209
+ def start(self):
210
+ self.root = tk.Tk()
211
+ self.root.withdraw()
212
+
213
+ # Calculate bounding box of all monitors
214
+ left = min(m['left'] for m in self.monitors)
215
+ top = min(m['top'] for m in self.monitors)
216
+ right = max(m['left'] + m['width'] for m in self.monitors)
217
+ bottom = max(m['top'] + m['height'] for m in self.monitors)
218
+ self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
219
+
220
+ sct_img = self.sct.grab(self.sct.monitors[0])
221
+ img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
222
+
223
+ window = tk.Toplevel(self.root)
224
+ window.geometry(f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}")
225
+ window.overrideredirect(1)
226
+ window.attributes('-topmost', 1)
227
+
228
+ self.photo_image = ImageTk.PhotoImage(img)
229
+ canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
230
+ canvas.pack(fill=tk.BOTH, expand=True)
231
+ canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
232
+
233
+ # Draw existing rectangles (which were converted to absolute pixels on load)
234
+ for _, abs_coords, is_excluded in self.rectangles:
235
+ x_abs, y_abs, w_abs, h_abs = abs_coords
236
+ canvas_x = x_abs - self.bounding_box['left']
237
+ canvas_y = y_abs - self.bounding_box['top']
238
+ rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
239
+ outline='orange' if is_excluded else 'green', width=2)
240
+ self.drawn_rect_ids.append(rect_id)
241
+
242
+ def on_click(event):
243
+ self.start_x, self.start_y = event.x, event.y
244
+ outline = 'purple' if bool(event.state & 0x0001) else 'red'
245
+ self.current_rect_id = canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
246
+ outline=outline, width=2)
247
+
248
+ def on_drag(event):
249
+ if self.current_rect_id: canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
250
+
251
+ def on_release(event):
252
+ if not self.current_rect_id: return
253
+ coords = canvas.coords(self.current_rect_id)
254
+ x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
255
+ y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
256
+ w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
257
+
258
+ if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
259
+ is_excl = bool(event.state & 0x0001)
260
+ canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
261
+
262
+ center_x, center_y = x_abs + w / 2, y_abs + h / 2
263
+ target_mon = self.monitors[0]
264
+ for mon in self.monitors:
265
+ if mon['left'] <= center_x < mon['left'] + mon['width'] and mon['top'] <= center_y < mon['top'] + \
266
+ mon['height']:
267
+ target_mon = mon
268
+ break
269
+
270
+ self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl))
271
+ self.drawn_rect_ids.append(self.current_rect_id)
272
+ self.redo_stack.clear()
273
+ else:
274
+ canvas.delete(self.current_rect_id)
275
+ self.current_rect_id = self.start_x = self.start_y = None
276
+
277
+ def on_right_click(event):
278
+ items = canvas.find_closest(event.x, event.y)
279
+ if items and items[0] in self.drawn_rect_ids:
280
+ item_id = items[0]
281
+ idx_to_del = self.drawn_rect_ids.index(item_id)
282
+ del self.drawn_rect_ids[idx_to_del]
283
+ del self.rectangles[idx_to_del]
284
+ self.redo_stack.clear()
285
+ canvas.delete(item_id)
286
+
287
+ def toggle_image_mode(e=None):
288
+ self.image_mode = not self.image_mode; window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
289
+
290
+ def on_enter(e=None):
291
+ canvas.focus_set()
292
+
293
+ canvas.bind('<Enter>', on_enter)
294
+ canvas.bind('<ButtonPress-1>', on_click)
295
+ canvas.bind('<B1-Motion>', on_drag)
296
+ canvas.bind('<ButtonRelease-1>', on_release)
297
+ canvas.bind('<Button-3>', on_right_click)
298
+ canvas.bind('<Control-s>', self.save_rects)
299
+ canvas.bind('<Control-z>', self.undo_last_rect)
300
+ canvas.bind('<Control-y>', self.redo_last_rect)
301
+ canvas.bind("<Escape>", self.quit_app)
302
+ canvas.bind("<m>", toggle_image_mode)
303
+
304
+ canvas.focus_set()
305
+ print("Starting UI. Press Esc to quit, Ctrl+S to save, M to toggle background.")
306
+ self.root.mainloop()
307
+
308
+ def quit_app(self, event=None):
309
+ if self.root and self.root.winfo_exists(): self.root.destroy()
310
+ self.root = None
311
+
312
+
313
+ def run_screen_selector(result_dict, window_name):
314
+ try:
315
+ selector = ScreenSelector(result_dict, window_name)
316
+ selector.start()
317
+ except Exception as e:
318
+ print(f"Error in selector process: {e}", file=sys.stderr)
319
+ import traceback
320
+ traceback.print_exc()
321
+ result_dict['error'] = str(e)
322
+
323
+
324
+ def get_screen_selection(window_name):
325
+ if not selector_available or not gw: return None
326
+ if not window_name:
327
+ print("Error: A target window name must be provided.", file=sys.stderr)
328
+ return None
329
+
330
+ with Manager() as manager:
331
+ result_data = manager.dict()
332
+ process = Process(target=run_screen_selector, args=(result_data, window_name))
333
+ print(f"Starting ScreenSelector process...")
334
+ process.start()
335
+ process.join()
336
+
337
+ if 'error' in result_data:
338
+ print(f"Selector process failed: {result_data['error']}", file=sys.stderr)
339
+ return None
340
+ elif 'rectangles' in result_data:
341
+ print("Screen selection successful.")
342
+ return dict(result_data)
343
+ else:
344
+ print("Selection was cancelled by the user.")
345
+ return {}
346
+
347
+
348
+ if __name__ == "__main__":
349
+ set_dpi_awareness()
350
+ target_window_title = "Windowed Projector (Preview)" # Default
351
+ if len(sys.argv) > 1:
352
+ target_window_title = sys.argv[1]
353
+ # else:
354
+ # print("Usage: python your_script_name.py \"Target Window Title\"", file=sys.stderr)
355
+ # print("Example: python selector.py \"Windowed Projector (Preview)\"", file=sys.stderr)
356
+ # sys.exit(1)
357
+
358
+ selection_result = get_screen_selection(target_window_title)
359
+
360
+ if selection_result is None:
361
+ print("\n--- Screen selection failed. ---")
362
+ elif not selection_result:
363
+ print("\n--- Screen selection cancelled. ---")
364
+ elif 'rectangles' in selection_result:
365
+ print("\n--- Selection Result ---")
366
+ rects = selection_result.get('rectangles', [])
367
+ win_geom = selection_result.get('window_geometry')
368
+ print(f"Saved relative to window: {win_geom}")
369
+ print(f"Selected rectangles ({len(rects)}):")
370
+ # The returned coordinates are absolute pixels for immediate use
371
+ for i, (monitor, coords, is_excluded) in enumerate(rects):
372
+ coord_str = f"(X:{coords[0]}, Y:{coords[1]}, W:{coords[2]}, H:{coords[3]})"
373
+ print(
374
+ f" Rect {i + 1}: On Monitor Idx:{monitor.get('index', 'N/A')}, Coords={coord_str}, Excluded={is_excluded}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.9
3
+ Version: 2.10.10
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.10
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.10"
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"