GameSentenceMiner 2.11.2__tar.gz → 2.11.4__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.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/obs.py +8 -1
  2. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/gsm_ocr_config.py +30 -4
  3. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/owocr_area_selector.py +207 -117
  4. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/owocr_helper.py +25 -12
  5. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/ocr.py +0 -1
  6. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/run.py +113 -8
  7. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/text_log.py +6 -3
  8. gamesentenceminer-2.11.4/GameSentenceMiner/util/window_transparency.py +168 -0
  9. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/vad.py +13 -11
  10. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  11. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/SOURCES.txt +1 -0
  12. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/PKG-INFO +1 -1
  13. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/pyproject.toml +1 -1
  14. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/__init__.py +0 -0
  15. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ai/__init__.py +0 -0
  16. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  17. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/anki.py +0 -0
  18. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/__init__.py +0 -0
  19. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon.png +0 -0
  20. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon128.png +0 -0
  21. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon256.png +0 -0
  22. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon32.png +0 -0
  23. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon512.png +0 -0
  24. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/icon64.png +0 -0
  25. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/assets/pickaxe.png +0 -0
  26. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/config_gui.py +0 -0
  27. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/gametext.py +0 -0
  28. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/gsm.py +0 -0
  29. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/__init__.py +0 -0
  30. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  31. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  32. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  33. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  34. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  35. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  36. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  37. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/__init__.py +0 -0
  38. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  39. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/communication/__init__.py +0 -0
  40. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/communication/send.py +0 -0
  41. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/communication/websocket.py +0 -0
  42. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/configuration.py +0 -0
  43. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  44. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  45. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  46. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  47. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/electron_config.py +0 -0
  48. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/ffmpeg.py +0 -0
  49. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/gsm_utils.py +0 -0
  50. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/model.py +0 -0
  51. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/notification.py +0 -0
  52. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/package.py +0 -0
  53. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/util/ss_selector.py +0 -0
  54. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/__init__.py +0 -0
  55. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/service.py +0 -0
  56. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/__init__.py +0 -0
  57. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  58. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  59. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/favicon.ico +0 -0
  60. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/favicon.svg +0 -0
  61. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  62. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/style.css +0 -0
  63. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  64. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  65. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/templates/__init__.py +0 -0
  66. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/templates/index.html +0 -0
  67. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  68. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/templates/utility.html +0 -0
  69. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner/web/texthooking_page.py +0 -0
  70. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  71. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  72. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/requires.txt +0 -0
  73. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  74. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/LICENSE +0 -0
  75. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/README.md +0 -0
  76. {gamesentenceminer-2.11.2 → gamesentenceminer-2.11.4}/setup.cfg +0 -0
@@ -203,6 +203,7 @@ def connect_to_obs_sync(retry=2):
203
203
  obs_connection_manager = OBSConnectionManager()
204
204
  obs_connection_manager.start()
205
205
  update_current_game()
206
+ logger.info("Connected to OBS WebSocket.")
206
207
  break # Exit the loop once connected
207
208
  except Exception as e:
208
209
  if retry <= 0:
@@ -295,6 +296,12 @@ def get_source_from_scene(scene_name):
295
296
  logger.error(f"Error getting source from scene: {e}")
296
297
  return ''
297
298
 
299
+ def get_active_source():
300
+ current_game = get_current_game()
301
+ if not current_game:
302
+ return None
303
+ return get_source_from_scene(current_game)
304
+
298
305
  def get_record_directory():
299
306
  try:
300
307
  response = client.get_record_directory()
@@ -362,7 +369,7 @@ def get_screenshot_base64(compression=0, width=None, height=None):
362
369
  return None
363
370
  response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
364
371
  if response and response.image_data:
365
- return response.image_data
372
+ return response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
366
373
  else:
367
374
  logger.error(f"Error getting base64 screenshot: {response}")
368
375
  return None
@@ -1,12 +1,15 @@
1
- import ctypes
1
+ import os
2
2
  from copy import deepcopy
3
3
  from dataclasses import dataclass
4
4
  from math import floor, ceil
5
+ from pathlib import Path
5
6
 
