GameSentenceMiner 2.12.0.dev5__py3-none-any.whl → 2.12.0.dev6__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/anki.py CHANGED
@@ -19,6 +19,7 @@ from GameSentenceMiner.util.model import AnkiCard
19
19
  from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_mined_line, lines_match
20
20
  from GameSentenceMiner.obs import get_current_game
21
21
  from GameSentenceMiner.web import texthooking_page
22
+ import re
22
23
 
23
24
  # Global variables to track state
24
25
  previous_note_ids = set()
@@ -161,7 +162,36 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
161
162
  game_line = get_text_event(last_note)
162
163
  sentences = []
163
164
  sentences_text = ''
164
- if selected_lines:
165
+
166
+ # TODO: REMOVE THIS, I DON'T THINK IT'S NEEDED
167
+ if get_config().wip.overlay_websocket_send:
168
+ sentence_in_anki = last_note.get_field(get_config().anki.sentence_field).replace("\n", "").replace("\r", "").strip()
169
+ if lines_match(game_line.text, remove_html_and_cloze_tags(sentence_in_anki)):
170
+ logger.info("Found matching line in Anki, Preserving HTML and fix spacing!")
171
+ if "<b>" in sentence_in_anki:
172
+ text_inside_bold = re.findall(r'<b>(.*?)</b>', sentence_in_anki)
173
+ logger.info(text_inside_bold)
174
+ if text_inside_bold:
175
+ text = text_inside_bold[0].replace(" ", "").replace('\n', '').strip()
176
+ note['fields'][get_config().anki.sentence_field] = game_line.text.replace(text_inside_bold[0], f"<b>{text}</b>")
177
+ logger.info(f"Preserved bold Tag for Sentence: {note['fields'][get_config().anki.sentence_field]}")
178
+ if "<i>" in sentence_in_anki:
179
+ text_inside_italic = re.findall(r'<i>(.*?)</i>', sentence_in_anki)
180
+ if text_inside_italic:
181
+ text = text_inside_italic[0].replace(" ", "").replace('\n', '').strip()
182
+ note['fields'][get_config().anki.sentence_field] = game_line.text.replace(text_inside_italic[0], f"<i>{text}</i>")
183
+ logger.info(f"Preserved italic Tag for Sentence: {note['fields'][get_config().anki.sentence_field]}")
184
+ if "<u>" in sentence_in_anki:
185
+ text_inside_underline = re.findall(r'<u>(.*?)</u>', sentence_in_anki)
186
+ if text_inside_underline:
187
+ text = text_inside_underline[0].replace(" ", "").replace('\n', '').strip()
188
+ note['fields'][get_config().anki.sentence_field] = game_line.text.replace(text_inside_underline[0], f"<u>{text}</u>")
189
+ logger.info(f"Preserved underline Tag for Sentence: {note['fields'][get_config().anki.sentence_field]}")
190
+
191
+ if get_config().anki.sentence_field not in note['fields']:
192
+ logger.info("No HTML tags found to preserve, just fixing spacing")
193
+ note['fields'][get_config().anki.sentence_field] = game_line.text
194
+ elif selected_lines:
165
195
  try:
166
196
  sentence_in_anki = last_note.get_field(get_config().anki.sentence_field)
167
197
  logger.info(f"Attempting Preserve HTML for multi-line")
@@ -113,6 +113,7 @@ class ConfigApp:
113
113
  self.window.title('GameSentenceMiner Configuration')
114
114
  self.window.protocol("WM_DELETE_WINDOW", self.hide)
115
115
  self.obs_scene_listbox_changed = False
116
+ self.test_func = None
116
117
 
117
118
  # self.window.geometry("800x500")
118
119
  self.current_row = 0
@@ -162,6 +163,9 @@ class ConfigApp:
162
163
  self.window.update_idletasks()
163
164
  self.window.geometry("")
164
165
  self.window.withdraw()
166
+
167
+ def set_test_func(self, func):
168
+ self.test_func = func
165
169
 
166
170
  def create_tabs(self):
167
171
  self.create_general_tab()
@@ -580,6 +584,12 @@ class ConfigApp:
580
584
  text="Every Label in settings has a tooltip with more information if you hover over them.",
581
585
  font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
582
586
  self.current_row += 1
587
+
588
+ if is_beangate:
589
+ ttk.Button(self.general_tab, text="Run Function", command=self.test_func, bootstyle="info").grid(
590
+ row=self.current_row, column=0, pady=5
591
+ )
592
+ self.current_row += 1
583
593
 
584
594
  # Add Reset to Default button
585
595
  self.add_reset_button(self.general_tab, "general", self.current_row, column=0, recreate_tab=self.create_general_tab)
@@ -9,7 +9,7 @@ from websockets import InvalidStatus
9
9
  from GameSentenceMiner.util.gsm_utils import do_text_replacements, TEXT_REPLACEMENTS_FILE, run_new_thread
10
10
  from GameSentenceMiner.util.configuration import *
11
11
  from GameSentenceMiner.util.text_log import *
12
- from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker, send_word_coordinates_to_overlay
12
+ from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker, send_word_coordinates_to_overlay, overlay_server_thread
13
13
  from GameSentenceMiner.wip import get_overlay_coords
14
14
 
15
15
  current_line = ''
@@ -125,7 +125,7 @@ async def handle_new_text_event(current_clipboard, line_time=None):
125
125
  add_line(current_line_after_regex, line_time)
126
126
  if len(get_text_log().values) > 0:
