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.
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/gsm.py +7 -0
- gamesentenceminer-2.10.11/GameSentenceMiner/ocr/owocr_area_selector.py +457 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/owocr_helper.py +2 -2
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/configuration.py +1 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/service.py +10 -21
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/texthooking_page.py +4 -4
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/PKG-INFO +1 -1
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/pyproject.toml +1 -1
- gamesentenceminer-2.10.9/GameSentenceMiner/ocr/owocr_area_selector.py +0 -905
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ai/ai_prompting.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/anki.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon128.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon256.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon32.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon512.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/icon64.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/assets/pickaxe.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/config_gui.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/gametext.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/obs.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/ss_picker.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/run.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/send.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/communication/websocket.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/electron_config.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/ffmpeg.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/gsm_utils.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/model.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/notification.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/package.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/ss_selector.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/util/text_log.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/vad.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/__init__.py +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/index.html +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/templates/utility.html +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/requires.txt +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/LICENSE +0 -0
- {gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/README.md +0 -0
- {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}")
|
{gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/ocr/owocr_helper.py
RENAMED
@@ -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,
|
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
|
@@ -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,
|
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
|
-
|
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
|
-
|
79
|
-
|
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(
|
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(
|
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
|
-
|
121
|
-
|
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
|
-
|
114
|
+
logger.error("VLC not found. Make sure it's installed and in your PATH.")
|
125
115
|
except Exception as e:
|
126
|
-
|
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):
|
{gamesentenceminer-2.10.9 → gamesentenceminer-2.10.11}/GameSentenceMiner/web/texthooking_page.py
RENAMED
@@ -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
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
7
7
|
|
8
8
|
[project]
|
9
9
|
name = "GameSentenceMiner"
|
10
|
-
version = "2.10.
|
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"
|