GameSentenceMiner 2.14.20__tar.gz → 2.14.21__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.14.21}/GameSentenceMiner/gsm.py +92 -81
  2. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/obs.py +4 -5
  3. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/ocr.py +16 -15
  4. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/run.py +1 -1
  5. gamesentenceminer-2.14.21/GameSentenceMiner/tools/furigana_filter_preview.py +330 -0
  6. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/vad.py +2 -2
  7. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  8. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/SOURCES.txt +1 -0
  9. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/PKG-INFO +1 -1
  10. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/pyproject.toml +1 -1
  11. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/__init__.py +0 -0
  12. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ai/__init__.py +0 -0
  13. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  14. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/anki.py +0 -0
  15. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/__init__.py +0 -0
  16. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon.png +0 -0
  17. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon128.png +0 -0
  18. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon256.png +0 -0
  19. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon32.png +0 -0
  20. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon512.png +0 -0
  21. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/icon64.png +0 -0
  22. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/assets/pickaxe.png +0 -0
  23. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/config_gui.py +0 -0
  24. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/gametext.py +0 -0
  25. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/locales/en_us.json +0 -0
  26. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/locales/ja_jp.json +0 -0
  27. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/locales/zh_cn.json +0 -0
  28. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/__init__.py +0 -0
  29. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  30. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  31. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  32. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  33. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  34. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  35. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  36. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  37. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  38. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  39. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/tools/__init__.py +0 -0
  40. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  41. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/tools/ss_selector.py +0 -0
  42. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/tools/window_transparency.py +0 -0
  43. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/__init__.py +0 -0
  44. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/communication/__init__.py +0 -0
  45. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/communication/send.py +0 -0
  46. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/communication/websocket.py +0 -0
  47. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/configuration.py +0 -0
  48. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/db.py +0 -0
  49. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  50. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  51. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  52. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  53. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/electron_config.py +0 -0
  54. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/ffmpeg.py +0 -0
  55. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  56. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/gsm_utils.py +0 -0
  57. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/model.py +0 -0
  58. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/notification.py +0 -0
  59. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/text_log.py +0 -0
  60. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  61. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  62. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/__init__.py +0 -0
  63. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/service.py +0 -0
  64. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/__init__.py +0 -0
  65. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  66. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  67. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/favicon.ico +0 -0
  68. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/favicon.svg +0 -0
  69. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  70. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/style.css +0 -0
  71. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  72. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  73. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/templates/__init__.py +0 -0
  74. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/templates/index.html +0 -0
  75. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  76. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/templates/utility.html +0 -0
  77. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/web/texthooking_page.py +0 -0
  78. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner/wip/__init___.py +0 -0
  79. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  80. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  81. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/requires.txt +0 -0
  82. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  83. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/LICENSE +0 -0
  84. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/README.md +0 -0
  85. {gamesentenceminer-2.14.20 → gamesentenceminer-2.14.21}/setup.cfg +0 -0
@@ -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
@@ -559,7 +559,6 @@ def main():
559
559
  disconnect_from_obs()
560
560
 
561
561
  if __name__ == '__main__':
562
- from mss import mss
563
562
  logging.basicConfig(level=logging.INFO)
564
563
  connect_to_obs_sync()
565
564
  set_fit_to_screen_for_scene_items(get_current_scene())
@@ -377,16 +377,13 @@ class GoogleLens:
377
377
  res += 'BLANK_LINE'
378
378
  for line in paragraph['lines']:
379
379
  if furigana_filter_sensitivity:
380
+ line_width = line['geometry']['bounding_box']['width'] * img.width
381
+ line_height = line['geometry']['bounding_box']['height'] * img.height
380
382
  for word in line['words']:
381
383
  if self.punctuation_regex.findall(word['plain_text']):
382
384
  res += word['plain_text'] + word['text_separator']
383
385
  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:
386
+ if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
390
387
  res += word['plain_text'] + word['text_separator']
391
388
  else:
392
389
  skipped.extend(word['plain_text'])