127
127
  await add_event_to_texthooker(get_text_log()[-1])
128
- if get_config().wip.overlay_websocket_port and get_config().wip.overlay_websocket_send:
128
+ if get_config().wip.overlay_websocket_port and get_config().wip.overlay_websocket_send and overlay_server_thread.has_clients():
129
129
  boxes = await find_box_for_sentence(current_line_after_regex)
130
130
  if boxes:
131
131
  await send_word_coordinates_to_overlay(boxes)
GameSentenceMiner/gsm.py CHANGED
@@ -545,6 +545,8 @@ def async_loop():
545
545
  await check_obs_folder_is_correct()
546
546
  logger.info("Post-Initialization started.")
547
547
  vad_processor.init()
548
+ # if is_beangate:
549
+ # await run_test_code()
548
550
 
549
551
  asyncio.run(loop())
550
552
 
@@ -577,6 +579,13 @@ async def register_scene_switcher_callback():
577
579
  update_icon()
578
580
 
579
581
  await obs.register_scene_change_callback(scene_switcher_callback)
582
+
583
+ async def run_test_code():
584
+ if get_config().wip.overlay_websocket_port and get_config().wip.overlay_websocket_send:
585
+ boxes = await gametext.find_box_for_sentence("ちぇっ少しなの?")
586
+ if boxes:
587
+ await texthooking_page.send_word_coordinates_to_overlay(boxes)
588
+ await asyncio.sleep(2)
580
589
 
581
590
  async def async_main(reloading=False):
582
591
  global root, settings_window
@@ -600,6 +609,9 @@ async def async_main(reloading=False):
600
609
  signal.signal(signal.SIGINT, handle_exit()) # Handle Ctrl+C
601
610
  if is_windows():
602
611
  win32api.SetConsoleCtrlHandler(handle_exit())
612
+
613
+ if is_beangate:
614
+ settings_window.set_test_func(lambda: run_new_thread(run_test_code))
603
615
 
604
616
  gsm_status.ready = True
605
617
  gsm_status.status = "Ready"
@@ -6,13 +6,14 @@ from pathlib import Path
6
6
  import sys
7
7
  import platform
8
8
  import logging
9
- from math import sqrt
9
+ from math import sqrt, floor
10
10
  import json
11
11
  import base64
12
12
  from urllib.parse import urlparse, parse_qs
13
13
 
14
14
  import jaconv
15
15
  import numpy as np
16
+ import rapidfuzz.fuzz
16
17
  from PIL import Image
17
18
  from loguru import logger
18
19
  import requests
@@ -164,6 +165,28 @@ def limit_image_size(img, max_size):
164
165
  return False, ''
165
166
 
166
167
 
168
+ def get_regex(lang):
169
+ if lang == "ja":
170
+ return re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
171
+ elif lang == "zh":
172
+ return re.compile(r'[\u4E00-\u9FFF]')
173
+ elif lang == "ko":
174
+ return re.compile(r'[\uAC00-\uD7AF]')
175
+ elif lang == "ar":
176
+ return re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]')
177
+ elif lang == "ru":
178
+ return re.compile(r'[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u1C80-\u1C8F]')
179
+ elif lang == "el":
180
+ return re.compile(r'[\u0370-\u03FF\u1F00-\u1FFF]')
181
+ elif lang == "he":
182
+ return re.compile(r'[\u0590-\u05FF\uFB1D-\uFB4F]')
183
+ elif lang == "th":
184
+ return re.compile(r'[\u0E00-\u0E7F]')
185
+ else:
186
+ return re.compile(
187
+ r'[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u0250-\u02AF\u1D00-\u1D7F\u1D80-\u1DBF\u1E00-\u1EFF\u2C60-\u2C7F\uA720-\uA7FF\uAB30-\uAB6F]')
188
+
189
+
167
190
  class MangaOcr:
168
191
  name = 'mangaocr'
169
192
  readable_name = 'Manga OCR'
@@ -243,15 +266,20 @@ class GoogleLens:
243
266
  available = False
244
267
 
245
268
  def __init__(self, lang='ja'):
246
- self.kana_kanji_regex = re.compile(r'[\u3041-\u3096\u30A1-\u30FA\u4E00-\u9FFF]')
269
+ self.regex = get_regex(lang)
270
+ self.initial_lang = lang
247
271
  if 'betterproto' not in sys.modules:
248
272
  logger.warning('betterproto not available, Google Lens will not work!')
249
273
  else:
250
274
  self.available = True
251
275
  logger.info('Google Lens ready')
252
276
 
253
- def __call__(self, img, furigana_filter_sensitivity=0):
277
+ def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False):
278
+ lang = get_ocr_language()
254
279
  img, is_path = input_to_pil_image(img)
280
+ if lang != self.initial_lang:
281
+ self.initial_lang = lang
282
+ self.regex = get_regex(lang)
255
283
  if not img:
256
284
  return (False, 'Invalid image provided')
257
285
 
@@ -309,12 +337,14 @@ class GoogleLens:
309
337
  response_proto = LensOverlayServerResponse().FromString(res.content)
310
338
  response_dict = response_proto.to_dict(betterproto.Casing.SNAKE)
311
339
 
