GameSentenceMiner 2.8.35__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.
@@ -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 and websocket.connected:
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()))
@@ -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.general.use_websocket != self.general.use_websocket,
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):
@@ -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
- util.run_new_thread(run_websocket_listener)
119
- if get_config().general.use_clipboard:
120
- if get_config().general.use_websocket:
121
- if get_config().general.use_both_clipboard_and_websocket:
122
- logger.info("Listening for Text on both WebSocket and Clipboard.")
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()
@@ -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
- from obswebsocket import obsws, requests
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: obsws = None
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
- # REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
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 check_obs_folder_is_correct():
63
- obs_record_directory = get_record_directory()
64
- if obs_record_directory and os.path.normpath(obs_record_directory) != os.path.normpath(
65
- get_config().paths.folder_to_watch):
66
- logger.info("OBS Path Setting wrong, OBS Recording folder in GSM Config")
67
- get_config().paths.folder_to_watch = os.path.normpath(obs_record_directory)
68
- get_master_config().sync_shared_fields()
69
- save_full_config(get_master_config())
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) # Default to 4455 if not set
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
- connected = False
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
- def connect_to_obs(retry_count=0):
121
- global client
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
- try:
129
- client = obsws(
130
- host=get_config().obs.host,
131
- port=get_config().obs.port,
132
- password=get_config().obs.password,
133
- authreconnect=1,
134
- on_connect=on_connect,
135
- on_disconnect=on_disconnect
136
- )
137
- client.connect()
138
- update_current_game()
139
- except Exception as e:
140
- if retry_count % 5 == 0:
141
- logger.error(f"Failed to connect to OBS WebSocket: {e}. Retrying...")
142
- time.sleep(1)
143
- connect_to_obs(retry_count=retry_count + 1)
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 = None, retry=10):
155
- try:
156
- if not client:
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
- do_obs_call(requests.ToggleReplayBuffer())
180
- logger.info("Replay buffer Toggled.")
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
- if not get_replay_buffer_status()['outputActive']:
189
- do_obs_call(requests.StartReplayBuffer(), retry=0)
232
+ status = get_replay_buffer_status()
233
+ if status:
234
+ client.start_replay_buffer()
190
235
  except Exception as e:
191
- if "socket is already closed" in str(e):
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 do_obs_call(requests.GetReplayBufferStatus())
240
+ return client.get_replay_buffer_status().output_active
201
241
  except Exception as e:
202
- logger.error(f"Error getting replay buffer status: {e}")
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.call(requests.StopReplayBuffer())
209
- logger.error("Replay buffer stopped.")
247
+ client.stop_replay_buffer()
210
248
  except Exception as e:
211
- logger.error(f"Error stopping replay buffer: {e}")
249
+ logger.warning(f"Error stopping replay buffer: {e}")
212
250
 
213
- # Save the current replay buffer
214
251
  def save_replay_buffer():
215
- replay_buffer_started = do_obs_call(requests.GetReplayBufferStatus())['outputActive']
216
- if replay_buffer_started:
217
- client.call(requests.SaveReplayBuffer())
218
- logger.info("Replay buffer saved. If your log stops bere, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
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.error("Replay Buffer is not active, could not save Replay Buffer!")
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
- return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict, retry=0).sceneName
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
- return ''
229
-
266
+ return ''
230
267
 
231
268
  def get_source_from_scene(scene_name):
232
269
  try:
233
- return do_obs_call(requests.GetSceneItemList(sceneName=scene_name), SceneItemsResponse.from_dict).sceneItems[0]
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
- return do_obs_call(requests.GetRecordDirectory(), RecordDirectory.from_dict).recordDirectory
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
- current_source = get_source_from_scene(get_current_game())
252
- current_source_name = current_source.sourceName
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 scene found.")
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.call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot, imageCompressionQuality=compression))
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
- current_source = get_source_from_scene(get_current_game())
269
- current_source_name = current_source.sourceName
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 scene found.")
272
- return
273
- response = do_obs_call(requests.GetSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageCompressionQuality=0))
274
- with open('screenshot_response.txt', 'wb') as f:
275
- f.write(str(response).encode())
276
- return response['imageData']
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
- ctypes.windll.shcore.SetProcessDpiAwareness(2)
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.connect_to_obs() # Connect to OBS (using mock or real)
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
- ctypes.windll.shcore.SetProcessDpiAwareness(2)
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.connect_to_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.connect_to_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.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
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.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
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", write_to="callback",
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
- ctypes.windll.shcore.SetProcessDpiAwareness(2)
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
- ctypes.windll.shcore.SetProcessDpiAwareness(2)
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, None, None, notify)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.8.35
3
+ Version: 2.8.37
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -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=2klOj2DmHhmvtURp4JjhDJ1vn0hN-uWoUu1lS0xDl3s,74246
4
- GameSentenceMiner/configuration.py,sha256=ndnxuQbLfhMruHld-yK3UjWt1DcXlVhaLRZD8l6SJ0E,22562
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=VogQDs-VQ4dorqy8uvoklweeS58r3Th_yP-zn36e0u4,5556
8
- GameSentenceMiner/gsm.py,sha256=WmVhcC_CGjFmqV75NMMVm9hOiiv-ER4Lu5DUF-6IULo,26012
9
- GameSentenceMiner/model.py,sha256=JdnkT4VoPOXmOpRgFdvERZ09c9wLN6tUJxdrKlGZcqo,5305
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=-tzVHejaGDXyERaDrRqrKmbgwT13oJKxTivGwfUij7Y,10284
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=oOJdCS6-LNX90amkRn5FL2xqx6THGm56zHR2ntVIFTE,229
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=-tw5l2wvv_sqUcGS0Y5X9LlXqmFxcCHcwZ76Q93T3I4,2127
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=gHvhsgvYmIMKmkq0cDOLzBTnmBAvVyP9JKNlTZouJk8,47141
28
- GameSentenceMiner/ocr/owocr_helper.py,sha256=_mRqQ8928ovdF5o_4qpK2DGWMmu3cGBYsmpgMtc0adU,17418
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=ZC0sG-3mCyGgg8FPSto9Hd6-k-N18T1hBe-IgsbSBLM,52345
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.35.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
56
- gamesentenceminer-2.8.35.dist-info/METADATA,sha256=nRrLCVthkT46nGeCmi-klAD7Yw2vkQUA-KYDQ6O2S74,7165
57
- gamesentenceminer-2.8.35.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
58
- gamesentenceminer-2.8.35.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
59
- gamesentenceminer-2.8.35.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
60
- gamesentenceminer-2.8.35.dist-info/RECORD,,
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,,