7
+ from GameSentenceMiner import obs
6
8
  from dataclasses_json import dataclass_json
7
9
  from typing import List, Optional, Union
8
10
 
9
- from GameSentenceMiner.util.configuration import logger
11
+ from GameSentenceMiner.util.configuration import logger, get_app_directory
12
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
10
13
 
11
14
 
12
15
  @dataclass_json
@@ -50,8 +53,10 @@ class OCRConfig:
50
53
  window: Optional[str] = None
51
54
  language: str = "ja"
52
55
 
53
- def scale_coords(self):
56
+ def __post_init__(self):
54
57
  self.pre_scale_rectangles = deepcopy(self.rectangles)
58
+
59
+ def scale_coords(self):
55
60
  if self.coordinate_system and self.coordinate_system == "percentage" and self.window:
56
61
  import pygetwindow as gw
57
62
  try:
@@ -116,7 +121,28 @@ def get_window(title):
116
121
  return window
117
122
  return ret
118
123
 
119
- # try w10+, fall back to w8.1+
124
+ # if windows, set dpi awareness to per-monitor v2
120
125
  def set_dpi_awareness():
126
+ import sys
127
+ if sys.platform != "win32":
128
+ return
129
+ import ctypes
121
130
  per_monitor_awareness = 2
122
131
  ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
132
+
133
+ def get_scene_ocr_config(use_window_as_config=False, window=""):
134
+ ocr_config_dir = get_ocr_config_path()
135
+ try:
136
+ if use_window_as_config:
137
+ scene = sanitize_filename(window)
138
+ else:
139
+ scene = sanitize_filename(obs.get_current_scene() or "Default")
140
+ except Exception as e:
141
+ print(f"Error getting OBS scene: {e}. Using default config name.")
142
+ scene = "Default"
143
+ return os.path.join(ocr_config_dir, f"{scene}.json")
144
+
145
+ def get_ocr_config_path():
146
+ ocr_config_dir = os.path.join(get_app_directory(), "ocr_config")
147
+ os.makedirs(ocr_config_dir, exist_ok=True)
148
+ return ocr_config_dir
@@ -1,23 +1,19 @@
1
+ import argparse
2
+ import base64
1
3
  import ctypes
4
+ import io
2
5
  import json
3
6
  import sys
4
7
  from multiprocessing import Process, Manager
5
8
  from pathlib import Path
6
9
 
7
- import mss
8
10
  from PIL import Image, ImageTk
9
11
 
10
12
  # Assuming a mock or real obs module exists in this path
11
13
  from GameSentenceMiner import obs
12
- from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window
14
+ from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window, get_scene_ocr_config
13
15
  from GameSentenceMiner.util.gsm_utils import sanitize_filename
14
16
 
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
17
  try:
22
18
  import tkinter as tk
23
19
  from tkinter import font as tkfont # NEW: Import for better font control
@@ -34,30 +30,59 @@ COORD_SYSTEM_PERCENTAGE = "percentage"
34
30
 
35
31
 
36
32
  class ScreenSelector:
37
- def __init__(self, result, window_name, use_window_as_config):
38
- if not selector_available or not gw:
39
- raise RuntimeError("tkinter or pygetwindow is not available.")
33
+ def __init__(self, result, window_name, use_window_as_config, use_obs_screenshot=False):
34
+ if not selector_available:
35
+ raise RuntimeError("tkinter is not available.")
40
36
  if not window_name:
41
- raise ValueError("A target window name is required for percentage-based coordinates.")
37
+ raise ValueError("A target window name is required for configuration.")
42
38
 
43
39
  obs.connect_to_obs_sync()
44
40
  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
