GameSentenceMiner 2.14.20__tar.gz → 2.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/config_gui.py +3 -0
  2. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/gsm.py +92 -81
  3. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/obs.py +18 -5
  4. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -1
  5. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/owocr_helper.py +0 -1
  6. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/ocr.py +24 -22
  7. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/run.py +102 -22
  8. gamesentenceminer-2.15.0/GameSentenceMiner/tools/furigana_filter_preview.py +330 -0
  9. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/get_overlay_coords.py +81 -48
  10. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/vad.py +2 -2
  11. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/PKG-INFO +2 -2
  12. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/SOURCES.txt +1 -0
  13. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/PKG-INFO +2 -2
  14. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/README.md +1 -1
  15. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/pyproject.toml +1 -1
  16. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/__init__.py +0 -0
  17. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ai/__init__.py +0 -0
  18. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  19. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/anki.py +0 -0
  20. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/__init__.py +0 -0
  21. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon.png +0 -0
  22. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon128.png +0 -0
  23. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon256.png +0 -0
  24. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon32.png +0 -0
  25. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon512.png +0 -0
  26. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/icon64.png +0 -0
  27. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/assets/pickaxe.png +0 -0
  28. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/gametext.py +0 -0
  29. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/locales/en_us.json +0 -0
  30. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/locales/ja_jp.json +0 -0
  31. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/locales/zh_cn.json +0 -0
  32. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/__init__.py +0 -0
  33. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  34. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  35. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  36. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  37. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  38. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  39. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  40. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  41. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/tools/__init__.py +0 -0
  42. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  43. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/tools/ss_selector.py +0 -0
  44. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/tools/window_transparency.py +0 -0
  45. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/__init__.py +0 -0
  46. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/communication/__init__.py +0 -0
  47. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/communication/send.py +0 -0
  48. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/communication/websocket.py +0 -0
  49. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/configuration.py +0 -0
  50. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/db.py +0 -0
  51. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  52. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  53. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  54. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  55. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/electron_config.py +0 -0
  56. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/ffmpeg.py +0 -0
  57. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/gsm_utils.py +0 -0
  58. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/model.py +0 -0
  59. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/notification.py +0 -0
  60. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/text_log.py +0 -0
  61. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  62. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  63. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/__init__.py +0 -0
  64. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/service.py +0 -0
  65. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/__init__.py +0 -0
  66. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  67. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  68. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/favicon.ico +0 -0
  69. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/favicon.svg +0 -0
  70. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  71. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/style.css +0 -0
  72. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  73. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  74. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/templates/__init__.py +0 -0
  75. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/templates/index.html +0 -0
  76. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  77. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/templates/utility.html +0 -0
  78. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/web/texthooking_page.py +0 -0
  79. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner/wip/__init___.py +0 -0
  80. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  81. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  82. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/requires.txt +0 -0
  83. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  84. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/LICENSE +0 -0
  85. {gamesentenceminer-2.14.20 → gamesentenceminer-2.15.0}/setup.cfg +0 -0
@@ -2192,6 +2192,9 @@ class ConfigApp:
2192
2192
  row=self.current_row, column=0)
2193
2193
  self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors, state="readonly")
2194
2194
  self.overlay_monitor.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2195
+ # disable selection for now, default to value 1
2196
+ self.overlay_monitor.current(1)
2197
+ self.overlay_monitor.config(state="disabled")
2195
2198
  self.current_row += 1
2196
2199
 
2197
2200
  if self.monitors:
@@ -123,8 +123,6 @@ if is_windows():
123
123
  procs_to_close = []
124
124
  settings_window: config_gui.ConfigApp = None
125
125
  obs_paused = False
126
- icon: Icon
127
- menu: Menu
128
126
  root = None
129
127
  warnings.simplefilter("ignore", DeprecationWarning)
130
128
 
@@ -401,83 +399,94 @@ def open_log():
401
399
  logger.info("Log opened.")
402
400
 
403
401
 
404
- def exit_program(passed_icon, item):
405
- """Exit the application."""
406
- if not passed_icon:
407
- passed_icon = icon
408
- logger.info("Exiting...")
409
- passed_icon.stop()
410
- cleanup()
402
+ def open_multimine(icon, item):
403
+ texthooking_page.open_texthooker()
411
404
 