312
- # with open(os.path.join(r"C:\Users\Beangate\GSM\Electron App\test", 'glens_response.json'), 'w', encoding='utf-8') as f:
313
- # json.dump(response_dict, f, indent=4, ensure_ascii=False)
340
+ if os.path.exists(r"C:\Users\Beangate\GSM\Electron App\test"):
341
+ with open(os.path.join(r"C:\Users\Beangate\GSM\Electron App\test", 'glens_response.json'), 'w', encoding='utf-8') as f:
342
+ json.dump(response_dict, f, indent=4, ensure_ascii=False)
314
343
  res = ''
315
344
  text = response_dict['objects_response']['text']
316
345
  skipped = []
317
346
  previous_line = None
347
+ lines = []
318
348
  if 'text_layout' in text:
319
349
  for paragraph in text['text_layout']['paragraphs']:
320
350
  if previous_line:
@@ -330,18 +360,38 @@ class GoogleLens:
330
360
  if vertical_space > avg_height * 2:
331
361
  res += 'BLANK_LINE'
332
362
  for line in paragraph['lines']:
363
+ # Build a list of word boxes for this line
364
+ words_info = []
365
+ for word in line['words']:
366
+ word_info = {
367
+ "word": word['plain_text'],
368
+ "x1": int(word['geometry']['bounding_box']['center_x'] * img.width - (word['geometry']['bounding_box']['width'] * img.width) / 2),
369
+ "y1": int(word['geometry']['bounding_box']['center_y'] * img.height - (word['geometry']['bounding_box']['height'] * img.height) / 2),
370
+ "x2": int(word['geometry']['bounding_box']['center_x'] * img.width + (word['geometry']['bounding_box']['width'] * img.width) / 2),
371
+ "y2": int(word['geometry']['bounding_box']['center_y'] * img.height + (word['geometry']['bounding_box']['height'] * img.height) / 2)
372
+ }
373
+ words_info.append(word_info)
374
+
375
+ line_text = ''.join([w['word'] for w in words_info])
376
+ line_box = {
377
+ "sentence": line_text,
378
+ "words": words_info
379
+ }
380
+
381
+ # Optionally apply furigana filter
333
382
  if furigana_filter_sensitivity:
334
- if furigana_filter_sensitivity < line['geometry']['bounding_box']['width'] * img.width and furigana_filter_sensitivity < line['geometry']['bounding_box']['height'] * img.height:
335
- for word in line['words']:
336
- res += word['plain_text'] + word['text_separator']
383
+ line_width = line['geometry']['bounding_box']['width'] * img.width
384
+ line_height = line['geometry']['bounding_box']['height'] * img.height
385
+ if furigana_filter_sensitivity < line_width and furigana_filter_sensitivity < line_height and self.regex.search(line_text):
386
+ for w in words_info:
387
+ res += w['word']
337
388
  else:
338
- skipped.append(word['plain_text'] for word in line['words'])
389
+ skipped.extend([w['word'] for w in words_info])
339
390
  continue
340
391
  else:
341
- for word in line['words']:
342
- res += word['plain_text'] + word['text_separator']
343
- else:
344
- continue
392
+ for w in words_info:
393
+ res += w['word']
394
+ lines.append(line_box)
345
395
  previous_line = paragraph
346
396
  res += '\n'
347
397
  # logger.info(
@@ -384,8 +434,11 @@ class GoogleLens:
384
434
  # else:
385
435
  # continue
386
436
  # res += '\n'
387
-
388
- x = (True, res)
437
+
438
+ if return_coords:
439
+ x = (True, res, lines)
440
+ else:
441
+ x = (True, res)
389
442
 
390
443
  # img.close()
391
444
  return x
@@ -812,7 +865,7 @@ class OneOCR:
812
865
 
813
866
  def __init__(self, config={}, lang='ja'):
814
867
  self.initial_lang = lang
815
- self.get_regex(lang)
868
+ self.regex = get_regex(lang)
816
869
  if sys.platform == 'win32':
817
870
  if int(platform.release()) < 10:
818
871
  logger.warning('OneOCR is not supported on Windows older than 10!')
@@ -834,32 +887,11 @@ class OneOCR:
834
887
  except:
835
888
  logger.warning('Error reading URL from config, OneOCR will not work!')
836
889
 
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
-
858
- def __call__(self, img, furigana_filter_sensitivity=0):
890
+ def __call__(self, img, furigana_filter_sensitivity=0, sentence_to_check=None, return_coords=False):
859
891
  lang = get_ocr_language()
860
892
  if lang != self.initial_lang:
861
893
  self.initial_lang = lang
862
- self.get_regex(lang)
894
+ self.regex = get_regex(lang)
863
895
  img, is_path = input_to_pil_image(img)
864
896
  if img.width < 51 or img.height < 51:
865
897
  new_width = max(img.width, 51)
@@ -873,20 +905,18 @@ class OneOCR:
873
905
  if sys.platform == 'win32':
874
906
  try:
875
907
  ocr_resp = self.model.recognize_pil(img)
908
+ if os.path.exists(os.path.expanduser("~/GSM/temp")):
909
+ with open(os.path.join(os.path.expanduser("~/GSM/temp"), 'oneocr_response.json'), 'w',
910
+ encoding='utf-8') as f:
911
+ json.dump(ocr_resp, f, indent=4, ensure_ascii=False)
876
912
  # print(json.dumps(ocr_resp))
877
913
  filtered_lines = [line for line in ocr_resp['lines'] if self.regex.search(line['text'])]