- # ---
41
+ self.use_obs_screenshot = use_obs_screenshot
42
+ self.screenshot_img = None
43
+ try:
44
+ import mss
45
+ self.sct = mss.mss()
46
+ self.monitors = self.sct.monitors[1:]
47
+ if not self.monitors:
48
+ raise RuntimeError("No monitors found by mss.")
49
+ for i, monitor in enumerate(self.monitors):
50
+ monitor['index'] = i
51
+ except ImportError:
52
+ print("Error: mss library not found. Please install it: pip install mss")
53
+ raise RuntimeError("mss is required for screen selection.")
54
+
55
+ if self.use_obs_screenshot:
56
+ print("Using OBS screenshot as target.")
57
+ screenshot_base64 = obs.get_screenshot_base64(compression=75)
58
+ # print(screenshot_base64)
59
+ if not screenshot_base64:
60
+ raise RuntimeError("Failed to get OBS screenshot.")
61
+ try:
62
+ img_data = base64.b64decode(screenshot_base64)
63
+ self.screenshot_img = Image.open(io.BytesIO(img_data))
64
+ # Scale image to 1280x720
65
+ self.screenshot_img = self.screenshot_img.resize((1280, 720), Image.LANCZOS)
66
+ except Exception as e:
67
+ raise RuntimeError(f"Failed to decode or open OBS screenshot: {e}")
68
+
69
+ self.target_window = None
70
+ self.target_window_geometry = {
71
+ "left": 0, "top": 0,
72
+ "width": self.screenshot_img.width,
73
+ "height": self.screenshot_img.height
74
+ }
75
+ print(f"OBS Screenshot dimensions: {self.target_window_geometry}")
76
+ else:
77
+ import pygetwindow as gw
78
+ if not gw:
79
+ raise RuntimeError("pygetwindow is not available for window selection.")
80
+ print(f"Targeting window: '{window_name}'")
81
+ self.target_window = self._find_target_window()
82
+ self.target_window_geometry = self._get_window_geometry(self.target_window)
83
+ if not self.target_window_geometry:
84
+ raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
85
+ print(f"Found target window at: {self.target_window_geometry}")
61
86
 
62
87
  self.root = None
63
88
  self.scene = ''
@@ -71,6 +96,11 @@ class ScreenSelector:
71
96
  self.redo_stack = []
72
97
  self.bounding_box = {} # Geometry of the single large canvas window
73
98
 
99
+ self.canvas = None
100
+ self.window = None
101
+ self.instructions_widget = None
102
+ self.instructions_window_id = None
103
+
74
104
  self.load_existing_rectangles()
75
105
 
76
106
  def _find_target_window(self):
@@ -91,23 +121,9 @@ class ScreenSelector:
91
121
  return None
92
122
  return None
93
123
 
94
- def get_scene_ocr_config(self):
95
- app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
96
- ocr_config_dir = app_dir / "ocr_config"
97
- ocr_config_dir.mkdir(parents=True, exist_ok=True)
98
- try:
99
- if self.use_window_as_config:
100
- self.scene = sanitize_filename(self.window_name)
101
- else:
102
- self.scene = sanitize_filename(obs.get_current_scene() or "")
103
- except Exception as e:
104
- print(f"Error getting OBS scene: {e}. Using default config name.")
105
- self.scene = ""
106
- return ocr_config_dir / f"{self.scene}.json"
107
-
108
124
  def load_existing_rectangles(self):
109
125
  """Loads rectangles from config, converting from percentage to absolute pixels for use."""
110
- config_path = self.get_scene_ocr_config()
126
+ config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
111
127
  win_geom = self.target_window_geometry # Use current geometry for conversion
112
128
  win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
113
129
 
@@ -152,7 +168,7 @@ class ScreenSelector:
152
168
 
153
169
  def save_rects(self, event=None):
154
170
  """Saves rectangles to config, converting from absolute pixels to percentages."""
155
- config_path = self.get_scene_ocr_config()
171
+ config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
156
172
  win_geom = self.target_window_geometry
157
173
  win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
158
174
  print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
@@ -200,6 +216,12 @@ class ScreenSelector:
200
216
  event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
201
217
  print("Undo: Removed last rectangle.")
202
218
 
219
+ def toggle_image_mode(self, e=None):
220
+ self.image_mode = not self.image_mode
221
+ # Only change alpha of the main window, not the text widget
222
+ self.window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
223
+ print("Toggled background visibility.")
224
+
203
225
  def redo_last_rect(self, event=None):