412
405
 
413
- def play_pause(icon, item):
414
- global obs_paused, menu
415
- obs.toggle_replay_buffer()
416
- update_icon()
406
+ def exit_program(passed_icon, item):
407
+ """Exit the application."""
408
+ if not passed_icon:
409
+ passed_icon = icon
410
+ logger.info("Exiting...")
411
+ passed_icon.stop()
412
+ cleanup()
417
413
 
414
+ class GSMTray(threading.Thread):
415
+ def __init__(self):
416
+ super().__init__()
417
+ self.daemon = True
418
+ self.menu = None
419
+ self.icon = None
420
+
421
+ def run(self):
422
+ self.run_tray()
423
+
424
+
425
+ def run_tray(self):
426
+ self.profile_menu = Menu(
427
+ *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, self.switch_profile) for
428
+ profile in
429
+ get_master_config().get_all_profile_names()]
430
+ )
431
+
432
+ menu = Menu(
433
+ MenuItem("Open Settings", open_settings, default=True),
434
+ MenuItem("Open Texthooker", texthooking_page.open_texthooker),
435
+ MenuItem("Open Log", open_log),
436
+ MenuItem("Toggle Replay Buffer", self.play_pause),
437
+ MenuItem("Restart OBS", restart_obs),
438
+ MenuItem("Switch Profile", self.profile_menu),
439
+ MenuItem("Exit", exit_program)
440
+ )
441
+
442
+ self.icon = Icon("TrayApp", create_image(), "GameSentenceMiner", menu)
443
+ self.icon.run()
444
+
445
+ def update_icon(self, profile=None):
446
+ global menu, icon
447
+ # Recreate the menu with the updated button text
448
+ profile_menu = Menu(
449
+ *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, self.switch_profile) for
450
+ profile in
451
+ get_master_config().get_all_profile_names()]
452
+ )
453
+
454
+ menu = Menu(
455
+ MenuItem("Open Settings", open_settings, default=True),
456
+ MenuItem("Open Multi-Mine GUI", open_multimine),
457
+ MenuItem("Open Log", open_log),
458
+ MenuItem("Toggle Replay Buffer", self.play_pause),
459
+ MenuItem("Restart OBS", restart_obs),
460
+ MenuItem("Switch Profile", profile_menu),
461
+ MenuItem("Exit", exit_program)
462
+ )
463
+
464
+ self.icon.menu = menu
465
+ self.icon.update_menu()
466
+
467
+ def switch_profile(self, icon, item):
468
+ if "Active:" in item.text:
469
+ logger.error("You cannot switch to the currently active profile!")
470
+ return
471
+ logger.info(f"Switching to profile: {item.text}")
472
+ prev_config = get_config()
473
+ get_master_config().current_profile = item.text
474
+ switch_profile_and_save(item.text)
475
+ settings_window.reload_settings()
476
+ self.update_icon()
477
+ if get_config().restart_required(prev_config):
478
+ send_restart_signal()
418
479
 
419
- def open_multimine(icon, item):
420
- texthooking_page.open_texthooker()
480
+ def play_pause(self, icon, item):
481
+ global obs_paused, menu
482
+ obs.toggle_replay_buffer()
483
+ self.update_icon()
421
484
 
485
+ def stop(self):
486
+ if self.icon:
487
+ self.icon.stop()
422
488
 
