GameSentenceMiner 2.14.9__py3-none-any.whl → 2.14.10__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.
Files changed (62) hide show
  1. GameSentenceMiner/ai/__init__.py +0 -0
  2. GameSentenceMiner/ai/ai_prompting.py +473 -0
  3. GameSentenceMiner/ocr/__init__.py +0 -0
  4. GameSentenceMiner/ocr/gsm_ocr_config.py +174 -0
  5. GameSentenceMiner/ocr/ocrconfig.py +129 -0
  6. GameSentenceMiner/ocr/owocr_area_selector.py +629 -0
  7. GameSentenceMiner/ocr/owocr_helper.py +638 -0
  8. GameSentenceMiner/ocr/ss_picker.py +140 -0
  9. GameSentenceMiner/owocr/owocr/__init__.py +1 -0
  10. GameSentenceMiner/owocr/owocr/__main__.py +9 -0
  11. GameSentenceMiner/owocr/owocr/config.py +148 -0
  12. GameSentenceMiner/owocr/owocr/lens_betterproto.py +1238 -0
  13. GameSentenceMiner/owocr/owocr/ocr.py +1690 -0
  14. GameSentenceMiner/owocr/owocr/run.py +1818 -0
  15. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +109 -0
  16. GameSentenceMiner/tools/__init__.py +0 -0
  17. GameSentenceMiner/tools/audio_offset_selector.py +215 -0
  18. GameSentenceMiner/tools/ss_selector.py +135 -0
  19. GameSentenceMiner/tools/window_transparency.py +214 -0
  20. GameSentenceMiner/util/__init__.py +0 -0
  21. GameSentenceMiner/util/communication/__init__.py +22 -0
  22. GameSentenceMiner/util/communication/send.py +7 -0
  23. GameSentenceMiner/util/communication/websocket.py +94 -0
  24. GameSentenceMiner/util/configuration.py +1199 -0
  25. GameSentenceMiner/util/db.py +408 -0
  26. GameSentenceMiner/util/downloader/Untitled_json.py +472 -0
  27. GameSentenceMiner/util/downloader/__init__.py +0 -0
  28. GameSentenceMiner/util/downloader/download_tools.py +194 -0
  29. GameSentenceMiner/util/downloader/oneocr_dl.py +250 -0
  30. GameSentenceMiner/util/electron_config.py +259 -0
  31. GameSentenceMiner/util/ffmpeg.py +571 -0
  32. GameSentenceMiner/util/get_overlay_coords.py +366 -0
  33. GameSentenceMiner/util/gsm_utils.py +323 -0
  34. GameSentenceMiner/util/model.py +206 -0
  35. GameSentenceMiner/util/notification.py +157 -0
  36. GameSentenceMiner/util/text_log.py +214 -0
  37. GameSentenceMiner/util/win10toast/__init__.py +154 -0
  38. GameSentenceMiner/util/win10toast/__main__.py +22 -0
  39. GameSentenceMiner/web/__init__.py +0 -0
  40. GameSentenceMiner/web/service.py +132 -0
  41. GameSentenceMiner/web/static/__init__.py +0 -0
  42. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  43. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  44. GameSentenceMiner/web/static/favicon.ico +0 -0
  45. GameSentenceMiner/web/static/favicon.svg +3 -0
  46. GameSentenceMiner/web/static/site.webmanifest +21 -0
  47. GameSentenceMiner/web/static/style.css +292 -0
  48. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  49. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  50. GameSentenceMiner/web/templates/__init__.py +0 -0
  51. GameSentenceMiner/web/templates/index.html +50 -0
  52. GameSentenceMiner/web/templates/text_replacements.html +238 -0
  53. GameSentenceMiner/web/templates/utility.html +483 -0
  54. GameSentenceMiner/web/texthooking_page.py +584 -0
  55. GameSentenceMiner/wip/__init___.py +0 -0
  56. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/METADATA +1 -1
  57. gamesentenceminer-2.14.10.dist-info/RECORD +79 -0
  58. gamesentenceminer-2.14.9.dist-info/RECORD +0 -24
  59. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/WHEEL +0 -0
  60. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/entry_points.txt +0 -0
  61. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/licenses/LICENSE +0 -0
  62. {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,109 @@
1
+ from multiprocessing import Process, Manager
2
+ import mss
3
+ from PIL import Image
4
+
5
+ try:
6
+ from PIL import ImageTk
7
+ import tkinter as tk
8
+ selector_available = True
9
+ except:
10
+ selector_available = False
11
+
12
+
13
+ class ScreenSelector:
14
+ def __init__(self, result):
15
+ self.sct = mss.mss()
16
+ self.monitors = self.sct.monitors[1:]
17
+ self.root = None
18
+ self.result = result
19
+
20
+ def on_select(self, monitor, coordinates):
21
+ self.result['monitor'] = monitor
22
+ self.result['coordinates'] = coordinates
23
+ self.root.destroy()
24
+
25
+ def create_window(self, monitor):
26
+ screenshot = self.sct.grab(monitor)
27
+ img = Image.frombytes('RGB', screenshot.size, screenshot.rgb)
28
+
29
+ if img.width != monitor['width']:
30
+ img = img.resize((monitor['width'], monitor['height']), Image.Resampling.LANCZOS)
31
+
32
+ window = tk.Toplevel(self.root)
33
+ window.geometry(f"{monitor['width']}x{monitor['height']}+{monitor['left']}+{monitor['top']}")
34
+ window.overrideredirect(1)
35
+ window.attributes('-topmost', 1)
36
+
37
+ img_tk = ImageTk.PhotoImage(img)
38
+
39
+ canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
40
+ canvas.pack(fill=tk.BOTH, expand=True)
41
+ canvas.image = img_tk
42
+ canvas.create_image(0, 0, image=img_tk, anchor=tk.NW)
43
+
44
+ start_x, start_y, rect = None, None, None
45
+
46
+ def on_click(event):
47
+ nonlocal start_x, start_y, rect
48
+ start_x, start_y = event.x, event.y
49
+ rect = canvas.create_rectangle(start_x, start_y, start_x, start_y, outline='red')
50
+
51
+ def on_drag(event):
52
+ nonlocal rect, start_x, start_y
53
+ if rect:
54
+ canvas.coords(rect, start_x, start_y, event.x, event.y)
55
+
56
+ def on_release(event):
57
+ nonlocal start_x, start_y
58
+ end_x, end_y = event.x, event.y
59
+
60
+ x1 = min(start_x, end_x)
61
+ y1 = min(start_y, end_y)
62
+ x2 = max(start_x, end_x)
63
+ y2 = max(start_y, end_y)
64
+
65
+ self.on_select(monitor, (x1, y1, x2 - x1, y2 - y1))
66
+
67
+ canvas.bind('<ButtonPress-1>', on_click)
68
+ canvas.bind('<B1-Motion>', on_drag)
69
+ canvas.bind('<ButtonRelease-1>', on_release)
70
+
71
+ def start(self):
72
+ self.root = tk.Tk()
73
+ self.root.withdraw()
74
+
75
+ for monitor in self.monitors:
76
+ self.create_window(monitor)
77
+
78
+ self.root.mainloop()
79
+ self.root.update()
80
+
81
+
82
+ def run_screen_selector(result):
83
+ selector = ScreenSelector(result)
84
+ selector.start()
85
+
86
+
87
+ def get_screen_selection():
88
+ if not selector_available:
89
+ raise ValueError('tkinter or PIL with tkinter support are not installed, unable to open picker')
90
+
91
+ with Manager() as manager:
92
+ res = manager.dict()
93
+ process = Process(target=run_screen_selector, args=(res,))
94
+
95
+ process.start()
96
+ process.join()
97
+
98
+ if 'monitor' in res and 'coordinates' in res:
99
+ return res.copy()
100
+ else:
101
+ return False
102
+
103
+ if __name__ == "__main__":
104
+ selection = get_screen_selection()
105
+ if selection:
106
+ print(f"Selected monitor: {selection['monitor']}")
107
+ print(f"Selected coordinates: {selection['coordinates']}")
108
+ else:
109
+ print("No selection made or process was interrupted.")
File without changes
@@ -0,0 +1,215 @@
1
+
2
+ import os
3
+ import sys
4
+
5
+ sys_stdout = sys.stdout
6
+ sys_stderr = sys.stderr
7
+ sys.stdout = open(os.devnull, 'w')
8
+ sys.stderr = open(os.devnull, 'w')
9
+ import tkinter as tk
10
+ from tkinter import filedialog, messagebox
11
+ import soundfile as sf
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
15
+ import sounddevice as sd
16
+
17
+ from GameSentenceMiner.util import ffmpeg
18
+
19
+
20
+ class AudioOffsetGUI:
21
+
22
+ def __init__(self, master, audio_file_path=None):
23
+ self.master = master
24
+ master.title("Audio Offset Adjuster")
25
+ master.geometry("1000x700")
26
+
27
+ master.tk_setPalette(background='#2E2E2E', foreground='white',
28
+ activeBackground='#4F4F4F', activeForeground='white')
29
+
30
+ self.audio_data = None
31
+ self.samplerate = None
32
+ self.duration = 0.0
33
+
34
+ self.fig, self.ax = plt.subplots(figsize=(10, 4))
35
+ self.canvas = FigureCanvasTkAgg(self.fig, master=master)
36
+ self.canvas_widget = self.canvas.get_tk_widget()
37
+ self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
38
+
39
+ plt.style.use('dark_background')
40
+ self.fig.set_facecolor('#2E2E2E')
41
+ self.ax.set_facecolor('#2E2E2E')
42
+ self.ax.tick_params(axis='x', colors='white')
43
+ self.ax.tick_params(axis='y', colors='white')
44
+ self.ax.spines['bottom'].set_color('white')
45
+ self.ax.spines['left'].set_color('white')
46
+ self.ax.spines['top'].set_color('white')
47
+ self.ax.spines['right'].set_color('white')
48
+ self.ax.set_xlabel("Time (s)", color='white')
49
+ self.ax.set_ylabel("Amplitude", color='white')
50
+
51
+ self.beg_offset_line = None
52
+ # self.end_offset_line is removed as there's no end slider
53
+
54
+ self.create_widgets()
55
+
56
+ self.load_audio(audio_file_path)
57
+
58
+
59
+ def create_widgets(self):
60
+ control_frame = tk.Frame(self.master, bg='#2E2E2E')
61
+ control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
62
+
63
+ self.play_button = tk.Button(control_frame, text="Play/Pause Segment", command=self.play_segment, bg='#4F4F4F', fg='white')
64
+ self.play_button.pack(side=tk.RIGHT, padx=5)
65
+
66
+ self.output_button = tk.Button(control_frame, text="Get Offset", command=self.get_offsets, bg='#4F4F4F', fg='white')
67
+ self.output_button.pack(side=tk.RIGHT, padx=5)
68
+
69
+ self.beg_offset_label = tk.Label(control_frame, text="Beginning Offset: 0.00s", bg='#2E2E2E', fg='white')
70
+ self.beg_offset_label.pack(side=tk.LEFT, padx=10)
71
+
72
+ self.end_offset_label = tk.Label(control_frame, text="End Offset: Full Duration", bg='#2E2E2E', fg='white')
73
+ self.end_offset_label.pack(side=tk.LEFT, padx=10)
74
+
75
+ slider_frame = tk.Frame(self.master, bg='#2E2E2E')
76
+ slider_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5, padx=10)
77
+
78
+ beg_slider_label = tk.Label(slider_frame, text="Start Trim:", bg='#2E2E2E', fg='white')
79
+ beg_slider_label.pack(side=tk.LEFT)
80
+ self.beg_slider = tk.Scale(slider_frame, from_=0, to=100, orient=tk.HORIZONTAL, resolution=0.5,
81
+ command=self.on_slider_change, bg='#2E2E2E', fg='white', troughcolor='#4F4F4F',
82
+ highlightbackground='#2E2E2E', length=300)
83
+ self.beg_slider.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5)
84
+
85
+ # Removed end_slider and its associated label
86
+
87
+ def load_audio(self, file_path):
88
+ if file_path:
89
+ try:
90
+ self.audio_data, self.samplerate = sf.read(file_path)
91
+ if self.audio_data.ndim > 1:
92
+ self.audio_data = self.audio_data[:, 0]
93
+ self.duration = len(self.audio_data) / self.samplerate
94
+ self.plot_waveform()
95
+ self.beg_slider.config(to=self.duration)
96
+ self.beg_slider.set(0) # Reset start slider to 0
97
+ except Exception as e:
98
+ messagebox.showerror("Error", f"Failed to load audio file: {e}")
99
+ self.audio_data = None
100
+ self.samplerate = None
101
+ self.duration = 0.0
102
+
103
+ def plot_waveform(self):
104
+ self.ax.clear()
105
+ if self.audio_data is not None:
106
+ time = np.linspace(0, self.duration, len(self.audio_data))
107
+ self.ax.plot(time, self.audio_data, color='#1E90FF')
108
+ self.ax.set_xlim(0, self.duration)
109
+ self.ax.set_ylim(np.min(self.audio_data), np.max(self.audio_data))
110
+ self.ax.set_title("Audio", color='white')
111
+
112
+ if self.beg_offset_line:
113
+ self.beg_offset_line.remove()
114
+ # self.end_offset_line.remove() is removed
115
+
116
+ self.beg_offset_line = self.ax.axvline(self.beg_slider.get(), color='red', linestyle='--', linewidth=2)
117
+ # The end line is now always at the duration
118
+ self.ax.axvline(self.duration, color='green', linestyle='--', linewidth=2)
119
+
120
+ self.update_offset_labels()
121
+ else:
122
+ self.ax.text(0.5, 0.5, "No audio loaded",
123
+ horizontalalignment='center', verticalalignment='center',
124
+ transform=self.ax.transAxes, color='white', fontsize=16)
125
+
126
+ self.fig.canvas.draw_idle()
127
+
128
+ def on_slider_change(self, val):
129
+ if self.audio_data is None:
130
+ return
131
+
132
+ beg_val = float(self.beg_slider.get())
133
+
134
+ if self.beg_offset_line:
135
+ self.beg_offset_line.set_xdata([beg_val])
136
+
137
+ self.update_offset_labels()
138
+ self.fig.canvas.draw_idle()
139
+
140
+ def play_segment(self):
141
+ if self.audio_data is None:
142
+ messagebox.showinfo("Play Audio", "No audio file loaded yet.")
143
+ return
144
+
145
+ if hasattr(self, 'is_playing') and self.is_playing:
146
+ sd.stop()
147
+ self.is_playing = False
148
+ return
149
+
150
+ beg_offset = self.beg_slider.get()
151
+ end_offset = self.duration # End offset is now always full duration
152
+
153
+ if beg_offset >= end_offset:
154
+ messagebox.showwarning("Play Audio", "Start offset must be less than end offset.")
155
+ return
156
+
157
+ start_frame = int(beg_offset * self.samplerate)
158
+ end_frame = int(end_offset * self.samplerate)
159
+
160
+ if start_frame >= len(self.audio_data) or end_frame <= 0:
161
+ messagebox.showwarning("Play Audio", "Selected segment is out of audio range.")
162
+ return
163
+
164
+ segment_to_play = self.audio_data[start_frame:end_frame]
165
+
166
+ try:
167
+ self.is_playing = True
168
+ sd.play(segment_to_play, self.samplerate)
169
+ except Exception as e:
170
+ self.is_playing = False
171
+ messagebox.showerror("Audio Playback Error", f"Failed to play audio: {e}")
172
+
173
+ def update_offset_labels(self):
174
+ if self.beg_offset_line: # We no longer have an end_offset_line object
175
+ beg_val = self.beg_offset_line.get_xdata()[0] - 5.0 # Adjusting for the 5 seconds offset
176
+ self.beg_offset_label.config(text=f"Beginning Offset: {beg_val:.2f}s")
177
+
178
+ def get_offsets(self):
179
+ if self.audio_data is None:
180
+ messagebox.showinfo("Offsets", "No audio file loaded yet.")
181
+ return
182
+
183
+ beg_offset = self.beg_slider.get() - 5.0
184
+ end_offset = self.duration # End offset is always full duration
185
+ sys.stdout.close()
186
+ sys.stderr.close()
187
+ sys.stdout = sys_stdout
188
+ sys.stderr = sys_stderr
189
+ print(f"{beg_offset:.2f}")
190
+ exit(0)
191
+
192
+ def run_audio_offset_gui(path=None, beginning_offset=0, end_offset=None):
193
+ temp_file_path = os.path.join(os.path.dirname(path), "temp_audio.opus")
194
+
195
+ if os.path.exists(temp_file_path):
196
+ os.remove(temp_file_path)
197
+
198
+ ffmpeg.trim_audio(path, beginning_offset - 5, end_offset, temp_file_path, True, 0, 0)
199
+
200
+ root = tk.Tk()
201
+ root.protocol("WM_DELETE_WINDOW", lambda: exit(1)) # Exit when the window is closed
202
+ app = AudioOffsetGUI(root, audio_file_path=temp_file_path)
203
+ root.mainloop()
204
+
205
+
206
+ if __name__ == "__main__":
207
+ import argparse
208
+
209
+ parser = argparse.ArgumentParser(description="Run Audio Offset GUI")
210
+ parser.add_argument("--path", type=str, required=True, help="Path to the audio file")
211
+ parser.add_argument("--beginning_offset", type=float, default=0, help="Beginning offset in seconds")
212
+ parser.add_argument("--end_offset", type=float, default=None, help="End offset in seconds")
213
+
214
+ args = parser.parse_args()
215
+ run_audio_offset_gui(path=args.path, beginning_offset=args.beginning_offset, end_offset=args.end_offset)
@@ -0,0 +1,135 @@
1
+
2
+
3
+ import os
4
+ import sys
5
+ import subprocess
6
+
7
+ # Suppress stdout and stderr during imports
8
+ sys_stdout = sys.stdout
9
+ sys_stderr = sys.stderr
10
+ sys.stdout = open(os.devnull, 'w')
11
+ sys.stderr = open(os.devnull, 'w')
12
+
13
+ import tkinter as tk
14
+ from PIL import Image, ImageTk
15
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
16
+ from GameSentenceMiner.util.configuration import get_temporary_directory, logger
17
+ from GameSentenceMiner.util.ffmpeg import ffmpeg_base_command_list
18
+ from GameSentenceMiner.util import ffmpeg
19
+
20
+ def extract_frames(video_path, timestamp, temp_dir, mode):
21
+ frame_paths = []
22
+ timestamp_number = float(timestamp)
23
+ golden_frame_index = 1 # Default to the first frame
24
+ golden_frame = None
25
+ video_duration = ffmpeg.get_video_duration(video_path)
26
+
27
+ if mode == 'middle':
28
+ timestamp_number = max(0.0, timestamp_number - 2.5)
29
+ elif mode == 'end':
30
+ timestamp_number = max(0.0, timestamp_number - 5.0)
31
+
32
+ if video_duration is not None and timestamp_number > video_duration:
33
+ logger.debug(f"Timestamp {timestamp_number} exceeds video duration {video_duration}.")
34
+ return None
35
+
36
+ try:
37
+ command = ffmpeg_base_command_list + [
38
+ "-y",
39
+ "-ss", str(timestamp_number),
40
+ "-i", video_path,
41
+ "-vf", f"fps=1/{0.25}",
42
+ "-vframes", "20",
43
+ os.path.join(temp_dir, "frame_%02d.png")
44
+ ]
45
+ subprocess.run(command, check=True, capture_output=True)
46
+ for i in range(1, 21):
47
+ if os.path.exists(os.path.join(temp_dir, f"frame_{i:02d}.png")):
48
+ frame_paths.append(os.path.join(temp_dir, f"frame_{i:02d}.png"))
49
+
50
+ if mode == "beginning":
51
+ golden_frame = frame_paths[0]
52
+ if mode == "middle":
53
+ golden_frame = frame_paths[len(frame_paths) // 2]
54
+ if mode == "end":
55
+ golden_frame = frame_paths[-1]
56
+ except subprocess.CalledProcessError as e:
57
+ logger.debug(f"Error extracting frames: {e}")
58
+ logger.debug(f"Command was: {' '.join(command)}")
59
+ logger.debug(f"FFmpeg output:\n{e.stderr.decode()}")
60
+ return None
61
+ except Exception as e:
62
+ logger.debug(f"An error occurred: {e}")
63
+ return None
64
+ return frame_paths, golden_frame
65
+
66
+ def timestamp_to_seconds(timestamp):
67
+ hours, minutes, seconds = map(int, timestamp.split(':'))
68
+ return hours * 3600 + minutes * 60 + seconds
69
+
70
+ def display_images(image_paths, golden_frame):
71
+ window = tk.Tk()
72
+ window.configure(bg="black") # Set the background color to black
73
+ window.title("Image Selector")
74
+ selected_path = tk.StringVar()
75
+ image_widgets = []
76
+
77
+ def on_image_click(event):
78
+ widget = event.widget
79
+ index = image_widgets.index(widget)
80
+ selected_path.set(image_paths[index])
81
+ window.quit()
82
+
83
+ for i, path in enumerate(image_paths):
84
+ img = Image.open(path)
85
+ img.thumbnail((img.width / 8, img.height / 8))
86
+ img_tk = ImageTk.PhotoImage(img)
87
+ if golden_frame and path == golden_frame:
88
+ label = tk.Label(window, image=img_tk, borderwidth=5, relief="solid")
89
+ label.config(highlightbackground="yellow", highlightthickness=5)
90
+ else:
91
+ label = tk.Label(window, image=img_tk)
92
+ label.image = img_tk
93
+ label.grid(row=i // 5, column=i % 5, padx=5, pady=5)
94
+ label.bind("<Button-1>", on_image_click) # Bind click event to the label
95
+ image_widgets.append(label)
96
+
97
+ window.attributes("-topmost", True)
98
+ window.mainloop()
99
+ return selected_path.get()
100
+
101
+ def run_extraction_and_display(video_path, timestamp_str, mode):
102
+ temp_dir = os.path.join(get_temporary_directory(False), "screenshot_frames", sanitize_filename(os.path.splitext(os.path.basename(video_path))[0]))
103
+ os.makedirs(temp_dir, exist_ok=True)
104
+ image_paths, golden_frame = extract_frames(video_path, timestamp_str, temp_dir, mode)
105
+ if image_paths:
106
+ selected_image_path = display_images(image_paths, golden_frame)
107
+ if selected_image_path:
108
+ sys.stdout.close()
109
+ sys.stderr.close()
110
+ sys.stdout = sys_stdout
111
+ sys.stderr = sys_stderr
112
+ print(selected_image_path)
113
+ else:
114
+ logger.debug("No image was selected.")
115
+ else:
116
+ logger.debug("Frame extraction failed.")
117
+
118
+ def main():
119
+
120
+
121
+ # if len(sys.argv) != 3:
122
+ # print("Usage: python script.py <video_path> <timestamp>")
123
+ # sys.exit(1)
124
+ try:
125
+ video_path = sys.argv[1]
126
+ timestamp_str = sys.argv[2]
127
+ mode = sys.argv[3] if len(sys.argv) > 3 else "beginning"
128
+ run_extraction_and_display(video_path, timestamp_str, mode)
129
+ except Exception as e:
130
+ logger.debug(e)
131
+ sys.exit(1)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ main()
@@ -0,0 +1,214 @@
1
+ import sys
2
+ import win32gui
3
+ import win32con
4
+ import win32api
5
+ import keyboard
6
+ import time
7
+ import threading
8
+ import signal
9
+
10
+ from GameSentenceMiner.util.configuration import logger
11
+
12
+ # --- Configuration (equivalent to AHK top-level variables) ---
13
+ TRANSPARENT_LEVEL = 1 # Almost invisible (0-255 scale)
14
+ OPAQUE_LEVEL = 255 # Fully opaque
15
+ HOTKEY = 'ctrl+alt+y'
16
+
17
+ # --- Global State Variables (equivalent to AHK global variables) ---
18
+ is_toggled = False
19
+ target_hwnd = None
20
+ # A lock to prevent race conditions when accessing global state from different threads
21
+ state_lock = threading.Lock()
22
+
23
+ # --- Core Functions (equivalent to AHK functions) ---
24
+
25
+ def set_window_transparency(hwnd, transparency):
26
+ """
27
+ Sets the transparency of a window.
28
+ This is the Python equivalent of WinSetTransparent.
29
+ """
30
+ if not hwnd or not win32gui.IsWindow(hwnd):
31
+ return
32
+ try:
33
+ # Get the current window style
34
+ style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
35
+ # Add the WS_EX_LAYERED style, which is required for transparency
36
+ win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, style | win32con.WS_EX_LAYERED)
37
+ # Set the transparency
38
+ win32gui.SetLayeredWindowAttributes(hwnd, 0, transparency, win32con.LWA_ALPHA)
39
+ except Exception as e:
40
+ # Some windows (like system or elevated ones) might deny permission
41
+ # logger.info(f"Error setting transparency for HWND {hwnd}: {e}")
42
+ pass
43
+
44
+ def set_always_on_top(hwnd, is_on_top):
45
+ """
46
+ Sets or removes the "Always on Top" status for a window.
47
+ This is the Python equivalent of WinSetAlwaysOnTop.
48
+ """
49
+ if not hwnd or not win32gui.IsWindow(hwnd):
50
+ return
51
+ try:
52
+ rect = win32gui.GetWindowRect(hwnd)
53
+ position = win32con.HWND_TOPMOST if is_on_top else win32con.HWND_NOTOPMOST
54
+ # Set the window position without moving or resizing it
55
+ win32gui.SetWindowPos(hwnd, position, rect[0], rect[1], 0, 0,
56
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
57
+ except Exception as e:
58
+ # logger.info(f"Error setting always-on-top for HWND {hwnd}: {e}")
59
+ pass
60
+
61
+ def reset_window_state(hwnd):
62
+ """A helper to reset a window to its default state."""
63
+ set_window_transparency(hwnd, OPAQUE_LEVEL)
64
+ set_always_on_top(hwnd, False)
65
+
66
+ # --- Hotkey Callback (equivalent to AHK ^!y::) ---
67
+
68
+ def toggle_functionality(window_hwnd=None):
69
+ """
70
+ This function is called when the hotkey is pressed.
71
+ It manages the toggling logic.
72
+ """
73
+ global is_toggled, target_hwnd
74
+
75
+ if window_hwnd:
76
+ current_hwnd = window_hwnd
77
+ else:
78
+ # Get the currently focused window (equivalent to WinGetID("A"))
79
+ current_hwnd = win32gui.GetForegroundWindow()
80
+ if not current_hwnd:
81
+ logger.info("No window is currently active!")
82
+ return
83
+
84
+ with state_lock:
85
+ # Case 1: The hotkey is pressed on the currently toggled window to disable it.
86
+ if is_toggled and target_hwnd == current_hwnd:
87
+ logger.info(f"Disabling functionality for window: {win32gui.GetWindowText(current_hwnd)}")
88
+ reset_window_state(current_hwnd)
89
+ is_toggled = False
90
+ target_hwnd = None
91
+ # Case 2: Enable functionality for a new window, or switch to a new one.
92
+ else:
93
+ # If another window was already toggled, reset it first.
94
+ if is_toggled and target_hwnd is not None:
95
+ logger.info(f"Resetting old window: {win32gui.GetWindowText(target_hwnd)}")
96
+ reset_window_state(target_hwnd)
97
+
98
+ # Enable functionality for the new window.
99
+ logger.info(f"Enabling functionality for window: {win32gui.GetWindowText(current_hwnd)}")
100
+ is_toggled = True
101
+ target_hwnd = current_hwnd
102
+ set_always_on_top(target_hwnd, True)
103
+ # The mouse_monitor_loop will handle setting the initial transparency
104
+
105
+ # --- Mouse Monitoring (equivalent to AHK Loop) ---
106
+
107
+ def mouse_monitor_loop():
108
+ """
109
+ A loop that runs in a separate thread to monitor the mouse position.
110
+ """
111
+ global is_toggled, target_hwnd
112
+
113
+ while True:
114
+ # We check the state without a lock first for performance,
115
+ # then use the lock when we need to read the shared variable.
116
+ if is_toggled:
117
+ with state_lock:
118
+ # Make a local copy of the target handle to work with
119
+ monitored_hwnd = target_hwnd
120
+
121
+ if monitored_hwnd:
122
+ # Get mouse position and the window handle under the cursor
123
+ pos = win32gui.GetCursorPos()
124
+ hwnd_under_mouse = win32gui.WindowFromPoint(pos)
125
+
126
+ # WindowFromPoint can return a child window (like a button).
127
+ # We need to walk up the parent chain to see if it belongs to our target window.
128
+ is_mouse_over_target = False
129
+ current_hwnd = hwnd_under_mouse
130
+ while current_hwnd != 0:
131
+ if current_hwnd == monitored_hwnd:
132
+ is_mouse_over_target = True
133
+ break
134
+ current_hwnd = win32gui.GetParent(current_hwnd)
135
+
136
+ # Apply transparency based on mouse position
137
+ if is_mouse_over_target:
138
+ set_window_transparency(monitored_hwnd, OPAQUE_LEVEL)
139
+ else:
140
+ set_window_transparency(monitored_hwnd, TRANSPARENT_LEVEL)
141
+
142
+ # A small delay to reduce CPU usage
143
+ time.sleep(0.1)
144
+
145
+ class HandleSTDINThread(threading.Thread):
146
+ def run(self):
147
+ while True:
148
+ try:
149
+ line = input()
150
+ if "exit" in line.strip().lower():
151
+ handle_quit()
152
+ break
153
+ except EOFError:
154
+ break
155
+
156
+ def handle_quit():
157
+ if is_toggled and target_hwnd:
158
+ reset_window_state(target_hwnd)
159
+ logger.info("Exiting Window Transparency Tool.")
160
+
161
+ # --- Main Execution Block ---
162
+
163
+ if __name__ == "__main__":
164
+ import argparse
165
+ # Start the mouse monitor in a separate, non-blocking thread.
166
+ # daemon=True ensures the thread will exit when the main script does.
167
+ monitor_thread = threading.Thread(target=mouse_monitor_loop, daemon=True)
168
+ monitor_thread.start()
169
+
170
+ # get hotkey from args
171
+ parser = argparse.ArgumentParser(description="Window Transparency Toggle Script")
172
+ parser.add_argument('--hotkey', type=str, default=HOTKEY, help='Hotkey to toggle transparency (default: ctrl+alt+y)')
173
+ parser.add_argument('--window', type=str, help='Window title to target (optional)')
174
+
175
+ args = parser.parse_args()
176
+ hotkey = args.hotkey.lower()
177
+ target_window_title = args.window
178
+
179
+ if target_window_title:
180
+ # Find the window by title if specified
181
+ target_hwnd = win32gui.FindWindow(None, target_window_title)
182
+ logger.info(f"Searching for window with title: {target_window_title}")
183
+ logger.info(f"Target HWND: {target_hwnd}")
184
+ if not target_hwnd:
185
+ logger.error(f"Window with title '{target_window_title}' not found.")
186
+ sys.exit(1)
187
+ else:
188
+ logger.info(f"Target window found: {target_window_title}")
189
+ toggle_functionality(target_hwnd) # Enable functionality for the specified window
190
+
191
+ # Register the global hotkey
192
+ keyboard.add_hotkey(hotkey, toggle_functionality)
193
+
194
+ # Handle SigINT/SigTERM gracefully
195
+ def signal_handler(sig, frame):
196
+ handle_quit()
197
+ sys.exit(0)
198
+
199
+ signal.signal(signal.SIGINT, signal_handler)
200
+ signal.signal(signal.SIGTERM, signal_handler)
201
+
202
+ logger.info(f"Script running. Press '{hotkey}' on a window to toggle transparency.")
203
+ logger.info("Press Ctrl+C in this console to exit.")
204
+
205
+ HandleSTDINThread().start()
206
+
207
+ # Keep the script running to listen for the hotkey.
208
+ # keyboard.wait() is a blocking call that waits indefinitely.
209
+ try:
210
+ keyboard.wait()
211
+ except KeyboardInterrupt:
212
+ if is_toggled and target_hwnd:
213
+ reset_window_state(target_hwnd)
214
+ logger.info("\nScript terminated by user.")
File without changes