204
226
  if not self.redo_stack: return
205
227
  monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
@@ -213,8 +235,63 @@ class ScreenSelector:
213
235
  print("Redo: Restored rectangle.")
214
236
 
215
237
  # --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
216
- def _create_instructions_widget(self, canvas):
217
- """Creates a text box with usage instructions on the canvas."""
238
+ def _create_instructions_widget(self, parent_canvas):
239
+ """Creates a separate, persistent window for instructions and control buttons."""
240
+ if self.instructions_widget and self.instructions_widget.winfo_exists():
241
+ self.instructions_widget.lift()
242
+ return
243
+
244
+ self.instructions_widget = tk.Toplevel(parent_canvas)
245
+ self.instructions_widget.title("Controls")
246
+
247
+ # --- Position it near the main window ---
248
+ parent_window = parent_canvas.winfo_toplevel()
249
+ # Make the instructions window transient to the main window to keep it on top
250
+ # self.instructions_widget.transient(parent_window)
251
+ self.instructions_widget.attributes('-topmost', 1)
252
+ # parent_window.update_idletasks() # Ensure dimensions are up-to-date
253
+ pos_x = parent_window.winfo_x() + 50
254
+ pos_y = parent_window.winfo_y() + 50
255
+ self.instructions_widget.geometry(f"+{pos_x}+{pos_y}")
256
+
257
+ main_frame = tk.Frame(self.instructions_widget, padx=10, pady=10)
258
+ main_frame.pack(fill=tk.BOTH, expand=True)
259
+
260
+ instructions_text = (
261
+ "How to Use:\n"
262
+ "• Left Click + Drag: Create a capture area (green).\n"
263
+ "• Shift + Left Click + Drag: Create an exclusion area (orange).\n"
264
+ "• Right-Click on a box: Delete it."
265
+ )
266
+ tk.Label(main_frame, text=instructions_text, justify=tk.LEFT, anchor="w").pack(pady=(0, 10), fill=tk.X)
267
+
268
+ button_frame = tk.Frame(main_frame)
269
+ button_frame.pack(fill=tk.X, pady=5)
270
+
271
+ def canvas_event_wrapper(func):
272
+ class MockEvent:
273
+ def __init__(self, widget):
274
+ self.widget = widget
275
+ return lambda: func(MockEvent(self.canvas))
276
+
277
+ def root_event_wrapper(func):
278
+ return lambda: func(None)
279
+
280
+ tk.Button(button_frame, text="Save and Quit (Ctrl+S)", command=root_event_wrapper(self.save_rects)).pack(fill=tk.X, pady=2)
281
+ tk.Button(button_frame, text="Undo (Ctrl+Z)", command=canvas_event_wrapper(self.undo_last_rect)).pack(fill=tk.X, pady=2)
282
+ tk.Button(button_frame, text="Redo (Ctrl+Y)", command=canvas_event_wrapper(self.redo_last_rect)).pack(fill=tk.X, pady=2)
283
+ tk.Button(button_frame, text="Toggle Background (M)", command=root_event_wrapper(self.toggle_image_mode)).pack(fill=tk.X, pady=2)
284
+ tk.Button(button_frame, text="Quit without Saving (Esc)", command=root_event_wrapper(self.quit_app)).pack(fill=tk.X, pady=2)
285
+
286
+ hotkeys_text = "\n• I: Toggle this instruction panel"
287
+ tk.Label(main_frame, text=hotkeys_text, justify=tk.LEFT, anchor="w").pack(pady=(10, 0), fill=tk.X)
288
+
289
+ self.instructions_widget.protocol("WM_DELETE_WINDOW", self.toggle_instructions)
290
+
291
+
292
+ # --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
293
+ def print_instructions_box(self, canvas):
294
+ """Creates a separate, persistent window for instructions and control buttons."""
218
295
  instructions_text = (
219
296
  "How to Use:\n"
220
297
  " • Left Click + Drag: Create a capture area (green).\n"
@@ -260,44 +337,51 @@ class ScreenSelector:
260
337
  canvas.tag_lower(rect_id, text_id)
261
338
 
262
339
  def toggle_instructions(self, event=None):
263
- canvas = event.widget.winfo_toplevel().winfo_children()[0]
264
- # Find all text and rectangle items (assuming only one of each for instructions)
265
- text_items = [item for item in canvas.find_all() if canvas.type(item) == 'text']
266
- rect_items = [item for item in canvas.find_all() if canvas.type(item) == 'rectangle']
267
-
268
- if text_items and rect_items:
269
- current_state = canvas.itemcget(text_items[0], 'state')
270
- new_state = tk.NORMAL if current_state == tk.HIDDEN else tk.HIDDEN
271
- for item in text_items + rect_items:
272
- canvas.itemconfigure(item, state=new_state)
273
- print("Toggled instructions visibility.")
340
+ if self.instructions_widget and self.instructions_widget.winfo_exists() and self.instructions_widget.state() == "normal":
341
+ self.instructions_widget.withdraw()
342
+ print("Toggled instructions visibility: OFF")
343
+ else:
344
+ self._create_instructions_widget(self.canvas)
345
+ print("Toggled instructions visibility: ON")
274
346
 
275
347
  def start(self):
276
348
  self.root = tk.Tk()
277
349
  self.root.withdraw()
278
350
 
279
- # Calculate bounding box of all monitors
280
- left = min(m['left'] for m in self.monitors)
281
- top = min(m['top'] for m in self.monitors)
282
- right = max(m['left'] + m['width'] for m in self.monitors)
283
- bottom = max(m['top'] + m['height'] for m in self.monitors)
284
- self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
285
-
286
- sct_img = self.sct.grab(self.sct.monitors[0])
287
- img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
288
-
289
- window = tk.Toplevel(self.root)
290
- window.geometry(f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}")
291
- window.overrideredirect(1)
292
- window.attributes('-topmost', 1)
351
+ if self.use_obs_screenshot:
352
+ # Use the pre-loaded OBS screenshot
353
+ img = self.screenshot_img
354
+ self.bounding_box = self.target_window_geometry
355
+ # Center the window on the primary monitor
356
+ primary_monitor = self.sct.monitors[1] if len(self.sct.monitors) > 1 else self.sct.monitors[0]
357
+ win_x = primary_monitor['left'] + (primary_monitor['width'] - img.width) // 2
358
+ win_y = primary_monitor['top'] + (primary_monitor['height'] - img.height) // 2
359
+ window_geometry = f"{img.width}x{img.height}+{int(win_x)}+{int(win_y)}"
360
+ else:
361
+ # Calculate bounding box of all monitors for the overlay
362
+ left = min(m['left'] for m in self.monitors)
363
+ top = min(m['top'] for m in self.monitors)
364
+ right = max(m['left'] + m['width'] for m in self.monitors)
365
+ bottom = max(m['top'] + m['height'] for m in self.monitors)
366
+ self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
367
+
368
+ # Capture the entire desktop area covered by all monitors
369
+ sct_img = self.sct.grab(self.bounding_box)
370
+ img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
371
+ window_geometry = f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}"
372
+
373
+ self.window = tk.Toplevel(self.root)
374
+ self.window.geometry(window_geometry)
375
+ self.window.overrideredirect(1)
376
+ self.window.attributes('-topmost', 1)
293
377
 