423
- def update_icon(profile=None):
424
- global menu, icon
425
- # Recreate the menu with the updated button text
426
- profile_menu = Menu(
427
- *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for
428
- profile in
429
- get_master_config().get_all_profile_names()]
430
- )
431
-
432
- menu = Menu(
433
- MenuItem("Open Settings", open_settings, default=True),
434
- MenuItem("Open Multi-Mine GUI", open_multimine),
435
- MenuItem("Open Log", open_log),
436
- MenuItem("Toggle Replay Buffer", play_pause),
437
- MenuItem("Restart OBS", restart_obs),
438
- MenuItem("Switch Profile", profile_menu),
439
- MenuItem("Exit", exit_program)
440
- )
441
-
442
- icon.menu = menu
443
- icon.update_menu()
444
-
445
-
446
- def switch_profile(icon, item):
447
- if "Active:" in item.text:
448
- logger.error("You cannot switch to the currently active profile!")
449
- return
450
- logger.info(f"Switching to profile: {item.text}")
451
- prev_config = get_config()
452
- get_master_config().current_profile = item.text
453
- switch_profile_and_save(item.text)
454
- settings_window.reload_settings()
455
- update_icon()
456
- if get_config().restart_required(prev_config):
457
- send_restart_signal()
458
-
459
-
460
- def run_tray():
461
- global menu, icon
462
-
463
- profile_menu = Menu(
464
- *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for
465
- profile in
466
- get_master_config().get_all_profile_names()]
467
- )
468
-
469
- menu = Menu(
470
- MenuItem("Open Settings", open_settings, default=True),
471
- MenuItem("Open Texthooker", texthooking_page.open_texthooker),
472
- MenuItem("Open Log", open_log),
473
- MenuItem("Toggle Replay Buffer", play_pause),
474
- MenuItem("Restart OBS", restart_obs),
475
- MenuItem("Switch Profile", profile_menu),
476
- MenuItem("Exit", exit_program)
477
- )
478
-
479
- icon = Icon("TrayApp", create_image(), "GameSentenceMiner", menu)
480
- icon.run()
489
+ gsm_tray = GSMTray()
481
490
 
482
491
 
483
492
  # def close_obs():
@@ -551,8 +560,8 @@ def cleanup():
551
560
  proc.kill()
552
561
  logger.error(f"Error terminating process {proc}: {e}")
553
562
 
554
- if icon:
555
- icon.stop()
563
+ if gsm_tray:
564
+ gsm_tray.stop()
556
565
 
557
566
  for video in gsm_state.videos_to_remove:
558
567
  try:
@@ -608,7 +617,7 @@ def initialize(reloading=False):
608
617
 
609
618
 
610
619
  def initialize_async():
611
- tasks = [connect_websocket, run_tray]
620
+ tasks = [connect_websocket]
612
621
  threads = []
613
622
  tasks.append(anki.start_monitoring_anki)
614
623
  for task in tasks:
@@ -633,11 +642,12 @@ def handle_websocket_message(message: Message):
633
642
  case FunctionName.OPEN_LOG:
634
643
  open_log()
635
644
  case FunctionName.TOGGLE_REPLAY_BUFFER:
636
- play_pause(None, None)
645
+ obs.toggle_replay_buffer()
637
646
  case FunctionName.RESTART_OBS:
638
647
  restart_obs()
639
648
  case FunctionName.EXIT:
640
- exit_program(None, None)
649
+ cleanup()
650
+ sys.exit(0)
641
651
  case FunctionName.CONNECT:
642
652
  logger.debug("Electron WSS connected")
643
653
  case _:
@@ -671,7 +681,7 @@ async def register_scene_switcher_callback():
671
681
  all_configured_scenes = [
672
682
  config.scenes for config in get_master_config().configs.values()]
673
683
  print(all_configured_scenes)
