GameSentenceMiner 2.12.12.post1__py3-none-any.whl → 2.13.0__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.
@@ -10,6 +10,7 @@ from rapidfuzz import fuzz
10
10
  from GameSentenceMiner.util.gsm_utils import do_text_replacements, TEXT_REPLACEMENTS_FILE, run_new_thread
11
11
  from GameSentenceMiner.util.configuration import *
12
12
  from GameSentenceMiner.util.text_log import *
13
+ from GameSentenceMiner import obs
13
14
  from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker, send_word_coordinates_to_overlay, overlay_server_thread
14
15
 
15
16
  if get_config().wip.overlay_websocket_send:
@@ -146,6 +147,7 @@ def schedule_merge(wait, coro, args):
146
147
 
147
148
  async def handle_new_text_event(current_clipboard, line_time=None):
148
149
  global current_line, current_line_time, current_line_after_regex, timer, current_sequence_start_time, last_raw_clipboard
150
+ obs.update_current_game()
149
151
  current_line = current_clipboard
150
152
  logger.info(f"Current Line: {current_line} last raw clipboard: {last_raw_clipboard}")
151
153
  # Only apply this logic if merging is enabled
@@ -11,6 +11,7 @@ import json
11
11
  import base64
12
12
  from urllib.parse import urlparse, parse_qs
13
13
 
14
+ import jaconv
14
15
  import numpy as np
15
16
  import rapidfuzz.fuzz
16
17
  from PIL import Image
@@ -94,7 +95,6 @@ def empty_post_process(text):
94
95
 
95
96
 
96
97
  def post_process(text, keep_blank_lines=False):
97
- import jaconv
98
98
  if keep_blank_lines:
99
99
  text = '\n'.join([''.join(i.split()) for i in text.splitlines()])
100
100
  else:
@@ -436,7 +436,7 @@ class GoogleLens:
436
436
  # res += '\n'
437
437
 
438
438
  if return_coords:
439
- x = (True, res, response_dict)
439
+ x = (True, res, lines)
440
440
  else:
441
441
  x = (True, res)
442
442
 
@@ -887,28 +887,7 @@ class OneOCR:
887
887
  except:
888
888
  logger.warning('Error reading URL from config, OneOCR will not work!')
889
889
 
890
- def get_regex(self, lang):
891
- if lang == "ja":
892
- self.regex = re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
893
- elif lang == "zh":
894
- self.regex = re.compile(r'[\u4E00-\u9FFF]')
895
- elif lang == "ko":
896
- self.regex = re.compile(r'[\uAC00-\uD7AF]')
897
- elif lang == "ar":
898
- self.regex = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]')
899
- elif lang == "ru":
900
- self.regex = re.compile(r'[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u1C80-\u1C8F]')
901
- elif lang == "el":
902
- self.regex = re.compile(r'[\u0370-\u03FF\u1F00-\u1FFF]')
903
- elif lang == "he":
904
- self.regex = re.compile(r'[\u0590-\u05FF\uFB1D-\uFB4F]')
905
- elif lang == "th":
906
- self.regex = re.compile(r'[\u0E00-\u0E7F]')
907
- else:
908
- self.regex = re.compile(
909
- r'[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u2C60-\u2C7F\uA720-\uA7FF\uAB30-\uAB6F]')
910
-
911
- def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False):
890
+ def __call__(self, img, furigana_filter_sensitivity=0, sentence_to_check=None, return_coords=False):
912
891
  lang = get_ocr_language()
913
892
  if lang != self.initial_lang:
914
893
  self.initial_lang = lang
@@ -932,10 +911,6 @@ class OneOCR:
932
911
  json.dump(ocr_resp, f, indent=4, ensure_ascii=False)
933
912
  # print(json.dumps(ocr_resp))
934
913
  filtered_lines = [line for line in ocr_resp['lines'] if self.regex.search(line['text'])]