294
378
  self.photo_image = ImageTk.PhotoImage(img)
295
- canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
296
- canvas.pack(fill=tk.BOTH, expand=True)
297
- canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
379
+ self.canvas = tk.Canvas(self.window, cursor='cross', highlightthickness=0)
380
+ self.canvas.pack(fill=tk.BOTH, expand=True)
381
+ self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
298
382
 
299
383
  # --- MODIFIED: CALL THE INSTRUCTION WIDGET CREATOR ---
300
- self._create_instructions_widget(canvas)
384
+ # self._create_instructions_widget(self.canvas)
301
385
  # --- END MODIFICATION ---
302
386
 
303
387
  # Draw existing rectangles (which were converted to absolute pixels on load)
@@ -305,29 +389,29 @@ class ScreenSelector:
305
389
  x_abs, y_abs, w_abs, h_abs = abs_coords
306
390
  canvas_x = x_abs - self.bounding_box['left']
307
391
  canvas_y = y_abs - self.bounding_box['top']
308
- rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
392
+ rect_id = self.canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
309
393
  outline='orange' if is_excluded else 'green', width=2)
310
394
  self.drawn_rect_ids.append(rect_id)
311
395
 
312
396
  def on_click(event):
313
397
  self.start_x, self.start_y = event.x, event.y
