GameSentenceMiner 2.12.12__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.
- GameSentenceMiner/config_gui.py +1092 -857
- GameSentenceMiner/gametext.py +2 -0
- GameSentenceMiner/obs.py +2 -2
- GameSentenceMiner/owocr/owocr/ocr.py +32 -29
- GameSentenceMiner/owocr/owocr/run.py +64 -30
- GameSentenceMiner/util/configuration.py +41 -1
- GameSentenceMiner/wip/get_overlay_coords.py +1 -1
- gamesentenceminer-2.13.0.dist-info/METADATA +42 -0
- {gamesentenceminer-2.12.12.dist-info → gamesentenceminer-2.13.0.dist-info}/RECORD +13 -13
- gamesentenceminer-2.12.12.dist-info/METADATA +0 -157
- {gamesentenceminer-2.12.12.dist-info → gamesentenceminer-2.13.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.12.12.dist-info → gamesentenceminer-2.13.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.12.12.dist-info → gamesentenceminer-2.13.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.12.12.dist-info → gamesentenceminer-2.13.0.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gametext.py
CHANGED
@@ -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
|
GameSentenceMiner/obs.py
CHANGED
@@ -419,8 +419,8 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
|
|
419
419
|
def update_current_game():
|
420
420
|
gsm_state.current_game = get_current_scene()
|
421
421
|
|
422
|
-
def get_current_game(sanitize=False):
|
423
|
-
if not gsm_state.current_game:
|
422
|
+
def get_current_game(sanitize=False, update=True):
|
423
|
+
if not gsm_state.current_game or update:
|
424
424
|
update_current_game()
|
425
425
|
|
426
426
|
if sanitize:
|
@@ -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,
|
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
|
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,
|
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
|
-
|
843
|
-
|
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(
|
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
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
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
|
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
|
-
|
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
|
-
|
1429
|
-
|
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
|
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,9 +1,9 @@
|
|
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=
|
4
|
-
GameSentenceMiner/gametext.py,sha256=
|
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
|
-
GameSentenceMiner/obs.py,sha256=
|
6
|
+
GameSentenceMiner/obs.py,sha256=hpFa33TSQnbOpzfucgnxp6vKqQ9AaQyLWQsdbuNYy1M,18741
|
7
7
|
GameSentenceMiner/vad.py,sha256=-Q1KtDJnT8zRFeEc4LLyAECf07YOUM15UDRrnWkuDgo,18817
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
GameSentenceMiner/ai/ai_prompting.py,sha256=iHkEx2pQJ-tEyejOgYy4G0DcZc8qvBugVL6-CQpPSME,26089
|
@@ -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=
|
29
|
-
GameSentenceMiner/owocr/owocr/run.py,sha256=
|
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=
|
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=
|
67
|
-
gamesentenceminer-2.
|
68
|
-
gamesentenceminer-2.
|
69
|
-
gamesentenceminer-2.
|
70
|
-
gamesentenceminer-2.
|
71
|
-
gamesentenceminer-2.
|
72
|
-
gamesentenceminer-2.
|
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
|
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).
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|