@@ -394,7 +391,8 @@ class GoogleLens:
394
391
  else:
395
392
  for word in line['words']:
396
393
  res += word['plain_text'] + word['text_separator']
397
-
394
+ res += '\n'
395
+
398
396
  previous_line = paragraph
399
397
  res += '\n'
400
398
  # logger.info(
@@ -920,7 +918,7 @@ class OneOCR:
920
918
  self.regex = re.compile(
921
919
  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
920
 
923
- def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False, multiple_crop_coords=False, return_one_box=True):
921
+ def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False, multiple_crop_coords=False, return_one_box=True, return_dict=False):
924
922
  lang = get_ocr_language()
925
923
  if furigana_filter_sensitivity != None:
926
924
  furigana_filter_sensitivity = get_furigana_filter_sensitivity()
@@ -940,6 +938,7 @@ class OneOCR:
940
938
  return (False, 'Invalid image provided')
941
939
  crop_coords = None
942
940
  crop_coords_list = []
941
+ ocr_resp = ''
943
942
  if sys.platform == 'win32':
944
943
  try:
945
944
  ocr_resp = self.model.recognize_pil(img)
@@ -959,17 +958,17 @@ class OneOCR:
959
958
  boxes = []
960
959
  if furigana_filter_sensitivity > 0:
961
960
  for line in filtered_lines:
961
+ line_x1, line_x2, line_x3, line_x4 = line['bounding_rect']['x1'], line['bounding_rect']['x2'], \
962
+ line['bounding_rect']['x3'], line['bounding_rect']['x4']
963
+ line_y1, line_y2, line_y3, line_y4 = line['bounding_rect']['y1'], line['bounding_rect']['y2'], \
964
+ line['bounding_rect']['y3'], line['bounding_rect']['y4']
965
+ line_width = max(line_x2 - line_x1, line_x3 - line_x4)
966
+ line_height = max(line_y3 - line_y1, line_y4 - line_y2)
962
967
  for char in line['words']:
963
968
  if self.punctuation_regex.findall(char['text']):
964
969
  res += char['text']
965
970
  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:
971
+ if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
973
972
  res += char['text']
974
973
  else:
975
974
  skipped.extend(char for char in line['text'])
@@ -1042,6 +1041,8 @@ class OneOCR:
1042
1041
  x.append(crop_coords_list)
1043
1042
  if return_one_box:
1044
1043
  x.append(crop_coords)
1044
+ if return_dict:
1045
+ x.append(ocr_resp)
1045
1046
  if is_path:
1046
1047
  img.close()
1047
1048
  return x
@@ -1785,7 +1785,7 @@ def run(read_from=None,
1785
1785
  continue
1786
1786
 
1787
1787
  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())
1788
+ ocr_start_time=ocr_start_time, furigana_filter_sensitivity=None if get_ocr_two_pass_ocr() else get_furigana_filter_sensitivity())
1789
1789
  if not text and not previous_text and time.time() - last_result_time > 10:
1790
1790
  sleep_time_to_add += .005
1791
1791
  logger.info(f"No text detected again, sleeping.")
