GameSentenceMiner 2.11.5__py3-none-any.whl → 2.11.7__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/gsm.py CHANGED
@@ -12,6 +12,7 @@ from GameSentenceMiner.util.communication.send import send_restart_signal
12
12
  from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
13
13
  from GameSentenceMiner.vad import vad_processor
14
14
  from GameSentenceMiner.util.model import VADResult
15
+ import time
15
16
 
16
17
  try:
17
18
  import os.path
@@ -45,7 +46,6 @@ try:
45
46
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
46
47
  except Exception as e:
47
48
  from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
48
- import time
49
49
  logger.info("Something bad happened during import/initialization, closing in 5 seconds")
50
50
  logger.exception(e)
51
51
  time.sleep(5)
GameSentenceMiner/obs.py CHANGED
@@ -334,6 +334,7 @@ async def register_scene_change_callback(callback):
334
334
 
335
335
  logger.info("Scene change callback registered.")
336
336
 
337
+
337
338
  def get_screenshot(compression=-1):
338
339
  try:
339
340
  screenshot = os.path.join(configuration.get_temporary_directory(), make_unique_file_name('screenshot.png'))
@@ -377,6 +378,7 @@ def get_screenshot_base64(compression=0, width=None, height=None):
377
378
  logger.error(f"Error getting screenshot: {e}")
378
379
  return None
379
380
 
381
+
380
382
  def update_current_game():
381
383
  gsm_state.current_game = get_current_scene()
382
384
 
@@ -131,6 +131,16 @@ def set_dpi_awareness():
131
131
  ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
132
132
 
133
133
  def get_scene_ocr_config(use_window_as_config=False, window=""):
134
+ path = get_scene_ocr_config_path(use_window_as_config, window)
135
+ if not os.path.exists(path):
136
+ return None
137
+ with open(path, "r", encoding="utf-8") as f:
138
+ from json import load
139
+ data = load(f)
140
+ ocr_config = OCRConfig.from_dict(data)
141
+ return ocr_config
142
+
143
+ def get_scene_ocr_config_path(use_window_as_config=False, window=""):
134
144
  ocr_config_dir = get_ocr_config_path()
135
145
  try:
136
146
  if use_window_as_config:
@@ -11,7 +11,7 @@ from PIL import Image, ImageTk
11
11
 
12
12
  # Assuming a mock or real obs module exists in this path
13
13
  from GameSentenceMiner import obs
14
- from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window, get_scene_ocr_config
14
+ from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window, get_scene_ocr_config_path
15
15
  from GameSentenceMiner.util.gsm_utils import sanitize_filename
16
16
 
17
17
  try:
@@ -123,7 +123,7 @@ class ScreenSelector:
123
123
 
124
124
  def load_existing_rectangles(self):
125
125
  """Loads rectangles from config, converting from percentage to absolute pixels for use."""
126
- config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
126
+ config_path = get_scene_ocr_config_path(self.use_window_as_config, self.window_name)
127
127
  win_geom = self.target_window_geometry # Use current geometry for conversion
128
128
  win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
129
129
 
@@ -168,7 +168,7 @@ class ScreenSelector:
168
168
 
169
169
  def save_rects(self, event=None):
170
170
  """Saves rectangles to config, converting from absolute pixels to percentages."""
171
- config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
171
+ config_path = get_scene_ocr_config_path(self.use_window_as_config, self.window_name)
172
172
  win_geom = self.target_window_geometry
173
173
  win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
174
174
  print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
@@ -18,13 +18,15 @@ from PIL import Image
18
18
  from rapidfuzz import fuzz
19
19
 
20
20
  from GameSentenceMiner import obs
21
+ from GameSentenceMiner.util.electron_config import *
21
22
  from GameSentenceMiner.ocr.ss_picker import ScreenCropper
22
23
  from GameSentenceMiner.owocr.owocr.run import TextFiltering
23
24
  from GameSentenceMiner.util.configuration import get_config, get_app_directory, get_temporary_directory
24
- from GameSentenceMiner.util.electron_config import get_ocr_scan_rate, get_requires_open_window
25
25
  from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, set_dpi_awareness, get_window, get_ocr_config_path
26
26
  from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
27
27
  from GameSentenceMiner.util.gsm_utils import sanitize_filename, do_text_replacements, OCR_REPLACEMENTS_FILE
28
+ import threading
29
+ import time
28
30
 
29
31
  CONFIG_FILE = Path("ocr_config.json")
30
32
  DEFAULT_IMAGE_PATH = r"C:\Users\Beangate\Pictures\msedge_acbl8GL7Ax.jpg" # CHANGE THIS
@@ -193,17 +195,17 @@ all_cords = None
193
195
  rectangles = None
194
196
  last_ocr2_result = []
195
197
 
196
- def do_second_ocr(ocr1_text, time, img, filtering, ignore_furigana_filter=False, ignore_previous_result=False):
198
+ def do_second_ocr(ocr1_text, time, img, filtering, pre_crop_image, ignore_furigana_filter=False, ignore_previous_result=False):
197
199
  global twopassocr, ocr2, last_ocr2_result
198
200
  try:
199
201
  orig_text, text = run.process_and_write_results(img, None, last_ocr2_result if not ignore_previous_result else None, filtering, None,
200
- engine=ocr2, furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
202
+ engine=get_ocr_ocr2(), furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
201
203
 
202
204
  if compare_ocr_results(last_ocr2_result, orig_text):
203
205
  if text:
204
206
  logger.info("Seems like Text we already sent, not doing anything.")
205
207
  return
206
- save_result_image(img)
208
+ save_result_image(img, pre_crop_image=pre_crop_image)
207
209
  last_ocr2_result = orig_text
208
210
  asyncio.run(send_result(text, time))
209
211
  except json.JSONDecodeError:
@@ -213,19 +215,19 @@ def do_second_ocr(ocr1_text, time, img, filtering, ignore_furigana_filter=False,
213
215
  print(f"Error processing message: {e}")
214
216
 
215
217
 
216
- def save_result_image(img):
218
+ def save_result_image(img, pre_crop_image=None):
217
219
  if isinstance(img, bytes):
218
220
  with open(os.path.join(get_temporary_directory(), "last_successful_ocr.png"), "wb") as f:
219
221
  f.write(img)
220
222
  else:
221
223
  img.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
222
- img.close()
224
+ run.set_last_image(pre_crop_image if pre_crop_image else img)
223
225
 
224
226
 
225
227
  async def send_result(text, time):
226
228
  if text:
227
229
  text = do_text_replacements(text, OCR_REPLACEMENTS_FILE)
228
- if clipboard_output:
230
+ if get_ocr_send_to_clipboard():
229
231
  import pyperclip
230
232
  pyperclip.copy(text)
231
233
  try:
@@ -244,6 +246,50 @@ previous_orig_text = "" # Store original text result
244
246
  TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
245
247
  force_stable = False
246
248
 
249
+ class ConfigChangeCheckThread(threading.Thread):
250
+ def __init__(self):
251
+ super().__init__(daemon=True)
252
+ self.last_changes = None
253
+ self.callbacks = []
254
+
255
+ def run(self):
256
+ global ocr_config
257
+ while True:
258
+ try:
259
+ section_changed, changes = has_ocr_config_changed()
260
+ if section_changed:
261
+ reload_electron_config()
262
+ self.last_changes = changes
263
+ # Only run this block after a change has occurred and then the section is stable (no change)
264
+ if self.last_changes is not None and not section_changed:
265
+ logger.info(f"Detected config changes: {self.last_changes}")
266
+ for cb in self.callbacks:
267
+ cb(self.last_changes)
268
+ if hasattr(run, 'handle_config_change'):
269
+ run.handle_config_change()
270
+ if any(c in self.last_changes for c in ('ocr1', 'ocr2', 'language', 'furigana_filter_sensitivity')):
271
+ reset_callback_vars()
272
+ self.last_changes = None
273
+ except Exception as e:
274
+ logger.debug(f"ConfigChangeCheckThread error: {e}")
275
+ time.sleep(0.25) # Lowered to 0.25s for more responsiveness
276
+
277
+ def add_callback(self, callback):
278
+ self.callbacks.append(callback)
279
+
280
+ def reset_callback_vars():
281
+ global previous_text, last_oneocr_time, text_stable_start_time, previous_orig_text, previous_img, force_stable, previous_ocr1_result, previous_text_list, last_ocr2_result
282
+ previous_text = None
283
+ previous_orig_text = ""
284
+ previous_img = None
285
+ text_stable_start_time = None
286
+ last_oneocr_time = None
287
+ force_stable = False
288
+ previous_ocr1_result = ""
289
+ previous_text_list = []
290
+ last_ocr2_result = ""
291
+ run.set_last_image(None)
292
+
247
293
  def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering=None, crop_coords=None):
248
294
  global twopassocr, ocr2, previous_text, last_oneocr_time, text_stable_start_time, previous_orig_text, previous_img, force_stable, previous_ocr1_result, previous_text_list
249
295
  orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
@@ -251,10 +297,13 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
251
297
  save_result_image(img)
252
298
  asyncio.run(send_result(text, time))
253
299
  return
300
+
301
+ if not text:
302
+ run.set_last_image(img)
254
303
 
255
304
  line_start_time = time if time else datetime.now()
256
305
 
257
- if manual or not twopassocr:
306
+ if manual or not get_ocr_two_pass_ocr():
258
307
  if compare_ocr_results(previous_orig_text, orig_text_string):
259
308
  if text:
260
309
  logger.info("Seems like Text we already sent, not doing anything.")
@@ -274,6 +323,7 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
274
323
  if previous_text and text_stable_start_time:
275
324
  stable_time = text_stable_start_time
276
325
  previous_img_local = previous_img
326
+ pre_crop_image = previous_img_local
277
327
  if compare_ocr_results(previous_orig_text, orig_text_string):
278
328
  if text:
279
329
  logger.info("Seems like Text we already sent, not doing anything.")
@@ -281,10 +331,10 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
281
331
  return
282
332
  previous_orig_text = orig_text_string
283
333
  previous_ocr1_result = previous_text
284
- if crop_coords and optimize_second_scan:
334
+ if crop_coords and get_ocr_optimize_second_scan():
285
335
  previous_img_local.save(os.path.join(get_temporary_directory(), "pre_oneocrcrop.png"))
286
336
  previous_img_local = previous_img_local.crop(crop_coords)
287
- second_ocr_queue.put((previous_text, stable_time, previous_img_local, filtering))
337
+ second_ocr_queue.put((previous_text, stable_time, previous_img_local, filtering, pre_crop_image))
288
338
  # threading.Thread(target=do_second_ocr, args=(previous_text, stable_time, previous_img_local, filtering), daemon=True).start()
289
339
  previous_img = None
290
340
  previous_text = None
@@ -294,7 +344,7 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
294
344
  return
295
345
 
296
346
  # Make sure it's an actual new line before starting the timer
297
- if compare_ocr_results(orig_text_string, previous_orig_text):
347
+ if text and compare_ocr_results(orig_text_string, previous_orig_text):
298
348
  return
299
349
 
300
350
  if not text_stable_start_time:
@@ -315,15 +365,15 @@ def process_task_queue():
315
365
  task = second_ocr_queue.get()
316
366
  if task is None: # Exit signal
317
367
  break
318
- ocr1_text, stable_time, previous_img_local, filtering = task
319
- do_second_ocr(ocr1_text, stable_time, previous_img_local, filtering)
368
+ ocr1_text, stable_time, previous_img_local, filtering, pre_crop_image = task
369
+ do_second_ocr(ocr1_text, stable_time, previous_img_local, filtering, pre_crop_image)
320
370
  except Exception as e:
321
371
  logger.exception(f"Error processing task: {e}")
322
372
  finally:
323
373
  second_ocr_queue.task_done()
324
374
 
325
375
 
326
- def run_oneocr(ocr_config: OCRConfig, rectangles):
376
+ def run_oneocr(ocr_config: OCRConfig, rectangles, config_check_thread):
327
377
  global done
328
378
  print("Running OneOCR")
329
379
  screen_area = None
@@ -344,11 +394,9 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
344
394
  screen_capture_area=screen_area,
345
395
  # screen_capture_monitor=monitor_config['index'],
346
396
  screen_capture_window=ocr_config.window if ocr_config and ocr_config.window else None,
347
- screen_capture_only_active_windows=get_requires_open_window(),
348
397
  screen_capture_delay_secs=get_ocr_scan_rate(), engine=ocr1,
349
398
  text_callback=text_callback,
350
399
  screen_capture_exclusions=exclusions,
351
- language=language,
352
400
  monitor_index=None,
353
401
  ocr1=ocr1,
354
402
  ocr2=ocr2,
@@ -356,7 +404,7 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
356
404
  screen_capture_areas=screen_areas,
357
405
  furigana_filter_sensitivity=furigana_filter_sensitivity,
358
406
  screen_capture_combo=manual_ocr_hotkey if manual_ocr_hotkey and manual else None,
359
- keep_line_breaks=keep_newline)
407
+ config_check_thread=config_check_thread)
360
408
  except Exception as e:
361
409
  logger.exception(f"Error running OneOCR: {e}")
362
410
  done = True
@@ -366,7 +414,7 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
366
414
  def add_ss_hotkey(ss_hotkey="ctrl+shift+g"):
367
415
  import keyboard
368
416
  secret_ss_hotkey = "F14"
369
- filtering = TextFiltering(lang=language)
417
+ filtering = TextFiltering(lang=get_ocr_language())
370
418
  cropper = ScreenCropper()
371
419
  def capture():
372
420
  print("Taking screenshot...")
@@ -462,6 +510,12 @@ if __name__ == "__main__":
462
510
  use_window_for_config = args.use_window_for_config
463
511
  keep_newline = args.keep_newline
464
512
  obs_ocr = args.obs_ocr
513
+
514
+ # Start config change checker thread
515
+ config_check_thread = ConfigChangeCheckThread()
516
+ config_check_thread.start()
517
+ # Example: add a callback to config_check_thread if needed
518
+ # config_check_thread.add_callback(lambda: print("Config changed!"))
465
519
 
466
520
  window = None
467
521
  logger.info(f"Received arguments: {vars(args)}")
@@ -487,7 +541,7 @@ if __name__ == "__main__":
487
541
  if manual or ocr_config:
488
542
  rectangles = ocr_config.rectangles if ocr_config and ocr_config.rectangles else []
489
543
  oneocr_threads = []
490
- ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config, rectangles), daemon=True)
544
+ ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config, rectangles, config_check_thread), daemon=True)
491
545
  ocr_thread.start()
492
546
  if not manual:
493
547
  worker_thread = threading.Thread(target=process_task_queue, daemon=True)
@@ -17,6 +17,8 @@ from PIL import Image
17
17
  from loguru import logger
18
18
  import requests
19
19
 
20
+ from GameSentenceMiner.util.electron_config import get_ocr_language
21
+
20
22
  # from GameSentenceMiner.util.configuration import get_temporary_directory
21
23
 
22
24
  try:
@@ -809,25 +811,8 @@ class OneOCR:
809
811
  available = False
810
812
 
811
813
  def __init__(self, config={}, lang='ja'):