314
398
  outline = 'purple' if bool(event.state & 0x0001) else 'red'
315
- self.current_rect_id = canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
399
+ self.current_rect_id = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
316
400
  outline=outline, width=2)
317
401
 
318
402
  def on_drag(event):
319
- if self.current_rect_id: canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
403
+ if self.current_rect_id: self.canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
320
404
 
321
405
  def on_release(event):
322
406
  if not self.current_rect_id: return
323
- coords = canvas.coords(self.current_rect_id)
407
+ coords = self.canvas.coords(self.current_rect_id)
324
408
  x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
325
409
  y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
326
410
  w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
327
411
 
328
412
  if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
329
413
  is_excl = bool(event.state & 0x0001)
330
- canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
414
+ self.canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
331
415
 
332
416
  center_x, center_y = x_abs + w / 2, y_abs + h / 2
333
417
  target_mon = self.monitors[0]
@@ -341,7 +425,7 @@ class ScreenSelector:
341
425
  self.drawn_rect_ids.append(self.current_rect_id)
342
426
  self.redo_stack.clear()
343
427
  else:
344
- canvas.delete(self.current_rect_id)
428
+ self.canvas.delete(self.current_rect_id)
345
429
  self.current_rect_id = self.start_x = self.start_y = None
346
430
 
347
431
  def on_right_click(event):
@@ -366,45 +450,45 @@ class ScreenSelector:
366
450
  # Now, perform the deletion
367
451
  del self.rectangles[i]
368
452
  del self.drawn_rect_ids[i]
369
- canvas.delete(item_id_to_del)
453
+ self.canvas.delete(item_id_to_del)
370
454
  print("Deleted rectangle.")
371
455
 
372
456
  break # Stop after deleting the topmost one
373
457
 
374
- def toggle_image_mode(e=None):
375
- self.image_mode = not self.image_mode
376
- # Only change alpha of the main window, not the text widget
377
- window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
378
- print("Toggled background visibility.")
379
-
380
458
  def on_enter(e=None):
381
- canvas.focus_set()
382
-
383
- canvas.bind('<Enter>', on_enter)
384
- canvas.bind('<ButtonPress-1>', on_click)
385
- canvas.bind('<B1-Motion>', on_drag)
386
- canvas.bind('<ButtonRelease-1>', on_release)
387
- canvas.bind('<Button-3>', on_right_click)
388
- canvas.bind('<Control-s>', self.save_rects)
389
- canvas.bind('<Control-y>', self.redo_last_rect)
390
- canvas.bind('<Control-z>', self.undo_last_rect)
391
- canvas.bind("<Escape>", self.quit_app)
392
- canvas.bind("<m>", toggle_image_mode)
393
- canvas.bind("<i>", self.toggle_instructions)
394
-
395
- canvas.focus_set()
459
+ self.canvas.focus_set()
460
+
461
+ self.canvas.bind('<Enter>', on_enter)
462
+ self.canvas.bind('<ButtonPress-1>', on_click)
463
+ self.canvas.bind('<B1-Motion>', on_drag)
464
+ self.canvas.bind('<ButtonRelease-1>', on_release)
465
+ self.canvas.bind('<Button-3>', on_right_click)
466
+ self.canvas.bind('<Control-s>', self.save_rects)
467
+ self.canvas.bind('<Control-y>', self.redo_last_rect)
468
+ self.canvas.bind('<Control-z>', self.undo_last_rect)
469
+ self.canvas.bind("<Escape>", self.quit_app)
470
+ self.canvas.bind("<m>", self.toggle_image_mode)
471
+ self.canvas.bind("<i>", self.toggle_instructions)
472
+
473
+ self.canvas.focus_set()
474
+ self._create_instructions_widget(self.window)
475
+ self.window.winfo_toplevel().update_idletasks()
476
+ self.print_instructions_box(self.canvas)
396
477
  # The print message is now redundant but kept for console feedback