878
- x_coords = [line['bounding_rect'][f'x{i}'] for line in filtered_lines for i in range(1, 5)]
879
- y_coords = [line['bounding_rect'][f'y{i}'] for line in filtered_lines for i in range(1, 5)]
880
- if x_coords and y_coords:
881
- crop_coords = (min(x_coords) - 5, min(y_coords) - 5, max(x_coords) + 5, max(y_coords) + 5)
882
-
883
- # with open(os.path.join(get_temporary_directory(), 'oneocr_response.json'), 'w',
884
- # encoding='utf-8') as f:
885
- # json.dump(ocr_resp, f, indent=4, ensure_ascii=False)
914
+ # logger.info(filtered_lines)
886
915
  res = ''
887
916
  skipped = []
917
+ boxes = []
888
918
  if furigana_filter_sensitivity > 0:
889
- for line in ocr_resp['lines']:
919
+ for line in filtered_lines:
890
920
  x1, x2, x3, x4 = line['bounding_rect']['x1'], line['bounding_rect']['x2'], \
891
921
  line['bounding_rect']['x3'], line['bounding_rect']['x4']
892
922
  y1, y2, y3, y4 = line['bounding_rect']['y1'], line['bounding_rect']['y2'], \
@@ -934,8 +964,46 @@ class OneOCR:
934
964
  # else:
935
965
  # continue
936
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)
991
+ elif return_coords:
992
+ for line in filtered_lines:
993
+ for word in line['words']:
994
+ box = {
995
+ "text": word['text'],
996
+ "bounding_rect": word['bounding_rect']
997
+ }
998
+ boxes.append(box)
999
+ res = ocr_resp['text']
937
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)
938
1005
  res = ocr_resp['text']
1006
+
939
1007
  except RuntimeError as e:
940
1008
  return (False, e)
941
1009
  else:
@@ -950,10 +1018,12 @@ class OneOCR:
950
1018
  return (False, 'Unknown error!')
951
1019
 
952
1020
  res = res.json()['text']
953
-
954
- x = (True, res, crop_coords)
955
-
956
- # img.close()
1021
+ if return_coords:
1022
+ x = (True, res, boxes)
1023
+ else:
1024
+ x = (True, res, crop_coords)
1025
+ if is_path:
1026
+ img.close()
957
1027
  return x
958
1028
 
959
1029
  def _preprocess(self, img):
@@ -384,7 +384,6 @@ class TextFiltering:
384
384
  block_filtered = self.latin_extended_regex.findall(block)
385
385
  else:
386
386
  block_filtered = self.latin_extended_regex.findall(block)
387
-
388
387
  if block_filtered:
389
388
  orig_text_filtered.append(''.join(block_filtered))
390
389
  else:
@@ -548,6 +547,39 @@ class ScreenshotThread(threading.Thread):
548
547
  else:
549
548
  raise ValueError('Window capture is only currently supported on Windows and macOS')
550
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
+
551
583
  def get_windows_window_handle(self, window_title):
552
584
  def callback(hwnd, window_title_part):
553
585
  window_title = win32gui.GetWindowText(hwnd)
@@ -570,7 +602,7 @@ class ScreenshotThread(threading.Thread):
570
602
 
571
603
  def windows_window_tracker(self):
572
604
  found = True
573
- while not terminated:
605
+ while not terminated or window_open:
574
606
  found = win32gui.IsWindow(self.window_handle)
575
607
  if not found:
576
608
  break
@@ -1086,10 +1118,11 @@ def signal_handler(sig, frame):
1086
1118
 
1087
1119
 
1088
1120
  def on_window_closed(alive):
1089
- global terminated
1121
+ global terminated, window_open
1090
1122
  if not (alive or terminated):
1091
1123
  logger.info('Window closed or error occurred, terminated!')
1092
- terminated = True
1124
+ window_open = False
1125
+ # terminated = True
1093
1126
 
1094
1127
 
1095
1128
  def on_screenshot_combo():
@@ -282,7 +282,7 @@ class CommonLanguages(str, Enum):
282
282
  Raises ValueError if not found.
