GameSentenceMiner 2.8.36__py3-none-any.whl → 2.8.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/communication/send.py +3 -3
- GameSentenceMiner/config_gui.py +59 -8
- GameSentenceMiner/configuration.py +3 -4
- GameSentenceMiner/gametext.py +13 -9
- GameSentenceMiner/gsm.py +55 -28
- GameSentenceMiner/model.py +18 -0
- GameSentenceMiner/obs.py +234 -119
- GameSentenceMiner/ocr/gsm_ocr_config.py +9 -1
- GameSentenceMiner/ocr/owocr_area_selector.py +3 -2
- GameSentenceMiner/ocr/owocr_helper.py +28 -10
- GameSentenceMiner/owocr/owocr/run.py +28 -4
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/RECORD +17 -17
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.8.36.dist-info → gamesentenceminer-2.8.37.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,6 @@ import json
|
|
2
2
|
|
3
3
|
from GameSentenceMiner.communication.websocket import websocket, Message
|
4
4
|
|
5
|
-
def send_restart_signal():
|
6
|
-
if websocket
|
7
|
-
websocket.send(json.dumps(Message(function="restart").to_json()))
|
5
|
+
async def send_restart_signal():
|
6
|
+
if websocket:
|
7
|
+
await websocket.send(json.dumps(Message(function="restart").to_json()))
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
import tkinter as tk
|
2
3
|
from tkinter import filedialog, messagebox, simpledialog, scrolledtext
|
3
4
|
|
@@ -7,6 +8,7 @@ from GameSentenceMiner import obs, configuration
|
|
7
8
|
from GameSentenceMiner.communication.send import send_restart_signal
|
8
9
|
from GameSentenceMiner.configuration import *
|
9
10
|
from GameSentenceMiner.downloader.download_tools import download_ocenaudio_if_needed
|
11
|
+
from GameSentenceMiner.model import SceneItem, SceneInfo
|
10
12
|
from GameSentenceMiner.package import get_current_version, get_latest_version
|
11
13
|
|
12
14
|
settings_saved = False
|
@@ -79,10 +81,27 @@ class ConfigApp:
|
|
79
81
|
self.create_advanced_tab()
|
80
82
|
self.create_ai_tab()
|
81
83
|
|
84
|
+
self.notebook.bind("<<NotebookTabChanged>>", self.on_profiles_tab_selected)
|
85
|
+
|
82
86
|
ttk.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
|
83
87
|
|
84
88
|
self.window.withdraw()
|
85
89
|
|
90
|
+
def show_scene_selection(self, matched_configs):
|
91
|
+
selected_scene = None
|
92
|
+
if matched_configs:
|
93
|
+
selection_window = tk.Toplevel(self.window)
|
94
|
+
selection_window.title("Select Profile")
|
95
|
+
ttk.Label(selection_window, text="Multiple profiles match the current scene. Please select the profile:").pack(pady=10)
|
96
|
+
profile_var = tk.StringVar(value=matched_configs[0])
|
97
|
+
profile_dropdown = ttk.Combobox(selection_window, textvariable=profile_var, values=matched_configs, state="readonly")
|
98
|
+
profile_dropdown.pack(pady=5)
|
99
|
+
ttk.Button(selection_window, text="OK", command=lambda: [selection_window.destroy(), setattr(self, 'selected_scene', profile_var.get())]).pack(pady=10)
|
100
|
+
self.window.wait_window(selection_window)
|
101
|
+
selected_scene = self.selected_scene
|
102
|
+
return selected_scene
|
103
|
+
|
104
|
+
|
86
105
|
def add_save_hook(self, func):
|
87
106
|
on_save.append(func)
|
88
107
|
|
@@ -104,6 +123,7 @@ class ConfigApp:
|
|
104
123
|
|
105
124
|
# Create a new Config instance
|
106
125
|
config = ProfileConfig(
|
126
|
+
scenes=[self.obs_scene_listbox.get(i) for i in self.obs_scene_listbox.curselection()],
|
107
127
|
general=General(
|
108
128
|
use_websocket=self.websocket_enabled.get(),
|
109
129
|
use_clipboard=self.clipboard_enabled.get(),
|
@@ -234,12 +254,15 @@ class ConfigApp:
|
|
234
254
|
|
235
255
|
current_profile = self.profile_combobox.get()
|
236
256
|
prev_config = self.master_config.get_config()
|
257
|
+
self.master_config.switch_to_default_if_not_found=self.switch_to_default_if_not_found.get()
|
237
258
|
if profile_change:
|
238
259
|
self.master_config.current_profile = current_profile
|
239
260
|
else:
|
240
261
|
self.master_config.current_profile = current_profile
|
241
262
|
self.master_config.set_config_for_profile(current_profile, config)
|
242
263
|
|
264
|
+
|
265
|
+
|
243
266
|
self.master_config = self.master_config.sync_shared_fields()
|
244
267
|
|
245
268
|
# Serialize the config instance to JSON
|
@@ -250,7 +273,7 @@ class ConfigApp:
|
|
250
273
|
|
251
274
|
if self.master_config.get_config().restart_required(prev_config):
|
252
275
|
logger.info("Restart Required for some settings to take affect!")
|
253
|
-
send_restart_signal()
|
276
|
+
asyncio.run(send_restart_signal())
|
254
277
|
|
255
278
|
settings_saved = True
|
256
279
|
configuration.reload_config()
|
@@ -684,6 +707,13 @@ class ConfigApp:
|
|
684
707
|
# ve.grid_configure(row=ve.grid_info()['row'] - 1)
|
685
708
|
# db.grid_configure(row=db.grid_info()['row'] - 1)
|
686
709
|
|
710
|
+
def on_profiles_tab_selected(self, event):
|
711
|
+
try:
|
712
|
+
if self.window.state() != "withdrawn" and self.notebook.tab(self.notebook.select(), "text") == "Profiles":
|
713
|
+
self.refresh_obs_scenes()
|
714
|
+
except Exception as e:
|
715
|
+
logger.debug(e)
|
716
|
+
|
687
717
|
@new_tab
|
688
718
|
def create_features_tab(self):
|
689
719
|
features_frame = ttk.Frame(self.notebook)
|
@@ -967,16 +997,11 @@ class ConfigApp:
|
|
967
997
|
self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
|
968
998
|
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
|
969
999
|
|
970
|
-
ttk.Label(hotkeys_frame, text="Open Utility Hotkey:").grid(row=self.current_row, column=0, sticky='W')
|
971
|
-
self.open_utility_hotkey = ttk.Entry(hotkeys_frame)
|
972
|
-
self.open_utility_hotkey.insert(0, self.settings.hotkeys.open_utility)
|
973
|
-
self.open_utility_hotkey.grid(row=self.current_row, column=1)
|
974
|
-
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to open the text utility.", row=self.current_row, column=2)
|
975
|
-
|
976
1000
|
|
977
1001
|
@new_tab
|
978
1002
|
def create_profiles_tab(self):
|
979
1003
|
profiles_frame = ttk.Frame(self.notebook)
|
1004
|
+
|
980
1005
|
self.notebook.add(profiles_frame, text='Profiles')
|
981
1006
|
|
982
1007
|
ttk.Label(profiles_frame, text="Select Profile:").grid(row=self.current_row, column=0, sticky='W')
|
@@ -990,6 +1015,32 @@ class ConfigApp:
|
|
990
1015
|
ttk.Button(profiles_frame, text="Copy Profile", command=self.copy_profile).grid(row=self.current_row, column=1, pady=5)
|
991
1016
|
if self.master_config.current_profile != DEFAULT_CONFIG:
|
992
1017
|
ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile).grid(row=self.current_row, column=2, pady=5)
|
1018
|
+
self.current_row += 1
|
1019
|
+
|
1020
|
+
ttk.Label(profiles_frame, text="OBS Scene:").grid(row=self.current_row, column=0, sticky='W')
|
1021
|
+
self.obs_scene_var = tk.StringVar(value="")
|
1022
|
+
self.obs_scene_listbox = tk.Listbox(profiles_frame, listvariable=self.obs_scene_var, selectmode=tk.MULTIPLE, height=10)
|
1023
|
+
self.obs_scene_listbox.grid(row=self.current_row, column=1)
|
1024
|
+
ttk.Button(profiles_frame, text="Refresh Scenes", command=self.refresh_obs_scenes).grid(row=self.current_row, column=2, pady=5)
|
1025
|
+
self.add_label_and_increment_row(profiles_frame, "Select an OBS scene to associate with this profile. (Optional)", row=self.current_row, column=3)
|
1026
|
+
|
1027
|
+
ttk.Label(profiles_frame, text="Switch To Default If Not Found:").grid(row=self.current_row, column=0, sticky='W')
|
1028
|
+
self.switch_to_default_if_not_found = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
|
1029
|
+
ttk.Checkbutton(profiles_frame, variable=self.switch_to_default_if_not_found).grid(row=self.current_row, column=1, sticky='W')
|
1030
|
+
self.add_label_and_increment_row(profiles_frame, "Enable to switch to the default profile if the selected OBS scene is not found.", row=self.current_row, column=2)
|
1031
|
+
|
1032
|
+
|
1033
|
+
def refresh_obs_scenes(self):
|
1034
|
+
scenes = obs.get_obs_scenes()
|
1035
|
+
obs_scene_names = [scene['sceneName'] for scene in scenes]
|
1036
|
+
self.obs_scene_listbox.delete(0, tk.END) # Clear existing items
|
1037
|
+
for scene_name in obs_scene_names:
|
1038
|
+
self.obs_scene_listbox.insert(tk.END, scene_name) # Add each scene to the Listbox
|
1039
|
+
for i, scene in enumerate(eval(self.obs_scene_var.get())): # Parse the string as a tuple
|
1040
|
+
if scene.strip() in self.settings.scenes: # Use strip() to remove extra spaces
|
1041
|
+
self.obs_scene_listbox.select_set(i) # Select the item in the Listbox
|
1042
|
+
self.obs_scene_listbox.activate(i)
|
1043
|
+
self.obs_scene_listbox.update_idletasks() # Ensure the GUI reflects the changes
|
993
1044
|
|
994
1045
|
@new_tab
|
995
1046
|
def create_advanced_tab(self):
|
@@ -1013,7 +1064,6 @@ class ConfigApp:
|
|
1013
1064
|
ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.video_player_path)).grid(row=self.current_row, column=2)
|
1014
1065
|
self.add_label_and_increment_row(advanced_frame, "Path to the video player executable. Will seek to the location of the line in the replay", row=self.current_row, column=3)
|
1015
1066
|
|
1016
|
-
|
1017
1067
|
ttk.Label(advanced_frame, text="Play Latest Video/Audio Hotkey:").grid(row=self.current_row, column=0, sticky='W')
|
1018
1068
|
self.play_latest_audio_hotkey = ttk.Entry(advanced_frame)
|
1019
1069
|
self.play_latest_audio_hotkey.insert(0, self.settings.hotkeys.play_latest_audio)
|
@@ -1132,6 +1182,7 @@ class ConfigApp:
|
|
1132
1182
|
print("profile Changed!")
|
1133
1183
|
self.save_settings(profile_change=True)
|
1134
1184
|
self.reload_settings()
|
1185
|
+
self.refresh_obs_scenes()
|
1135
1186
|
|
1136
1187
|
def add_profile(self):
|
1137
1188
|
new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
|
@@ -222,6 +222,7 @@ class Ai:
|
|
222
222
|
@dataclass
|
223
223
|
class ProfileConfig:
|
224
224
|
name: str = 'Default'
|
225
|
+
scenes: List[str] = field(default_factory=list)
|
225
226
|
general: General = field(default_factory=General)
|
226
227
|
paths: Paths = field(default_factory=Paths)
|
227
228
|
anki: Anki = field(default_factory=Anki)
|
@@ -304,10 +305,7 @@ class ProfileConfig:
|
|
304
305
|
|
305
306
|
def restart_required(self, previous):
|
306
307
|
previous: ProfileConfig
|
307
|
-
if any([previous.
|
308
|
-
previous.general.use_clipboard != self.general.use_clipboard,
|
309
|
-
previous.general.websocket_uri != self.general.websocket_uri,
|
310
|
-
previous.paths.folder_to_watch != self.paths.folder_to_watch,
|
308
|
+
if any([previous.paths.folder_to_watch != self.paths.folder_to_watch,
|
311
309
|
previous.obs.open_obs != self.obs.open_obs,
|
312
310
|
previous.obs.host != self.obs.host,
|
313
311
|
previous.obs.port != self.obs.port
|
@@ -325,6 +323,7 @@ class ProfileConfig:
|
|
325
323
|
class Config:
|
326
324
|
configs: Dict[str, ProfileConfig] = field(default_factory=dict)
|
327
325
|
current_profile: str = DEFAULT_CONFIG
|
326
|
+
switch_to_default_if_not_found: bool = True
|
328
327
|
|
329
328
|
@classmethod
|
330
329
|
def new(cls):
|
GameSentenceMiner/gametext.py
CHANGED
@@ -28,6 +28,9 @@ async def monitor_clipboard():
|
|
28
28
|
|
29
29
|
skip_next_clipboard = False
|
30
30
|
while True:
|
31
|
+
if not get_config().general.use_clipboard:
|
32
|
+
await asyncio.sleep(1)
|
33
|
+
continue
|
31
34
|
if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
|
32
35
|
await asyncio.sleep(1)
|
33
36
|
skip_next_clipboard = True
|
@@ -44,8 +47,11 @@ async def monitor_clipboard():
|
|
44
47
|
async def listen_websocket():
|
45
48
|
global current_line, current_line_time, reconnecting, websocket_connected
|
46
49
|
try_other = False
|
47
|
-
websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
|
48
50
|
while True:
|
51
|
+
if not get_config().general.use_websocket:
|
52
|
+
await asyncio.sleep(1)
|
53
|
+
continue
|
54
|
+
websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
|
49
55
|
if try_other:
|
50
56
|
websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
|
51
57
|
try:
|
@@ -114,13 +120,11 @@ def run_websocket_listener():
|
|
114
120
|
|
115
121
|
|
116
122
|
async def start_text_monitor():
|
123
|
+
util.run_new_thread(run_websocket_listener)
|
117
124
|
if get_config().general.use_websocket:
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
if
|
122
|
-
|
123
|
-
else:
|
124
|
-
logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
|
125
|
-
await monitor_clipboard()
|
125
|
+
if get_config().general.use_both_clipboard_and_websocket:
|
126
|
+
logger.info("Listening for Text on both WebSocket and Clipboard.")
|
127
|
+
else:
|
128
|
+
logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
|
129
|
+
await monitor_clipboard()
|
126
130
|
await asyncio.sleep(1)
|
GameSentenceMiner/gsm.py
CHANGED
@@ -67,6 +67,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
67
67
|
|
68
68
|
@staticmethod
|
69
69
|
def convert_to_audio(video_path):
|
70
|
+
vad_trimmed_audio = ''
|
70
71
|
try:
|
71
72
|
if texthooking_page.event_manager.line_for_audio:
|
72
73
|
line: GameLine = texthooking_page.event_manager.line_for_audio
|
@@ -174,9 +175,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
174
175
|
logger.debug(f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
|
175
176
|
notification.send_error_no_anki_update()
|
176
177
|
finally:
|
177
|
-
if get_config().paths.remove_video and os.path.exists(video_path):
|
178
|
+
if video_path and get_config().paths.remove_video and os.path.exists(video_path):
|
178
179
|
os.remove(video_path) # Optionally remove the video after conversion
|
179
|
-
if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
180
|
+
if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
180
181
|
os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
|
181
182
|
|
182
183
|
|
@@ -289,8 +290,6 @@ def register_hotkeys():
|
|
289
290
|
keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
|
290
291
|
if get_config().hotkeys.play_latest_audio:
|
291
292
|
keyboard.add_hotkey(get_config().hotkeys.play_latest_audio, play_most_recent_audio)
|
292
|
-
if get_config().hotkeys.open_utility:
|
293
|
-
keyboard.add_hotkey(get_config().hotkeys.open_utility, texthooking_page.open_texthooker)
|
294
293
|
|
295
294
|
|
296
295
|
def get_screenshot():
|
@@ -386,7 +385,7 @@ def open_multimine(icon, item):
|
|
386
385
|
texthooking_page.open_texthooker()
|
387
386
|
|
388
387
|
|
389
|
-
def update_icon():
|
388
|
+
def update_icon(profile=None):
|
390
389
|
global menu, icon
|
391
390
|
# Recreate the menu with the updated button text
|
392
391
|
profile_menu = Menu(
|
@@ -483,7 +482,6 @@ def restart_obs():
|
|
483
482
|
close_obs()
|
484
483
|
time.sleep(1)
|
485
484
|
obs.start_obs()
|
486
|
-
obs.connect_to_obs()
|
487
485
|
|
488
486
|
|
489
487
|
def cleanup():
|
@@ -558,25 +556,6 @@ def initialize_async():
|
|
558
556
|
threads.append(util.run_new_thread(task))
|
559
557
|
return threads
|
560
558
|
|
561
|
-
|
562
|
-
def post_init():
|
563
|
-
def do_post_init():
|
564
|
-
global silero_trim, whisper_helper, vosk_helper
|
565
|
-
logger.info("Post-Initialization started.")
|
566
|
-
if get_config().obs.enabled:
|
567
|
-
obs.connect_to_obs()
|
568
|
-
check_obs_folder_is_correct()
|
569
|
-
from GameSentenceMiner.vad import vosk_helper
|
570
|
-
from GameSentenceMiner.vad import whisper_helper
|
571
|
-
if get_config().vad.is_vosk():
|
572
|
-
vosk_helper.get_vosk_model()
|
573
|
-
if get_config().vad.is_whisper():
|
574
|
-
whisper_helper.initialize_whisper_model()
|
575
|
-
if get_config().vad.is_silero():
|
576
|
-
from GameSentenceMiner.vad import silero_trim
|
577
|
-
|
578
|
-
util.run_new_thread(do_post_init)
|
579
|
-
|
580
559
|
def handle_websocket_message(message: Message):
|
581
560
|
match FunctionName(message.function):
|
582
561
|
case FunctionName.QUIT:
|
@@ -586,19 +565,67 @@ def handle_websocket_message(message: Message):
|
|
586
565
|
close_obs()
|
587
566
|
case FunctionName.START_OBS:
|
588
567
|
obs.start_obs()
|
589
|
-
obs.connect_to_obs()
|
590
568
|
case _:
|
591
569
|
logger.debug(f"unknown message from electron websocket: {message.to_json()}")
|
592
570
|
|
593
571
|
def post_init2():
|
594
572
|
asyncio.run(gametext.start_text_monitor())
|
595
573
|
|
574
|
+
|
575
|
+
def async_loop():
|
576
|
+
async def loop():
|
577
|
+
await obs.connect_to_obs()
|
578
|
+
if get_config().obs.enabled:
|
579
|
+
await register_scene_switcher_callback()
|
580
|
+
await check_obs_folder_is_correct()
|
581
|
+
logger.info("Post-Initialization started.")
|
582
|
+
if get_config().vad.is_vosk():
|
583
|
+
from GameSentenceMiner.vad import vosk_helper
|
584
|
+
vosk_helper.get_vosk_model()
|
585
|
+
if get_config().vad.is_whisper():
|
586
|
+
from GameSentenceMiner.vad import whisper_helper
|
587
|
+
whisper_helper.initialize_whisper_model()
|
588
|
+
if get_config().vad.is_silero():
|
589
|
+
from GameSentenceMiner.vad import silero_trim
|
590
|
+
|
591
|
+
asyncio.run(loop())
|
592
|
+
|
593
|
+
|
594
|
+
async def register_scene_switcher_callback():
|
595
|
+
def scene_switcher_callback(scene):
|
596
|
+
logger.info(f"Scene changed to: {scene}")
|
597
|
+
all_configured_scenes = [config.scenes for config in get_master_config().configs.values()]
|
598
|
+
print(all_configured_scenes)
|
599
|
+
matching_configs = [name.strip() for name, config in config_instance.configs.items() if scene.strip() in config.scenes]
|
600
|
+
switch_to = None
|
601
|
+
|
602
|
+
if len(matching_configs) > 1:
|
603
|
+
selected_scene = settings_window.show_scene_selection(matched_configs=matching_configs)
|
604
|
+
if selected_scene:
|
605
|
+
switch_to = selected_scene
|
606
|
+
else:
|
607
|
+
return
|
608
|
+
elif matching_configs:
|
609
|
+
switch_to = matching_configs[0]
|
610
|
+
elif get_master_config().switch_to_default_if_not_found:
|
611
|
+
switch_to = configuration.DEFAULT_CONFIG
|
612
|
+
|
613
|
+
if switch_to and switch_to != get_master_config().current_profile:
|
614
|
+
logger.info(f"Switching to profile: {switch_to}")
|
615
|
+
get_master_config().current_profile = switch_to
|
616
|
+
switch_profile_and_save(switch_to)
|
617
|
+
settings_window.reload_settings()
|
618
|
+
update_icon()
|
619
|
+
|
620
|
+
logger.info("Registering scene switcher callback")
|
621
|
+
await obs.register_scene_change_callback(scene_switcher_callback)
|
622
|
+
|
596
623
|
async def main(reloading=False):
|
597
624
|
global root, settings_window
|
625
|
+
initialize(reloading)
|
598
626
|
logger.info("Script started.")
|
599
627
|
root = ttk.Window(themename='darkly')
|
600
628
|
settings_window = config_gui.ConfigApp(root)
|
601
|
-
initialize(reloading)
|
602
629
|
initialize_async()
|
603
630
|
observer = Observer()
|
604
631
|
observer.schedule(VideoToAudioHandler(), get_config().paths.folder_to_watch, recursive=False)
|
@@ -608,6 +635,7 @@ async def main(reloading=False):
|
|
608
635
|
|
609
636
|
util.run_new_thread(post_init2)
|
610
637
|
util.run_new_thread(run_text_hooker_page)
|
638
|
+
util.run_new_thread(async_loop)
|
611
639
|
|
612
640
|
# Register signal handlers for graceful shutdown
|
613
641
|
signal.signal(signal.SIGTERM, handle_exit()) # Handle `kill` commands
|
@@ -619,7 +647,6 @@ async def main(reloading=False):
|
|
619
647
|
try:
|
620
648
|
# if get_config().general.open_config_on_startup:
|
621
649
|
# root.after(0, settings_window.show)
|
622
|
-
root.after(50, post_init)
|
623
650
|
settings_window.add_save_hook(update_icon)
|
624
651
|
settings_window.on_exit = exit_program
|
625
652
|
root.mainloop()
|
GameSentenceMiner/model.py
CHANGED
@@ -83,6 +83,24 @@ class SceneItemsResponse:
|
|
83
83
|
class RecordDirectory:
|
84
84
|
recordDirectory: str
|
85
85
|
|
86
|
+
|
87
|
+
@dataclass_json
|
88
|
+
@dataclass
|
89
|
+
class SceneItemInfo:
|
90
|
+
sceneIndex: int
|
91
|
+
sceneName: str
|
92
|
+
sceneUuid: str
|
93
|
+
|
94
|
+
|
95
|
+
@dataclass_json
|
96
|
+
@dataclass
|
97
|
+
class SceneListResponse:
|
98
|
+
scenes: List[SceneItemInfo]
|
99
|
+
currentProgramSceneName: Optional[str] = None
|
100
|
+
currentProgramSceneUuid: Optional[str] = None
|
101
|
+
currentPreviewSceneName: Optional[str] = None
|
102
|
+
currentPreviewSceneUuid: Optional[str] = None
|
103
|
+
|
86
104
|
#
|
87
105
|
# @dataclass_json
|
88
106
|
# @dataclass
|
GameSentenceMiner/obs.py
CHANGED
@@ -1,21 +1,39 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
1
3
|
import os.path
|
2
4
|
import subprocess
|
5
|
+
import threading
|
3
6
|
import time
|
4
7
|
import psutil
|
5
8
|
|
6
|
-
|
9
|
+
import obsws_python as obs
|
7
10
|
|
8
11
|
from GameSentenceMiner import util, configuration
|
9
12
|
from GameSentenceMiner.configuration import *
|
10
13
|
from GameSentenceMiner.model import *
|
11
14
|
|
12
|
-
client:
|
15
|
+
client: obs.ReqClient = None
|
16
|
+
event_client: obs.EventClient = None
|
13
17
|
obs_process_pid = None
|
14
|
-
logging.getLogger('obswebsocket').setLevel(logging.CRITICAL)
|
15
18
|
OBS_PID_FILE = os.path.join(configuration.get_app_directory(), 'obs-studio', 'obs_pid.txt')
|
19
|
+
obs_connection_manager = None
|
20
|
+
logging.getLogger("obsws_python").setLevel(logging.CRITICAL)
|
16
21
|
|
17
|
-
|
22
|
+
class OBSConnectionManager(threading.Thread):
|
23
|
+
def __init__(self):
|
24
|
+
super().__init__()
|
25
|
+
self.daemon = True
|
26
|
+
self.running = True
|
18
27
|
|
28
|
+
def run(self):
|
29
|
+
while self.running:
|
30
|
+
time.sleep(5)
|
31
|
+
if not client.get_version():
|
32
|
+
logger.info("OBS WebSocket not connected. Attempting to reconnect...")
|
33
|
+
connect_to_obs()
|
34
|
+
|
35
|
+
def stop(self):
|
36
|
+
self.running = False
|
19
37
|
|
20
38
|
def get_obs_path():
|
21
39
|
return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
|
@@ -37,7 +55,6 @@ def start_obs():
|
|
37
55
|
obs_process_pid = int(f.read().strip())
|
38
56
|
if is_process_running(obs_process_pid):
|
39
57
|
print(f"OBS is already running with PID: {obs_process_pid}")
|
40
|
-
connect_to_obs()
|
41
58
|
return obs_process_pid
|
42
59
|
except ValueError:
|
43
60
|
print("Invalid PID found in file. Launching new OBS instance.")
|
@@ -59,36 +76,49 @@ def start_obs():
|
|
59
76
|
print(f"Error launching OBS: {e}")
|
60
77
|
return None
|
61
78
|
|
62
|
-
def
|
63
|
-
|
64
|
-
if
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
79
|
+
async def wait_for_obs_connected():
|
80
|
+
global client
|
81
|
+
if not client:
|
82
|
+
return False
|
83
|
+
for _ in range(10):
|
84
|
+
try:
|
85
|
+
response = client.get_version()
|
86
|
+
if response:
|
87
|
+
return True
|
88
|
+
except Exception as e:
|
89
|
+
logger.debug(f"Waiting for OBS connection: {e}")
|
90
|
+
await asyncio.sleep(1)
|
91
|
+
return False
|
92
|
+
|
93
|
+
async def check_obs_folder_is_correct():
|
94
|
+
if await wait_for_obs_connected():
|
95
|
+
obs_record_directory = get_record_directory()
|
96
|
+
if obs_record_directory and os.path.normpath(obs_record_directory) != os.path.normpath(
|
97
|
+
get_config().paths.folder_to_watch):
|
98
|
+
logger.info("OBS Path Setting wrong, OBS Recording folder in GSM Config")
|
99
|
+
get_config().paths.folder_to_watch = os.path.normpath(obs_record_directory)
|
100
|
+
get_master_config().sync_shared_fields()
|
101
|
+
save_full_config(get_master_config())
|
102
|
+
else:
|
103
|
+
logger.info("OBS Recording path looks correct")
|
70
104
|
|
71
105
|
|
72
106
|
def get_obs_websocket_config_values():
|
73
107
|
config_path = os.path.join(get_app_directory(), 'obs-studio', 'config', 'obs-studio', 'plugin_config', 'obs-websocket', 'config.json')
|
74
108
|
|
75
|
-
# Check if config file exists
|
76
109
|
if not os.path.isfile(config_path):
|
77
110
|
raise FileNotFoundError(f"OBS WebSocket config not found at {config_path}")
|
78
111
|
|
79
|
-
# Read the JSON configuration
|
80
112
|
with open(config_path, 'r') as file:
|
81
113
|
config = json.load(file)
|
82
114
|
|
83
|
-
# Extract values
|
84
115
|
server_enabled = config.get("server_enabled", False)
|
85
|
-
server_port = config.get("server_port", 7274)
|
116
|
+
server_port = config.get("server_port", 7274)
|
86
117
|
server_password = config.get("server_password", None)
|
87
118
|
|
88
119
|
if not server_enabled:
|
89
120
|
logger.info("OBS WebSocket server is not enabled. Enabling it now... Restart OBS for changes to take effect.")
|
90
121
|
config["server_enabled"] = True
|
91
|
-
|
92
122
|
with open(config_path, 'w') as file:
|
93
123
|
json.dump(config, file, indent=4)
|
94
124
|
|
@@ -101,49 +131,69 @@ def get_obs_websocket_config_values():
|
|
101
131
|
full_config.save()
|
102
132
|
reload_config()
|
103
133
|
|
134
|
+
async def connect_to_obs(retry_count=0):
|
135
|
+
global client, obs_connection_manager, event_client
|
136
|
+
if not get_config().obs.enabled or client:
|
137
|
+
return
|
104
138
|
|
105
|
-
|
106
|
-
|
107
|
-
def on_connect(obs):
|
108
|
-
global connected
|
109
|
-
logger.info("Reconnected to OBS WebSocket.")
|
110
|
-
start_replay_buffer()
|
111
|
-
connected = True
|
112
|
-
|
113
|
-
|
114
|
-
def on_disconnect(obs):
|
115
|
-
global connected
|
116
|
-
logger.error("OBS Connection Lost!")
|
117
|
-
connected = False
|
118
|
-
|
139
|
+
if util.is_windows():
|
140
|
+
get_obs_websocket_config_values()
|
119
141
|
|
120
|
-
|
121
|
-
|
142
|
+
while True:
|
143
|
+
try:
|
144
|
+
client = obs.ReqClient(
|
145
|
+
host=get_config().obs.host,
|
146
|
+
port=get_config().obs.port,
|
147
|
+
password=get_config().obs.password,
|
148
|
+
timeout=1,
|
149
|
+
)
|
150
|
+
event_client = obs.EventClient(
|
151
|
+
host=get_config().obs.host,
|
152
|
+
port=get_config().obs.port,
|
153
|
+
password=get_config().obs.password,
|
154
|
+
timeout=1,
|
155
|
+
)
|
156
|
+
if not obs_connection_manager:
|
157
|
+
obs_connection_manager = OBSConnectionManager()
|
158
|
+
obs_connection_manager.start()
|
159
|
+
update_current_game()
|
160
|
+
break # Exit the loop once connected
|
161
|
+
except Exception as e:
|
162
|
+
await asyncio.sleep(1)
|
163
|
+
retry_count += 1
|
164
|
+
|
165
|
+
def connect_to_obs_sync(retry_count=0):
|
166
|
+
global client, obs_connection_manager, event_client
|
122
167
|
if not get_config().obs.enabled or client:
|
123
168
|
return
|
124
169
|
|
125
170
|
if util.is_windows():
|
126
171
|
get_obs_websocket_config_values()
|
127
172
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
173
|
+
while True:
|
174
|
+
try:
|
175
|
+
client = obs.ReqClient(
|
176
|
+
host=get_config().obs.host,
|
177
|
+
port=get_config().obs.port,
|
178
|
+
password=get_config().obs.password,
|
179
|
+
timeout=1,
|
180
|
+
)
|
181
|
+
event_client = obs.EventClient(
|
182
|
+
host=get_config().obs.host,
|
183
|
+
port=get_config().obs.port,
|
184
|
+
password=get_config().obs.password,
|
185
|
+
timeout=1,
|
186
|
+
)
|
187
|
+
if not obs_connection_manager:
|
188
|
+
obs_connection_manager = OBSConnectionManager()
|
189
|
+
obs_connection_manager.start()
|
190
|
+
update_current_game()
|
191
|
+
break # Exit the loop once connected
|
192
|
+
except Exception as e:
|
193
|
+
time.sleep(1)
|
194
|
+
retry_count += 1
|
144
195
|
|
145
196
|
|
146
|
-
# Disconnect from OBS WebSocket
|
147
197
|
def disconnect_from_obs():
|
148
198
|
global client
|
149
199
|
if client:
|
@@ -151,136 +201,160 @@ def disconnect_from_obs():
|
|
151
201
|
client = None
|
152
202
|
logger.info("Disconnected from OBS WebSocket.")
|
153
203
|
|
154
|
-
def do_obs_call(request, from_dict
|
155
|
-
|
156
|
-
|
157
|
-
time.sleep(1)
|
158
|
-
return do_obs_call(request, from_dict, retry - 1)
|
159
|
-
logger.debug("Sending obs call: " + str(request))
|
160
|
-
response = client.call(request)
|
161
|
-
if not response.status and retry > 0:
|
162
|
-
time.sleep(1)
|
163
|
-
return do_obs_call(request, from_dict, retry - 1)
|
164
|
-
if from_dict:
|
165
|
-
return from_dict(response.datain)
|
166
|
-
else:
|
167
|
-
return response.datain
|
168
|
-
except Exception as e:
|
169
|
-
if "socket is already closed" in str(e) or "object has no attribute" in str(e):
|
170
|
-
if retry > 0:
|
171
|
-
time.sleep(1)
|
172
|
-
return do_obs_call(request, from_dict, retry - 1)
|
173
|
-
else:
|
174
|
-
raise e
|
204
|
+
def do_obs_call(request, *args, from_dict=None, retry=3):
|
205
|
+
connect_to_obs()
|
206
|
+
if not client:
|
175
207
|
return None
|
208
|
+
for _ in range(retry + 1):
|
209
|
+
try:
|
210
|
+
response = request(*args)
|
211
|
+
if response and response.ok:
|
212
|
+
return from_dict(response.datain) if from_dict else response.datain
|
213
|
+
time.sleep(0.3)
|
214
|
+
except Exception as e:
|
215
|
+
logger.error(f"Error calling OBS: {e}")
|
216
|
+
if "socket is already closed" in str(e) or "object has no attribute" in str(e):
|
217
|
+
time.sleep(0.3)
|
218
|
+
else:
|
219
|
+
return None
|
220
|
+
return None
|
176
221
|
|
177
222
|
def toggle_replay_buffer():
|
178
223
|
try:
|
179
|
-
|
180
|
-
|
224
|
+
response = client.toggle_replay_buffer()
|
225
|
+
if response:
|
226
|
+
logger.info("Replay buffer Toggled.")
|
181
227
|
except Exception as e:
|
182
228
|
logger.error(f"Error toggling buffer: {e}")
|
183
229
|
|
184
|
-
|
185
|
-
# Start replay buffer
|
186
|
-
def start_replay_buffer(retry=5):
|
230
|
+
def start_replay_buffer():
|
187
231
|
try:
|
188
|
-
|
189
|
-
|
232
|
+
status = get_replay_buffer_status()
|
233
|
+
if status:
|
234
|
+
client.start_replay_buffer()
|
190
235
|
except Exception as e:
|
191
|
-
|
192
|
-
if retry > 0:
|
193
|
-
time.sleep(1)
|
194
|
-
start_replay_buffer(retry - 1)
|
195
|
-
else:
|
196
|
-
logger.error(f"Error starting replay buffer: {e}")
|
236
|
+
logger.error(f"Error starting replay buffer: {e}")
|
197
237
|
|
198
238
|
def get_replay_buffer_status():
|
199
239
|
try:
|
200
|
-
return
|
240
|
+
return client.get_replay_buffer_status().output_active
|
201
241
|
except Exception as e:
|
202
|
-
logger.
|
203
|
-
|
242
|
+
logger.warning(f"Error getting replay buffer status: {e}")
|
243
|
+
return None
|
204
244
|
|
205
|
-
# Stop replay buffer
|
206
245
|
def stop_replay_buffer():
|
207
246
|
try:
|
208
|
-
client.
|
209
|
-
logger.error("Replay buffer stopped.")
|
247
|
+
client.stop_replay_buffer()
|
210
248
|
except Exception as e:
|
211
|
-
logger.
|
249
|
+
logger.warning(f"Error stopping replay buffer: {e}")
|
212
250
|
|
213
|
-
# Save the current replay buffer
|
214
251
|
def save_replay_buffer():
|
215
|
-
|
216
|
-
if
|
217
|
-
client.
|
218
|
-
|
252
|
+
status = get_replay_buffer_status()
|
253
|
+
if status:
|
254
|
+
response = client.save_replay_buffer()
|
255
|
+
if response and response.ok:
|
256
|
+
logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
|
219
257
|
else:
|
220
|
-
logger.
|
221
|
-
|
258
|
+
logger.warning("Replay Buffer is not active, could not save Replay Buffer!")
|
222
259
|
|
223
260
|
def get_current_scene():
|
224
261
|
try:
|
225
|
-
|
262
|
+
response = client.get_current_program_scene()
|
263
|
+
return response.scene_name if response else ''
|
226
264
|
except Exception as e:
|
227
265
|
logger.debug(f"Couldn't get scene: {e}")
|
228
|
-
|
229
|
-
|
266
|
+
return ''
|
230
267
|
|
231
268
|
def get_source_from_scene(scene_name):
|
232
269
|
try:
|
233
|
-
|
270
|
+
response = client.get_scene_item_list(name=scene_name)
|
271
|
+
return response.scene_items[0] if response and response.scene_items else ''
|
234
272
|
except Exception as e:
|
235
273
|
logger.error(f"Error getting source from scene: {e}")
|
236
274
|
return ''
|
237
275
|
|
238
276
|
def get_record_directory():
|
239
277
|
try:
|
240
|
-
|
278
|
+
response = client.get_record_directory()
|
279
|
+
return response.record_directory if response else ''
|
241
280
|
except Exception as e:
|
242
281
|
logger.error(f"Error getting recording folder: {e}")
|
243
282
|
return ''
|
244
283
|
|
284
|
+
def get_obs_scenes():
|
285
|
+
try:
|
286
|
+
response = client.get_scene_list()
|
287
|
+
return response.scenes if response else None
|
288
|
+
except Exception as e:
|
289
|
+
logger.error(f"Error getting scenes: {e}")
|
290
|
+
return None
|
291
|
+
|
292
|
+
async def register_scene_change_callback(callback):
|
293
|
+
global client
|
294
|
+
if await wait_for_obs_connected():
|
295
|
+
if not client:
|
296
|
+
logger.error("OBS client is not connected.")
|
297
|
+
return
|
298
|
+
|
299
|
+
def on_current_program_scene_changed(data):
|
300
|
+
scene_name = data.scene_name
|
301
|
+
if scene_name:
|
302
|
+
callback(scene_name)
|
303
|
+
|
304
|
+
event_client.callback.register(on_current_program_scene_changed)
|
305
|
+
|
306
|
+
logger.info("Scene change callback registered.")
|
245
307
|
|
246
308
|
def get_screenshot(compression=-1):
|
247
309
|
try:
|
248
310
|
screenshot = util.make_unique_file_name(os.path.abspath(
|
249
311
|
configuration.get_temporary_directory()) + '/screenshot.png')
|
250
312
|
update_current_game()
|
251
|
-
|
252
|
-
|
313
|
+
if not configuration.current_game:
|
314
|
+
logger.error("No active game scene found.")
|
315
|
+
return None
|
316
|
+
current_source = get_source_from_scene(configuration.current_game)
|
317
|
+
current_source_name = current_source.get('sourceName') if isinstance(current_source, dict) else None
|
253
318
|
if not current_source_name:
|
254
|
-
logger.error("No active
|
255
|
-
return
|
319
|
+
logger.error("No active source found in the current scene.")
|
320
|
+
return None
|
256
321
|
start = time.time()
|
257
322
|
logger.debug(f"Current source name: {current_source_name}")
|
258
|
-
response = client.
|
323
|
+
response = client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
|
259
324
|
logger.debug(f"Screenshot response: {response}")
|
260
325
|
logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
|
261
326
|
return screenshot
|
262
327
|
except Exception as e:
|
263
328
|
logger.error(f"Error getting screenshot: {e}")
|
329
|
+
return None
|
264
330
|
|
265
331
|
def get_screenshot_base64():
|
266
332
|
try:
|
267
|
-
update_current_game()
|
268
|
-
|
269
|
-
|
333
|
+
# update_current_game()
|
334
|
+
current_game = get_current_game()
|
335
|
+
if not current_game:
|
336
|
+
logger.error("No active game scene found.")
|
337
|
+
return None
|
338
|
+
current_source = get_source_from_scene(current_game)
|
339
|
+
current_source_name = current_source.get('sourceName') if isinstance(current_source, dict) else None
|
270
340
|
if not current_source_name:
|
271
|
-
logger.error("No active
|
272
|
-
return
|
273
|
-
response =
|
274
|
-
|
275
|
-
|
276
|
-
|
341
|
+
logger.error("No active source found in the current scene.")
|
342
|
+
return None
|
343
|
+
response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=0, width=None, height=None)
|
344
|
+
if response and response.image_data:
|
345
|
+
with open('screenshot_response.txt', 'wb') as f:
|
346
|
+
f.write(str(response).encode())
|
347
|
+
return response.image_data
|
348
|
+
else:
|
349
|
+
logger.error(f"Error getting base64 screenshot: {response}")
|
350
|
+
return None
|
277
351
|
except Exception as e:
|
278
352
|
logger.error(f"Error getting screenshot: {e}")
|
353
|
+
return None
|
279
354
|
|
280
355
|
def update_current_game():
|
281
356
|
configuration.current_game = get_current_scene()
|
282
357
|
|
283
|
-
|
284
358
|
def get_current_game(sanitize=False):
|
285
359
|
if not configuration.current_game:
|
286
360
|
update_current_game()
|
@@ -288,3 +362,44 @@ def get_current_game(sanitize=False):
|
|
288
362
|
if sanitize:
|
289
363
|
return util.sanitize_filename(configuration.current_game)
|
290
364
|
return configuration.current_game
|
365
|
+
|
366
|
+
|
367
|
+
def main():
|
368
|
+
start_obs()
|
369
|
+
connect_to_obs()
|
370
|
+
# Test each method
|
371
|
+
print("Testing `get_obs_path`:", get_obs_path())
|
372
|
+
print("Testing `is_process_running` with PID 1:", is_process_running(1))
|
373
|
+
print("Testing `check_obs_folder_is_correct`:")
|
374
|
+
check_obs_folder_is_correct()
|
375
|
+
print("Testing `get_obs_websocket_config_values`:")
|
376
|
+
try:
|
377
|
+
get_obs_websocket_config_values()
|
378
|
+
except FileNotFoundError as e:
|
379
|
+
print(e)
|
380
|
+
print("Testing `toggle_replay_buffer`:")
|
381
|
+
toggle_replay_buffer()
|
382
|
+
print("Testing `start_replay_buffer`:")
|
383
|
+
start_replay_buffer()
|
384
|
+
print("Testing `get_replay_buffer_status`:", get_replay_buffer_status())
|
385
|
+
print("Testing `stop_replay_buffer`:")
|
386
|
+
stop_replay_buffer()
|
387
|
+
print("Testing `save_replay_buffer`:")
|
388
|
+
save_replay_buffer()
|
389
|
+
current_scene = get_current_scene()
|
390
|
+
print("Testing `get_current_scene`:", current_scene)
|
391
|
+
print("Testing `get_source_from_scene` with dummy scene:", get_source_from_scene(current_scene))
|
392
|
+
print("Testing `get_record_directory`:", get_record_directory())
|
393
|
+
print("Testing `get_obs_scenes`:", get_obs_scenes())
|
394
|
+
print("Testing `get_screenshot`:", get_screenshot())
|
395
|
+
print("Testing `get_screenshot_base64`:")
|
396
|
+
get_screenshot_base64()
|
397
|
+
print("Testing `update_current_game`:")
|
398
|
+
update_current_game()
|
399
|
+
print("Testing `get_current_game`:", get_current_game())
|
400
|
+
disconnect_from_obs()
|
401
|
+
|
402
|
+
if __name__ == '__main__':
|
403
|
+
logging.basicConfig(level=logging.INFO)
|
404
|
+
main()
|
405
|
+
|
@@ -50,7 +50,7 @@ class OCRConfig:
|
|
50
50
|
if self.coordinate_system and self.coordinate_system == "percentage" and self.window:
|
51
51
|
import pygetwindow as gw
|
52
52
|
try:
|
53
|
-
|
53
|
+
set_dpi_awareness()
|
54
54
|
window = gw.getWindowsWithTitle(self.window)[0]
|
55
55
|
self.window_geometry = WindowGeometry(
|
56
56
|
left=window.left,
|
@@ -69,3 +69,11 @@ class OCRConfig:
|
|
69
69
|
ceil(rectangle.coordinates[3] * self.window_geometry.height),
|
70
70
|
]
|
71
71
|
|
72
|
+
# try w10+, fall back to w8.1+
|
73
|
+
def set_dpi_awareness():
|
74
|
+
try:
|
75
|
+
awareness_context = -4
|
76
|
+
ctypes.windll.user32.SetProcessDpiAwarenessContext(awareness_context)
|
77
|
+
except AttributeError:
|
78
|
+
per_monitor_awareness = 2
|
79
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
|
@@ -8,6 +8,7 @@ import mss
|
|
8
8
|
from PIL import Image, ImageTk, ImageDraw
|
9
9
|
|
10
10
|
from GameSentenceMiner import obs # Import your actual obs module
|
11
|
+
from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness
|
11
12
|
from GameSentenceMiner.util import sanitize_filename # Import your actual util module
|
12
13
|
|
13
14
|
try:
|
@@ -42,7 +43,7 @@ class ScreenSelector:
|
|
42
43
|
if not gw:
|
43
44
|
raise ImportError("pygetwindow is required but not installed.")
|
44
45
|
|
45
|
-
obs.
|
46
|
+
obs.connect_to_obs_sync() # Connect to OBS (using mock or real)
|
46
47
|
self.window_name = window_name
|
47
48
|
print(f"Target window name: {window_name or 'None (Absolute Mode)'}")
|
48
49
|
self.sct = mss.mss()
|
@@ -857,7 +858,7 @@ def get_screen_selection(window_name):
|
|
857
858
|
if __name__ == "__main__":
|
858
859
|
target_window_title = None # Default to absolute coordinates
|
859
860
|
# Check for command line arguments to specify window title
|
860
|
-
|
861
|
+
set_dpi_awareness()
|
861
862
|
if len(sys.argv) > 1:
|
862
863
|
target_window_title = sys.argv[1]
|
863
864
|
print(f"Attempting to target window title from args: '{target_window_title}'")
|
@@ -19,7 +19,7 @@ from rapidfuzz import fuzz
|
|
19
19
|
from GameSentenceMiner import obs, util
|
20
20
|
from GameSentenceMiner.configuration import get_config, get_app_directory, get_temporary_directory
|
21
21
|
from GameSentenceMiner.electron_config import get_ocr_scan_rate, get_requires_open_window
|
22
|
-
from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, Rectangle
|
22
|
+
from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, Rectangle, set_dpi_awareness
|
23
23
|
from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
|
24
24
|
from GameSentenceMiner.owocr.owocr.run import TextFiltering
|
25
25
|
from GameSentenceMiner.util import do_text_replacements, OCR_REPLACEMENTS_FILE
|
@@ -65,7 +65,7 @@ def get_new_game_cords():
|
|
65
65
|
app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
|
66
66
|
ocr_config_dir = app_dir / "ocr_config"
|
67
67
|
ocr_config_dir.mkdir(parents=True, exist_ok=True)
|
68
|
-
obs.
|
68
|
+
obs.connect_to_obs_sync()
|
69
69
|
scene = util.sanitize_filename(obs.get_current_scene())
|
70
70
|
config_path = ocr_config_dir / f"{scene}.json"
|
71
71
|
with open(config_path, 'w') as f:
|
@@ -79,7 +79,7 @@ def get_ocr_config() -> OCRConfig:
|
|
79
79
|
app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
|
80
80
|
ocr_config_dir = app_dir / "ocr_config"
|
81
81
|
os.makedirs(ocr_config_dir, exist_ok=True)
|
82
|
-
obs.
|
82
|
+
obs.connect_to_obs_sync()
|
83
83
|
scene = util.sanitize_filename(obs.get_current_scene())
|
84
84
|
config_path = ocr_config_dir / f"{scene}.json"
|
85
85
|
if not config_path.exists():
|
@@ -215,16 +215,25 @@ def do_second_ocr(ocr1_text, rectangle_index, time, img):
|
|
215
215
|
if fuzz.ratio(previous_ocr2_text, text) >= 80:
|
216
216
|
logger.info("Seems like the same text from previous ocr2 result, not sending")
|
217
217
|
return
|
218
|
-
img
|
218
|
+
save_result_image(img)
|
219
219
|
last_ocr2_results[rectangle_index] = text
|
220
220
|
send_result(text, time)
|
221
|
-
img.close()
|
222
221
|
except json.JSONDecodeError:
|
223
222
|
print("Invalid JSON received.")
|
224
223
|
except Exception as e:
|
225
224
|
logger.exception(e)
|
226
225
|
print(f"Error processing message: {e}")
|
227
226
|
|
227
|
+
|
228
|
+
def save_result_image(img):
|
229
|
+
if isinstance(img, bytes):
|
230
|
+
with open(os.path.join(get_temporary_directory(), "last_successful_ocr.png"), "wb") as f:
|
231
|
+
f.write(img)
|
232
|
+
else:
|
233
|
+
img.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
|
234
|
+
img.close()
|
235
|
+
|
236
|
+
|
228
237
|
def send_result(text, time):
|
229
238
|
if text:
|
230
239
|
text = do_text_replacements(text, OCR_REPLACEMENTS_FILE)
|
@@ -241,10 +250,14 @@ previous_imgs = {}
|
|
241
250
|
orig_text_results = {} # Store original text results for each rectangle
|
242
251
|
TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
|
243
252
|
|
244
|
-
def text_callback(text, orig_text, rectangle_index, time, img=None):
|
253
|
+
def text_callback(text, orig_text, rectangle_index, time, img=None, came_from_ss=False):
|
245
254
|
global twopassocr, ocr2, last_oneocr_results_to_check, last_oneocr_times, text_stable_start_times, orig_text_results
|
246
255
|
orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
|
247
256
|
# logger.debug(orig_text_string)
|
257
|
+
if came_from_ss:
|
258
|
+
save_result_image(img)
|
259
|
+
send_result(text, time)
|
260
|
+
return
|
248
261
|
|
249
262
|
current_time = time if time else datetime.now()
|
250
263
|
|
@@ -258,10 +271,11 @@ def text_callback(text, orig_text, rectangle_index, time, img=None):
|
|
258
271
|
if previous_orig_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 80:
|
259
272
|
logger.info("Seems like Text we already sent, not doing anything.")
|
260
273
|
return
|
261
|
-
img
|
274
|
+
save_result_image(img)
|
262
275
|
send_result(text, time)
|
263
276
|
orig_text_results[rectangle_index] = orig_text_string
|
264
277
|
last_ocr1_results[rectangle_index] = previous_text
|
278
|
+
return
|
265
279
|
if not text:
|
266
280
|
if previous_text:
|
267
281
|
if rectangle_index in text_stable_start_times:
|
@@ -312,7 +326,9 @@ def run_oneocr(ocr_config: OCRConfig, i, area=False):
|
|
312
326
|
monitor_config = rect_config.monitor
|
313
327
|
screen_area = ",".join(str(c) for c in coords) if area else None
|
314
328
|
exclusions = list(rect.coordinates for rect in list(filter(lambda x: x.is_excluded, ocr_config.rectangles)))
|
315
|
-
run.run(read_from="screencapture",
|
329
|
+
run.run(read_from="screencapture",
|
330
|
+
read_from_secondary="clipboard" if i == 0 else None,
|
331
|
+
write_to="callback",
|
316
332
|
screen_capture_area=screen_area,
|
317
333
|
# screen_capture_monitor=monitor_config['index'],
|
318
334
|
screen_capture_window=ocr_config.window,
|
@@ -321,7 +337,9 @@ def run_oneocr(ocr_config: OCRConfig, i, area=False):
|
|
321
337
|
text_callback=text_callback,
|
322
338
|
screen_capture_exclusions=exclusions,
|
323
339
|
rectangle=i,
|
324
|
-
language=language
|
340
|
+
language=language,
|
341
|
+
monitor_index=ocr_config.window,
|
342
|
+
ocr2=ocr2)
|
325
343
|
done = True
|
326
344
|
|
327
345
|
|
@@ -367,7 +385,7 @@ if __name__ == "__main__":
|
|
367
385
|
logger.info(f"Received arguments: ocr1={ocr1}, ocr2={ocr2}, twopassocr={twopassocr}")
|
368
386
|
global ocr_config
|
369
387
|
ocr_config: OCRConfig = get_ocr_config()
|
370
|
-
|
388
|
+
set_dpi_awareness()
|
371
389
|
if ocr_config:
|
372
390
|
if ocr_config.window:
|
373
391
|
start_time = time.time()
|
@@ -512,7 +512,7 @@ class ScreenshotClass:
|
|
512
512
|
if not self.window_handle:
|
513
513
|
raise ValueError(area_invalid_error)
|
514
514
|
|
515
|
-
|
515
|
+
self.set_dpi_awareness()
|
516
516
|
|
517
517
|
self.windows_window_tracker_instance = threading.Thread(target=self.windows_window_tracker)
|
518
518
|
self.windows_window_tracker_instance.start()
|
@@ -526,6 +526,25 @@ class ScreenshotClass:
|
|
526
526
|
elif self.windows_window_tracker_instance:
|
527
527
|
self.windows_window_tracker_instance.join()
|
528
528
|
|
529
|
+
@staticmethod
|
530
|
+
def set_dpi_awareness():
|
531
|
+
try:
|
532
|
+
# Try Windows 10+ API first
|
533
|
+
awareness_context = -4
|
534
|
+
success = ctypes.windll.user32.SetProcessDpiAwarenessContext(awareness_context)
|
535
|
+
if success:
|
536
|
+
print("Set DPI awareness: PER_MONITOR_AWARE_V2")
|
537
|
+
else:
|
538
|
+
print("Failed to set DPI awareness context (-4); falling back.")
|
539
|
+
except AttributeError:
|
540
|
+
# Fallback for older Windows versions (8.1+)
|
541
|
+
try:
|
542
|
+
per_monitor_awareness = 2
|
543
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
|
544
|
+
print("Set DPI awareness: PER_MONITOR_DPI_AWARE")
|
545
|
+
except Exception as e:
|
546
|
+
print("Could not set DPI awareness:", e)
|
547
|
+
|
529
548
|
def get_windows_window_handle(self, window_title):
|
530
549
|
def callback(hwnd, window_title_part):
|
531
550
|
window_title = win32gui.GetWindowText(hwnd)
|
@@ -881,7 +900,7 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
|
|
881
900
|
elif write_to == 'clipboard':
|
882
901
|
pyperclipfix.copy(text)
|
883
902
|
elif write_to == "callback":
|
884
|
-
txt_callback(text, orig_text, rectangle, ocr_start_time, img_or_path)
|
903
|
+
txt_callback(text, orig_text, rectangle, ocr_start_time, img_or_path, bool(engine))
|
885
904
|
elif write_to:
|
886
905
|
with Path(write_to).open('a', encoding='utf-8') as f:
|
887
906
|
f.write(text + '\n')
|
@@ -907,6 +926,7 @@ def init_config(parse_args=True):
|
|
907
926
|
|
908
927
|
|
909
928
|
def run(read_from=None,
|
929
|
+
read_from_secondary=None,
|
910
930
|
write_to=None,
|
911
931
|
engine=None,
|
912
932
|
pause_at_startup=None,
|
@@ -927,6 +947,8 @@ def run(read_from=None,
|
|
927
947
|
rectangle=None,
|
928
948
|
text_callback=None,
|
929
949
|
language=None,
|
950
|
+
monitor_index=None,
|
951
|
+
ocr2=None,
|
930
952
|
):
|
931
953
|
"""
|
932
954
|
Japanese OCR client
|
@@ -955,6 +977,9 @@ def run(read_from=None,
|
|
955
977
|
if read_from is None:
|
956
978
|
read_from = config.get_general('read_from')
|
957
979
|
|
980
|
+
if read_from_secondary is None:
|
981
|
+
read_from_secondary = config.get_general('read_from_secondary')
|
982
|
+
|
958
983
|
if screen_capture_area is None:
|
959
984
|
screen_capture_area = config.get_general('screen_capture_area')
|
960
985
|
|
@@ -1054,7 +1079,6 @@ def run(read_from=None,
|
|
1054
1079
|
delay_secs = config.get_general('delay_secs')
|
1055
1080
|
|
1056
1081
|
non_path_inputs = ('screencapture', 'clipboard', 'websocket', 'unixsocket')
|
1057
|
-
read_from_secondary = config.get_general('read_from_secondary')
|
1058
1082
|
read_from_path = None
|
1059
1083
|
read_from_readable = []
|
1060
1084
|
terminated = False
|
@@ -1181,7 +1205,7 @@ def run(read_from=None,
|
|
1181
1205
|
if res:
|
1182
1206
|
last_result = (res, engine_index)
|
1183
1207
|
else:
|
1184
|
-
process_and_write_results(img,
|
1208
|
+
process_and_write_results(img, write_to, None, notify=notify, rectangle=rectangle, ocr_start_time=ocr_start_time, engine=ocr2)
|
1185
1209
|
if isinstance(img, Path):
|
1186
1210
|
if delete_images:
|
1187
1211
|
Path.unlink(img)
|
@@ -1,37 +1,37 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
GameSentenceMiner/anki.py,sha256=fu59gdyp1en_yKTK1WOVZX5lwGzGFKvhccXffgkoYlY,14190
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=
|
4
|
-
GameSentenceMiner/configuration.py,sha256=
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=anp13ryjZhZuBcD8JrAffHpqB3yfDKE3SkETvnwAFnk,77372
|
4
|
+
GameSentenceMiner/configuration.py,sha256=5XdL7ZBEouj6Rz8pHvWKyTmhdDNs5p6UkdNng589n7Y,22428
|
5
5
|
GameSentenceMiner/electron_config.py,sha256=dGcPYCISPehXubYSzsDuI2Gl092MYK0u3bTnkL9Jh1Y,9787
|
6
6
|
GameSentenceMiner/ffmpeg.py,sha256=pNgBRaaZ_efvUnqOapMiJbsl8ZbL3eWPwjZPiJH8DhE,14558
|
7
|
-
GameSentenceMiner/gametext.py,sha256=
|
8
|
-
GameSentenceMiner/gsm.py,sha256=
|
9
|
-
GameSentenceMiner/model.py,sha256=
|
7
|
+
GameSentenceMiner/gametext.py,sha256=hcyZQ69B7xB5ZG85wLzM5au7ZPKxmeUXsmUD26oyk_0,5660
|
8
|
+
GameSentenceMiner/gsm.py,sha256=KsFj49e6XtoTDVT3_wTfq_YNkuPqRTkZgWM9PPSnq20,27176
|
9
|
+
GameSentenceMiner/model.py,sha256=1lRyJFf_LND_4O16h8CWVqDfosLgr0ZS6ufBZ3qJHpY,5699
|
10
10
|
GameSentenceMiner/notification.py,sha256=FY39ChSRK0Y8TQ6lBGsLnpZUFPtFpSy2tweeXVoV7kc,2809
|
11
|
-
GameSentenceMiner/obs.py,sha256
|
11
|
+
GameSentenceMiner/obs.py,sha256=m0z9FoHGJIlG2Ol4UR5PpgZjTkag6TssXQC0rhOrsoQ,14485
|
12
12
|
GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
|
13
13
|
GameSentenceMiner/text_log.py,sha256=MD7LB5D-v4G0Bnm3uGvZQ0aV38Fcj4E0vgq7mmyQ7_4,5157
|
14
14
|
GameSentenceMiner/util.py,sha256=LzWGIDZb8NLv-RyrE_d6ycoQEwM1zpaDhWp0LKb6_Zc,8928
|
15
15
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
16
|
GameSentenceMiner/ai/ai_prompting.py,sha256=O1QBgCL6AkkDyhzxZuW8FPCKgUDfkl_ZlKGcEUfbRnk,9508
|
17
17
|
GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
|
18
|
-
GameSentenceMiner/communication/send.py,sha256=
|
18
|
+
GameSentenceMiner/communication/send.py,sha256=X0MytGv5hY-uUvkfvdCqQA_ljZFmV6UkJ6in1TA1bUE,217
|
19
19
|
GameSentenceMiner/communication/websocket.py,sha256=pTcUe_ZZRp9REdSU4qalhPmbT_1DKa7w18j6RfFLELA,3074
|
20
20
|
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
21
21
|
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
GameSentenceMiner/downloader/download_tools.py,sha256=aRfpCqEmKUFRVsGipwY-7PhY6AeWiFJanW4ZCB9e2iE,8124
|
23
23
|
GameSentenceMiner/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
|
24
24
|
GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
-
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256
|
25
|
+
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=WFTGg2J8FAqoILVbJaMt5XWhF-btXeDnj38Iuz-kf8k,2410
|
26
26
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
27
|
-
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=
|
28
|
-
GameSentenceMiner/ocr/owocr_helper.py,sha256=
|
27
|
+
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Q8ETMHL7BKMA1mbtjrntDLyqCQB0lZ5T4RCZsodjH7Y,47186
|
28
|
+
GameSentenceMiner/ocr/owocr_helper.py,sha256=actkNPsLGmhW0n1LGpgqsbiCc98MMRrStXXTsfGSyfI,17871
|
29
29
|
GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
|
30
30
|
GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
|
31
31
|
GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
|
32
32
|
GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
|
33
33
|
GameSentenceMiner/owocr/owocr/ocr.py,sha256=dPnDmtG-I24kcfxC3iudeRIVgGhLmiWMGyRiMANcYsA,41573
|
34
|
-
GameSentenceMiner/owocr/owocr/run.py,sha256=
|
34
|
+
GameSentenceMiner/owocr/owocr/run.py,sha256=upwGML6oFb_SQNwU5P7tP9pe-dlJEbTo3QTp7ISrxUU,53363
|
35
35
|
GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
|
36
36
|
GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
37
|
GameSentenceMiner/vad/result.py,sha256=C08HsYH4qVjTRh_dvrWrskmXHJ950w0GWxPjGx_BfGY,275
|
@@ -52,9 +52,9 @@ GameSentenceMiner/web/static/web-app-manifest-512x512.png,sha256=wyqgCWCrLEUxSRX
|
|
52
52
|
GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
53
53
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
54
54
|
GameSentenceMiner/web/templates/utility.html,sha256=P659ZU2j7tcbJ5xPO3p7E_SQpkp3CrrFtSvvXJNNuLI,16330
|
55
|
-
gamesentenceminer-2.8.
|
56
|
-
gamesentenceminer-2.8.
|
57
|
-
gamesentenceminer-2.8.
|
58
|
-
gamesentenceminer-2.8.
|
59
|
-
gamesentenceminer-2.8.
|
60
|
-
gamesentenceminer-2.8.
|
55
|
+
gamesentenceminer-2.8.37.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
56
|
+
gamesentenceminer-2.8.37.dist-info/METADATA,sha256=9yT7aEEZ-DC7pFx3T50fWQh2FZlpgEmeckgPHi3PhCc,7165
|
57
|
+
gamesentenceminer-2.8.37.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
58
|
+
gamesentenceminer-2.8.37.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
59
|
+
gamesentenceminer-2.8.37.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
60
|
+
gamesentenceminer-2.8.37.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|