674
- matching_configs = [name.strip() for name, config in config_instance.configs.items(
684
+ matching_configs = [name.strip() for name, config in get_master_config().configs.items(
675
685
  ) if scene.strip() in config.scenes]
676
686
  switch_to = None
677
687
 
@@ -692,7 +702,7 @@ async def register_scene_switcher_callback():
692
702
  get_master_config().current_profile = switch_to
693
703
  switch_profile_and_save(switch_to)
694
704
  settings_window.reload_settings()
695
- update_icon()
705
+ gsm_tray.update_icon()
696
706
 
697
707
  await obs.register_scene_change_callback(scene_switcher_callback)
698
708
 
@@ -763,7 +773,8 @@ async def async_main(reloading=False):
763
773
  try:
764
774
  if get_config().general.open_config_on_startup:
765
775
  root.after(50, settings_window.show)
766
- settings_window.add_save_hook(update_icon)
776
+ root.after(50, gsm_tray.start)
777
+ settings_window.add_save_hook(gsm_tray.update_icon)
767
778
  settings_window.on_exit = exit_program
768
779
  root.mainloop()
769
780
  except KeyboardInterrupt:
@@ -1,16 +1,17 @@
1
1
  import asyncio
2
+ import json
2
3
  import os.path
3
4
  import subprocess
4
5
  import threading
5
6
  import time
6
- from pprint import pprint
7
+ import logging
7
8
 
8
9
  import psutil
9
10
 
10
11
  import obsws_python as obs
11
12
 
12
13
  from GameSentenceMiner.util import configuration
13
- from GameSentenceMiner.util.configuration import *
14
+ from GameSentenceMiner.util.configuration import get_app_directory, get_config, get_master_config, is_windows, save_full_config, reload_config, logger, gsm_status, gsm_state
14
15
  from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name
15
16
  import tkinter as tk
16
17
  from tkinter import messagebox
@@ -54,7 +55,7 @@ class OBSConnectionManager(threading.Thread):
54
55
  if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
55
56
  try:
56
57
  self.check_output()
57
- except Exception as e:
58
+ except Exception:
58
59
  pass
59
60
  except Exception as e:
60
61
  logger.error(f"Error when running Extra Utils in OBS Health Check, Keeping ConnectionManager Alive: {e}")
@@ -251,7 +252,6 @@ def connect_to_obs_sync(retry=2, check_output=True):
251
252
  logger.error(f"Failed to connect to OBS WebSocket: {e}")
252
253
  client = None
253
254
  event_client = None
254
- connecting = False
255
255
  break
256
256
  time.sleep(1)
257
257
  retry -= 1
@@ -483,6 +483,9 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
483
483
  try:
484
484
  # 1. Get the canvas (base) resolution from OBS video settings
485
485
  video_settings = client.get_video_settings()
486
+ if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
487
+ logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
488
+ return
486
489
  canvas_width = video_settings.base_width
487
490
  canvas_height = video_settings.base_height
488
491
 
@@ -499,12 +502,23 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
499
502
  item_id = item['sceneItemId']
500
503
  source_name = item['sourceName']
501
504
 
505
+ scene_item_transform = item.get('sceneItemTransform', {})
506
+
507
+ source_width = scene_item_transform.get('sourceWidth', None)
508
+ source_height = scene_item_transform.get('sourceHeight', None)
509
+
502
510
  # This transform object is the equivalent of "Fit to Screen"
503
511
  fit_to_screen_transform = {
504
512
  'boundsType': 'OBS_BOUNDS_SCALE_INNER',
505
513
  'alignment': 5, # 5 = Center alignment (horizontal and vertical)
506
514
  'boundsWidth': canvas_width,
507
515
  'boundsHeight': canvas_height,
516
+ 'positionX': 0,
517
+ 'positionY': 0,
518
+ 'cropLeft': 0 if canvas_width >= source_width else (source_width - canvas_width) // 2,
519
+ 'cropRight': 0 if canvas_width >= source_width else (source_width - canvas_width) // 2,
520
+ 'cropTop': 0 if canvas_height >= source_height else (source_height - canvas_height) // 2,
521
+ 'cropBottom': 0 if canvas_height >= source_height else (source_height - canvas_height) // 2,
508
522
  }
509
523
 
510
524
  try:
@@ -559,7 +573,6 @@ def main():
559
573
  disconnect_from_obs()
560
574
 
561
575
  if __name__ == '__main__':
562
- from mss import mss
563
576
  logging.basicConfig(level=logging.INFO)
564
577
  connect_to_obs_sync()
565
578
  set_fit_to_screen_for_scene_items(get_current_scene())
@@ -83,7 +83,6 @@ class OCRConfig:
83
83
  ]
84
84
 
85
85
  def scale_to_custom_size(self, width, height):
86
- print(self.pre_scale_rectangles)
87
86
  self.rectangles = self.pre_scale_rectangles.copy()
88
87
  if self.coordinate_system and self.coordinate_system == "percentage":
89
88
  for rectangle in self.rectangles:
@@ -228,7 +228,6 @@ def save_result_image(img, pre_crop_image=None):
228
228
 
229
229
  async def send_result(text, time):
230
230
  if text:
231
- text = do_text_replacements(text, OCR_REPLACEMENTS_FILE)
232
231
  if get_ocr_send_to_clipboard():
233
232
  import pyperclip
234
233
  pyperclip.copy(text)
@@ -106,6 +106,7 @@ def empty_post_process(text):
106
106
 
107
107
  def post_process(text, keep_blank_lines=False):
108
108
  import jaconv
109
+ text = text.replace("\"", "")
109
110
  if keep_blank_lines:
110
111
  text = '\n'.join([''.join(i.split()) for i in text.splitlines()])
111
112
  else:
@@ -354,8 +355,8 @@ class GoogleLens:
354
355
  response_proto = LensOverlayServerResponse().FromString(res.content)
355
356
  response_dict = response_proto.to_dict(betterproto.Casing.SNAKE)
356
357
 
357
- if os.path.exists(r"C:\Users\Beangate\GSM\Electron App\test"):
358
- with open(os.path.join(r"C:\Users\Beangate\GSM\Electron App\test", 'glens_response.json'), 'w', encoding='utf-8') as f:
358
+ if os.path.exists(r"C:\Users\Beangate\GSM\test"):
359
+ with open(os.path.join(r"C:\Users\Beangate\GSM\test", 'glens_response.json'), 'w', encoding='utf-8') as f:
359
360
  json.dump(response_dict, f, indent=4, ensure_ascii=False)
360
361
  res = ''
361
362
  text = response_dict['objects_response']['text']
@@ -377,16 +378,13 @@ class GoogleLens:
377
378
  res += 'BLANK_LINE'
378
379
  for line in paragraph['lines']:
379
380
  if furigana_filter_sensitivity:
381
+ line_width = line['geometry']['bounding_box']['width'] * img.width
382
+ line_height = line['geometry']['bounding_box']['height'] * img.height
380
383
  for word in line['words']:
381
384
  if self.punctuation_regex.findall(word['plain_text']):
382
385
  res += word['plain_text'] + word['text_separator']
383
386
  continue
384
- if 'geometry' not in word:
385
- res += word['plain_text'] + word['text_separator']
386
- continue
387
- word_width = word['geometry']['bounding_box']['width'] * img.width
388
- word_height = word['geometry']['bounding_box']['height'] * img.height
389
- if word_width > furigana_filter_sensitivity and word_height > furigana_filter_sensitivity:
387
+ if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
390
388
  res += word['plain_text'] + word['text_separator']
391
389
  else:
392
390
  skipped.extend(word['plain_text'])
@@ -394,7 +392,8 @@ class GoogleLens:
394
392
  else:
395
393
  for word in line['words']:
396
394
  res += word['plain_text'] + word['text_separator']
397
-
395
+ res += '\n'
396
+
398
397
  previous_line = paragraph
399
398
  res += '\n'
400
399
  # logger.info(
@@ -920,7 +919,7 @@ class OneOCR:
920
919
  self.regex = re.compile(
921
920
  r'[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u2C60-\u2C7F\uA720-\uA7FF\uAB30-\uAB6F]')
922
921
 
923
- def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False, multiple_crop_coords=False, return_one_box=True):
922
+ def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False, multiple_crop_coords=False, return_one_box=True, return_dict=False):
924
923
  lang = get_ocr_language()