812
- if lang == "ja":
813
- self.regex = re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
814
- elif lang == "zh":
815
- self.regex = re.compile(r'[\u4E00-\u9FFF]')
816
- elif lang == "ko":
817
- self.regex = re.compile(r'[\uAC00-\uD7AF]')
818
- elif lang == "ar":
819
- self.regex = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]')
820
- elif lang == "ru":
821
- self.regex = re.compile(r'[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u1C80-\u1C8F]')
822
- elif lang == "el":
823
- self.regex = re.compile(r'[\u0370-\u03FF\u1F00-\u1FFF]')
824
- elif lang == "he":
825
- self.regex = re.compile(r'[\u0590-\u05FF\uFB1D-\uFB4F]')
826
- elif lang == "th":
827
- self.regex = re.compile(r'[\u0E00-\u0E7F]')
828
- else:
829
- self.regex = re.compile(
830
- r'[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u2C60-\u2C7F\uA720-\uA7FF\uAB30-\uAB6F]')
814
+ self.initial_lang = lang
815
+ self.get_regex(lang)
831
816
  if sys.platform == 'win32':
832
817
  if int(platform.release()) < 10:
833
818
  logger.warning('OneOCR is not supported on Windows older than 10!')
@@ -849,7 +834,32 @@ class OneOCR:
849
834
  except:
850
835
  logger.warning('Error reading URL from config, OneOCR will not work!')
851
836
 
837
+ def get_regex(self, lang):
838
+ if lang == "ja":
839
+ self.regex = re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
840
+ elif lang == "zh":
841
+ self.regex = re.compile(r'[\u4E00-\u9FFF]')
842
+ elif lang == "ko":
843
+ self.regex = re.compile(r'[\uAC00-\uD7AF]')
844
+ elif lang == "ar":
845
+ self.regex = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]')
846
+ elif lang == "ru":
847
+ self.regex = re.compile(r'[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u1C80-\u1C8F]')
848
+ elif lang == "el":
849
+ self.regex = re.compile(r'[\u0370-\u03FF\u1F00-\u1FFF]')
850
+ elif lang == "he":
851
+ self.regex = re.compile(r'[\u0590-\u05FF\uFB1D-\uFB4F]')
852
+ elif lang == "th":
853
+ self.regex = re.compile(r'[\u0E00-\u0E7F]')
854
+ else:
855
+ self.regex = re.compile(
856
+ r'[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u2C60-\u2C7F\uA720-\uA7FF\uAB30-\uAB6F]')
857
+
852
858
  def __call__(self, img, furigana_filter_sensitivity=0):
859
+ lang = get_ocr_language()
860
+ if lang != self.initial_lang:
861
+ self.initial_lang = lang
862
+ self.get_regex(lang)
853
863
  img, is_path = input_to_pil_image(img)
854
864
  if img.width < 51 or img.height < 51:
855
865
  new_width = max(img.width, 51)
@@ -1,4 +1,5 @@
1
- from ...ocr.gsm_ocr_config import set_dpi_awareness
1
+ from ...ocr.gsm_ocr_config import set_dpi_awareness, get_scene_ocr_config_path, OCRConfig, get_scene_ocr_config
2
+ from ...util.electron_config import *
2
3
 
3
4
  try:
4
5
  import win32gui
@@ -58,6 +59,7 @@ from .screen_coordinate_picker import get_screen_selection
58
59
  from GameSentenceMiner.util.configuration import get_temporary_directory, get_config
59
60
 
60
61
  config = None
62
+ last_image = None
61
63
 
62
64
 
63
65
  class ClipboardThread(threading.Thread):
@@ -309,9 +311,10 @@ class RequestHandler(socketserver.BaseRequestHandler):
309
311
  class TextFiltering:
310
312
  accurate_filtering = False
311
313
 
312
- def __init__(self, lang="ja"):
314
+ def __init__(self, lang='ja'):
313
315
  from pysbd import Segmenter
314
- self.segmenter = Segmenter(language=lang, clean=True)
316
+ self.initial_lang = get_ocr_language() or lang
317
+ self.segmenter = Segmenter(language=get_ocr_language(), clean=True)
315
318
  self.kana_kanji_regex = re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
316
319
  self.chinese_common_regex = re.compile(r'[\u4E00-\u9FFF]')
317
320
  self.english_regex = re.compile(r'[a-zA-Z0-9.,!?;:"\'()\[\]{}]')
@@ -349,8 +352,13 @@ class TextFiltering:
349
352
  self.classify = langid.classify
350
353
 
351
354
  def __call__(self, text, last_result):
352
- orig_text = self.segmenter.segment(text)
355
+ lang = get_ocr_language()
356
+ if self.initial_lang != lang:
357
+ from pysbd import Segmenter
358
+ self.segmenter = Segmenter(language=get_ocr_language(), clean=True)
359
+ self.initial_lang = get_ocr_language()
353
360
 
361
+ orig_text = self.segmenter.segment(text)
354
362
  orig_text_filtered = []
355
363
  for block in orig_text:
356
364
  if "BLANK_LINE" in block:
@@ -419,7 +427,7 @@ class TextFiltering:
419
427
 
420
428
 
421
429
  class ScreenshotThread(threading.Thread):
422
- def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas, screen_capture_on_combo):
430
+ def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_areas, screen_capture_on_combo):
423
431
  super().__init__(daemon=True)
424
432
  self.macos_window_tracker_instance = None
425
433
  self.windows_window_tracker_instance = None
@@ -490,7 +498,6 @@ class ScreenshotThread(threading.Thread):
490
498
 
491
499
 
492
500
  if self.screencapture_mode == 2 or self.screen_capture_window:
493
- self.screen_capture_only_active_windows = screen_capture_only_active_windows
494
501
  area_invalid_error = '"screen_capture_area" must be empty, "screen_N" where N is a screen number starting from 1, a valid set of coordinates, or a valid window name'
495
502
  if sys.platform == 'darwin':
496
503
  if config.get_general('screen_capture_old_macos_api') or int(platform.mac_ver()[0].split('.')[0]) < 14:
@@ -523,7 +530,7 @@ class ScreenshotThread(threading.Thread):
523
530
  self.window_id = window_ids[window_index]
524
531
  window_title = window_titles[window_index]
525
532
 
526
- if self.screen_capture_only_active_windows:
533
+ if get_ocr_requires_open_window():
527
534
  self.macos_window_tracker_instance = threading.Thread(target=self.macos_window_tracker)
528
535
  self.macos_window_tracker_instance.start()
529
536
  logger.opt(ansi=True).info(f'Selected window: {window_title}')
@@ -567,7 +574,7 @@ class ScreenshotThread(threading.Thread):
567
574
  found = win32gui.IsWindow(self.window_handle)
568
575
  if not found:
569
576
  break
570
- if self.screen_capture_only_active_windows:
577
+ if get_ocr_requires_open_window():
571
578
  self.screencapture_window_active = self.window_handle == win32gui.GetForegroundWindow()
572
579
  else:
573
580
  self.screencapture_window_visible = not win32gui.IsIconic(self.window_handle)
@@ -656,7 +663,14 @@ class ScreenshotThread(threading.Thread):
656
663
  def run(self):
657
664
  if self.screencapture_mode != 2:
658
665
  sct = mss.mss()
666
+ start = time.time()
659
667
  while not terminated:
668
+ if time.time() - start > 1:
669
+ start = time.time()
670
+ section_changed = has_ocr_config_changed()
671
+ if section_changed:
672
+ reload_electron_config()
673
+
660
674
  if not screenshot_event.wait(timeout=0.1):
661
675
  continue
662
676
  if self.screencapture_mode == 2 or self.screen_capture_window:
@@ -719,6 +733,11 @@ class ScreenshotThread(threading.Thread):
719
733
  else:
720
734
  sct_img = sct.grab(self.sct_params)