935
- x_coords = [line['bounding_rect'][f'x{i}'] for line in filtered_lines for i in range(1, 5)]
936
- y_coords = [line['bounding_rect'][f'y{i}'] for line in filtered_lines for i in range(1, 5)]
937
- if x_coords and y_coords:
938
- crop_coords = (min(x_coords) - 5, min(y_coords) - 5, max(x_coords) + 5, max(y_coords) + 5)
939
914
  # logger.info(filtered_lines)
940
915
  res = ''
941
916
  skipped = []
@@ -989,6 +964,30 @@ class OneOCR:
989
964
  # else:
990
965
  # continue
991
966
  # res += '\n'
967
+ elif sentence_to_check:
968
+ lines_to_build_area = []
969
+ widths = []
970
+ heights = []
971
+ for line in ocr_resp['lines']:
972
+ print(line['text'])
973
+ if sentence_to_check in line['text'] or line['text'] in sentence_to_check or rapidfuzz.fuzz.partial_ratio(sentence_to_check, line['text']) > 50:
974
+ lines_to_build_area.append(line)
975
+ res += line['text']
976
+ for word in line['words']:
977
+ widths.append(word['bounding_rect']['x2'] - word['bounding_rect']['x1'])
978
+ heights.append(word['bounding_rect']['y3'] - word['bounding_rect']['y1'])
979
+
980
+ x_coords = [line['bounding_rect'][f'x{i}'] for line in lines_to_build_area for i in
981
+ range(1, 5)]
982
+ y_coords = [line['bounding_rect'][f'y{i}'] for line in lines_to_build_area for i in
983
+ range(1, 5)]
984
+ if widths:
985
+ avg_width = sum(widths) / len(widths)
986
+ if heights:
987
+ avg_height = sum(heights) / len(heights)
988
+ if x_coords and y_coords:
989
+ crop_coords = (
990
+ min(x_coords) - 5, min(y_coords) - 5, max(x_coords) + 5, max(y_coords) + 5)
992
991
  elif return_coords:
993
992
  for line in filtered_lines:
994
993
  for word in line['words']:
@@ -999,6 +998,10 @@ class OneOCR:
999
998
  boxes.append(box)
1000
999
  res = ocr_resp['text']
1001
1000
  else:
1001
+ x_coords = [line['bounding_rect'][f'x{i}'] for line in filtered_lines for i in range(1, 5)]
1002
+ y_coords = [line['bounding_rect'][f'y{i}'] for line in filtered_lines for i in range(1, 5)]
1003
+ if x_coords and y_coords:
1004
+ crop_coords = (min(x_coords) - 5, min(y_coords) - 5, max(x_coords) + 5, max(y_coords) + 5)
1002
1005
  res = ocr_resp['text']
1003
1006
 
1004
1007
  except RuntimeError as e:
@@ -1016,7 +1019,7 @@ class OneOCR:
1016
1019
 
1017
1020
  res = res.json()['text']
1018
1021
  if return_coords:
1019
- x = (True, res, filtered_lines)
1022
+ x = (True, res, boxes)
1020
1023
  else:
1021
1024
  x = (True, res, crop_coords)
1022
1025
  if is_path:
@@ -44,6 +44,7 @@ import queue
44
44
  from datetime import datetime
45
45
  from PIL import Image, ImageDraw, UnidentifiedImageError
46
46
  from loguru import logger
47
+ from pynput import keyboard
47
48
  from desktop_notifier import DesktopNotifierSync
48
49
  import psutil
49
50
 
@@ -383,7 +384,6 @@ class TextFiltering:
383
384
  block_filtered = self.latin_extended_regex.findall(block)
384
385
  else:
385
386
  block_filtered = self.latin_extended_regex.findall(block)
386
-
387
387
  if block_filtered:
388
388
  orig_text_filtered.append(''.join(block_filtered))
389
389
  else:
@@ -547,6 +547,39 @@ class ScreenshotThread(threading.Thread):
547
547
  else:
548
548
  raise ValueError('Window capture is only currently supported on Windows and macOS')
549
549
 