925
924
  if furigana_filter_sensitivity != None:
926
925
  furigana_filter_sensitivity = get_furigana_filter_sensitivity()
@@ -940,6 +939,7 @@ class OneOCR:
940
939
  return (False, 'Invalid image provided')
941
940
  crop_coords = None
942
941
  crop_coords_list = []
942
+ ocr_resp = ''
943
943
  if sys.platform == 'win32':
944
944
  try:
945
945
  ocr_resp = self.model.recognize_pil(img)
@@ -959,17 +959,17 @@ class OneOCR:
959
959
  boxes = []
960
960
  if furigana_filter_sensitivity > 0:
961
961
  for line in filtered_lines:
962
+ line_x1, line_x2, line_x3, line_x4 = line['bounding_rect']['x1'], line['bounding_rect']['x2'], \
963
+ line['bounding_rect']['x3'], line['bounding_rect']['x4']
964
+ line_y1, line_y2, line_y3, line_y4 = line['bounding_rect']['y1'], line['bounding_rect']['y2'], \
965
+ line['bounding_rect']['y3'], line['bounding_rect']['y4']
966
+ line_width = max(line_x2 - line_x1, line_x3 - line_x4)
967
+ line_height = max(line_y3 - line_y1, line_y4 - line_y2)
962
968
  for char in line['words']:
963
969
  if self.punctuation_regex.findall(char['text']):
964
970
  res += char['text']
965
971
  continue
966
- x1, x2, x3, x4 = char['bounding_rect']['x1'], char['bounding_rect']['x2'], \
967
- char['bounding_rect']['x3'], char['bounding_rect']['x4']
968
- y1, y2, y3, y4 = char['bounding_rect']['y1'], char['bounding_rect']['y2'], \
969
- char['bounding_rect']['y3'], char['bounding_rect']['y4']
970
- width = max(x2 - x1, x3 - x4)
971
- height = max(y3 - y1, y4 - y2)
972
- if width > furigana_filter_sensitivity and height > furigana_filter_sensitivity:
972
+ if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
973
973
  res += char['text']
974
974
  else:
975
975
  skipped.extend(char for char in line['text'])
@@ -1042,6 +1042,8 @@ class OneOCR:
1042
1042
  x.append(crop_coords_list)
1043
1043
  if return_one_box:
1044
1044
  x.append(crop_coords)
1045
+ if return_dict:
1046
+ x.append(ocr_resp)
1045
1047
  if is_path:
1046
1048
  img.close()
1047
1049
  return x
@@ -1432,10 +1434,10 @@ class localLLMOCR:
1432
1434
  self.keep_warm = config.get('keep_warm', True)
1433
1435
  self.custom_prompt = config.get('prompt', None)
1434
1436
  self.available = True