283
283
  """
284
284
  try:
285
- return cls[name]
285
+ return cls[name.upper()]
286
286
  except KeyError:
287
287
  raise ValueError(f"Language '{name}' not found in CommonLanguages")
288
288
 
@@ -527,7 +527,7 @@ class Ai:
527
527
  @dataclass_json
528
528
  @dataclass
529
529
  class WIP:
530
- overlay_websocket_port: int = 55003
530
+ overlay_websocket_port: int = 55499
531
531
  overlay_websocket_send: bool = False
532
532
 
533
533
 
@@ -549,7 +549,14 @@ class ProfileConfig:
549
549
  advanced: Advanced = field(default_factory=Advanced)
550
550
  ai: Ai = field(default_factory=Ai)
551
551
  wip: WIP = field(default_factory=WIP)
552
-
552
+
553
+
554
+ def get_field_value(self, section: str, field_name: str):
555
+ section_obj = getattr(self, section, None)
556
+ if section_obj and hasattr(section_obj, field_name):
557
+ return getattr(section_obj, field_name)
558
+ else:
559
+ raise ValueError(f"Field '{field_name}' not found in section '{section}' of ProfileConfig.")
553
560
 
554
561
  # This is just for legacy support
555
562
  def load_from_toml(self, file_path: str):
@@ -992,4 +999,7 @@ anki_results = {}
992
999
  gsm_state = GsmAppState()
993
1000
  is_dev = is_running_from_source()
994
1001
 
1002
+ is_beangate = os.path.exists("C:/Users/Beangate")
1003
+
995
1004
  logger.debug(f"Running in development mode: {is_dev}")
1005
+ logger.debug(f"Running on Beangate's PC: {is_beangate}")
@@ -113,8 +113,8 @@ def similar(a, b):
113
113
 
114
114
 
115
115
  def lines_match(texthooker_sentence, anki_sentence):
116
- texthooker_sentence = texthooker_sentence.replace("\n", "").replace("\r", "").strip()
117
- anki_sentence = anki_sentence.replace("\n", "").replace("\r", "").strip()
116
+ texthooker_sentence = texthooker_sentence.replace("\n", "").replace("\r", "").replace(' ', '').strip()
117
+ anki_sentence = anki_sentence.replace("\n", "").replace("\r", "").replace(' ', '').strip()
118
118
  similarity = similar(texthooker_sentence, anki_sentence)
119
119
  if texthooker_sentence in anki_sentence:
120
120
  logger.debug(f"One contains the other: {texthooker_sentence} in {anki_sentence} - Similarity: {similarity}")
@@ -389,19 +389,18 @@ def start_web_server():
389
389
  app.run(host='0.0.0.0', port=port, debug=False) # debug=True provides helpful error messages during development
390
390
 
391
391
 
392
- websocket_server_thread = None
393
392
  websocket_queue = queue.Queue()
394
393
  paused = False
395
394
 
396
395
 
397
396
  class WebsocketServerThread(threading.Thread):
398
- def __init__(self, read, ws_port):
397
+ def __init__(self, read, get_ws_port_func):
399
398
  super().__init__(daemon=True)
400
399
  self._loop = None
401
400
  self.read = read
402
401
  self.clients = set()
403
402
  self._event = threading.Event()
404
- self.ws_port = ws_port
403
+ self.get_ws_port_func = get_ws_port_func
405
404
  self.backedup_text = []
406
405
 
407
406
  @property
@@ -446,6 +445,9 @@ class WebsocketServerThread(threading.Thread):
446
445
  text = json.dumps(text)
447
446
  return asyncio.run_coroutine_threadsafe(
448
447
  self.send_text_coroutine(text), self.loop)
448
+
449
+ def has_clients(self):
450
+ return len(self.clients) > 0
449
451
 
450
452
  def stop_server(self):
451
453
  self.loop.call_soon_threadsafe(self._stop_event.set)
@@ -459,7 +461,7 @@ class WebsocketServerThread(threading.Thread):
459
461
  try:
460
462
  self.server = start_server = websockets.serve(self.server_handler,
461
463
  "0.0.0.0",
462
- self.ws_port,
464
+ self.get_ws_port_func(),
463
465
  max_size=1000000000)
464
466
  async with start_server:
465
467
  await stop_event.wait()
@@ -474,6 +476,16 @@ def handle_exit_signal(loop):
474
476
  logger.info("Received exit signal. Shutting down...")
475
477
  for task in asyncio.all_tasks(loop):
476
478
  task.cancel()
479
+
480
+ websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda : get_config().get_field_value('advanced', 'texthooker_communication_websocket_port'))
481
+ websocket_server_thread.start()
482
+
483
+ if get_config().advanced.plaintext_websocket_port:
484
+ plaintext_websocket_server_thread = WebsocketServerThread(read=False, get_ws_port_func=lambda : get_config().get_field_value('advanced', 'plaintext_websocket_port'))
485
+ plaintext_websocket_server_thread.start()
486
+
487
+ overlay_server_thread = WebsocketServerThread(read=False, get_ws_port_func=lambda : get_config().get_field_value('wip', 'overlay_websocket_port'))
488
+ overlay_server_thread.start()
477
489
 
478
490
  async def texthooker_page_coro():
479
491
  global websocket_server_thread, plaintext_websocket_server_thread, overlay_server_thread
@@ -482,16 +494,6 @@ async def texthooker_page_coro():
482
494
  flask_thread.daemon = True
483
495
  flask_thread.start()
484
496
 
485
- websocket_server_thread = WebsocketServerThread(read=True, ws_port=get_config().advanced.texthooker_communication_websocket_port)
486
- websocket_server_thread.start()
487
-
488
- if get_config().advanced.plaintext_websocket_port:
489
- plaintext_websocket_server_thread = WebsocketServerThread(read=False, ws_port=get_config().advanced.plaintext_websocket_port)
490
- plaintext_websocket_server_thread.start()
491
-
492
- overlay_server_thread = WebsocketServerThread(read=False, ws_port=get_config().wip.overlay_websocket_port)
493
- overlay_server_thread.start()
494
-
495
497
  # Keep the main asyncio event loop running (for the WebSocket server)
496
498
 
497
499
  def run_text_hooker_page():
@@ -2,7 +2,6 @@ import asyncio
2
2
  import io
3
3
  import base64
4
4
  from PIL import Image
5
- import obsws_python as obs
6
5
 
7
6
  from GameSentenceMiner.owocr.owocr.ocr import GoogleLens, OneOCR
8
7
  from GameSentenceMiner.obs import *
@@ -18,13 +17,77 @@ HEIGHT = 1440
18
17
  oneocr = OneOCR()
19
18
  lens = GoogleLens()
20
19
 
20
+ def correct_ocr_text(detected_text: str, reference_text: str) -> str:
21
+ """
22
+ Correct OCR text by comparing character-by-character with reference text.
23
+ When mismatches are found, look for subsequent matches and correct previous mismatches.
24
+ """
25
+ if not detected_text or not reference_text:
26
+ return detected_text
27
+
28
+ detected_chars = list(detected_text)
29
+ reference_chars = list(reference_text)
30
+
31
+ # Track positions where mismatches occurred
32
+ mismatched_positions = []
33
+
34
+ min_length = min(len(detected_chars), len(reference_chars))
35
+
36
+ for i in range(min_length):
37
+ if detected_chars[i] != reference_chars[i]:
38
+ mismatched_positions.append(i)
39
+ logger.info(f"Mismatch at position {i}: detected '{detected_chars[i]}' vs reference '{reference_chars[i]}'")
40
+ else:
41
+ # We found a match - if we have previous mismatches, correct the most recent one
42
+ if mismatched_positions:
43
+ # Correct the most recent mismatch (simple 1-for-1 strategy)
44
+ last_mismatch_pos = mismatched_positions.pop()
45
+ old_char = detected_chars[last_mismatch_pos]
46
+ detected_chars[last_mismatch_pos] = reference_chars[last_mismatch_pos]
47
+ logger.info(f"Corrected position {last_mismatch_pos}: '{old_char}' -> '{reference_chars[last_mismatch_pos]}'")
48
+
49
+ corrected_text = ''.join(detected_chars)
50
+ return corrected_text
51
+
52
+ def redistribute_corrected_text(original_boxes: list, original_text: str, corrected_text: str) -> list:
53
+ """
54
+ Redistribute corrected text back to the original text boxes while maintaining their positions.
55
+ """
56
+ if original_text == corrected_text:
57
+ return original_boxes
58
+
59
+ corrected_boxes = []
60
+ text_position = 0
61
+
62
+ for box in original_boxes:
63
+ original_word = box['text']
64
+ word_length = len(original_word)
65
+
66
+ # Extract the corrected portion for this box
67
+ if text_position + word_length <= len(corrected_text):
68
+ corrected_word = corrected_text[text_position:text_position + word_length]
69
+ else:
70
+ # Handle case where corrected text is shorter
71
+ corrected_word = corrected_text[text_position:] if text_position < len(corrected_text) else ""
72
+
73
+ # Create a new box with corrected text but same coordinates
74
+ corrected_box = box.copy()
75
+ corrected_box['text'] = corrected_word
76
+ corrected_boxes.append(corrected_box)
77
+
78
+ text_position += word_length
79
+
80
+ logger.info(f"Redistributed: '{original_word}' -> '{corrected_word}'")
81
+
82
+ return corrected_boxes
83
+
21
84
  async def get_full_screenshot() -> Image.Image | None:
22
- # print(f"Attempting to connect to OBS WebSocket at ws://{OBS_HOST}:{OBS_PORT}")
85
+ # logger.info(f"Attempting to connect to OBS WebSocket at ws://{OBS_HOST}:{OBS_PORT}")
23
86
  # try:
24
87
  # client = obs.ReqClient(host=OBS_HOST, port=OBS_PORT, password=OBS_PASSWORD, timeout=30)
25
- # print("Connected to OBS WebSocket.")
88
+ # logger.info("Connected to OBS WebSocket.")
26
89
  # except Exception as e:
27
- # print(f"Failed to connect to OBS: {e}")
90
+ # logger.info(f"Failed to connect to OBS: {e}")
28
91
  # return None
29
92
  #
30
93
  # try:
@@ -37,10 +100,10 @@ async def get_full_screenshot() -> Image.Image | None:
37
100
  # )
38
101
  #
39
102
  # if not response.image_data:
40
- # print("Failed to get screenshot data from OBS.")
103
+ # logger.info("Failed to get screenshot data from OBS.")
41
104
  # return None
42
105
 
43
- print("Getting Screenshot from OBS")
106
+ logger.info("Getting Screenshot from OBS")
44
107
  try:
45
108
  update_current_game()
46
109
  start_time = time.time()
@@ -48,48 +111,131 @@ async def get_full_screenshot() -> Image.Image | None:
48
111
  image_data = base64.b64decode(image_data)
49
112
  img = Image.open(io.BytesIO(image_data)).convert("RGBA").resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS)
50
113
  # img.show()
51
- print(f"Screenshot captured in {time.time() - start_time:.2f} seconds.")
114
+ logger.info(f"Screenshot captured in {time.time() - start_time:.2f} seconds.")
52
115
 
53
116
  return img
54
117
 
55
118
  except Exception as e:
56
- print(f"An unexpected error occurred during screenshot capture: {e}")
119
+ logger.info(f"An unexpected error occurred during screenshot capture: {e}")
57
120
  return None
58
-
59
- async def find_box_for_sentence(sentence_to_check):
121
+
122
+ async def do_work(sentence_to_check=None):
60
123
  # connect_to_obs_sync(5)
61
- print("in find_box")
124
+ logger.info("in find_box")
62
125
  # await asyncio.sleep(.5)
63
- print("after_initial_sleep")
126
+ logger.info("after_initial_sleep")
64
127
  full_screenshot_image = await get_full_screenshot()
128
+ if os.path.exists("C:\\Users\\Beangate\\GSM\\temp"):
129
+ full_screenshot_image.save("C:\\Users\\Beangate\\GSM\\temp\\full_screenshot.png")
130
+ # full_screenshot_image.show()
65
131
  if full_screenshot_image:
66
- print("Full screenshot captured successfully. Now performing local OCR...")
67
- ocr_results = lens(full_screenshot_image, return_coords=True)
132
+ logger.info("Full screenshot captured successfully. Now performing local OCR...")
133
+ ocr_results = oneocr(full_screenshot_image, return_coords=True)
134
+
135
+ boxes_of_text = ocr_results[2]
136
+ # logger.info(f"Boxes of text found: {boxes_of_text}")
137
+
138
+ words = []
139
+
140
+ # If we have a reference sentence, perform character-by-character correction
141
+ if sentence_to_check:
142
+ # Concatenate all OCR text to form the detected sentence
143
+ detected_sentence = ''.join([box['text'] for box in boxes_of_text])
144
+ logger.info(f"Original detected sentence: '{detected_sentence}'")
145
+ logger.info(f"Reference sentence: '{sentence_to_check}'")
146
+
147
+ # Perform character-by-character comparison and correction
148
+ corrected_sentence = correct_ocr_text(detected_sentence, sentence_to_check)
149
+ logger.info(f"Corrected sentence: '{corrected_sentence}'")
150
+
151
+ # Redistribute corrected text back to boxes while maintaining positions
152
+ corrected_boxes = redistribute_corrected_text(boxes_of_text, detected_sentence, corrected_sentence)
153
+ else:
154
+ corrected_boxes = boxes_of_text
155
+
156
+ sentence_position = 0
157
+ for box in corrected_boxes:
158
+ word = box['text']
159
+ # logger.info(f"Box: {box}")
160
+ x1, y1 = box['bounding_rect']['x1'], box['bounding_rect']['y1']
161
+ x2, y2 = box['bounding_rect']['x3'], box['bounding_rect']['y3']
162
+ words.append({
163
+ "x1": x1,
164
+ "y1": y1,
165
+ "x2": x2,
166
+ "y2": y2,
167
+ "word": box['text']
168
+ })
169
+
170
+ # logger.info(f"Returning words: {words}")
171
+
172
+ ret = [
173
+ {
174
+ "words": words,
175
+ }
176
+ ]
177
+ # cropped_sections = []
178
+ # for box in boxes_of_text:
179
+ # # Ensure crop coordinates are within image bounds
180
+ # left = max(0, box['bounding_rect']['x1'])
181
+ # top = max(0, box['bounding_rect']['y1'])
182
+ # right = min(full_screenshot_image.width, box['bounding_rect']['x3'])
183
+ # bottom = min(full_screenshot_image.height, box['bounding_rect']['y3'])
184
+ # cropped_sections.append(full_screenshot_image.crop((left, top, right, bottom)))
185
+
186
+ # if len(cropped_sections) > 1:
187
+ # # Create a transparent image with the same size as the full screenshot
188
+ # combined_img = Image.new("RGBA", (full_screenshot_image.width, full_screenshot_image.height), (0, 0, 0, 0))
189
+
190
+ # combined_img.show()
191
+
192
+ # # Paste each cropped section at its original coordinates
193
+ # for box, section in zip(boxes_of_text, cropped_sections):
194
+ # left = max(0, box['bounding_rect']['x1'])
195
+ # top = max(0, box['bounding_rect']['y1'])
196
+ # combined_img.paste(section, (left, top))
197
+
198
+ # new_image = combined_img
199
+ # elif cropped_sections:
200
+ # new_image = cropped_sections[0]
201
+ # else:
202
+ # new_image = Image.new("RGBA", full_screenshot_image.size)
203
+
204
+ # new_image.show()
205
+ # ocr_results = lens(new_image, return_coords=True)
68
206
  # ocr_results = oneocr(full_screenshot_image, sentence_to_check=sentence_to_check)
69
- print("\n--- OCR Results ---")
70
- print(ocr_results)
207
+ # logger.info("\n--- OCR Results ---")
208
+ # logger.info(ocr_results)
71
209
 
72
- return ocr_results[2], 48
210
+ return ret, 48
73
211
  # from PIL import ImageDraw
74
212
  # draw = ImageDraw.Draw(full_screenshot_image)
75
213
  # draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
76
214
  # full_screenshot_image.save("full_screenshot_with_ocr.png")
77
215
  # full_screenshot_image.show()
78
216
  #
79
- # print(ocr_results)
217
+ # logger.info(ocr_results)
80
218
  # if ocr_results:
81
219
  # for i, result in enumerate(ocr_results):
82
- # print(f"Result {i + 1}:\n{result}\n")
220
+ # logger.info(f"Result {i + 1}:\n{result}\n")
83
221
  # else:
84
- # print("No OCR results found.")
222
+ # logger.info("No OCR results found.")
85
223
  else:
86
- print("Failed to get full screenshot for OCR.")
224
+ logger.info("Failed to get full screenshot for OCR.")
225
+
226
+ async def find_box_for_sentence(sentence_to_check):
227
+ try:
228
+ return await do_work(sentence_to_check=sentence_to_check)
229
+ except Exception as e:
230
+ logger.info(f"Error in find_box_for_sentence: {e}", exc_info=True)
231
+ return [], 48
87
232
 
88
233
  async def main():
234
+ connect_to_obs_sync(5)
89
235
  await find_box_for_sentence("はじめから")
90
236
 
91
237
  if __name__ == '__main__':
92
238
  try:
93
239
  asyncio.run(main())
94
240
  except KeyboardInterrupt:
95
- print("Script terminated by user.")
241
+ logger.info("Script terminated by user.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.12.0.dev5
3
+ Version: 2.12.0.dev6
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=3BVFXAM7tpJAxHMbsMpnMHUoDfyqHQ1JSYJThW18QWA,16846
3
- GameSentenceMiner/config_gui.py,sha256=Zl4Ad46DYkb4NsBkxk05lKBswGQur7AbO1-WNwnDyHs,102899
4
- GameSentenceMiner/gametext.py,sha256=0QbG9HuNjJtDi0TljeiTTHV4lTMxluvbsxZpysS8WsA,7713
5
- GameSentenceMiner/gsm.py,sha256=qVHxnvly-yJ85v9RAxsGN2MqZxU-C1JA5wSRxVxMPMg,24950
2
+ GameSentenceMiner/anki.py,sha256=VC87YMX_XiigB-Pqsz5y1zMh7DLmPAyhXeryAhmI1nI,19101
3
+ GameSentenceMiner/config_gui.py,sha256=bK7mHLxDIXhghOJqL0VphgUOsEAPyI3mjl0uWtR4mPk,103249
4
+ GameSentenceMiner/gametext.py,sha256=fNkz1dvvJCLQ1AD6NuAJhSiGUjG-OSNGzwQOGv8anGo,7776
5
+ GameSentenceMiner/gsm.py,sha256=MO2ekb3C8vWDflrslcgWRMQIpoqIpzn2pqg_3NDkCgQ,25447
6
6
  GameSentenceMiner/obs.py,sha256=-5j4k1_sYYR1Lnbn9C-_yN9prqgGLICgx5l3uguv4xk,15917
7
7
  GameSentenceMiner/vad.py,sha256=zo9JpuEOCXczPXM-dq8lbr-zM-MPpfJ8aajggR3mKk4,18710
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -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=z0w7kcPjXvFabMQTWaQyiBehxmjeIVaS2p53yvFyPbg,59707
29
- GameSentenceMiner/owocr/owocr/run.py,sha256=p7DBHTbhey1DeW1SRqNQ5-y3H4Cq2zoMPCMED5C0Rws,65945
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=fR0n8bjPN8fP8IEdW-YSvjrGLXSLZ7cf9LgJyGj0PF0,35490
33
+ GameSentenceMiner/util/configuration.py,sha256=XUKA4-XFCk4tEjqj9GtB-Yq9OgBRrILXlcthRkLQpP8,35959
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
@@ -38,7 +38,7 @@ GameSentenceMiner/util/model.py,sha256=hmA_seopP2bK40v9T4ulua9TrAeWtbkdCv-sTBPBQ
38
38
  GameSentenceMiner/util/notification.py,sha256=0OnEYjn3DUEZ6c6OtPjdVZe-DG-QSoMAl9fetjjCvNU,3874
39
39
  GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
40
40
  GameSentenceMiner/util/ss_selector.py,sha256=cbjMxiKOCuOfbRvLR_PCRlykBrGtm1LXd6u5czPqkmc,4793
41
- GameSentenceMiner/util/text_log.py,sha256=jhG7ny8-DAilMAAPauN5HLoBNSIJ-cXAm68NLBxGNT8,5997
41
+ GameSentenceMiner/util/text_log.py,sha256=Gbm8H2DHUX4u0QBnbjs3jOfzkS6_5WBczgD3mwant7Q,6031
42
42
  GameSentenceMiner/util/window_transparency.py,sha256=hmeQYqK3mUEh47hZ8pODldUbxCC5eluMddanXfC_epQ,7325
43
43
  GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
44
44
  GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
@@ -49,7 +49,7 @@ GameSentenceMiner/util/downloader/download_tools.py,sha256=zR-aEHiFVkyo-9oPoSx6n
49
49
  GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=EJbKISaZ9p2x9P4x0rpMM5nAInTTc9b7arraGBcd-SA,10381
50
50
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  GameSentenceMiner/web/service.py,sha256=S7bYf2kSk08u-8R9Qpv7piM-pxfFjYZUvU825xupmuI,5279
52
- GameSentenceMiner/web/texthooking_page.py,sha256=c6wOItEVJDkjJsq6jKemv1mytZh5N6bi2V--3jwH_T0,17219
52
+ GameSentenceMiner/web/texthooking_page.py,sha256=2ZS89CAI17xVkx64rGmHHbF96eKR8gPWiR_WAoDJ0Mw,17399
53
53
  GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
55
55
  GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
@@ -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=yE8LzXlR-Sw3rz3N-judgQa5z4egJptfJ97KEvdmEH4,3189
67
- gamesentenceminer-2.12.0.dev5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
- gamesentenceminer-2.12.0.dev5.dist-info/METADATA,sha256=OYb2bqQEqbXBg8OaH72DmaCeDuZMWayEFlpMdTa5oZw,7004
69
- gamesentenceminer-2.12.0.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
- gamesentenceminer-2.12.0.dev5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
71
- gamesentenceminer-2.12.0.dev5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
72
- gamesentenceminer-2.12.0.dev5.dist-info/RECORD,,
66
+ GameSentenceMiner/wip/get_overlay_coords.py,sha256=dzP8EbexXuRnSEP4lWUzyMn24B9UQkAGA6zbxGS_W2M,9506
67
+ gamesentenceminer-2.12.0.dev6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
+ gamesentenceminer-2.12.0.dev6.dist-info/METADATA,sha256=DvW1QUhxzRIx2pQdp8JLrV7vPohTFPYd7pnM7INR27o,7004
69
+ gamesentenceminer-2.12.0.dev6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ gamesentenceminer-2.12.0.dev6.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
71
+ gamesentenceminer-2.12.0.dev6.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
72
+ gamesentenceminer-2.12.0.dev6.dist-info/RECORD,,