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