550
+ def __del__(self):
551
+ if self.macos_window_tracker_instance:
552
+ self.macos_window_tracker_instance.join()
553
+ elif self.windows_window_tracker_instance:
554
+ self.windows_window_tracker_instance.join()
555
+
556
+ def setup_persistent_windows_window_tracker(self):
557
+ global window_open
558
+ window_open = False
559
+ def setup_tracker():
560
+ global window_open
561
+ self.window_handle, window_title = self.get_windows_window_handle(self.screen_capture_window)
562
+
563
+ if not self.window_handle:
564
+ # print(f"Window '{screen_capture_window}' not found.")
565
+ return
566
+
567
+ set_dpi_awareness()
568
+ window_open = True
569
+ self.windows_window_tracker_instance = threading.Thread(target=self.windows_window_tracker)
570
+ self.windows_window_tracker_instance.start()
571
+ logger.opt(ansi=True).info(f'Selected window: {window_title}')
572
+
573
+ while not terminated:
574
+ if not window_open:
575
+ try:
576
+ setup_tracker()
577
+ except ValueError as e:
578
+ logger.error(f"Error setting up persistent windows window tracker: {e}")
579
+ break
580
+ time.sleep(5)
581
+
582
+
550
583
  def get_windows_window_handle(self, window_title):
551
584
  def callback(hwnd, window_title_part):
552
585
  window_title = win32gui.GetWindowText(hwnd)
@@ -569,7 +602,7 @@ class ScreenshotThread(threading.Thread):
569
602
 
570
603
  def windows_window_tracker(self):
571
604
  found = True
572
- while not terminated:
605
+ while not terminated or window_open:
573
606
  found = win32gui.IsWindow(self.window_handle)
574
607
  if not found:
575
608
  break
@@ -839,9 +872,18 @@ class OBSScreenshotThread(threading.Thread):
839
872
  image_queue.put((result, True))
840
873
 
841
874
  def connect_obs(self):
842
- import GameSentenceMiner.obs as obs
843
- obs.connect_to_obs_sync()
844
-
875
+ try:
876
+ import obsws_python as obs
877
+ self.obs_client = obs.ReqClient(
878
+ host=get_config().obs.host,
879
+ port=get_config().obs.port,
880
+ password=get_config().obs.password,
881
+ timeout=10
882
+ )
883
+ logger.info("Connected to OBS WebSocket.")
884
+ except Exception as e:
885
+ logger.error(f"Failed to connect to OBS: {e}")
886
+ self.obs_client = None
845
887
 
846
888
  def run(self):
847
889
  global last_image
@@ -853,12 +895,9 @@ class OBSScreenshotThread(threading.Thread):
853
895
  def init_config(source=None, scene=None):
854
896
  obs.update_current_game()
855
897
  self.current_source = source if source else obs.get_active_source()
856
- self.current_source_name = self.current_source.get("sourceName") or None
898
+ self.current_source_name = self.current_source.get('sourceName') if isinstance(self.current_source, dict) else None
857
899
  self.current_scene = scene if scene else obs.get_current_game()
858
900
  self.ocr_config = get_scene_ocr_config()
859
- if not self.ocr_config:
860
- logger.error("No OCR config found for the current scene.")
861
- return
862
901
  self.ocr_config.scale_to_custom_size(self.width, self.height)
863
902
 
864
903
  # Register a scene switch callback in obsws
@@ -886,23 +925,22 @@ class OBSScreenshotThread(threading.Thread):
886
925
  continue
887
926
 
888
927
  if not self.ocr_config:
889
- logger.info("No OCR config found for the current scene. Waiting for scene switch.")
890
928
  time.sleep(1)
891
929
  continue
892
-
893
- if not self.current_source_name:
894
- obs.update_current_game()
895
- self.current_source = obs.get_active_source()
896
- self.current_source_name = self.current_source.get("sourceName") or None
897
930
 
898
931
  try:
899
- if not self.current_source_name:
900
- logger.error("No active source found in the current scene.")
901
- time.sleep(1)
902
- continue
903
- img = obs.get_screenshot_PIL(source_name=self.current_source_name, width=self.width, height=self.height, img_format='png', compression=80)
932
+ response = self.obs_client.get_source_screenshot(
933
+ name=self.current_source_name,
934
+ img_format='png',
935
+ quality=75,
936
+ width=self.width,
937
+ height=self.height,
938
+ )
904
939
 
905
- if img is not None:
940
+ if response.image_data:
941
+ image_data = base64.b64decode(response.image_data.split(",")[1])
942
+ img = Image.open(io.BytesIO(image_data)).convert("RGBA")
943
+
906
944
  if not img.getbbox():
907
945
  logger.info("OBS Not Capturing anything, sleeping.")
908
946
  time.sleep(1)
@@ -1080,10 +1118,11 @@ def signal_handler(sig, frame):
1080
1118
 
1081
1119
 
1082
1120
  def on_window_closed(alive):
1083
- global terminated
1121
+ global terminated, window_open
1084
1122
  if not (alive or terminated):
1085
1123
  logger.info('Window closed or error occurred, terminated!')
1086
- terminated = True
1124
+ window_open = False
1125
+ # terminated = True
1087
1126
 
1088
1127
 
1089
1128
  def on_screenshot_combo():