@@ -0,0 +1,330 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk
3
+ from PIL import Image, ImageTk
4
+ import threading
5
+
6
+ import regex
7
+
8
+ from GameSentenceMiner import obs
9
+ from GameSentenceMiner.util.configuration import logger
10
+ from GameSentenceMiner.owocr.owocr.ocr import GoogleLens, OneOCR
11
+
12
+ def get_ocr_results_from_image(image_obj: Image.Image) -> tuple:
13
+ """
14
+ This is the function where you will plug in your OCR logic.
15
+
16
+ Args:
17
+ image_obj: A PIL Image object of the screenshot (used by your actual OCR call).
18
+
19
+ Returns:
20
+ A tuple containing the OCR results from both engines.
21
+ """
22
+ lens = GoogleLens()
23
+ oneocr = OneOCR()
24
+ oneocr_res = oneocr(image_obj, return_dict=True)
25
+ res = lens(image_obj, return_coords=True)
26
+
27
+ return res[2], oneocr_res[3]
28
+
29
+
30
+ class FuriganaFilterVisualizer:
31
+ def __init__(self, master, image: Image.Image, current_furigana_sensitivity: int = 0):
32
+ self.master = master
33
+ self.image = image
34
+ self.ocr1_result = None
35
+ self.ocr2_result = None
36
+ self.current_ocr = 1
37
+ self.master.title("Furigana Filter Visualizer - Lens")
38
+
39
+ self.words_data = []
40
+ self.lines_data = []
41
+ self.drawn_rects = []
42
+
43
+ main_frame = tk.Frame(master)
44
+ main_frame.pack(fill=tk.BOTH, expand=True)
45
+
46
+ self.photo_image = ImageTk.PhotoImage(self.image)
47
+ self.canvas = tk.Canvas(main_frame, width=self.image.width, height=self.image.height)
48
+ self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
49
+ self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
50
+
51
+ self.loading_bg = self.canvas.create_rectangle(
52
+ self.image.width/2 - 100, self.image.height/2 - 25,
53
+ self.image.width/2 + 100, self.image.height/2 + 25,
54
+ fill="black", outline="white", width=2
55
+ )
56
+ self.loading_text = self.canvas.create_text(
57
+ self.image.width / 2, self.image.height / 2,
58
+ text="Loading OCR data...", fill="white", font=("Helvetica", 16)
59
+ )
60
+
61
+ self.control_frame = tk.Frame(main_frame, padx=10, pady=10)
62
+ self.control_frame.pack(side=tk.BOTTOM, fill=tk.X)
63
+
64
+ ttk.Label(self.control_frame, text="Furigana Filter Sensitivity:").pack(side=tk.LEFT, padx=(0, 10))
65
+
66
+ self.slider = ttk.Scale(
67
+ self.control_frame, from_=0, to=100, orient=tk.HORIZONTAL, command=self.update_filter_visualization
68
+ )
69
+ self.slider.set(current_furigana_sensitivity)
70
+ self.slider.pack(side=tk.LEFT, fill=tk.X, expand=True)
71
+
72
+ self.slider_value_label = ttk.Label(self.control_frame, text=f"{self.slider.get():.0f} px", width=6)
73
+ self.slider_value_label.pack(side=tk.LEFT, padx=(10, 0))
74
+
75
+ self.swap_button = ttk.Button(self.control_frame, text="Switch to OneOCR", command=self.swap_ocr)
76
+ self.swap_button.pack(side=tk.LEFT, padx=(10, 0))
77
+
78
+ self.ok_button = ttk.Button(self.control_frame, text="OK", command=self.on_ok)
79
+ self.ok_button.pack(side=tk.LEFT, padx=(10, 0))
80
+
81
+ self.slider.config(state=tk.DISABLED)
82
+ self.swap_button.config(state=tk.DISABLED)
83
+ self.ok_button.config(state=tk.DISABLED)
84
+
85
+ self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
86
+ self.master.protocol("WM_DELETE_WINDOW", self.on_ok)
87
+
88
+ def update_with_ocr_data(self, ocr1_result, ocr2_result):
89
+ """Called by the background thread to populate the GUI with OCR data."""
90
+ self.ocr1_result = ocr1_result
91
+ self.ocr2_result = ocr2_result
92
+
93
+ # Remove loading message
94
+ self.canvas.delete(self.loading_bg)
95
+ self.canvas.delete(self.loading_text)
96
+
97
+ if not self.ocr1_result:
98
+ logger.error("OCR processing failed or returned no data.")
99
+ self.canvas.create_text(
100
+ self.image.width / 2, self.image.height / 2,
101
+ text="OCR Failed!", fill="red", font=("Helvetica", 16)
102
+ )
103
+ # Still enable OK button to allow closing
104
+ self.ok_button.config(state=tk.NORMAL)
105
+ return
106
+
107
+ # Enable controls
108
+ self.slider.config(state=tk.NORMAL)
109
+ self.ok_button.config(state=tk.NORMAL)
110
+ if self.ocr2_result:
111
+ self.swap_button.config(state=tk.NORMAL)
112
+
113
+ # Process and display initial data
114
+ self.pre_process_word_geometries()
115
+ self.update_filter_visualization(self.slider.get())
116
+
117
+ def on_ok(self):
118
+ print(f"RESULT:[{self.slider.get():.0f}]")
119
+ self.master.destroy()
120
+
121
+ def swap_ocr(self):
122
+ self.current_ocr = 2 if self.current_ocr == 1 else 1
123
+ # Change to oneocr or lens, in title too
124
+ if self.current_ocr == 1:
125
+ self.swap_button.config(text="Switch to OneOCR")
126
+ self.master.title("Furigana Filter Visualizer - Lens")
127
+ else:
128
+ self.swap_button.config(text="Switch to Lens")
129
+ self.master.title("Furigana Filter Visualizer - OneOCR")
130
+ self.pre_process_word_geometries()
131
+ self.update_filter_visualization(self.slider.get())
132
+
133
+ def pre_process_word_geometries(self):
134
+ """
135
+ Parses the OCR result structure (supports both original and new JSON formats),
136
+ calculates absolute pixel values, and stores them for high-performance updates.
137
+ """
138
+ img_w, img_h = self.image.size
139
+ logger.info(f"Processing word geometries for image size {img_w}x{img_h}...")
140
+
141
+ # Select the current OCR result
142
+ ocr_result = self.ocr1_result if self.current_ocr == 1 else self.ocr2_result
143
+ if not ocr_result:
144
+ return
145
+ self.words_data.clear()
146
+ self.lines_data.clear()
147
+
148
+ # Try to detect the format: oneocr has 'lines' as a top-level key
149
+ if 'lines' in ocr_result:
150
+ for line in ocr_result.get('lines', []):
151
+ for word in line.get('words', []):
152
+ try:
153
+ bbox = word['bounding_rect']
154
+ x1 = bbox['x1']
155
+ y1 = bbox['y1']
156
+ x2 = bbox['x3']
157
+ y2 = bbox['y3']
158
+ px_w = abs(x2 - x1)
159
+ px_h = abs(y2 - y1)
160
+ self.words_data.append({
161
+ 'text': word.get('text', ''),
162
+ 'px_w': px_w,
163
+ 'px_h': px_h,
164
+ 'coords': (x1, y1, x2, y2)
165
+ })
166
+ except Exception as e:
167
+ logger.warning(f"Skipping malformed word data (new format): {e}. Data: {word}")
168
+ continue
169
+ try:
170
+ bbox = line['bounding_rect']
171
+ x1 = bbox['x1']
172
+ y1 = bbox['y1']
173
+ x2 = bbox['x3']
174
+ y2 = bbox['y3']
175
+ px_w = abs(x2 - x1)
176
+ px_h = abs(y2 - y1)
177
+ self.lines_data.append({
178
+ 'text': line.get('text', ''),
179
+ 'px_w': px_w,
180
+ 'px_h': px_h,
181
+ 'coords': (x1, y1, x2, y2)
182
+ })
183
+ except Exception as e:
184
+ logger.warning(f"Skipping malformed line data (new format): {e}. Data: {line}")
185
+ continue
186
+ else:
187
+ # Lens format (nested paragraphs/lines/words)
188
+ text_layout = ocr_result.get('objects_response', {}).get('text', {}).get('text_layout', {})
189
+ if not text_layout:
190
+ logger.error("Could not find 'text_layout' in the OCR response.")
191
+ return
192
+ for paragraph in text_layout.get('paragraphs', []):
193
+ for line in paragraph.get('lines', []):
194
+ for word in line.get('words', []):
195
+ try:
196
+ bbox_pct = word['geometry']['bounding_box']
197
+ width_pct = bbox_pct['width']
198
+ height_pct = bbox_pct['height']
199
+ top_left_x_pct = bbox_pct['center_x'] - (width_pct / 2)
200
+ top_left_y_pct = bbox_pct['center_y'] - (height_pct / 2)
201
+ px_w = width_pct * img_w
202
+ px_h = height_pct * img_h
203
+ x1 = top_left_x_pct * img_w
204
+ y1 = top_left_y_pct * img_h
205
+ x2 = x1 + px_w
206
+ y2 = y1 + px_h
207
+ self.words_data.append({
208
+ 'text': word.get('plain_text', ''),
209
+ 'px_w': px_w,
210
+ 'px_h': px_h,
211
+ 'coords': (x1, y1, x2, y2)
212
+ })
213
+ except (KeyError, TypeError) as e:
214
+ logger.warning(f"Skipping malformed word data (orig format): {e}. Data: {word}")
215
+ continue
216
+ try:
217
+ line_bbox = line['geometry']['bounding_box']
218
+ width_pct = line_bbox['width']
219
+ height_pct = line_bbox['height']
220
+ top_left_x_pct = line_bbox['center_x'] - (width_pct / 2)
221
+ top_left_y_pct = line_bbox['center_y'] - (height_pct / 2)
222
+ px_w = width_pct * img_w
223
+ px_h = height_pct * img_h
224
+ x1 = top_left_x_pct * img_w
225
+ y1 = top_left_y_pct * img_h
226
+ x2 = x1 + px_w
227
+ y2 = y1 + px_h
228
+ self.lines_data.append({
229
+ 'text': ''.join([w.get('plain_text', '') for w in line.get('words', [])]),
230
+ 'px_w': px_w,
231
+ 'px_h': px_h,
232
+ 'coords': (x1, y1, x2, y2)
233
+ })
234
+ except (KeyError, TypeError) as e:
235
+ logger.warning(f"Skipping malformed line data (orig format): {e}. Data: {line}")
236
+ continue
237
+ logger.info(f"Successfully pre-processed {len(self.lines_data)} lines.")
238
+
239
+
240
+ def update_filter_visualization(self, slider_value):
241
+ """
242
+ Called on every slider move. Clears old rectangles and draws new ones
243
+ for words that pass the sensitivity filter.
244
+ """
245
+ sensitivity = float(slider_value)
246
+ self.slider_value_label.config(text=f"{sensitivity:.0f} px")
247
+
248
+ for rect_id in self.drawn_rects:
249
+ self.canvas.delete(rect_id)
250
+ self.drawn_rects.clear()
251
+
252
+ for line_data in self.lines_data:
253
+ if line_data['px_w'] > sensitivity and line_data['px_h'] > sensitivity:
254
+ x1, y1, x2, y2 = line_data['coords']
255
+ rect_id = self.canvas.create_rectangle(
256
+ x1, y1, x2, y2, outline='blue', width=2
257
+ )
258
+ self.drawn_rects.append(rect_id)
259
+
260
+ def scale_down_width_height(width, height):
261
+ if width == 0 or height == 0:
262
+ return width, height
263
+ aspect_ratio = width / height
264
+ if aspect_ratio > 2.66:
265
+ # Ultra-wide (32:9) - use 1920x540
266
+ return 1920, 540
267
+ elif aspect_ratio > 2.33:
268
+ # 21:9 - use 1920x800
269
+ return 1920, 800
270
+ elif aspect_ratio > 1.77:
271
+ # 16:9 - use 1280x720
272
+ return 1280, 720
273
+ elif aspect_ratio > 1.6:
274
+ # 16:10 - use 1280x800
275
+ return 1280, 800
276
+ elif aspect_ratio > 1.33:
277
+ # 4:3 - use 960x720
278
+ return 960, 720
279
+ elif aspect_ratio > 1.25:
280
+ # 5:4 - use 900x720
281
+ return 900, 720
282
+ elif aspect_ratio > 1.5:
283
+ # 3:2 - use 1080x720
284
+ return 1080, 720
285
+ else:
286
+ # Default/fallback - use original resolution
287
+ print(f"Unrecognized aspect ratio {aspect_ratio}. Using original resolution.")
288
+ return width, height
289
+
290
+ def main():
291
+ import sys
292
+ current_furigana_sensitivity = int(sys.argv[1]) if len(sys.argv) > 1 else 0
293
+
294
+ """Main execution function."""
295
+ try:
296
+ logger.info("Connecting to OBS...")
297
+ obs.connect_to_obs_sync()
298
+ except Exception as e:
299
+ logger.error(f"Failed to connect to OBS. Please ensure OBS is running and the WebSocket server is enabled. Error: {e}")
300
+ return
301
+
302
+ logger.info("Taking OBS screenshot...")
303
+ screenshot_img = obs.get_screenshot_PIL(compression=90, img_format='jpg')
304
+
305
+ screenshot_img = screenshot_img.resize(scale_down_width_height(screenshot_img.width, screenshot_img.height), Image.LANCZOS)
306
+
307
+ if not screenshot_img:
308
+ logger.error("Failed to get screenshot from OBS.")
309
+ return
310
+
311
+ logger.info(f"Screenshot received ({screenshot_img.width}x{screenshot_img.height}).")
312
+
313
+ root = tk.Tk()
314
+ app = FuriganaFilterVisualizer(root, screenshot_img, current_furigana_sensitivity)
315
+
316
+ def ocr_worker():
317
+ logger.info("Starting OCR process in background thread...")
318
+ try:
319
+ ocr1_data, ocr2_data = get_ocr_results_from_image(screenshot_img)
320
+ root.after(0, app.update_with_ocr_data, ocr1_data, ocr2_data)
321
+ except Exception as e:
322
+ logger.error(f"Error in OCR background thread: {e}")
323
+ root.after(0, app.update_with_ocr_data, None, None)
324
+
325
+ threading.Thread(target=ocr_worker, daemon=True).start()
326
+
327
+ root.mainloop()
328
+
329
+ if __name__ == "__main__":
330
+ main()
@@ -173,7 +173,7 @@ class WhisperVADProcessor(VADProcessor):
173
173
 