721
735
  img = Image.frombytes('RGB', sct_img.size, sct_img.bgra, 'raw', 'BGRX')
736
+
737
+ if not img.getbbox():
738
+ logger.info("Screen Capture Didn't get Capturing anything, sleeping.")
739
+ time.sleep(1)
740
+ continue
722
741
 
723
742
  import random # Ensure this is imported at the top of the file if not already
724
743
  rand_int = random.randint(1, 20) # Executes only once out of 10 times
@@ -735,7 +754,12 @@ class ScreenshotThread(threading.Thread):
735
754
 
736
755
  cropped_sections = []
737
756
  for area in self.areas:
738
- cropped_sections.append(img.crop((area[0], area[1], area[0] + area[2], area[1] + area[3])))
757
+ # Ensure crop coordinates are within image bounds
758
+ left = max(0, area[0])
759
+ top = max(0, area[1])
760
+ right = min(img.width, area[0] + area[2])
761
+ bottom = min(img.height, area[1] + area[3])
762
+ cropped_sections.append(img.crop((left, top, right, bottom)))
739
763
 
740
764
  if len(cropped_sections) > 1:
741
765
  combined_width = max(section.width for section in cropped_sections)
@@ -755,13 +779,44 @@ class ScreenshotThread(threading.Thread):
755
779
  if rand_int == 1:
756
780
  img.save(os.path.join(get_temporary_directory(), 'after_crop.png'), 'PNG')
757
781
 
758
- self.write_result(img)
759
- screenshot_event.clear()
782
+ if last_image and are_images_identical(img, last_image):
783
+ logger.debug("Captured screenshot is identical to the last one, sleeping.")
784
+ time.sleep(max(.5, get_ocr_scan_rate()))
785
+ else:
786
+ self.write_result(img)
787
+ screenshot_event.clear()
760
788
 
761
789
  if self.macos_window_tracker_instance:
762
790
  self.macos_window_tracker_instance.join()
763
791
  elif self.windows_window_tracker_instance:
764
792
  self.windows_window_tracker_instance.join()
793
+
794
+
795
+ def set_last_image(image):
796
+ global last_image
797
+ if image == last_image:
798
+ return
799
+ try:
800
+ if last_image is not None and hasattr(last_image, "close"):
801
+ last_image.close()
802
+ except Exception:
803
+ pass
804
+ last_image = image
805
+
806
+ def are_images_identical(img1, img2):
807
+ if None in (img1, img2):
808
+ return img1 == img2
809
+
810
+ try:
811
+ img1 = np.array(img1)
812
+ img2 = np.array(img2)
813
+ except Exception:
814
+ logger.warning("Failed to convert images to numpy arrays for comparison.")
815
+ # If conversion to numpy array fails, consider them not identical
816
+ return False
817
+
818
+ return (img1.shape == img2.shape) and np.array_equal(img1, img2)
819
+
765
820
 
766
821
  # Use OBS for Screenshot Source (i.e. Linux)
767
822
  class OBSScreenshotThread(threading.Thread):
@@ -771,6 +826,9 @@ class OBSScreenshotThread(threading.Thread):
771
826
  self.interval = interval
772
827
  self.obs_client = None
773
828
  self.websocket = None
829
+ self.current_source = None
830
+ self.current_source_name = None
831
+ self.current_scene = None
774
832
  self.width = width
775
833
  self.height = height
776
834
  self.use_periodic_queue = not screen_capture_on_combo
@@ -796,23 +854,51 @@ class OBSScreenshotThread(threading.Thread):
796
854
  self.obs_client = None
797
855
 
798
856
  def run(self):
857
+ global last_image
799
858
  import base64
800
859
  import io
801
860
  from PIL import Image
802
861
  import GameSentenceMiner.obs as obs
803
862
 
804
- loop = asyncio.new_event_loop()
805
- asyncio.set_event_loop(loop)
863
+ def init_config(source=None, scene=None):
864
+ obs.update_current_game()
865
+ self.current_source = source if source else obs.get_active_source()
866
+ self.current_source_name = self.current_source.get('sourceName') if isinstance(self.current_source, dict) else None
867
+ self.current_scene = scene if scene else obs.get_current_game()
868
+ self.ocr_config = get_scene_ocr_config()
869
+ self.ocr_config.scale_to_custom_size(self.width, self.height)
806
870
 
807
- self.connect_obs()
808
- self.ocr_config.scale_to_custom_size(self.width, self.height)
809
- current_source = obs.get_active_source()
810
- current_source_name = current_source.get('sourceName') if isinstance(current_source, dict) else None
871
+ # Register a scene switch callback in obsws
872
+ def on_scene_switch(scene):
873
+ logger.info(f"Scene switched to: {scene}. Loading new OCR config.")
874
+ init_config(scene=scene)
811
875
 
876
+ asyncio.run(obs.register_scene_change_callback(on_scene_switch))
877
+
878
+ self.connect_obs()
879
+ init_config()
880
+ start = time.time()
812
881
  while not terminated:
882
+ if time.time() - start > 5:
883
+ if not self.obs_client:
884
+ self.connect_obs()
885
+ else:
886
+ try:
887
+ self.obs_client.get_version()
888
+ except Exception as e:
889
+ logger.error(f"Lost connection to OBS: {e}")
890
+ self.obs_client = None
891
+ self.connect_obs()
892
+ if not screenshot_event.wait(timeout=0.1):
893
+ continue
894
+
895
+ if not self.ocr_config:
896
+ time.sleep(1)
897
+ continue
898
+
813
899
  try:
814
900
  response = self.obs_client.get_source_screenshot(
815
- name=current_source_name,
901
+ name=self.current_source_name,
816
902
  img_format='png',
817
903
  quality=75,
818
904
  width=self.width,
@@ -822,6 +908,11 @@ class OBSScreenshotThread(threading.Thread):
822
908
  if response.image_data:
823
909
  image_data = base64.b64decode(response.image_data.split(",")[1])
824
910
  img = Image.open(io.BytesIO(image_data)).convert("RGBA")
911
+
912
+ if not img.getbbox():
913
+ logger.info("OBS Not Capturing anything, sleeping.")
914
+ time.sleep(1)
915
+ continue
825
916
 
826
917
  for rectangle in self.ocr_config.rectangles:
827
918
  if rectangle.is_excluded:
@@ -832,7 +923,12 @@ class OBSScreenshotThread(threading.Thread):
832
923
  cropped_sections = []
833
924
  for rectangle in [r for r in self.ocr_config.rectangles if not r.is_excluded]:
834
925
  area = rectangle.coordinates
835
- cropped_sections.append(img.crop((area[0], area[1], area[0] + area[2], area[1] + area[3])))
926
+ # Ensure crop coordinates are within image bounds
927
+ left = max(0, area[0])
928
+ top = max(0, area[1])
929
+ right = min(img.width, area[0] + area[2])
930
+ bottom = min(img.height, area[1] + area[3])
931
+ cropped_sections.append(img.crop((left, top, right, bottom)))
836
932
 
837
933
  if len(cropped_sections) > 1:
838
934
  combined_width = max(section.width for section in cropped_sections)
@@ -846,17 +942,20 @@ class OBSScreenshotThread(threading.Thread):
846
942
  img = combined_img
847
943
  elif cropped_sections:
848
944
  img = cropped_sections[0]
849
-
850
- self.write_result(img)
945
+
946
+ if last_image and are_images_identical(img, last_image):
947
+ logger.debug("Captured screenshot is identical to the last one, sleeping.")
948
+ time.sleep(max(.5, get_ocr_scan_rate()))
949
+ else:
950
+ self.write_result(img)
951
+ screenshot_event.clear()
851
952
  else:
852
953
  logger.error("Failed to get screenshot data from OBS.")
853
954
 
854
955
  except Exception as e:
855
- logger.error(f"An unexpected error occurred with OBS connection: {e}")
956
+ logger.error(f"An unexpected error occurred during OBS Capture : {e}", exc_info=True)
856
957
  continue
857
958
 
858
- time.sleep(self.interval)
859
-
860
959
  class AutopauseTimer:
861
960
  def __init__(self, timeout):
862
961
  self.stop_event = threading.Event()
@@ -919,6 +1018,22 @@ def engine_change_handler(user_input='s', is_combo=True):
919
1018
  logger.opt(ansi=True).info(f'Switched to <{engine_color}>{new_engine_name}</{engine_color}>!')
920
1019
 
921
1020
 
1021
+ def engine_change_handler_name(engine):
1022
+ global engine_index
1023
+ old_engine_index = engine_index
1024
+
1025
+ for i, instance in enumerate(engine_instances):
1026
+ if instance.name.lower() in engine.lower():
1027
+ engine_index = i
1028
+ break
1029
+
1030
+ if engine_index != old_engine_index:
1031
+ new_engine_name = engine_instances[engine_index].readable_name
1032
+ notifier.send(title='owocr', message=f'Switched to {new_engine_name}')
1033
+ engine_color = config.get_general('engine_color')
1034
+ logger.opt(ansi=True).info(f'Switched to <{engine_color}>{new_engine_name}</{engine_color}>!')
1035
+
1036
+
922
1037
  def user_input_thread_run():
923
1038
  def _terminate_handler():
924
1039
  global terminated
@@ -1034,8 +1149,8 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
1034
1149
  if res:
1035
1150
  if filtering:
1036
1151
  text, orig_text = filtering(text, last_result)
1037
- if lang == "ja" or lang == "zh":
1038
- text = post_process(text, keep_blank_lines=keep_new_lines)
1152
+ if get_ocr_language() == "ja" or get_ocr_language() == "zh":
1153
+ text = post_process(text, keep_blank_lines=get_ocr_keep_newline())
1039
1154
  logger.opt(ansi=True).info(f'Text recognized in {end_time - start_time:0.03f}s using <{engine_color}>{engine_instance.readable_name}</{engine_color}>: {text}')
1040
1155
  if notify and config.get_general('notifications'):
1041
1156
  notifier.send(title='owocr', message='Text recognized: ' + text)
@@ -1086,18 +1201,16 @@ def run(read_from=None,
1086
1201
  screen_capture_exclusions=None,
1087
1202
  screen_capture_window=None,
1088
1203
  screen_capture_delay_secs=None,
1089
- screen_capture_only_active_windows=None,
1090
1204
  screen_capture_combo=None,
1091
1205
  stop_running_flag=None,
1092
1206
  screen_capture_event_bus=None,
1093
1207
  text_callback=None,
1094
- language=None,
1095
1208
  monitor_index=None,
1096
1209
  ocr1=None,
1097
1210
  ocr2=None,
1098
1211
  gsm_ocr_config=None,
1099
1212
  furigana_filter_sensitivity=None,
1100
- keep_line_breaks=False,
1213
+ config_check_thread=None
1101
1214
  ):
1102
1215
  """
1103
1216
  Japanese OCR client
@@ -1132,8 +1245,8 @@ def run(read_from=None,
1132
1245
  if screen_capture_area is None:
1133
1246
  screen_capture_area = config.get_general('screen_capture_area')
1134
1247
 
1135
- if screen_capture_only_active_windows is None:
1136
- screen_capture_only_active_windows = config.get_general('screen_capture_only_active_windows')
1248
+ # if screen_capture_only_active_windows is None:
1249
+ # screen_capture_only_active_windows = config.get_general('screen_capture_only_active_windows')
1137
1250
 
1138
1251
  if screen_capture_exclusions is None:
1139
1252
  screen_capture_exclusions = config.get_general('screen_capture_exclusions')
@@ -1159,9 +1272,6 @@ def run(read_from=None,
1159
1272
  if write_to is None:
1160
1273
  write_to = config.get_general('write_to')
1161
1274
 
1162
- if language is None:
1163
- language = config.get_general('language', "ja")
1164
-
1165
1275
  logger.configure(handlers=[{'sink': sys.stderr, 'format': config.get_general('logger_format')}])
1166
1276
 
1167
1277
  if config.has_config:
@@ -1173,14 +1283,10 @@ def run(read_from=None,
1173
1283
 
1174
1284
  global engine_instances
1175
1285
  global engine_keys
1176
- global lang
1177
- global keep_new_lines
1178
- lang = language
1179
1286
  engine_instances = []
1180
1287
  config_engines = []
1181
1288
  engine_keys = []
1182
1289
  default_engine = ''
1183
- keep_new_lines = keep_line_breaks
1184
1290
 
1185
1291
  if len(config.get_general('engines')) > 0:
1186
1292
  for config_engine in config.get_general('engines').split(','):
@@ -1194,7 +1300,7 @@ def run(read_from=None,
1194
1300
  if config.get_engine(engine_class.name) == None:
1195
1301
  engine_instance = engine_class()
1196
1302
  else:
1197
- engine_instance = engine_class(config.get_engine(engine_class.name), lang=lang)
1303
+ engine_instance = engine_class(config.get_engine(engine_class.name), lang=get_ocr_language())
1198
1304
 
1199
1305
  if engine_instance.available:
1200
1306
  engine_instances.append(engine_instance)
@@ -1270,7 +1376,7 @@ def run(read_from=None,
1270
1376
  global txt_callback
1271
1377
  txt_callback = text_callback
1272
1378
 
1273
- if 'screencapture' in (read_from, read_from_secondary) or 'obs' in (read_from, read_from_secondary):
1379
+ if any(x in ('screencapture', 'obs') for x in (read_from, read_from_secondary)):
1274
1380
  global screenshot_event
1275
1381
  global take_screenshot
1276
1382
  if screen_capture_combo != '':
@@ -1285,7 +1391,7 @@ def run(read_from=None,
1285
1391
  last_result = ([], engine_index)
1286
1392
 
1287
1393
  screenshot_event = threading.Event()
1288
- screenshot_thread = ScreenshotThread(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas, screen_capture_on_combo)
1394
+ screenshot_thread = ScreenshotThread(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_areas, screen_capture_on_combo)
1289
1395
  screenshot_thread.start()
1290
1396
  filtering = TextFiltering()
1291
1397
  read_from_readable.append('screen capture')
@@ -1347,6 +1453,13 @@ def run(read_from=None,
1347
1453
  if screen_capture_combo:
1348
1454
  logger.opt(ansi=True).info(f'Manual OCR Running... Press <{engine_color}>{screen_capture_combo.replace("<", "").replace(">", "")}</{engine_color}> to run OCR')
1349
1455
 
1456
+ def handle_config_changes(changes):
1457
+ nonlocal last_result
1458
+ if any(c in changes for c in ('ocr1', 'ocr2', 'language', 'furigana_filter_sensitivity')):
1459
+ last_result = ([], engine_index)
1460
+ engine_change_handler_name(get_ocr_ocr1())
1461
+ config_check_thread.add_callback(handle_config_changes)
1462
+
1350
1463
  while not terminated:
1351
1464
  ocr_start_time = datetime.now()
1352
1465
  start_time = time.time()
@@ -1361,7 +1474,7 @@ def run(read_from=None,
1361
1474
  pass
1362
1475
 
1363
1476
  if (not img) and process_screenshots:
1364
- if (not paused) and (not screenshot_thread or (screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible)) and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
1477
+ if (not paused) and (not screenshot_thread or (screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible)) and (time.time() - last_screenshot_time) > get_ocr_scan_rate():
1365
1478
  screenshot_event.set()
1366
1479
  img = periodic_screenshot_queue.get()
1367
1480
  filter_img = True
@@ -1374,7 +1487,7 @@ def run(read_from=None,
1374
1487
  break
1375
1488
  elif img:
1376
1489
  if filter_img:
1377
- res, _ = process_and_write_results(img, write_to, last_result, filtering, notify, ocr_start_time=ocr_start_time, furigana_filter_sensitivity=furigana_filter_sensitivity)
1490
+ res, _ = process_and_write_results(img, write_to, last_result, filtering, notify, ocr_start_time=ocr_start_time, furigana_filter_sensitivity=get_ocr_furigana_filter_sensitivity())
1378
1491
  if res:
1379
1492
  last_result = (res, engine_index)
1380
1493
  else:
@@ -1398,8 +1511,10 @@ def run(read_from=None,
1398
1511
  directory_watcher_thread.join()
1399
1512
  if unix_socket_server:
1400
1513
  unix_socket_server.shutdown()
1401
- unix_socket_server_thread.join()
1514
+ unix_socket_server.join()
1402
1515
  if screenshot_thread:
1403
1516
  screenshot_thread.join()
1404
1517
  if key_combo_listener:
1405
1518
  key_combo_listener.stop()
1519
+ if config_check_thread:
1520
+ config_check_thread.join()
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import List, Optional
5
5
  from dataclasses_json import dataclass_json
6
6
 
7
- from GameSentenceMiner.util.configuration import get_app_directory
7
+ from GameSentenceMiner.util.configuration import get_app_directory, logger
8
8
 
9
9
 
10
10
  # @dataclass_json
@@ -42,12 +42,26 @@ class VNConfig:
42
42
  @dataclass_json
43
43
  @dataclass
44
44
  class OCRConfig:
45
- twoPassOCR: bool = False
45
+ twoPassOCR: bool = True
46
+ optimize_second_scan: bool = True
46
47
  ocr1: str = "oneOCR"
47
48
  ocr2: str = "glens"
48
49
  window_name: str = ""
49
- requiresOpenWindow: Optional[bool] = None
50
- scanRate: Optional[float] = None
50
+ language: str = "ja"
51
+ ocr_screenshots: bool = False
52
+ furigana_filter_sensitivity: int = 0
53
+ manualOcrHotkey: str = "Ctrl+Shift+G"
54
+ areaSelectOcrHotkey: str = "Ctrl+Shift+O"
55
+ sendToClipboard: bool = True
56
+ scanRate: float = 0.5
57
+ requiresOpenWindow: bool = False
58
+ useWindowForConfig: bool = False
59
+ lastWindowSelected: str = ""
60
+ keep_newline: bool = False
61
+ useObsAsSource: bool = False
62
+
63
+ def has_changed(self, other: 'OCRConfig') -> bool:
64
+ return self.to_dict() != other.to_dict()
51
65
 
52
66
  @dataclass_json
53
67
  @dataclass
@@ -66,15 +80,21 @@ class StoreConfig:
66
80
 
67
81
  class Store:
68
82
  def __init__(self, config_path=os.path.join(get_app_directory(), "electron", "config.json"), defaults: Optional[StoreConfig] = None):
83
+ self.data: StoreConfig = StoreConfig()
69
84
  self.config_path = config_path
70
85
  self.defaults = defaults if defaults is not None else StoreConfig()
71
86
  self._load_config()
72
87
 
73
88
  def _load_config(self):
74
89
  if os.path.exists(self.config_path):
75
- with open(self.config_path, 'r', encoding='utf-8') as f:
76
- data = json.load(f)
77
- self.data = StoreConfig.from_dict(data)
90
+ while True:
91
+ try:
92
+ with open(self.config_path, 'r', encoding='utf-8') as f:
93
+ data = json.load(f)
94
+ self.data = StoreConfig.from_dict(data)
95
+ break
96
+ except (json.JSONDecodeError, IOError) as e:
97
+ logger.debug(f"File being written to: {e}. Retrying...")
78
98
  else:
79
99
  self.data = self.defaults
80
100
  self._save_config()
@@ -83,6 +103,9 @@ class Store:
83
103
  with open(self.config_path, 'w', encoding='utf-8') as f:
84
104
  json.dump(self.data.to_dict(), f, indent=4)
85
105
 
106
+ def reload_config(self):
107
+ self._load_config()
108
+
86
109
  def get(self, key, default=None):
87
110
  keys = key.split('.')
88
111
  value = self.data
@@ -127,189 +150,107 @@ class Store:
127
150
  print(json.dumps(self.data.to_dict(), indent=4))
128
151
 
129
152
  # Initialize the store
130
- store = Store(config_path=os.path.join(get_app_directory(), "electron", "config.json"), defaults=StoreConfig())
131
-
132
- # --- Convenience functions ---
133
-
134
- def get_auto_update_gsm_app() -> bool:
135
- return store.get("autoUpdateGSMApp")
136
-
137
- def set_auto_update_gsm_app(auto_update: bool):
138
- store.set("autoUpdateGSMApp", auto_update)
139
-
140
- def get_auto_update_electron() -> bool:
141
- return store.get("autoUpdateElectron")
142
-
143
- def set_auto_update_electron(auto_update: bool):
144
- store.set("autoUpdateElectron", auto_update)
145
-
146
- def get_python_path() -> str:
147
- return store.get("pythonPath")
148
-
149
- def set_python_path(path: str):
150
- store.set("pythonPath", path)
151
-
152
- # OCR
153
-
154
- def get_ocr_config() -> OCRConfig:
155
- ocr_data = store.get("OCR")
156
- return ocr_data if isinstance(ocr_data, OCRConfig) else OCRConfig.from_dict(ocr_data) if isinstance(ocr_data, dict) else OCRConfig()
157
-
158
- def set_ocr_config(config: OCRConfig):
159
- store.set("OCR", config)
160
-
161
- def get_two_pass_ocr() -> bool:
162
- return store.get("OCR.twoPassOCR")
163
-
164
- def set_two_pass_ocr(two_pass: bool):
165
- store.set("OCR.twoPassOCR", two_pass)
166
-
167
- def get_ocr1() -> str:
168
- return store.get("OCR.ocr1")
169
-
170
- def set_ocr1(ocr: str):
171
- store.set("OCR.ocr1", ocr)
172
-
173
- def get_ocr2() -> str:
174
- return store.get("OCR.ocr2")
175
-
176
- def set_ocr2(ocr: str):
177
- store.set("OCR.ocr2", ocr)
178
-
179
- def get_window_name() -> str:
180
- return store.get("OCR.window_name")
181
-
182
- def set_window_name(name: str):
183
- store.set("OCR.window_name", name)
184
-
185
- def get_requires_open_window() -> Optional[bool]:
186
- return store.get("OCR.requiresOpenWindow")
187
-
188
- def set_requires_open_window(requires_open_window: Optional[bool]):
189
- store.set("OCR.requiresOpenWindow", requires_open_window)
190
-
191
- def get_ocr_scan_rate() -> Optional[int]:
192
- return store.get("OCR.scanRate")
193
-
194
- def set_ocr_scan_rate(scan_rate: Optional[int]):
195
- store.set("OCR.scanRate", scan_rate)
196
-
197
- # Yuzu config getters and setters
198
- def get_yuzu_config() -> YuzuConfig:
199
- yuzu_data = store.get('yuzu')
200
- return yuzu_data if isinstance(yuzu_data, YuzuConfig) else YuzuConfig.from_dict(yuzu_data) if isinstance(yuzu_data, dict) else YuzuConfig()
201
-
202
- def set_yuzu_config(config: YuzuConfig):
203
- store.set('yuzu', config)
204
-
205
-
206
- # Yuzu emulator path getters and setters
207
- def get_yuzu_emu_path() -> str:
208
- return store.get('yuzu.emuPath')
209
-
210
- def set_yuzu_emu_path(path: str):
211
- store.set('yuzu.emuPath', path)
212
-
213
- # Yuzu ROMs path getters and setters
214
- def get_yuzu_roms_path() -> str:
215
- return store.get('yuzu.romsPath')
216
-
217
- def set_yuzu_roms_path(path: str):
218
- store.set('yuzu.romsPath', path)
219
-
220
- def get_launch_yuzu_game_on_start() -> str:
221
- return store.get("yuzu.launchGameOnStart")
222
-
223
- def set_launch_yuzu_game_on_start(path: str):
224
- store.set("yuzu.launchGameOnStart", path)
225
-
226
- def get_last_yuzu_game_launched() -> str:
227
- return store.get("yuzu.lastGameLaunched")
228
-
229
- def set_last_yuzu_game_launched(path: str):
230
- store.set("yuzu.lastGameLaunched", path)
231
-
232
- # Agent scripts path getters and setters
233
- def get_agent_scripts_path() -> str:
234
- return store.get('agentScriptsPath')
235
-
236
- def set_agent_scripts_path(path: str):
237
- store.set('agentScriptsPath', path)
238
-
239
- def set_agent_path(path: str):
240
- store.set('agentPath', path)
241
-
242
- def get_agent_path() -> str:
243
- return store.get('agentPath')
244
-
245
- def get_start_console_minimized() -> bool:
246
- return store.get("startConsoleMinimized")
247
-
248
- def set_start_console_minimized(should_minimize: bool):
249
- store.set("startConsoleMinimized", should_minimize)
250
-
251
- def get_vns() -> List[str]:
252
- return store.get('VN.vns')
253
-
254
- def set_vns(vns: List[str]):
255
- store.set('VN.vns', vns)
256
-
257
- def get_textractor_path() -> str:
258
- return store.get("VN.textractorPath")
259
-
260
- def set_textractor_path(path: str):
261
- store.set("VN.textractorPath", path)
262
-
263
- def get_launch_vn_on_start() -> str:
264
- return store.get("VN.launchVNOnStart")
265
-
266
- def set_launch_vn_on_start(vn: str):
267
- store.set("VN.launchVNOnStart", vn)
268
-
269
- def get_last_vn_launched() -> str:
270
- return store.get("VN.lastVNLaunched")
271
-
272
- def set_last_vn_launched(vn: str):
273
- store.set("VN.lastVNLaunched", vn)
274
-
275
- def get_steam_path() -> str:
276
- return store.get('steam.steamPath')
277
-
278
- def set_steam_path(path: str):
279
- store.set('steam.steamPath', path)
280
-
281
- def get_launch_steam_on_start() -> int:
282
- return store.get('steam.launchSteamOnStart')
283
-
284
- def set_launch_steam_on_start(game_id: int):
285
- store.set('steam.launchSteamOnStart', game_id)
286
-
287
- def get_last_steam_game_launched() -> int:
288
- return store.get('steam.lastGameLaunched')
289
-
290
- def set_last_steam_game_launched(game_id: int):
291
- store.set('steam.lastGameLaunched', game_id)
292
-
293
- # def get_steam_games() -> List[SteamGame]:
294
- # steam_games_data = store.get('steam.steamGames')
295
- # return [SteamGame.from_dict(game_data) for game_data in steam_games_data] if isinstance(steam_games_data, list) else []
296
-
297
- # def set_steam_games(games: List[SteamGame]):
298
- # store.set('steam.steamGames', [game.to_dict() for game in games])
299
-
300
- # if __name__ == "__main__":
301
- # # Example usage:
302
- # print(f"Initial Yuzu Emulator Path: {get_yuzu_emu_path()}")
303
- # set_yuzu_emu_path("D:\\NewEmulators\\yuzu\\yuzu.exe")
304
- # print(f"Updated Yuzu Emulator Path: {get_yuzu_emu_path()}")
305
- #
306
- # ocr_config = get_ocr_config()
307
- # print(f"Initial Two-Pass OCR: {ocr_config.twoPassOCR}")
308
- # set_two_pass_ocr(True)
309
- # print(f"Updated Two-Pass OCR: {get_two_pass_ocr()}")
310
- #
311
- # steam_games = get_steam_games()
312
- # print(f"Initial Steam Games: {[game.name for game in steam_games]}")
313
- # new_games = [SteamGame(123, "Game One"), SteamGame(456, "Game Two")]
314
- # set_steam_games(new_games)
315
- # print(f"Updated Steam Games: {[game.name for game in get_steam_games()]}")
153
+ electron_store = Store(config_path=os.path.join(get_app_directory(), "electron", "config.json"), defaults=StoreConfig())
154
+
155
+
156
+ # def has_section_changed(section_class: type) -> bool:
157
+ # global electron_store
158
+ # # Get the attribute name from the class (e.g. OCRConfig -> OCR)
159
+ # section_name = None
160
+ # for attr, value in StoreConfig.__dataclass_fields__.items():
161
+ # if value.type == section_class:
162
+ # section_name = attr
163
+ # break
164
+ # if not section_name:
165
+ # return False
166
+ # if not os.path.exists(electron_store.config_path):
167
+ # return False
168
+ # with open(electron_store.config_path, 'r', encoding='utf-8') as f:
169
+ # data = json.load(f)
170
+ # current = StoreConfig.from_dict(data)
171
+ # current_section = getattr(current, section_name)
172
+ # old_section = getattr(electron_store, section_name)
173
+ # if hasattr(current_section, 'to_dict') and hasattr(old_section, 'to_dict'):
174
+ # return current_section.to_dict() != old_section.to_dict()
175
+ # electron_store = Store(config_path=electron_store.config_path)
176
+ # return True
177
+
178
+ # Helper Methods
179
+ def get_electron_store() -> Store:
180
+ global electron_store
181
+ return electron_store
182
+
183
+ def get_ocr_two_pass_ocr():
184
+ return electron_store.data.OCR.twoPassOCR
185
+
186
+ def get_ocr_optimize_second_scan():
187
+ return electron_store.data.OCR.optimize_second_scan
188
+
189
+ def get_ocr_ocr1():
190
+ return electron_store.data.OCR.ocr1
191
+
192
+ def get_ocr_ocr2():
193
+ return electron_store.data.OCR.ocr2
194
+
195
+ def get_ocr_window_name():
196
+ return electron_store.data.OCR.window_name or ""
197
+
198
+ def get_ocr_language():
199
+ return electron_store.data.OCR.language or "ja"
200
+
201
+ def get_ocr_ocr_screenshots():
202
+ return electron_store.data.OCR.ocr_screenshots
203
+
204
+ def get_ocr_furigana_filter_sensitivity():
205
+ return electron_store.data.OCR.furigana_filter_sensitivity
206
+
207
+ def get_ocr_manual_ocr_hotkey():
208
+ return electron_store.data.OCR.manualOcrHotkey
209
+
210
+ def get_ocr_area_select_ocr_hotkey():
211
+ return electron_store.data.OCR.areaSelectOcrHotkey
212
+
213
+ def get_ocr_send_to_clipboard():
214
+ return electron_store.data.OCR.sendToClipboard
215
+
216
+ def get_ocr_scan_rate():
217
+ return electron_store.data.OCR.scanRate
218
+
219
+ def get_ocr_requires_open_window():
220
+ return electron_store.data.OCR.requiresOpenWindow
221
+
222
+ def get_ocr_use_window_for_config():
223
+ return electron_store.data.OCR.useWindowForConfig
224
+
225
+ def get_ocr_last_window_selected():
226
+ return electron_store.data.OCR.lastWindowSelected
227
+
228
+ def get_ocr_keep_newline():
229
+ return electron_store.data.OCR.keep_newline
230
+
231
+ def get_ocr_use_obs_as_source():
232
+ return electron_store.data.OCR.useObsAsSource
233
+
234
+ def has_ocr_config_changed() -> bool:
235
+ global electron_store
236
+ if not os.path.exists(electron_store.config_path):
237
+ return False, {}
238
+ with open(electron_store.config_path, 'r', encoding='utf-8') as f:
239
+ data = json.load(f)
240
+ current = StoreConfig.from_dict(data)
241
+ current_section = current.OCR
242
+ old_section = electron_store.data.OCR
243
+ if not (hasattr(current_section, 'to_dict') and hasattr(old_section, 'to_dict')):
244
+ return False, {}
245
+ current_dict = current_section.to_dict()
246
+ old_dict = old_section.to_dict()
247
+ if current_dict != old_dict:
248
+ changes = {k: (old_dict[k], current_dict[k]) for k in current_dict if old_dict.get(k) != current_dict.get(k)}
249
+ # logger.info(f"OCR Config changes detected: {changes}")
250
+ return True, changes
251
+ return False, {}
252
+
253
+ def reload_electron_config():
254
+ global electron_store
255
+ electron_store.reload_config()
256
+ return electron_store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.11.5
3
+ Version: 2.11.7
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -23,7 +23,8 @@ Requires-Dist: rapidfuzz~=3.9.7
23
23
  Requires-Dist: plyer~=2.1.0
24
24
  Requires-Dist: keyboard~=0.13.5
25
25
  Requires-Dist: websockets~=15.0.1
26
- Requires-Dist: stable-ts~=2.19.0
26
+ Requires-Dist: openai-whisper
27
+ Requires-Dist: stable-ts-whisperless
27
28
  Requires-Dist: silero-vad~=5.1.2
28
29
  Requires-Dist: ttkbootstrap~=1.10.1
29
30
  Requires-Dist: dataclasses_json~=0.6.7
@@ -2,8 +2,8 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
2
2
  GameSentenceMiner/anki.py,sha256=3BVFXAM7tpJAxHMbsMpnMHUoDfyqHQ1JSYJThW18QWA,16846
3
3
  GameSentenceMiner/config_gui.py,sha256=QTK1yBDcfHaIUR_JyekkRQY9CVI_rh3Cae0bi7lviIo,99198
4
4
  GameSentenceMiner/gametext.py,sha256=6VkjmBeiuZfPk8T6PHFdIAElBH2Y_oLVYvmcafqN7RM,6747
5
- GameSentenceMiner/gsm.py,sha256=wTERcvG37SeDel51TCFusoQqk5B_b11YY4QZMTF0a6s,24954
6
- GameSentenceMiner/obs.py,sha256=rapxY9PTDczGr7e8_41hVuD5VoRExe3IFFbSWZcYDsQ,15470
5
+ GameSentenceMiner/gsm.py,sha256=qVHxnvly-yJ85v9RAxsGN2MqZxU-C1JA5wSRxVxMPMg,24950
6
+ GameSentenceMiner/obs.py,sha256=450jRo2jeOMtlDJN3doNT2wJ5z5YQ0entamxdaLE8mo,15472
7
7
  GameSentenceMiner/vad.py,sha256=Xj_9TM0fiaz9K8JcmW0QqGYASFnPEmYepsTHQrxP38c,18711
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  GameSentenceMiner/ai/ai_prompting.py,sha256=ojp7i_xg2YB1zALgFbivwtXPMVkThnSbPoUiAs-nz_g,25892
@@ -16,22 +16,22 @@ GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB
16
16
  GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
17
17
  GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
18
18
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=76IuoOMsBxNvU8z8lixqz58YSZpenNVugnHjrUXgCf4,4963
19
+ GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ezj-0k6Wo-una91FvYhMp6KGkRhWYihXzLAoh_Wu2xY,5329
20
20
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
21
- GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Aj6t-cCePPeYNSF-XxQKo2gVNWmWqK3f3qR-0vxdtuE,25523
22
- GameSentenceMiner/ocr/owocr_helper.py,sha256=sxmZcori9_ujldclwQFpmMwTyfJyflAQ3mn_3BvIdQs,22888
21
+ GameSentenceMiner/ocr/owocr_area_selector.py,sha256=O8qKOTDglk-D4N-2_ORLeZacXT-OVOCNxUI8sQHAlx4,25538
22
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=3sJW3SmXghUKwCFIHtweEq0t42jcBOyBhFbIHTvxYwc,25301
23
23
  GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
24
24
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
25
25
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
26
26
  GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
27
27
  GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
28
- GameSentenceMiner/owocr/owocr/ocr.py,sha256=xAhqCfVY2xKKvUhskAiAaYiL3yQrAl8oYi5GU46NOgI,59392
29
- GameSentenceMiner/owocr/owocr/run.py,sha256=824KFS5v3c4ZLx7RYafBOezvFmnB4Idexf4mJAJhfp8,61100
28
+ GameSentenceMiner/owocr/owocr/ocr.py,sha256=z0w7kcPjXvFabMQTWaQyiBehxmjeIVaS2p53yvFyPbg,59707
29
+ GameSentenceMiner/owocr/owocr/run.py,sha256=p7DBHTbhey1DeW1SRqNQ5-y3H4Cq2zoMPCMED5C0Rws,65945
30
30
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
31
31
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  GameSentenceMiner/util/audio_offset_selector.py,sha256=8Stk3BP-XVIuzRv9nl9Eqd2D-1yD3JrgU-CamBywJmY,8542
33
33
  GameSentenceMiner/util/configuration.py,sha256=vwBehuEP2QVvbsyJx5x1EPXZEOtGsqeIQjlEES0vVdQ,28999
34
- GameSentenceMiner/util/electron_config.py,sha256=3VmIrcXhC-wIMMc4uqV85NrNenRl4ZUbnQfSjWEwuig,9852
34
+ GameSentenceMiner/util/electron_config.py,sha256=8LZwl-T_uF5z_ig-IZcm9QI-VKaD7zaHX9u6MaLYuo4,8648
35
35
  GameSentenceMiner/util/ffmpeg.py,sha256=t0tflxq170n8PZKkdw8fTZIUQfXD0p_qARa9JTdhBTc,21530
36
36
  GameSentenceMiner/util/gsm_utils.py,sha256=iRyLVcodMptRhkCzLf3hyqc6_RCktXnwApi6mLju6oQ,11565
37
37
  GameSentenceMiner/util/model.py,sha256=AaOzgqSbaN7yks_rr1dQpLQR45FpBYdoLebMbrIYm34,6638
@@ -63,9 +63,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
63
63
  GameSentenceMiner/web/templates/index.html,sha256=Gv3CJvNnhAzIVV_QxhNq4OD-pXDt1vKCu9k6WdHSXuA,215343
64
64
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
65
65
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
66
- gamesentenceminer-2.11.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
67
- gamesentenceminer-2.11.5.dist-info/METADATA,sha256=xaKLwY8WkbwAYG_32q_mg8xeJnuT5S778I0CyIgonXo,7319
68
- gamesentenceminer-2.11.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- gamesentenceminer-2.11.5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
70
- gamesentenceminer-2.11.5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
71
- gamesentenceminer-2.11.5.dist-info/RECORD,,
66
+ gamesentenceminer-2.11.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
67
+ gamesentenceminer-2.11.7.dist-info/METADATA,sha256=bJTODm-zidIxVgTh8e7jE1IPLn1NFv39ReZpda0JQ68,7353
68
+ gamesentenceminer-2.11.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
+ gamesentenceminer-2.11.7.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
70
+ gamesentenceminer-2.11.7.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
71
+ gamesentenceminer-2.11.7.dist-info/RECORD,,