1435
- # if any(x in self.api_url for x in ['localhost', '127.0.0.1']):
1436
- # if not self.check_connection(self.api_url):
1437
- # logger.warning('Local LLM OCR API is not reachable')
1438
- # return
1437
+ if any(x in self.api_url for x in ['localhost', '127.0.0.1']):
1438
+ if not self.check_connection(self.api_url):
1439
+ logger.warning('Local LLM OCR API is not reachable')
1440
+ return
1439
1441
  self.client = openai.OpenAI(
1440
1442
  base_url=self.api_url.replace('/v1/chat/completions', '/v1'),
1441
1443
  api_key=self.api_key
@@ -1492,7 +1494,7 @@ class localLLMOCR:
1492
1494
  prompt = self.custom_prompt.strip()
1493
1495
  else:
1494
1496
  prompt = f"""
1495
- Extract all {CommonLanguages.from_code(get_ocr_language()).name} Text from Image. Ignore all Furigana. Do not return any commentary, just the text in the image. If there is no text in the image, return "" (Empty String).
1497
+ Extract all {CommonLanguages.from_code(get_ocr_language()).name} Text from Image. Ignore all Furigana. Do not return any commentary, just the text in the image. Do not Translate. If there is no text in the image, return "" (Empty String).
1496
1498
  """
1497
1499
 
1498
1500
  response = self.client.chat.completions.create(
@@ -1,5 +1,6 @@
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 *
1
+ from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_scene_ocr_config
2
+ from GameSentenceMiner.util.electron_config import * # noqa: F403
3
+ from GameSentenceMiner.util.gsm_utils import do_text_replacements, OCR_REPLACEMENTS_FILE
3
4
 
4
5
  try:
5
6
  import win32gui
@@ -28,7 +29,6 @@ import signal
28
29
  import threading
29
30
  from pathlib import Path
30
31
  import queue
31
- import io
32
32
  import re
33
33
  import logging
34
34
  import inspect
@@ -39,23 +39,22 @@ import mss
39
39
  import asyncio
40
40
  import websockets
41
41
  import socketserver
42
- import queue
42
+ import cv2
43
+ import numpy as np
43
44
 
44
45
  from datetime import datetime, timedelta
45
- from PIL import Image, ImageDraw, UnidentifiedImageError
46
+ from PIL import Image, ImageDraw
46
47
  from loguru import logger
47
48
  from desktop_notifier import DesktopNotifierSync
48
49
  import psutil
49
50
 
50
- import inspect
51
- from .ocr import *
52
- try:
53
- from .secret import *
54
- except ImportError:
55
- pass
51
+ from .ocr import * # noqa: F403
56
52
  from .config import Config
57
53
  from .screen_coordinate_picker import get_screen_selection
58
- from GameSentenceMiner.util.configuration import get_temporary_directory, get_config
54
+ from GameSentenceMiner.util.configuration import get_temporary_directory
55
+
56
+ from skimage.metrics import structural_similarity as ssim
57
+ from typing import Union
59
58
 
60
59
  config = None
61
60
  last_image = None
@@ -799,8 +798,6 @@ class ScreenshotThread(threading.Thread):
799
798
  self.windows_window_tracker_instance.join()
800
799
 
801
800
 
802
- import cv2
803
- import numpy as np
804
801
 
805
802
  def apply_adaptive_threshold_filter(img):
806
803
  img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
@@ -865,11 +862,6 @@ def are_images_identical(img1, img2, img2_np=None):
865
862
  return (img1_np.shape == img2_np.shape) and np.array_equal(img1_np, img2_np)
866
863
 
867
864
 
868
- import cv2
869
- import numpy as np
870
- from skimage.metrics import structural_similarity as ssim
871
- from typing import Union
872
-
873
865
  ImageType = Union[np.ndarray, Image.Image]
874
866
 
875
867
  def _prepare_image(image: ImageType) -> np.ndarray:
@@ -1319,6 +1311,10 @@ def on_screenshot_combo():
1319
1311
  def on_window_minimized(minimized):
1320
1312
  global screencapture_window_visible
1321
1313
  screencapture_window_visible = not minimized
1314
+
1315
+
1316
+ def do_configured_ocr_replacements(text: str) -> str:
1317
+ return do_text_replacements(text, OCR_REPLACEMENTS_FILE)
1322
1318
 
1323
1319
 
1324
1320
  def process_and_write_results(img_or_path, write_to=None, last_result=None, filtering=None, notify=None, engine=None, ocr_start_time=None, furigana_filter_sensitivity=0):
@@ -1364,14 +1360,21 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
1364
1360
  # print(engine_index)
1365
1361
 
1366
1362
  if res:
1363
+ text = do_configured_ocr_replacements(text)
1367
1364
  if filtering:
1368
1365
  text, orig_text = filtering(text, last_result)
1369
1366
  if get_ocr_language() == "ja" or get_ocr_language() == "zh":
1370
1367
  text = post_process(text, keep_blank_lines=get_ocr_keep_newline())
1371
- logger.opt(ansi=True).info(
1372
- f'Text recognized in {end_time - start_time:0.03f}s using <{engine_color}>{engine_instance.readable_name}</{engine_color}>: {text}')
1373
1368
  if notify and config.get_general('notifications'):
1374
1369
  notifier.send(title='owocr', message='Text recognized: ' + text)
1370
+
1371
+ if text and write_to is not None:
1372
+ if check_text_is_all_menu(text, crop_coords):
1373
+ logger.opt(ansi=True).info('Text is identified as all menu items, skipping further processing.')
1374
+ return orig_text, ''
1375
+
1376
+ logger.opt(ansi=True).info(
1377
+ f'Text recognized in {end_time - start_time:0.03f}s using <{engine_color}>{engine_instance.readable_name}</{engine_color}>: {text}')
1375
1378
 
1376
1379
  if write_to == 'websocket':
1377
1380
  websocket_server_thread.send_text(text)
@@ -1395,6 +1398,83 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
1395
1398
 
1396
1399
  return orig_text, text
1397
1400
 
1401
+ def check_text_is_all_menu(text: str, crop_coords: tuple) -> bool:
1402
+ """
1403
+ Checks if the recognized text consists entirely of menu items.
1404
+ This function checks if the detected text area falls entirely within secondary rectangles (menu areas).
1405
+
1406
+ :param text: The recognized text from OCR.
1407
+ :param crop_coords: Tuple containing (x, y, width, height) of the detected text area relative to the cropped image.
1408
+ :return: True if the text is all menu items (within secondary rectangles), False otherwise.
1409
+ """
1410
+ if not text or not crop_coords:
1411
+ return False
1412
+
1413
+ original_width = obs_screenshot_thread.width
1414
+ original_height = obs_screenshot_thread.height
1415
+ crop_x, crop_y, crop_w, crop_h = crop_coords
1416
+
1417
+ ocr_config = get_scene_ocr_config()
1418
+
1419
+ if not any(rect.is_secondary for rect in ocr_config.rectangles):
1420
+ return False
1421
+
1422
+ ocr_config.scale_to_custom_size(original_width, original_height)
1423
+ if not ocr_config or not ocr_config.rectangles:
1424
+ return False
1425
+
1426
+ primary_rectangles = [rect for rect in ocr_config.rectangles if not rect.is_excluded and not rect.is_secondary]
1427
+ menu_rectangles = [rect for rect in ocr_config.rectangles if rect.is_secondary and not rect.is_excluded]
1428
+
1429
+ if not menu_rectangles:
1430
+ return False
1431
+
1432
+ if not primary_rectangles:
1433
+ if crop_x < 0 or crop_y < 0 or crop_x + crop_w > original_width or crop_y + crop_h > original_height:
1434
+ return False
1435
+ for menu_rect in menu_rectangles:
1436
+ rect_left, rect_top, rect_width, rect_height = menu_rect.coordinates
1437
+ rect_right = rect_left + rect_width
1438
+ rect_bottom = rect_top + rect_height
1439
+ if (crop_x >= rect_left and crop_y >= rect_top and
1440
+ crop_x + crop_w <= rect_right and crop_y + crop_h <= rect_bottom):
1441
+ return True
1442
+ return False
1443
+
1444
+ primary_rectangles.sort(key=lambda r: r.coordinates[1])
1445
+
1446
+ if len(primary_rectangles) == 1:
1447
+ primary_rect = primary_rectangles[0]
1448
+ primary_left, primary_top = primary_rect.coordinates[0], primary_rect.coordinates[1]
1449
+ original_x = crop_x + primary_left
1450
+ original_y = crop_y + primary_top
1451
+ else:
1452
+ current_y_offset = 0
1453
+ original_x = None
1454
+ original_y = None
1455
+ for i, primary_rect in enumerate(primary_rectangles):
1456
+ primary_left, primary_top, primary_width, primary_height = primary_rect.coordinates
1457
+ section_height = primary_height
1458
+ if crop_y >= current_y_offset and crop_y < current_y_offset + section_height:
1459
+ original_x = crop_x + primary_left
1460
+ original_y = (crop_y - current_y_offset) + primary_top
1461
+ break
1462
+ current_y_offset += section_height + 50
1463
+ if original_x is None or original_y is None:
1464
+ return False
1465
+
1466
+ if original_x < 0 or original_y < 0 or original_x > original_width or original_y > original_height:
1467
+ return False
1468
+
1469
+ for menu_rect in menu_rectangles:
1470
+ rect_left, rect_top, rect_width, rect_height = menu_rect.coordinates
1471
+ rect_right = rect_left + rect_width
1472
+ rect_bottom = rect_top + rect_height
1473
+ if (original_x >= rect_left and original_y >= rect_top and
1474
+ original_x <= rect_right and original_y <= rect_bottom):
1475
+ return True
1476
+
1477
+ return False
1398
1478
 
1399
1479
  def get_path_key(path):
1400
1480
  return path, path.lstat().st_mtime
@@ -1785,7 +1865,7 @@ def run(read_from=None,
1785
1865
  continue
1786
1866
 
1787
1867
  res, text = process_and_write_results(img, write_to, last_result, filtering, notify,
1788
- ocr_start_time=ocr_start_time, furigana_filter_sensitivity=get_ocr_furigana_filter_sensitivity())
1868
+ ocr_start_time=ocr_start_time, furigana_filter_sensitivity=None if get_ocr_two_pass_ocr() else get_furigana_filter_sensitivity())
1789
1869
  if not text and not previous_text and time.time() - last_result_time > 10:
1790
1870
  sleep_time_to_add += .005
1791
1871
  logger.info(f"No text detected again, sleeping.")