174
174
  def _detect_voice_activity(self, input_audio):
175
175
  from stable_whisper import WhisperResult
176
- # Convert the audio to 16kHz mono WAV
176
+ # Convert the audio to 16kHz mono WAV, evidence https://discord.com/channels/1286409772383342664/1286518821913362445/1407017127529152533
177
177
  temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
178
178
  ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
179
179
 
@@ -376,7 +376,7 @@ vad_processor = VADSystem()
376
376
  # Test cases for all VADProcessors
377
377
  def test_vad_processors():
378
378
  logger.setLevel(logging.DEBUG)
379
- test_audio = r"C:\Users\Beangate\GSM\GameSentenceMiner\GameSentenceMiner\test\NEKOPARAvol.1_2025-08-18-16-42-32-020.opus"
379
+ test_audio = r"C:\Users\Beangate\GSM\GameSentenceMiner\GameSentenceMiner\test\NEKOPARAvol.1_2025-08-18-17-20-43-614.opus"
380
380
  output_dir = r"C:\Users\Beangate\GSM\GameSentenceMiner\GameSentenceMiner\test\output"
381
381
  os.makedirs(output_dir, exist_ok=True)
382
382
  processors = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.14.20
3
+ Version: 2.14.21
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -42,6 +42,7 @@ GameSentenceMiner/owocr/owocr/run.py
42
42
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py
43
43
  GameSentenceMiner/tools/__init__.py
44
44
  GameSentenceMiner/tools/audio_offset_selector.py
45
+ GameSentenceMiner/tools/furigana_filter_preview.py
45
46
  GameSentenceMiner/tools/ss_selector.py
46
47
  GameSentenceMiner/tools/window_transparency.py
47
48
  GameSentenceMiner/util/__init__.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.14.20
3
+ Version: 2.14.21
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GameSentenceMiner"
10
- version = "2.14.20"
10
+ version = "2.14.21"
11
11
  description = "A tool for mining sentences from games. Update: Overlay?"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"