@@ -1425,12 +1464,8 @@ def run(read_from=None,
1425
1464
  read_from_readable.append(f'directory {read_from_path}')
1426
1465
 
1427
1466
  if len(key_combos) > 0:
1428
- try:
1429
- from pynput import keyboard
1430
- key_combo_listener = keyboard.GlobalHotKeys(key_combos)
1431
- key_combo_listener.start()
1432
- except ImportError:
1433
- pass
1467
+ key_combo_listener = keyboard.GlobalHotKeys(key_combos)
1468
+ key_combo_listener.start()
1434
1469
 
1435
1470
  if write_to in ('clipboard', 'websocket', 'callback'):
1436
1471
  write_to_readable = write_to
@@ -1478,7 +1513,6 @@ def run(read_from=None,
1478
1513
  filter_img = True
1479
1514
  notify = False
1480
1515
  last_screenshot_time = time.time()
1481
- ocr_start_time = datetime.now()
1482
1516
 
1483
1517
  if img == 0:
1484
1518
  on_window_closed(False)
@@ -58,6 +58,39 @@ def is_linux():
58
58
  def is_windows():
59
59
  return platform == 'win32'
60
60
 
61
+ class Locale(Enum):
62
+ English = 'en_us'
63
+ 日本語 = 'ja_jp'
64
+ 한국어 = 'ko_kr'
65
+ 中文 = 'zh_cn'
66
+ Español = 'es_es'
67
+ Français = 'fr_fr'
68
+ Deutsch = 'de_de'
69
+ Italiano = 'it_it'
70
+ Русский = 'ru_ru'
71
+
72
+ @classmethod
73
+ def from_any(cls, value: str) -> 'Locale':
74
+ """
75
+ Lookup Locale by either enum name (e.g. 'English') or value (e.g. 'en_us').
76
+ Case-insensitive.
77
+ """
78
+ value_lower = value.lower()
79
+ for locale in cls:
80
+ if locale.name.lower() == value_lower or locale.value.lower() == value_lower:
81
+ return locale
82
+ raise KeyError(f"Locale '{value}' not found.")
83
+
84
+ def __getitem__(cls, item):
85
+ try:
86
+ return cls.from_any(item)
87
+ except KeyError:
88
+ raise
89
+
90
+
91
+ # Patch Enum's __getitem__ for this class
92
+ Locale.__getitem__ = classmethod(Locale.__getitem__)
93
+
61
94
 
62
95
  class Language(Enum):
63
96
  JAPANESE = "ja"
@@ -79,6 +112,8 @@ class Language(Enum):
79
112
  DANISH = "da"
80
113
  NORWEGIAN = "no"
81
114
 
115
+
116
+
82
117
 
83
118
  AVAILABLE_LANGUAGES = [lang.value for lang in Language]
84
119
  AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
@@ -504,7 +539,7 @@ class Ai:
504
539
  enabled: bool = False
505
540
  anki_field: str = ''
506
541
  provider: str = AI_GEMINI
507
- gemini_model: str = 'gemini-2.5-flash'
542
+ gemini_model: str = 'gemini-2.5-flash-lite'
508
543
  local_model: str = OFF
509
544
  groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
510
545
  api_key: str = '' # Deprecated
@@ -523,6 +558,10 @@ class Ai:
523
558
  if self.provider == 'groq':
524
559
  self.provider = AI_GROQ
525
560
 
561
+ # Change Legacy Model Name
562
+ if self.gemini_model == 'gemini-2.5-flash-lite-preview-06-17':
563
+ self.gemini_model = 'gemini-2.5-flash-lite'
564
+
526
565
 
527
566
  # Experimental Features section, will change often
528
567
  @dataclass_json
@@ -650,6 +689,7 @@ class Config:
650
689
  configs: Dict[str, ProfileConfig] = field(default_factory=dict)
651
690
  current_profile: str = DEFAULT_CONFIG
652
691
  switch_to_default_if_not_found: bool = True
692
+ locale: Locale = Locale.English
653
693
 
654
694
  @classmethod
655
695
  def new(cls):
@@ -371,7 +371,7 @@ async def do_work(sentence_to_check=None):
371
371
  cropped_image.save("C:\\Users\\Beangate\\GSM\\temp\\full_screenshot.png")
372
372
  # full_screenshot_image.show()
373
373
  if cropped_image:
374
- logger.info("Full screenshot captured successfully. Now performing local OCR...")
374
+ logger.info("Full screenshot captured successfully. Now performing OCR...")
375
375
  # ocr_results = oneocr(full_screenshot_image, return_coords=True)
376
376
  google_ocr_results = lens(cropped_image, return_coords=True)[2]
377
377
 
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: GameSentenceMiner
3
+ Version: 2.13.0
4
+ Summary: A tool for mining sentences from games. Update: Overlay?
5
+ Author-email: Beangate <bpwhelan95@gmail.com>
6
+ License: MIT License
7
+ Project-URL: Homepage, https://github.com/bpwhelan/GameSentenceMiner
8
+ Project-URL: Repository, https://github.com/bpwhelan/GameSentenceMiner
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests~=2.32.3
16
+ Requires-Dist: watchdog~=5.0.2
17
+ Requires-Dist: DateTime~=5.5
18
+ Requires-Dist: pyperclip~=1.9.0
19
+ Requires-Dist: soundfile~=0.12.1
20
+ Requires-Dist: toml~=0.10.2
21
+ Requires-Dist: psutil~=6.0.0
22
+ Requires-Dist: rapidfuzz~=3.9.7
23
+ Requires-Dist: plyer~=2.1.0
24
+ Requires-Dist: keyboard~=0.13.5
25
+ Requires-Dist: websockets~=15.0.1
26
+ Requires-Dist: openai-whisper
27
+ Requires-Dist: stable-ts-whisperless
28
+ Requires-Dist: silero-vad~=5.1.2
29
+ Requires-Dist: ttkbootstrap~=1.10.1
30
+ Requires-Dist: dataclasses_json~=0.6.7
31
+ Requires-Dist: win10toast; sys_platform == "win32"
32
+ Requires-Dist: numpy==2.2.6
33
+ Requires-Dist: pystray
34
+ Requires-Dist: pywin32; sys_platform == "win32"
35
+ Requires-Dist: pygetwindow; sys_platform == "win32"
36
+ Requires-Dist: flask
37
+ Requires-Dist: groq
38
+ Requires-Dist: obsws-python~=1.7.2
39
+ Requires-Dist: matplotlib
40
+ Requires-Dist: sounddevice
41
+ Requires-Dist: google-genai
42
+ Dynamic: license-file
@@ -1,7 +1,7 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  GameSentenceMiner/anki.py,sha256=FUwcWO0-arzfQjejQmDKP7pNNakhboo8InQ4s_jv6AY,19099
3
- GameSentenceMiner/config_gui.py,sha256=UCipZVAVupJeA8Kaa1tqTBsZpErNIIePJabqKh4SI7s,105693
4
- GameSentenceMiner/gametext.py,sha256=TYlkgM5-J2o8-WCKypSUitmKq_UcjOGpsZBINiR-mWk,10875
3
+ GameSentenceMiner/config_gui.py,sha256=x8H3HXoRlnfgiFczAoCe1wiCoQDP8MWV0v7am36q3Co,126479
4
+ GameSentenceMiner/gametext.py,sha256=qR32LhXAo1_a4r01zd7Pm2Yj4ByYCw58u78JdFkSxh4,10939
5
5
  GameSentenceMiner/gsm.py,sha256=fG_3z-l6ADtx8Au2b6u514_kCWPdwYE03_U7IVLiE3Y,26649
6
6
  GameSentenceMiner/obs.py,sha256=hpFa33TSQnbOpzfucgnxp6vKqQ9AaQyLWQsdbuNYy1M,18741
7
7
  GameSentenceMiner/vad.py,sha256=-Q1KtDJnT8zRFeEc4LLyAECf07YOUM15UDRrnWkuDgo,18817
@@ -25,12 +25,12 @@ GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9
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=jjm7TGIOaR-7JfoP_4B24uW7tLsxJL36s2kFndJy_SA,63025
29
- GameSentenceMiner/owocr/owocr/run.py,sha256=Qm4srtzqj6tZhgicMME4SyjP6NP_2IQRN-AOzVzE828,66011
28
+ GameSentenceMiner/owocr/owocr/ocr.py,sha256=6ArGr0xd-Fhkw9uPn4MH3urxbLBwZ-UmxfwoKUUgxio,63459
29
+ GameSentenceMiner/owocr/owocr/run.py,sha256=nkDpXICJCTKgJTS4MYRnaz-GYqAS-GskcSg1ZkGIRuE,67285
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
- GameSentenceMiner/util/configuration.py,sha256=M9pMUykKhwlp5sEfIip9Ikakdl_im6ArItBWeCUcgaY,36199
33
+ GameSentenceMiner/util/configuration.py,sha256=hHGNGu0ybULyLwiBnIoTq4Fy1dC2ZxpnMFN5dfscwsc,37331
34
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
@@ -63,10 +63,10 @@ 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/wip/get_overlay_coords.py,sha256=_re9zfyuFryZAUKbMQ1LAfQBDIRUmq_1kniisN7J7xE,19793
67
- gamesentenceminer-2.12.12.post1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
- gamesentenceminer-2.12.12.post1.dist-info/METADATA,sha256=l2y5zcBUlGvyxYtdZ33p_o_zBn8M6JuVc1s0LHvRsEI,7075
69
- gamesentenceminer-2.12.12.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
- gamesentenceminer-2.12.12.post1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
71
- gamesentenceminer-2.12.12.post1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
72
- gamesentenceminer-2.12.12.post1.dist-info/RECORD,,
66
+ GameSentenceMiner/wip/get_overlay_coords.py,sha256=pxTuOicSsMMmOLRQH0-3FPoQqsolbncvIMgX2q8ArHc,19787
67
+ gamesentenceminer-2.13.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
+ gamesentenceminer-2.13.0.dist-info/METADATA,sha256=vstCSpe07K_nuP8m0Ur3Zn4Dbfgk8ttM2xxjsTtmVd4,1463
69
+ gamesentenceminer-2.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ gamesentenceminer-2.13.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
71
+ gamesentenceminer-2.13.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
72
+ gamesentenceminer-2.13.0.dist-info/RECORD,,
@@ -1,157 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: GameSentenceMiner
3
- Version: 2.12.12.post1
4
- Summary: A tool for mining sentences from games. Update: Overlay?
5
- Author-email: Beangate <bpwhelan95@gmail.com>
6
- License: MIT License
7
- Project-URL: Homepage, https://github.com/bpwhelan/GameSentenceMiner
8
- Project-URL: Repository, https://github.com/bpwhelan/GameSentenceMiner
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.10
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: requests~=2.32.3
16
- Requires-Dist: watchdog~=5.0.2
17
- Requires-Dist: DateTime~=5.5
18
- Requires-Dist: pyperclip~=1.9.0
19
- Requires-Dist: soundfile~=0.12.1
20
- Requires-Dist: toml~=0.10.2
21
- Requires-Dist: psutil~=6.0.0
22
- Requires-Dist: rapidfuzz~=3.9.7
23
- Requires-Dist: plyer~=2.1.0
24
- Requires-Dist: keyboard~=0.13.5
25
- Requires-Dist: websockets~=15.0.1
26
- Requires-Dist: openai-whisper
27
- Requires-Dist: stable-ts-whisperless
28
- Requires-Dist: silero-vad~=5.1.2
29
- Requires-Dist: ttkbootstrap~=1.10.1
30
- Requires-Dist: dataclasses_json~=0.6.7
31
- Requires-Dist: win10toast; sys_platform == "win32"
32
- Requires-Dist: numpy==2.2.6
33
- Requires-Dist: pystray
34
- Requires-Dist: pywin32; sys_platform == "win32"
35
- Requires-Dist: pygetwindow; sys_platform == "win32"
36
- Requires-Dist: flask
37
- Requires-Dist: groq
38
- Requires-Dist: obsws-python~=1.7.2
39
- Requires-Dist: matplotlib
40
- Requires-Dist: sounddevice
41
- Requires-Dist: google-genai
42
- Dynamic: license-file
43
-
44
- # GameSentenceMiner (GSM)
45
-
46
- An application designed to assist with language learning through games. Aiming to be the "[asbplayer](https://github.com/killergerbah/asbplayer)" for games.
47
-
48
- Short Demo (Watch this first): https://www.youtube.com/watch?v=FeFBL7py6HY
49
-
50
- Installation: https://youtu.be/h5ksXallc-o
51
-
52
- Discord: https://discord.gg/yP8Qse6bb8
53
-
54
- ## Features
55
-
56
- ### Anki Card Enhancement
57
-
58
- GSM significantly enhances your Anki cards with rich contextual information:
59
-
60
- * **Automated Audio Capture**: Automatically records the voice line associated with the text.
61
-
62
- * **Automatic Trim**: Some simple math around the time that the text event came in, in combination with a "Voice Activation Detection" (VAD) library gives us neatly cut audio.
63
- * **Manual Trim**: If Automatic voiceline trim is not perfect, it's possible to [open the audio in an external program](https://youtu.be/LKFQFy2Qm64) for trimming.
64
-
65
- * **Screenshot**: Captures a screenshot of the game at the moment the voice line is spoken.
66
-
67
- * **Multi-Line**: It's possible to capture multiple lines at once with sentence audio with GSM's very own Texthooker.
68
-
69
- * **AI Translation**: Integrates AI to provide quick translations of the captured sentence. Custom Prompts also supported. (Optional, Bring your own Key)
70
-
71
-
72
- #### Game Example (Has Audio)
73
-
74
- https://github.com/user-attachments/assets/df6bc38e-d74d-423e-b270-8a82eec2394c
75
-
76
- ---
77
-
78
- #### VN Example (Has Audio)
79
-
80
- https://github.com/user-attachments/assets/ee670fda-1a8b-4dec-b9e6-072264155c6e
81
-
82
- ### OCR
83
-
84
- GSM runs a fork of [OwOCR](https://github.com/AuroraWright/owocr/) to provide accurate text capture from games that do not have a hook. Here are some improvements GSM makes on stock OwOCR:
85
-
86
- * **Easier Setup**: With GSM's managed Python install, setup is only a matter of clicking a few buttons.
87
-
88
- * **Exclusion Zones**: Instead of choosing an area to OCR, you can choose an area to exclude from OCR. Useful if you have a static interface in your game and text appears randomly throughout.
89
-
90
- * **Two-Pass OCR**: To cut down on API calls and keep output clean, GSM features a "Two-Pass" OCR System. A Local OCR will be constantly running, and when the text on screen stabilizes, it will run a second, more accurate scan that gets sent to clipboard/WebSocket.
91
-
92
- * **Consistent Audio Timing**: With the two-pass system, we can still get accurate audio recorded and into Anki without the use of crazy offsets or hacks.
93
-
94
- * **More Language Support**: Stock OwOCR is hard-coded to Japanese, while in GSM you can use a variety of languages.
95
-
96
-
97
- https://github.com/user-attachments/assets/07240472-831a-40e6-be22-c64b880b0d66
98
-
99
-
100
-
101
- ### Game Launcher Capabilities (WIP)
102
-
103
- This is probably the feature I care least about, but if you are lazy like me, you may find this helpful.
104
-
105
- * **Launch**: GSM can launch your games directly, simplifying the setup process.
106
-
107
- * **Hook**: Streamlines the process of hooking your games (Agent).
108
-
109
- This feature simplifies the process of launching games and (potentially) hooking them, making the entire workflow more efficient.
110
-
111
- <img width="2560" height="1392" alt="GameSentenceMiner_1zuov0R9xK" src="https://github.com/user-attachments/assets/205769bb-3dd2-493b-9383-2d6e2ca05c2d" />
112
-
113
- ## Basic Requirements
114
-
115
- * **Anki card creation tool**: [Yomitan](https://github.com/yomidevs/yomitan), [JL](https://github.com/rampaa/JL), etc.
116
-
117
- * **A method of getting text from the game**: [Agent](https://github.com/0xDC00/agent), [Textractor](https://github.com/Artikash/Textractor), [LunaTranslator](https://github.com/HIllya51/LunaTranslator), GSM's OCR, etc.
118
-
119
- * **A game :)**
120
-
121
- ## Documentation
122
-
123
- For help with installation, setup, and other information, please visit the project's [Wiki](https://github.com/bpwhelan/GameSentenceMiner/wiki).
124
-
125
- ## FAQ
126
-
127
- ### How Does It Work?
128
-
129
- This is a common question, and understanding this process will help clarify any issues you might encounter while using GSM.
130
-
131
- 1. The beginning of the voice line is marked by a text event. This usually comes from Textractor, Agent, or another texthooker. GSM can listen for a clipboard copy and/or a WebSocket server (configurable in GSM).
132
-
133
- 2. The end of the voice line is detected using a Voice Activity Detection (VAD) library running locally. ([Example](https://github.com/snakers4/silero-vad))
134
-
135
- In essence, GSM relies on accurately timed text events to capture the corresponding audio.
136
-
137
- GSM provides settings to accommodate less-than-ideal hooks. However, if you experience significant audio inconsistencies, they likely stem from a poorly timed hook, loud background music, or other external factors, rather than GSM itself. The core audio trimming logic has been stable and effective for many users across various games.
138
-
139
- ## Contact
140
-
141
- If you encounter issues, please ask for help in my [Discord](https://discord.gg/yP8Qse6bb8) or create an issue here.
142
-
143
- ## Acknowledgements
144
-
145
- * [OwOCR](https://github.com/AuroraWright/owocr) for their outstanding OCR implementation, which I've integrated into GSM.
146
-
147
- * [chaiNNer](https://github.com/chaiNNer-org/chaiNNer) for the idea of installing Python within an Electron app.
148
-
149
- * [OBS](https://obsproject.com/) and [FFMPEG](https://ffmpeg.org/), without which GSM would not be possible.
150
-
151
- * [Renji's Texthooker](https://github.com/Renji-XD/texthooker-ui)
152
-
153
- * https://github.com/Saplling/transparent-texthooker-overlay
154
-
155
- ## Donations
156
-
157
- If you've found this or any of my other projects helpful, please consider supporting my work through [GitHub Sponsors](https://github.com/sponsors/bpwhelan), [Ko-fi](https://ko-fi.com/beangate), or [Patreon](https://www.patreon.com/GameSentenceMiner).