GameSentenceMiner 2.12.0.dev4__tar.gz → 2.12.0.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/anki.py +31 -1
  2. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/config_gui.py +10 -0
  3. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/gametext.py +2 -2
  4. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/gsm.py +12 -0
  5. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/ocr.py +122 -52
  6. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/run.py +37 -4
  7. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/configuration.py +13 -3
  8. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/text_log.py +2 -2
  9. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/window_transparency.py +28 -0
  10. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/texthooking_page.py +16 -14
  11. gamesentenceminer-2.12.0.dev6/GameSentenceMiner/wip/get_overlay_coords.py +241 -0
  12. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  13. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/PKG-INFO +1 -1
  14. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/pyproject.toml +1 -1
  15. gamesentenceminer-2.12.0.dev4/GameSentenceMiner/wip/get_overlay_coords.py +0 -95
  16. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/__init__.py +0 -0
  17. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ai/__init__.py +0 -0
  18. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  19. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/__init__.py +0 -0
  20. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon.png +0 -0
  21. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon128.png +0 -0
  22. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon256.png +0 -0
  23. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon32.png +0 -0
  24. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon512.png +0 -0
  25. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/icon64.png +0 -0
  26. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/assets/pickaxe.png +0 -0
  27. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/obs.py +0 -0
  28. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/__init__.py +0 -0
  29. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  30. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  31. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  32. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  33. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  34. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  35. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  36. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  37. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  38. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  39. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/__init__.py +0 -0
  40. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  41. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/communication/__init__.py +0 -0
  42. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/communication/send.py +0 -0
  43. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/communication/websocket.py +0 -0
  44. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  45. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  46. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  47. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  48. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/electron_config.py +0 -0
  49. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/ffmpeg.py +0 -0
  50. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/gsm_utils.py +0 -0
  51. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/model.py +0 -0
  52. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/notification.py +0 -0
  53. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/package.py +0 -0
  54. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/util/ss_selector.py +0 -0
  55. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/vad.py +0 -0
  56. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/__init__.py +0 -0
  57. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/service.py +0 -0
  58. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/__init__.py +0 -0
  59. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  60. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  61. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/favicon.ico +0 -0
  62. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/favicon.svg +0 -0
  63. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  64. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/style.css +0 -0
  65. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  66. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  67. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/templates/__init__.py +0 -0
  68. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/templates/index.html +0 -0
  69. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  70. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner/web/templates/utility.html +0 -0
  71. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  72. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  73. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  74. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/requires.txt +0 -0
  75. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  76. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/LICENSE +0 -0
  77. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/README.md +0 -0
  78. {gamesentenceminer-2.12.0.dev4 → gamesentenceminer-2.12.0.dev6}/setup.cfg +0 -0
@@ -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)
@@ -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}")
@@ -1,9 +1,11 @@
1
+ import sys
1
2
  import win32gui
2
3
  import win32con
3
4
  import win32api
4
5
  import keyboard
5
6
  import time
6
7
  import threading
8
+ import signal
7
9
 
8
10
  from GameSentenceMiner.util.configuration import logger
9
11
 
@@ -137,6 +139,22 @@ def mouse_monitor_loop():
137
139
  # A small delay to reduce CPU usage
138
140
  time.sleep(0.1)
139
141
 
142
+ class HandleSTDINThread(threading.Thread):
143
+ def run(self):
144
+ while True:
145
+ try:
146
+ line = input()
147
+ if "exit" in line.strip().lower():
148
+ handle_quit()
149
+ break
150
+ except EOFError:
151
+ break
152
+
153
+ def handle_quit():
154
+ if is_toggled and target_hwnd:
155
+ reset_window_state(target_hwnd)
156
+ logger.info("Exiting Window Transparency Tool.")
157
+
140
158
  # --- Main Execution Block ---
141
159
 
142
160
  if __name__ == "__main__":
@@ -155,8 +173,18 @@ if __name__ == "__main__":
155
173
  # Register the global hotkey
156
174
  keyboard.add_hotkey(hotkey, toggle_functionality)
157
175
 
176
+ # Handle SigINT/SigTERM gracefully
177
+ def signal_handler(sig, frame):
178
+ handle_quit()
179
+ sys.exit(0)
180
+
181
+ signal.signal(signal.SIGINT, signal_handler)
182
+ signal.signal(signal.SIGTERM, signal_handler)
183
+
158
184
  logger.info(f"Script running. Press '{hotkey}' on a window to toggle transparency.")
159
185
  logger.info("Press Ctrl+C in this console to exit.")
186
+
187
+ HandleSTDINThread().start()
160
188
 
161
189
  # Keep the script running to listen for the hotkey.
162
190
  # keyboard.wait() is a blocking call that waits indefinitely.
@@ -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=49999)
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():