GameSentenceMiner 2.14.7__py3-none-any.whl → 2.14.9__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.
- GameSentenceMiner/config_gui.py +19 -10
- GameSentenceMiner/gsm.py +68 -8
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +12 -8
- {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/METADATA +1 -2
- gamesentenceminer-2.14.9.dist-info/RECORD +24 -0
- GameSentenceMiner/ai/__init__.py +0 -0
- GameSentenceMiner/ai/ai_prompting.py +0 -473
- GameSentenceMiner/ocr/__init__.py +0 -0
- GameSentenceMiner/ocr/gsm_ocr_config.py +0 -174
- GameSentenceMiner/ocr/ocrconfig.py +0 -129
- GameSentenceMiner/ocr/owocr_area_selector.py +0 -629
- GameSentenceMiner/ocr/owocr_helper.py +0 -638
- GameSentenceMiner/ocr/ss_picker.py +0 -140
- GameSentenceMiner/owocr/owocr/__init__.py +0 -1
- GameSentenceMiner/owocr/owocr/__main__.py +0 -9
- GameSentenceMiner/owocr/owocr/config.py +0 -148
- GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -1238
- GameSentenceMiner/owocr/owocr/ocr.py +0 -1691
- GameSentenceMiner/owocr/owocr/run.py +0 -1817
- GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -109
- GameSentenceMiner/tools/__init__.py +0 -0
- GameSentenceMiner/tools/audio_offset_selector.py +0 -215
- GameSentenceMiner/tools/ss_selector.py +0 -135
- GameSentenceMiner/tools/window_transparency.py +0 -214
- GameSentenceMiner/util/__init__.py +0 -0
- GameSentenceMiner/util/communication/__init__.py +0 -22
- GameSentenceMiner/util/communication/send.py +0 -7
- GameSentenceMiner/util/communication/websocket.py +0 -94
- GameSentenceMiner/util/configuration.py +0 -1198
- GameSentenceMiner/util/db.py +0 -408
- GameSentenceMiner/util/downloader/Untitled_json.py +0 -472
- GameSentenceMiner/util/downloader/__init__.py +0 -0
- GameSentenceMiner/util/downloader/download_tools.py +0 -194
- GameSentenceMiner/util/downloader/oneocr_dl.py +0 -250
- GameSentenceMiner/util/electron_config.py +0 -259
- GameSentenceMiner/util/ffmpeg.py +0 -571
- GameSentenceMiner/util/get_overlay_coords.py +0 -366
- GameSentenceMiner/util/gsm_utils.py +0 -323
- GameSentenceMiner/util/model.py +0 -206
- GameSentenceMiner/util/notification.py +0 -147
- GameSentenceMiner/util/text_log.py +0 -214
- GameSentenceMiner/web/__init__.py +0 -0
- GameSentenceMiner/web/service.py +0 -132
- GameSentenceMiner/web/static/__init__.py +0 -0
- GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- GameSentenceMiner/web/static/favicon.ico +0 -0
- GameSentenceMiner/web/static/favicon.svg +0 -3
- GameSentenceMiner/web/static/site.webmanifest +0 -21
- GameSentenceMiner/web/static/style.css +0 -292
- GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- GameSentenceMiner/web/templates/__init__.py +0 -0
- GameSentenceMiner/web/templates/index.html +0 -50
- GameSentenceMiner/web/templates/text_replacements.html +0 -238
- GameSentenceMiner/web/templates/utility.html +0 -483
- GameSentenceMiner/web/texthooking_page.py +0 -584
- GameSentenceMiner/wip/__init___.py +0 -0
- gamesentenceminer-2.14.7.dist-info/RECORD +0 -77
- {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/top_level.txt +0 -0
@@ -1,629 +0,0 @@
|
|
1
|
-
import argparse
|
2
|
-
import base64
|
3
|
-
import ctypes
|
4
|
-
import io
|
5
|
-
import json
|
6
|
-
import sys
|
7
|
-
from multiprocessing import Process, Manager
|
8
|
-
from pathlib import Path
|
9
|
-
|
10
|
-
from PIL import Image, ImageTk
|
11
|
-
|
12
|
-
# Assuming a mock or real obs module exists in this path
|
13
|
-
from GameSentenceMiner import obs
|
14
|
-
from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window, get_scene_ocr_config_path
|
15
|
-
from GameSentenceMiner.util.gsm_utils import sanitize_filename
|
16
|
-
from GameSentenceMiner.util.configuration import logger
|
17
|
-
|
18
|
-
try:
|
19
|
-
import tkinter as tk
|
20
|
-
from tkinter import font as tkfont # NEW: Import for better font control
|
21
|
-
|
22
|
-
selector_available = True
|
23
|
-
except ImportError:
|
24
|
-
print("Error: tkinter library not found. GUI selection is unavailable.")
|
25
|
-
selector_available = False
|
26
|
-
|
27
|
-
MIN_RECT_WIDTH = 25
|
28
|
-
MIN_RECT_HEIGHT = 25
|
29
|
-
|
30
|
-
COORD_SYSTEM_PERCENTAGE = "percentage"
|
31
|
-
|
32
|
-
|
33
|
-
class ScreenSelector:
|
34
|
-
def __init__(self, result, window_name, use_window_as_config, use_obs_screenshot=False):
|
35
|
-
if not selector_available:
|
36
|
-
raise RuntimeError("tkinter is not available.")
|
37
|
-
if not window_name and not use_obs_screenshot:
|
38
|
-
raise ValueError("A target window name is required for configuration.")
|
39
|
-
|
40
|
-
obs.connect_to_obs_sync()
|
41
|
-
self.window_name = window_name
|
42
|
-
self.use_obs_screenshot = use_obs_screenshot
|
43
|
-
self.screenshot_img = None
|
44
|
-
try:
|
45
|
-
import mss
|
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
|
-
except ImportError:
|
53
|
-
print("Error: mss library not found. Please install it: pip install mss")
|
54
|
-
raise RuntimeError("mss is required for screen selection.")
|
55
|
-
|
56
|
-
if self.use_obs_screenshot:
|
57
|
-
print("Using OBS screenshot as target.")
|
58
|
-
self.screenshot_img = obs.get_screenshot_PIL(compression=75)
|
59
|
-
# print(screenshot_base64)
|
60
|
-
if not self.screenshot_img:
|
61
|
-
raise RuntimeError("Failed to get OBS screenshot.")
|
62
|
-
try:
|
63
|
-
# Scale image to 1280x720
|
64
|
-
self.screenshot_img = self.screenshot_img.resize(self.scale_down_width_height(self.screenshot_img.width, self.screenshot_img.height), Image.LANCZOS)
|
65
|
-
except Exception as e:
|
66
|
-
raise RuntimeError(f"Failed to decode or open OBS screenshot: {e}")
|
67
|
-
|
68
|
-
self.target_window = None
|
69
|
-
self.target_window_geometry = {
|
70
|
-
"left": 0, "top": 0,
|
71
|
-
"width": self.screenshot_img.width,
|
72
|
-
"height": self.screenshot_img.height
|
73
|
-
}
|
74
|
-
print(f"OBS Screenshot dimensions: {self.target_window_geometry}")
|
75
|
-
else:
|
76
|
-
import pygetwindow as gw
|
77
|
-
if not gw:
|
78
|
-
raise RuntimeError("pygetwindow is not available for window selection.")
|
79
|
-
print(f"Targeting window: '{window_name}'")
|
80
|
-
self.target_window = self._find_target_window()
|
81
|
-
self.target_window_geometry = self._get_window_geometry(self.target_window)
|
82
|
-
if not self.target_window_geometry:
|
83
|
-
raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
|
84
|
-
print(f"Found target window at: {self.target_window_geometry}")
|
85
|
-
|
86
|
-
self.root = None
|
87
|
-
self.scene = ''
|
88
|
-
self.use_window_as_config = use_window_as_config
|
89
|
-
self.result = result
|
90
|
-
self.rectangles = [] # Internal storage is ALWAYS absolute pixels for drawing
|
91
|
-
self.drawn_rect_ids = []
|
92
|
-
self.current_rect_id = None
|
93
|
-
self.start_x = self.start_y = None
|
94
|
-
self.image_mode = True
|
95
|
-
self.redo_stack = []
|
96
|
-
self.bounding_box = {} # Geometry of the single large canvas window
|
97
|
-
self.instructions_showing = True
|
98
|
-
|
99
|
-
self.canvas = None
|
100
|
-
self.window = None
|
101
|
-
self.instructions_widget = None
|
102
|
-
self.instructions_window_id = None
|
103
|
-
|
104
|
-
self.load_existing_rectangles()
|
105
|
-
|
106
|
-
def scale_down_width_height(self, width, height):
|
107
|
-
if width == 0 or height == 0:
|
108
|
-
return width, height
|
109
|
-
aspect_ratio = width / height
|
110
|
-
if aspect_ratio > 2.66:
|
111
|
-
# Ultra-wide (32:9) - use 1920x540
|
112
|
-
return 1920, 540
|
113
|
-
elif aspect_ratio > 2.33:
|
114
|
-
# 21:9 - use 1920x800
|
115
|
-
return 1920, 800
|
116
|
-
elif aspect_ratio > 1.77:
|
117
|
-
# 16:9 - use 1280x720
|
118
|
-
return 1280, 720
|
119
|
-
elif aspect_ratio > 1.6:
|
120
|
-
# 16:10 - use 1280x800
|
121
|
-
return 1280, 800
|
122
|
-
elif aspect_ratio > 1.33:
|
123
|
-
# 4:3 - use 960x720
|
124
|
-
return 960, 720
|
125
|
-
elif aspect_ratio > 1.25:
|
126
|
-
# 5:4 - use 900x720
|
127
|
-
return 900, 720
|
128
|
-
elif aspect_ratio > 1.5:
|
129
|
-
# 3:2 - use 1080x720
|
130
|
-
return 1080, 720
|
131
|
-
else:
|
132
|
-
# Default/fallback - use original resolution
|
133
|
-
print(f"Unrecognized aspect ratio {aspect_ratio}. Using original resolution.")
|
134
|
-
return width, height
|
135
|
-
|
136
|
-
def _find_target_window(self):
|
137
|
-
try:
|
138
|
-
return get_window(self.window_name)
|
139
|
-
except Exception as e:
|
140
|
-
print(f"Error finding window '{self.window_name}': {e}")
|
141
|
-
return None
|
142
|
-
|
143
|
-
def _get_window_geometry(self, window):
|
144
|
-
if window:
|
145
|
-
try:
|
146
|
-
# Ensure width/height are positive and non-zero
|
147
|
-
width = max(1, window.width)
|
148
|
-
height = max(1, window.height)
|
149
|
-
return {"left": window.left, "top": window.top, "width": width, "height": height}
|
150
|
-
except Exception:
|
151
|
-
return None
|
152
|
-
return None
|
153
|
-
|
154
|
-
def load_existing_rectangles(self):
|
155
|
-
"""Loads rectangles from config, converting from percentage to absolute pixels for use."""
|
156
|
-
config_path = get_scene_ocr_config_path(self.use_window_as_config, self.window_name)
|
157
|
-
win_geom = self.target_window_geometry # Use current geometry for conversion
|
158
|
-
win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
|
159
|
-
|
160
|
-
try:
|
161
|
-
with open(config_path, 'r', encoding='utf-8') as f:
|
162
|
-
config_data = json.load(f)
|
163
|
-
|
164
|
-
if config_data.get("coordinate_system") != COORD_SYSTEM_PERCENTAGE:
|
165
|
-
print(
|
166
|
-
f"Warning: Config file '{config_path}' does not use '{COORD_SYSTEM_PERCENTAGE}' system. Please re-create selections.")
|
167
|
-
return
|
168
|
-
|
169
|
-
print(f"Loading rectangles from {config_path}...")
|
170
|
-
self.rectangles = []
|
171
|
-
loaded_count = 0
|
172
|
-
|
173
|
-
for rect_data in config_data.get("rectangles", []):
|
174
|
-
try:
|
175
|
-
coords_pct = rect_data["coordinates"]
|
176
|
-
x_pct, y_pct, w_pct, h_pct = map(float, coords_pct)
|
177
|
-
|
178
|
-
# Convert from percentage to absolute pixel coordinates
|
179
|
-
x_abs = (x_pct * win_w) + win_l
|
180
|
-
y_abs = (y_pct * win_h) + win_t
|
181
|
-
w_abs = w_pct * win_w
|
182
|
-
h_abs = h_pct * win_h
|
183
|
-
abs_coords = (int(x_abs), int(y_abs), int(w_abs), int(h_abs))
|
184
|
-
|
185
|
-
monitor_index = rect_data["monitor"]['index']
|
186
|
-
target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
|
187
|
-
if target_monitor:
|
188
|
-
self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"], rect_data.get("is_secondary", False)))
|
189
|
-
loaded_count += 1
|
190
|
-
except (KeyError, ValueError, TypeError) as e:
|
191
|
-
print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
|
192
|
-
|
193
|
-
print(f"Loaded {loaded_count} valid rectangles.")
|
194
|
-
except FileNotFoundError:
|
195
|
-
print(f"No config found at {config_path}. Starting fresh.")
|
196
|
-
except Exception as e:
|
197
|
-
print(f"Error loading config: {e}. Starting fresh.")
|
198
|
-
|
199
|
-
def save_rects(self, event=None):
|
200
|
-
"""Saves rectangles to config, converting from absolute pixels to percentages."""
|
201
|
-
config_path = get_scene_ocr_config_path(self.use_window_as_config, self.window_name)
|
202
|
-
win_geom = self.target_window_geometry
|
203
|
-
win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
|
204
|
-
print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
|
205
|
-
|
206
|
-
serializable_rects = []
|
207
|
-
for monitor_dict, abs_coords, is_excluded, is_secondary in self.rectangles:
|
208
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
209
|
-
|
210
|
-
# Convert absolute pixel coordinates to percentages
|
211
|
-
x_pct = (x_abs - win_l) / win_w
|
212
|
-
y_pct = (y_abs - win_t) / win_h
|
213
|
-
w_pct = w_abs / win_w
|
214
|
-
h_pct = h_abs / win_h
|
215
|
-
coords_to_save = [x_pct, y_pct, w_pct, h_pct]
|
216
|
-
|
217
|
-
serializable_rects.append({
|
218
|
-
"monitor": {'index': monitor_dict['index']},
|
219
|
-
"coordinates": coords_to_save,
|
220
|
-
"is_excluded": is_excluded,
|
221
|
-
"is_secondary": is_secondary
|
222
|
-
})
|
223
|
-
|
224
|
-
save_data = {
|
225
|
-
"scene": self.scene or "",
|
226
|
-
"window": self.window_name,
|
227
|
-
"coordinate_system": COORD_SYSTEM_PERCENTAGE, # Always save as percentage
|
228
|
-
"window_geometry": win_geom, # Save the geometry used for conversion
|
229
|
-
"rectangles": serializable_rects
|
230
|
-
}
|
231
|
-
|
232
|
-
with open(config_path, 'w', encoding="utf-8") as f:
|
233
|
-
json.dump(save_data, f, indent=4, ensure_ascii=False)
|
234
|
-
|
235
|
-
print(f"Successfully saved {len(serializable_rects)} rectangles.")
|
236
|
-
# Pass back the internal absolute coords for any immediate post-processing
|
237
|
-
self.result['rectangles'] = [(r[0], list(r[1]), r[2]) for r in self.rectangles]
|
238
|
-
self.result['window_geometry'] = win_geom
|
239
|
-
self.result['coordinate_system'] = COORD_SYSTEM_PERCENTAGE
|
240
|
-
self.quit_app()
|
241
|
-
|
242
|
-
def undo_last_rect(self, event=None):
|
243
|
-
if self.rectangles and self.drawn_rect_ids:
|
244
|
-
last_rect_tuple = self.rectangles.pop()
|
245
|
-
last_rect_id = self.drawn_rect_ids.pop()
|
246
|
-
self.redo_stack.append((*last_rect_tuple, last_rect_id))
|
247
|
-
event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
|
248
|
-
print("Undo: Removed last rectangle.")
|
249
|
-
|
250
|
-
def toggle_image_mode(self, e=None):
|
251
|
-
self.image_mode = not self.image_mode
|
252
|
-
# Only change alpha of the main window, not the text widget
|
253
|
-
self.window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
|
254
|
-
print("Toggled background visibility.")
|
255
|
-
|
256
|
-
def redo_last_rect(self, event=None):
|
257
|
-
if not self.redo_stack: return
|
258
|
-
monitor, abs_coords, is_excluded, is_secondary, old_rect_id = self.redo_stack.pop()
|
259
|
-
canvas = event.widget.winfo_toplevel().winfo_children()[0]
|
260
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
261
|
-
canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
|
262
|
-
outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
|
263
|
-
new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
264
|
-
outline=outline_color, width=2)
|
265
|
-
self.rectangles.append((monitor, abs_coords, is_excluded, is_secondary))
|
266
|
-
self.drawn_rect_ids.append(new_rect_id)
|
267
|
-
print("Redo: Restored rectangle.")
|
268
|
-
|
269
|
-
# --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
|
270
|
-
def _create_instructions_widget(self, parent_canvas):
|
271
|
-
"""Creates a separate, persistent window for instructions and control buttons."""
|
272
|
-
if self.instructions_widget and self.instructions_widget.winfo_exists():
|
273
|
-
self.instructions_widget.lift()
|
274
|
-
return
|
275
|
-
|
276
|
-
self.instructions_widget = tk.Toplevel(parent_canvas)
|
277
|
-
self.instructions_widget.title("Controls")
|
278
|
-
|
279
|
-
# --- Position it near the main window ---
|
280
|
-
parent_window = parent_canvas.winfo_toplevel()
|
281
|
-
# Make the instructions window transient to the main window to keep it on top
|
282
|
-
# self.instructions_widget.transient(parent_window)
|
283
|
-
self.instructions_widget.attributes('-topmost', 1)
|
284
|
-
# parent_window.update_idletasks() # Ensure dimensions are up-to-date
|
285
|
-
pos_x = parent_window.winfo_x() + 50
|
286
|
-
pos_y = parent_window.winfo_y() + 50
|
287
|
-
self.instructions_widget.geometry(f"+{pos_x}+{pos_y}")
|
288
|
-
|
289
|
-
main_frame = tk.Frame(self.instructions_widget, padx=10, pady=10)
|
290
|
-
main_frame.pack(fill=tk.BOTH, expand=True)
|
291
|
-
|
292
|
-
instructions_text = (
|
293
|
-
"How to Use:\n"
|
294
|
-
"• Left Click + Drag: Create a capture area (green).\n"
|
295
|
-
"• Shift + Left Click + Drag: Create an exclusion area (orange).\n"
|
296
|
-
"• Ctrl + Left Click + Drag: Create a secondary (menu) area (purple).\n"
|
297
|
-
"• Right-Click on a box: Delete it."
|
298
|
-
)
|
299
|
-
tk.Label(main_frame, text=instructions_text, justify=tk.LEFT, anchor="w").pack(pady=(0, 10), fill=tk.X)
|
300
|
-
|
301
|
-
button_frame = tk.Frame(main_frame)
|
302
|
-
button_frame.pack(fill=tk.X, pady=5)
|
303
|
-
|
304
|
-
def canvas_event_wrapper(func):
|
305
|
-
class MockEvent:
|
306
|
-
def __init__(self, widget):
|
307
|
-
self.widget = widget
|
308
|
-
return lambda: func(MockEvent(self.canvas))
|
309
|
-
|
310
|
-
def root_event_wrapper(func):
|
311
|
-
return lambda: func(None)
|
312
|
-
|
313
|
-
tk.Button(button_frame, text="Save and Quit (Ctrl+S)", command=root_event_wrapper(self.save_rects)).pack(fill=tk.X, pady=2)
|
314
|
-
tk.Button(button_frame, text="Undo (Ctrl+Z)", command=canvas_event_wrapper(self.undo_last_rect)).pack(fill=tk.X, pady=2)
|
315
|
-
tk.Button(button_frame, text="Redo (Ctrl+Y)", command=canvas_event_wrapper(self.redo_last_rect)).pack(fill=tk.X, pady=2)
|
316
|
-
tk.Button(button_frame, text="Toggle Background (M)", command=root_event_wrapper(self.toggle_image_mode)).pack(fill=tk.X, pady=2)
|
317
|
-
tk.Button(button_frame, text="Quit without Saving (Esc)", command=root_event_wrapper(self.quit_app)).pack(fill=tk.X, pady=2)
|
318
|
-
tk.Button(button_frame, text="Toggle Instructions (I)", command=canvas_event_wrapper(self.toggle_instructions)).pack(fill=tk.X, pady=2)
|
319
|
-
|
320
|
-
# hotkeys_text = "\n• I: Toggle this instruction panel"
|
321
|
-
# tk.Label(main_frame, text=hotkeys_text, justify=tk.LEFT, anchor="w").pack(pady=(10, 0), fill=tk.X)
|
322
|
-
|
323
|
-
|
324
|
-
# --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
|
325
|
-
def print_instructions_box(self, canvas):
|
326
|
-
"""Creates a separate, persistent window for instructions and control buttons."""
|
327
|
-
instructions_text = (
|
328
|
-
"How to Use:\n"
|
329
|
-
" • Left Click + Drag: Create a capture area (green).\n"
|
330
|
-
" • Shift + Left Click + Drag: Create an exclusion area (orange).\n"
|
331
|
-
" • Right-Click on a box: Delete it.\n\n"
|
332
|
-
"Hotkeys:\n"
|
333
|
-
" • Ctrl + S: Save and Quit\n"
|
334
|
-
" • Ctrl + Z / Ctrl + Y: Undo / Redo\n"
|
335
|
-
" • M: Toggle background visibility\n"
|
336
|
-
" • I: Toggle these instructions\n"
|
337
|
-
" • Esc: Quit without saving"
|
338
|
-
" "
|
339
|
-
)
|
340
|
-
|
341
|
-
# Use a common, readable font
|
342
|
-
instruction_font = tkfont.Font(family="Segoe UI", size=10, weight="normal")
|
343
|
-
|
344
|
-
# Create the text item first to get its size
|
345
|
-
self.instructions_overlay = canvas.create_text(
|
346
|
-
20, 20, # Position with a small margin
|
347
|
-
text=instructions_text,
|
348
|
-
anchor=tk.NW,
|
349
|
-
fill='white',
|
350
|
-
font=instruction_font,
|
351
|
-
justify=tk.LEFT
|
352
|
-
)
|
353
|
-
|
354
|
-
# Get the bounding box of the text to draw a background
|
355
|
-
text_bbox = canvas.bbox(self.instructions_overlay)
|
356
|
-
|
357
|
-
# Create a background rectangle with padding
|
358
|
-
self.instructions_rect = canvas.create_rectangle(
|
359
|
-
text_bbox[0] - 10, # left
|
360
|
-
text_bbox[1] - 10, # top
|
361
|
-
text_bbox[2] + 10, # right
|
362
|
-
text_bbox[3] + 10, # bottom
|
363
|
-
fill='#2B2B2B', # Dark, semi-opaque background
|
364
|
-
outline='white',
|
365
|
-
width=1
|
366
|
-
)
|
367
|
-
|
368
|
-
# Lower the rectangle so it's behind the text
|
369
|
-
canvas.tag_lower(self.instructions_rect, self.instructions_overlay)
|
370
|
-
|
371
|
-
# Add hover effect: make rectangle transparent on mouse over
|
372
|
-
def on_motion(event):
|
373
|
-
# Check if mouse is over the rectangle
|
374
|
-
x, y = event.x, event.y
|
375
|
-
rect_bbox = canvas.bbox(self.instructions_rect)
|
376
|
-
if rect_bbox and rect_bbox[0] <= x <= rect_bbox[2] and rect_bbox[1] <= y <= y <= rect_bbox[3]:
|
377
|
-
# Set fill to more transparent using denser stipple
|
378
|
-
canvas.itemconfigure(self.instructions_rect, fill='#2B2B2B', stipple='gray12')
|
379
|
-
# Make text more transparent by changing its color to a lighter gray
|
380
|
-
canvas.itemconfigure(self.instructions_overlay, fill='#CCCCCC')
|
381
|
-
else:
|
382
|
-
# Restore solid fill and opaque text
|
383
|
-
canvas.itemconfigure(self.instructions_rect, fill='#2B2B2B', stipple='')
|
384
|
-
canvas.itemconfigure(self.instructions_overlay, fill='white')
|
385
|
-
|
386
|
-
canvas.bind('<Motion>', on_motion)
|
387
|
-
|
388
|
-
|
389
|
-
def toggle_instructions(self, event=None):
|
390
|
-
canvas = event.widget.winfo_toplevel().winfo_children()[0]
|
391
|
-
for element in [self.instructions_overlay, self.instructions_rect]:
|
392
|
-
current_state = canvas.itemcget(element, 'state')
|
393
|
-
new_state = tk.NORMAL if current_state == tk.HIDDEN else tk.HIDDEN
|
394
|
-
canvas.itemconfigure(element, state=new_state)
|
395
|
-
|
396
|
-
# if self.instructions_showing:
|
397
|
-
# self.instructions_widget.withdraw()
|
398
|
-
# logger.info(f"Toggled instructions visibility: OFF")
|
399
|
-
# self.instructions_showing = False
|
400
|
-
# else:
|
401
|
-
# self.instructions_widget.deiconify()
|
402
|
-
# self.instructions_widget.lift()
|
403
|
-
# self.canvas.focus_set()
|
404
|
-
# self.instructions_widget.update_idletasks() # Ensure it is fully rendered
|
405
|
-
# logger.info("Toggled instructions visibility: ON")
|
406
|
-
# self.instructions_showing = True
|
407
|
-
|
408
|
-
def start(self):
|
409
|
-
self.root = tk.Tk()
|
410
|
-
self.root.withdraw()
|
411
|
-
|
412
|
-
if self.use_obs_screenshot:
|
413
|
-
# Use the pre-loaded OBS screenshot
|
414
|
-
img = self.screenshot_img
|
415
|
-
self.bounding_box = self.target_window_geometry
|
416
|
-
# Center the window on the primary monitor
|
417
|
-
primary_monitor = self.sct.monitors[1] if len(self.sct.monitors) > 1 else self.sct.monitors[0]
|
418
|
-
win_x = primary_monitor['left'] + (primary_monitor['width'] - img.width) // 2
|
419
|
-
win_y = primary_monitor['top'] + (primary_monitor['height'] - img.height) // 2
|
420
|
-
window_geometry = f"{img.width}x{img.height}+{int(win_x)}+{int(win_y)}"
|
421
|
-
else:
|
422
|
-
# Calculate bounding box of all monitors for the overlay
|
423
|
-
left = min(m['left'] for m in self.monitors)
|
424
|
-
top = min(m['top'] for m in self.monitors)
|
425
|
-
right = max(m['left'] + m['width'] for m in self.monitors)
|
426
|
-
bottom = max(m['top'] + m['height'] for m in self.monitors)
|
427
|
-
self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
|
428
|
-
|
429
|
-
# Capture the entire desktop area covered by all monitors
|
430
|
-
sct_img = self.sct.grab(self.bounding_box)
|
431
|
-
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
|
432
|
-
window_geometry = f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}"
|
433
|
-
|
434
|
-
self.window = tk.Toplevel(self.root)
|
435
|
-
self.window.geometry(window_geometry)
|
436
|
-
self.window.overrideredirect(1)
|
437
|
-
self.window.attributes('-topmost', 1)
|
438
|
-
|
439
|
-
self.photo_image = ImageTk.PhotoImage(img)
|
440
|
-
self.canvas = tk.Canvas(self.window, cursor='cross', highlightthickness=0)
|
441
|
-
self.canvas.pack(fill=tk.BOTH, expand=True)
|
442
|
-
self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
|
443
|
-
|
444
|
-
# --- MODIFIED: CALL THE INSTRUCTION WIDGET CREATOR ---
|
445
|
-
# self._create_instructions_widget(self.canvas)
|
446
|
-
# --- END MODIFICATION ---
|
447
|
-
|
448
|
-
# Draw existing rectangles (which were converted to absolute pixels on load)
|
449
|
-
for _, abs_coords, is_excluded, is_secondary in self.rectangles:
|
450
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
451
|
-
canvas_x = x_abs - self.bounding_box['left']
|
452
|
-
canvas_y = y_abs - self.bounding_box['top']
|
453
|
-
outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
|
454
|
-
rect_id = self.canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
455
|
-
outline=outline_color, width=2)
|
456
|
-
self.drawn_rect_ids.append(rect_id)
|
457
|
-
|
458
|
-
def on_click(event):
|
459
|
-
self.start_x, self.start_y = event.x, event.y
|
460
|
-
ctrl_held = bool(event.state & 0x0004)
|
461
|
-
shift_held = bool(event.state & 0x0001)
|
462
|
-
if ctrl_held:
|
463
|
-
outline = 'purple'
|
464
|
-
elif shift_held:
|
465
|
-
outline = 'orange'
|
466
|
-
else:
|
467
|
-
outline = 'green'
|
468
|
-
self.current_rect_id = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
|
469
|
-
outline=outline, width=2)
|
470
|
-
|
471
|
-
def on_drag(event):
|
472
|
-
if self.current_rect_id: self.canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
|
473
|
-
|
474
|
-
def on_release(event):
|
475
|
-
if not self.current_rect_id: return
|
476
|
-
coords = self.canvas.coords(self.current_rect_id)
|
477
|
-
x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
|
478
|
-
y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
|
479
|
-
w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
|
480
|
-
|
481
|
-
if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
|
482
|
-
ctrl_held = bool(event.state & 0x0004)
|
483
|
-
shift_held = bool(event.state & 0x0001)
|
484
|
-
is_excl = shift_held
|
485
|
-
is_secondary = ctrl_held
|
486
|
-
outline_color = 'purple' if is_secondary else ('orange' if is_excl else 'green')
|
487
|
-
self.canvas.itemconfig(self.current_rect_id, outline=outline_color)
|
488
|
-
|
489
|
-
center_x, center_y = x_abs + w / 2, y_abs + h / 2
|
490
|
-
target_mon = self.monitors[0]
|
491
|
-
for mon in self.monitors:
|
492
|
-
if mon['left'] <= center_x < mon['left'] + mon['width'] and mon['top'] <= center_y < mon['top'] + \
|
493
|
-
mon['height']:
|
494
|
-
target_mon = mon
|
495
|
-
break
|
496
|
-
|
497
|
-
self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl, is_secondary))
|
498
|
-
self.drawn_rect_ids.append(self.current_rect_id)
|
499
|
-
self.redo_stack.clear()
|
500
|
-
else:
|
501
|
-
self.canvas.delete(self.current_rect_id)
|
502
|
-
self.current_rect_id = self.start_x = self.start_y = None
|
503
|
-
|
504
|
-
def on_right_click(event):
|
505
|
-
# Iterate through our rectangles in reverse to find the topmost one.
|
506
|
-
for i in range(len(self.rectangles) - 1, -1, -1):
|
507
|
-
_monitor, abs_coords, _is_excluded, _is_secondary = self.rectangles[i]
|
508
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
509
|
-
canvas_x1 = x_abs - self.bounding_box['left']
|
510
|
-
canvas_y1 = y_abs - self.bounding_box['top']
|
511
|
-
canvas_x2 = canvas_x1 + w_abs
|
512
|
-
canvas_y2 = canvas_y1 + h_abs
|
513
|
-
|
514
|
-
if canvas_x1 <= event.x <= canvas_x2 and canvas_y1 <= event.y <= canvas_y2:
|
515
|
-
# --- UNDO/REDO CHANGE ---
|
516
|
-
# We found the rectangle. Prepare the 'remove' action.
|
517
|
-
# We need to save the data AND its original index to restore it correctly.
|
518
|
-
rect_tuple_to_del = self.rectangles[i]
|
519
|
-
item_id_to_del = self.drawn_rect_ids[i]
|
520
|
-
|
521
|
-
self.redo_stack.append((*rect_tuple_to_del, i))
|
522
|
-
|
523
|
-
# Now, perform the deletion
|
524
|
-
del self.rectangles[i]
|
525
|
-
del self.drawn_rect_ids[i]
|
526
|
-
self.canvas.delete(item_id_to_del)
|
527
|
-
print("Deleted rectangle.")
|
528
|
-
|
529
|
-
break # Stop after deleting the topmost one
|
530
|
-
|
531
|
-
def on_enter(e=None):
|
532
|
-
self.canvas.focus_set()
|
533
|
-
|
534
|
-
self.canvas.bind('<Enter>', on_enter)
|
535
|
-
self.canvas.bind('<ButtonPress-1>', on_click)
|
536
|
-
self.canvas.bind('<B1-Motion>', on_drag)
|
537
|
-
self.canvas.bind('<ButtonRelease-1>', on_release)
|
538
|
-
self.canvas.bind('<Button-3>', on_right_click)
|
539
|
-
self.canvas.bind('<Control-s>', self.save_rects)
|
540
|
-
self.canvas.bind('<Control-y>', self.redo_last_rect)
|
541
|
-
self.canvas.bind('<Control-z>', self.undo_last_rect)
|
542
|
-
self.canvas.bind("<Escape>", self.quit_app)
|
543
|
-
self.canvas.bind("<m>", self.toggle_image_mode)
|
544
|
-
self.canvas.bind("<i>", self.toggle_instructions)
|
545
|
-
|
546
|
-
self.canvas.focus_set()
|
547
|
-
self._create_instructions_widget(self.window)
|
548
|
-
self.window.winfo_toplevel().update_idletasks()
|
549
|
-
self.print_instructions_box(self.canvas)
|
550
|
-
# The print message is now redundant but kept for console feedback
|
551
|
-
print("Starting UI. See on-screen instructions. Press Esc to quit, Ctrl+S to save.")
|
552
|
-
# self.canvas.update_idletasks()
|
553
|
-
self.root.mainloop()
|
554
|
-
|
555
|
-
def quit_app(self, event=None):
|
556
|
-
if self.instructions_widget and self.instructions_widget.winfo_exists():
|
557
|
-
self.instructions_widget.destroy()
|
558
|
-
if self.root and self.root.winfo_exists(): self.root.destroy()
|
559
|
-
self.root = None
|
560
|
-
|
561
|
-
|
562
|
-
def run_screen_selector(result_dict, window_name, use_window_as_config, use_obs_screenshot):
|
563
|
-
try:
|
564
|
-
selector = ScreenSelector(result_dict, window_name, use_window_as_config, use_obs_screenshot)
|
565
|
-
selector.start()
|
566
|
-
except Exception as e:
|
567
|
-
print(f"Error in selector process: {e}", file=sys.stderr)
|
568
|
-
import traceback
|
569
|
-
traceback.print_exc()
|
570
|
-
result_dict['error'] = str(e)
|
571
|
-
|
572
|
-
|
573
|
-
def get_screen_selection(window_name, use_window_as_config=False, use_obs_screenshot=False):
|
574
|
-
if not selector_available: return None
|
575
|
-
if not window_name and not use_obs_screenshot:
|
576
|
-
print("Error: A target window name must be provided.", file=sys.stderr)
|
577
|
-
return None
|
578
|
-
|
579
|
-
with Manager() as manager:
|
580
|
-
result_data = manager.dict()
|
581
|
-
process = Process(target=run_screen_selector, args=(result_data, window_name, use_window_as_config, use_obs_screenshot))
|
582
|
-
print(f"Starting ScreenSelector process...")
|
583
|
-
process.start()
|
584
|
-
process.join()
|
585
|
-
|
586
|
-
if 'error' in result_data:
|
587
|
-
print(f"Selector process failed: {result_data['error']}", file=sys.stderr)
|
588
|
-
return None
|
589
|
-
elif 'rectangles' in result_data:
|
590
|
-
print("Screen selection successful.")
|
591
|
-
return dict(result_data)
|
592
|
-
else:
|
593
|
-
print("Selection was cancelled by the user.")
|
594
|
-
return {}
|
595
|
-
|
596
|
-
|
597
|
-
if __name__ == "__main__":
|
598
|
-
set_dpi_awareness()
|
599
|
-
|
600
|
-
parser = argparse.ArgumentParser(description="Screen Selector Arguments")
|
601
|
-
parser.add_argument("window_title", nargs="?", default="", help="Target window title")
|
602
|
-
parser.add_argument("--obs_ocr", action="store_true", default=True, help="Use OBS screenshot")
|
603
|
-
parser.add_argument("--use_window_for_config", action="store_true", help="Use window for config")
|
604
|
-
args = parser.parse_args()
|
605
|
-
|
606
|
-
target_window_title = args.window_title
|
607
|
-
use_obs_screenshot = args.obs_ocr
|
608
|
-
use_window_as_config = args.use_window_for_config
|
609
|
-
|
610
|
-
print(f"Arguments: Window Title='{target_window_title}', Use OBS Screenshot={use_obs_screenshot}, Use Window for Config={use_window_as_config}")
|
611
|
-
|
612
|
-
# Example of how to call it
|
613
|
-
selection_result = get_screen_selection(target_window_title, use_window_as_config, use_obs_screenshot)
|
614
|
-
|
615
|
-
if selection_result is None:
|
616
|
-
print("--- Screen selection failed. ---")
|
617
|
-
elif not selection_result:
|
618
|
-
print("\n--- Screen selection cancelled. ---")
|
619
|
-
elif 'rectangles' in selection_result:
|
620
|
-
print("\n--- Selection Result ---")
|
621
|
-
rects = selection_result.get('rectangles', [])
|
622
|
-
win_geom = selection_result.get('window_geometry')
|
623
|
-
print(f"Saved relative to window: {win_geom}")
|
624
|
-
print(f"Selected rectangles ({len(rects)}):")
|
625
|
-
# The returned coordinates are absolute pixels for immediate use
|
626
|
-
for i, (monitor, coords, is_excluded) in enumerate(rects):
|
627
|
-
coord_str = f"(X:{coords[0]}, Y:{coords[1]}, W:{coords[2]}, H:{coords[3]})"
|
628
|
-
print(
|
629
|
-
f" Rect {i + 1}: On Monitor Idx:{monitor.get('index', 'N/A')}, Coords={coord_str}, Excluded={is_excluded}")
|