397
478
  print("Starting UI. See on-screen instructions. Press Esc to quit, Ctrl+S to save.")
479
+ # self.canvas.update_idletasks()
398
480
  self.root.mainloop()
399
481
 
400
482
  def quit_app(self, event=None):
483
+ if self.instructions_widget and self.instructions_widget.winfo_exists():
484
+ self.instructions_widget.destroy()
401
485
  if self.root and self.root.winfo_exists(): self.root.destroy()
402
486
  self.root = None
403
487
 
404
488
 
405
- def run_screen_selector(result_dict, window_name, use_window_as_config):
489
+ def run_screen_selector(result_dict, window_name, use_window_as_config, use_obs_screenshot):
406
490
  try:
407
- selector = ScreenSelector(result_dict, window_name, use_window_as_config)
491
+ selector = ScreenSelector(result_dict, window_name, use_window_as_config, use_obs_screenshot)
408
492
  selector.start()
409
493
  except Exception as e:
410
494
  print(f"Error in selector process: {e}", file=sys.stderr)
@@ -413,15 +497,15 @@ def run_screen_selector(result_dict, window_name, use_window_as_config):
413
497
  result_dict['error'] = str(e)
414
498
 
415
499
 
416
- def get_screen_selection(window_name, use_window_as_config=False):
417
- if not selector_available or not gw: return None
500
+ def get_screen_selection(window_name, use_window_as_config=False, use_obs_screenshot=False):
501
+ if not selector_available: return None
418
502
  if not window_name:
419
503
  print("Error: A target window name must be provided.", file=sys.stderr)
420
504
  return None
421
505
 
422
506
  with Manager() as manager:
423
507
  result_data = manager.dict()
424
- process = Process(target=run_screen_selector, args=(result_data, window_name, use_window_as_config))
508
+ process = Process(target=run_screen_selector, args=(result_data, window_name, use_window_as_config, use_obs_screenshot))
425
509
  print(f"Starting ScreenSelector process...")
426
510
  process.start()
427
511
  process.join()
@@ -439,18 +523,24 @@ def get_screen_selection(window_name, use_window_as_config=False):
439
523
 
440
524
  if __name__ == "__main__":
441
525
  set_dpi_awareness()
442
- target_window_title = "YouTube - JP"
443
- use_window_as_config = False
444
- if len(sys.argv) > 1:
445
- target_window_title = sys.argv[1]
446
- if len(sys.argv) > 2:
447
- use_window_as_config = True
448
- target_window_title = sys.argv[1]
449
526
 
450
- selection_result = get_screen_selection(target_window_title, use_window_as_config)
527
+ parser = argparse.ArgumentParser(description="Screen Selector Arguments")
528
+ parser.add_argument("window_title", nargs="?", default="", help="Target window title")
529
+ parser.add_argument("--obs_ocr", action="store_true", help="Use OBS screenshot")
530
+ parser.add_argument("--use_window_for_config", action="store_true", help="Use window for config")
531
+ args = parser.parse_args()
532
+
533
+ target_window_title = args.window_title
534
+ use_obs_screenshot = args.obs_ocr
535
+ use_window_as_config = args.use_window_for_config
536
+
537
+ print(f"Arguments: Window Title='{target_window_title}', Use OBS Screenshot={use_obs_screenshot}, Use Window for Config={use_window_as_config}")
538
+
539
+ # Example of how to call it
540
+ selection_result = get_screen_selection(target_window_title, use_window_as_config, use_obs_screenshot)
451
541
 
452
542
  if selection_result is None:
453
- print("\n--- Screen selection failed. ---")
543
+ print("--- Screen selection failed. ---")
454
544
  elif not selection_result:
455
545
  print("\n--- Screen selection cancelled. ---")
456
546
  elif 'rectangles' in selection_result: