GameSentenceMiner 2.14.7__py3-none-any.whl → 2.14.9__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.
Files changed (66) hide show
  1. GameSentenceMiner/config_gui.py +19 -10
  2. GameSentenceMiner/gsm.py +68 -8
  3. GameSentenceMiner/locales/en_us.json +4 -0
  4. GameSentenceMiner/locales/ja_jp.json +4 -0
  5. GameSentenceMiner/locales/zh_cn.json +4 -0
  6. GameSentenceMiner/obs.py +12 -8
  7. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/METADATA +1 -2
  8. gamesentenceminer-2.14.9.dist-info/RECORD +24 -0
  9. GameSentenceMiner/ai/__init__.py +0 -0
  10. GameSentenceMiner/ai/ai_prompting.py +0 -473
  11. GameSentenceMiner/ocr/__init__.py +0 -0
  12. GameSentenceMiner/ocr/gsm_ocr_config.py +0 -174
  13. GameSentenceMiner/ocr/ocrconfig.py +0 -129
  14. GameSentenceMiner/ocr/owocr_area_selector.py +0 -629
  15. GameSentenceMiner/ocr/owocr_helper.py +0 -638
  16. GameSentenceMiner/ocr/ss_picker.py +0 -140
  17. GameSentenceMiner/owocr/owocr/__init__.py +0 -1
  18. GameSentenceMiner/owocr/owocr/__main__.py +0 -9
  19. GameSentenceMiner/owocr/owocr/config.py +0 -148
  20. GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -1238
  21. GameSentenceMiner/owocr/owocr/ocr.py +0 -1691
  22. GameSentenceMiner/owocr/owocr/run.py +0 -1817
  23. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -109
  24. GameSentenceMiner/tools/__init__.py +0 -0
  25. GameSentenceMiner/tools/audio_offset_selector.py +0 -215
  26. GameSentenceMiner/tools/ss_selector.py +0 -135
  27. GameSentenceMiner/tools/window_transparency.py +0 -214
  28. GameSentenceMiner/util/__init__.py +0 -0
  29. GameSentenceMiner/util/communication/__init__.py +0 -22
  30. GameSentenceMiner/util/communication/send.py +0 -7
  31. GameSentenceMiner/util/communication/websocket.py +0 -94
  32. GameSentenceMiner/util/configuration.py +0 -1198
  33. GameSentenceMiner/util/db.py +0 -408
  34. GameSentenceMiner/util/downloader/Untitled_json.py +0 -472
  35. GameSentenceMiner/util/downloader/__init__.py +0 -0
  36. GameSentenceMiner/util/downloader/download_tools.py +0 -194
  37. GameSentenceMiner/util/downloader/oneocr_dl.py +0 -250
  38. GameSentenceMiner/util/electron_config.py +0 -259
  39. GameSentenceMiner/util/ffmpeg.py +0 -571
  40. GameSentenceMiner/util/get_overlay_coords.py +0 -366
  41. GameSentenceMiner/util/gsm_utils.py +0 -323
  42. GameSentenceMiner/util/model.py +0 -206
  43. GameSentenceMiner/util/notification.py +0 -147
  44. GameSentenceMiner/util/text_log.py +0 -214
  45. GameSentenceMiner/web/__init__.py +0 -0
  46. GameSentenceMiner/web/service.py +0 -132
  47. GameSentenceMiner/web/static/__init__.py +0 -0
  48. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  49. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  50. GameSentenceMiner/web/static/favicon.ico +0 -0
  51. GameSentenceMiner/web/static/favicon.svg +0 -3
  52. GameSentenceMiner/web/static/site.webmanifest +0 -21
  53. GameSentenceMiner/web/static/style.css +0 -292
  54. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  55. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  56. GameSentenceMiner/web/templates/__init__.py +0 -0
  57. GameSentenceMiner/web/templates/index.html +0 -50
  58. GameSentenceMiner/web/templates/text_replacements.html +0 -238
  59. GameSentenceMiner/web/templates/utility.html +0 -483
  60. GameSentenceMiner/web/texthooking_page.py +0 -584
  61. GameSentenceMiner/wip/__init___.py +0 -0
  62. gamesentenceminer-2.14.7.dist-info/RECORD +0 -77
  63. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/WHEEL +0 -0
  64. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/entry_points.txt +0 -0
  65. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/licenses/LICENSE +0 -0
  66. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/top_level.txt +0 -0
@@ -1,638 +0,0 @@
1
- import asyncio
2
- import io
3
- import json
4
- import logging
5
- import os
6
- import queue
7
- import threading
8
- import time
9
- from datetime import datetime
10
- from logging.handlers import RotatingFileHandler
11
- from pathlib import Path
12
- from tkinter import messagebox
13
-
14
- import mss
15
- import mss.tools
16
- import websockets
17
- from PIL import Image
18
- from rapidfuzz import fuzz
19
-
20
- from GameSentenceMiner import obs
21
- from GameSentenceMiner.util.electron_config import *
22
- from GameSentenceMiner.ocr.ss_picker import ScreenCropper
23
- from GameSentenceMiner.owocr.owocr.run import TextFiltering
24
- from GameSentenceMiner.util.configuration import get_config, get_app_directory, get_temporary_directory
25
- from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, has_config_changed, set_dpi_awareness, get_window, get_ocr_config_path
26
- from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
27
- from GameSentenceMiner.util.gsm_utils import sanitize_filename, do_text_replacements, OCR_REPLACEMENTS_FILE
28
- import threading
29
- import time
30
-
31
- CONFIG_FILE = Path("ocr_config.json")
32
- DEFAULT_IMAGE_PATH = r"C:\Users\Beangate\Pictures\msedge_acbl8GL7Ax.jpg" # CHANGE THIS
33
- logger = logging.getLogger("GSM_OCR")
34
- logger.setLevel(logging.DEBUG)
35
- # Create a file handler for logging
36
- log_file = os.path.join(get_app_directory(), "logs", "ocr_log.txt")
37
- os.makedirs(os.path.join(get_app_directory(), "logs"), exist_ok=True)
38
- file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
39
- file_handler.setLevel(logging.DEBUG)
40
- # Create a formatter and set it for the handler
41
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
42
- file_handler.setFormatter(formatter)
43
- # Add the handler to the logger
44
- logger.addHandler(file_handler)
45
-
46
- console_handler = logging.StreamHandler()
47
- console_handler.setLevel(logging.INFO)
48
- console_handler.setFormatter(formatter)
49
- logger.addHandler(console_handler)
50
-
51
-
52
- def get_ocr_config(window=None, use_window_for_config=False) -> OCRConfig:
53
- """Loads and updates screen capture areas from the corresponding JSON file."""
54
- ocr_config_dir = get_ocr_config_path()
55
- obs.update_current_game()
56
- if use_window_for_config and window:
57
- scene = sanitize_filename(window)
58
- else:
59
- scene = sanitize_filename(obs.get_current_scene())
60
- config_path = Path(ocr_config_dir) / f"{scene}.json"
61
- if not config_path.exists():
62
- ocr_config = OCRConfig(scene=scene, window=window, rectangles=[], coordinate_system="percentage")
63
- with open(config_path, 'w', encoding="utf-8") as f:
64
- json.dump(ocr_config.to_dict(), f, indent=4)
65
- return ocr_config
66
- try:
67
- with open(config_path, 'r', encoding="utf-8") as f:
68
- config_data = json.load(f)
69
- if "rectangles" in config_data and isinstance(config_data["rectangles"], list) and all(
70
- isinstance(item, list) and len(item) == 4 for item in config_data["rectangles"]):
71
- # Old config format, convert to new
72
- new_rectangles = []
73
- with mss.mss() as sct:
74
- monitors = sct.monitors
75
- default_monitor = monitors[1] if len(monitors) > 1 else monitors[0]
76
- for rect in config_data["rectangles"]:
77
- new_rectangles.append({
78
- "monitor": {
79
- "left": default_monitor["left"],
80
- "top": default_monitor["top"],
81
- "width": default_monitor["width"],
82
- "height": default_monitor["height"],
83
- "index": 0 # Assuming single monitor for old config
84
- },
85
- "coordinates": rect,
86
- "is_excluded": False
87
- })
88
- if 'excluded_rectangles' in config_data:
89
- for rect in config_data['excluded_rectangles']:
90
- new_rectangles.append({
91
- "monitor": {
92
- "left": default_monitor["left"],
93
- "top": default_monitor["top"],
94
- "width": default_monitor["width"],
95
- "height": default_monitor["height"],
96
- "index": 0 # Assuming single monitor for old config
97
- },
98
- "coordinates": rect,
99
- "is_excluded": True
100
- })
101
- new_config_data = {"scene": config_data.get("scene", scene), "window": config_data.get("window", None),
102
- "rectangles": new_rectangles, "coordinate_system": "absolute"}
103
- with open(config_path, 'w', encoding="utf-8") as f:
104
- json.dump(new_config_data, f, indent=4)
105
- return OCRConfig.from_dict(new_config_data)
106
- elif "rectangles" in config_data and isinstance(config_data["rectangles"], list) and all(
107
- isinstance(item, dict) and "coordinates" in item for item in config_data["rectangles"]):
108
- return OCRConfig.from_dict(config_data)
109
- else:
110
- raise Exception(f"Invalid config format in {config_path}.")
111
- except json.JSONDecodeError:
112
- print("Error decoding JSON. Please check your config file.")
113
- return None
114
- except Exception as e:
115
- print(f"Error loading config: {e}")
116
- return None
117
-
118
-
119
- websocket_server_thread = None
120
- websocket_queue = queue.Queue()
121
- paused = False
122
-
123
-
124
- class WebsocketServerThread(threading.Thread):
125
- def __init__(self, read):
126
- super().__init__(daemon=True)
127
- self._loop = None
128
- self.read = read
129
- self.clients = set()
130
- self._event = threading.Event()
131
-
132
- @property
133
- def loop(self):
134
- self._event.wait()
135
- return self._loop
136
-
137
- async def send_text_coroutine(self, message):
138
- for client in self.clients:
139
- await client.send(message)
140
-
141
- async def server_handler(self, websocket):
142
- self.clients.add(websocket)
143
- try:
144
- async for message in websocket:
145
- if self.read and not paused:
146
- websocket_queue.put(message)
147
- try:
148
- await websocket.send('True')
149
- except websockets.exceptions.ConnectionClosedOK:
150
- pass
151
- else:
152
- try:
153
- await websocket.send('False')
154
- except websockets.exceptions.ConnectionClosedOK:
155
- pass
156
- except websockets.exceptions.ConnectionClosedError:
157
- pass
158
- finally:
159
- self.clients.remove(websocket)
160
-
161
- async def send_text(self, text, line_time: datetime):
162
- if text:
163
- return asyncio.run_coroutine_threadsafe(
164
- self.send_text_coroutine(json.dumps({"sentence": text, "time": line_time.isoformat(), "process_path": obs.get_current_game()})), self.loop)
165
-
166
- def stop_server(self):
167
- self.loop.call_soon_threadsafe(self._stop_event.set)
168
-
169
- def run(self):
170
- async def main():
171
- self._loop = asyncio.get_running_loop()
172
- self._stop_event = stop_event = asyncio.Event()
173
- self._event.set()
174
- self.server = start_server = websockets.serve(self.server_handler,
175
- "0.0.0.0",
176
- get_config().advanced.ocr_websocket_port,
177
- max_size=1000000000)
178
- async with start_server:
179
- await stop_event.wait()
180
-
181
- asyncio.run(main())
182
-
183
-
184
- def compare_ocr_results(prev_text, new_text, threshold=90):
185
- if not prev_text or not new_text:
186
- return False
187
- if isinstance(prev_text, list):
188
- prev_text = ''.join([item for item in prev_text if item is not None]) if prev_text else ""
189
- if isinstance(new_text, list):
190
- new_text = ''.join([item for item in new_text if item is not None]) if new_text else ""
191
- similarity = fuzz.ratio(prev_text, new_text)
192
- return similarity >= threshold
193
-
194
- all_cords = None
195
- rectangles = None
196
- last_ocr2_result = []
197
- last_sent_result = ""
198
-
199
- def do_second_ocr(ocr1_text, time, img, filtering, pre_crop_image=None, ignore_furigana_filter=False, ignore_previous_result=False):
200
- global twopassocr, ocr2, last_ocr2_result, last_sent_result
201
- try:
202
- orig_text, text = run.process_and_write_results(img, None, last_ocr2_result if not ignore_previous_result else None, filtering, None,
203
- engine=get_ocr_ocr2(), furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
204
-
205
- if compare_ocr_results(last_sent_result, text, threshold=80):
206
- if text:
207
- logger.info("Seems like Text we already sent, not doing anything.")
208
- return
209
- save_result_image(img, pre_crop_image=pre_crop_image)
210
- last_ocr2_result = orig_text
211
- last_sent_result = text
212
- asyncio.run(send_result(text, time))
213
- except json.JSONDecodeError:
214
- print("Invalid JSON received.")
215
- except Exception as e:
216
- logger.exception(e)
217
- print(f"Error processing message: {e}")
218
-
219
-
220
- def save_result_image(img, pre_crop_image=None):
221
- if isinstance(img, bytes):
222
- with open(os.path.join(get_temporary_directory(), "last_successful_ocr.png"), "wb") as f:
223
- f.write(img)
224
- else:
225
- img.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
226
- run.set_last_image(pre_crop_image if pre_crop_image else img)
227
-
228
-
229
- async def send_result(text, time):
230
- if text:
231
- text = do_text_replacements(text, OCR_REPLACEMENTS_FILE)
232
- if get_ocr_send_to_clipboard():
233
- import pyperclip
234
- pyperclip.copy(text)
235
- try:
236
- await websocket_server_thread.send_text(text, time)
237
- except Exception as e:
238
- logger.debug(f"Error sending text to websocket: {e}")
239
-
240
-
241
- previous_text_list = []
242
- previous_text = "" # Store last OCR result
243
- previous_ocr1_result = "" # Store last OCR1 result
244
- last_oneocr_time = None # Store last OCR time
245
- text_stable_start_time = None # Store the start time when text becomes stable
246
- previous_img = None
247
- previous_orig_text = "" # Store original text result
248
- TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
249
- force_stable = False
250
-
251
- class ConfigChangeCheckThread(threading.Thread):
252
- def __init__(self):
253
- super().__init__(daemon=True)
254
- self.last_changes = None
255
- self.config_callbacks = []
256
- self.area_callbacks = []
257
-
258
- def run(self):
259
- global ocr_config
260
- while True:
261
- try:
262
- section_changed, changes = has_ocr_config_changed()
263
- if section_changed:
264
- reload_electron_config()
265
- self.last_changes = changes
266
- # Only run this block after a change has occurred and then the section is stable (no change)
267
- if self.last_changes is not None and not section_changed:
268
- logger.info(f"Detected config changes: {self.last_changes}")
269
- for cb in self.config_callbacks:
270
- cb(self.last_changes)
271
- if hasattr(run, 'handle_config_change'):
272
- run.handle_config_change()
273
- if any(c in self.last_changes for c in ('ocr1', 'ocr2', 'language', 'furigana_filter_sensitivity')):
274
- reset_callback_vars()
275
- self.last_changes = None
276
- ocr_config_changed = has_config_changed(ocr_config)
277
- if ocr_config_changed:
278
- logger.info("OCR config has changed, reloading...")
279
- ocr_config = get_ocr_config(use_window_for_config=True, window=obs.get_current_game())
280
- for cb in self.area_callbacks:
281
- cb(ocr_config)
282
- if hasattr(run, 'handle_area_config_changes'):
283
- run.handle_area_config_changes(ocr_config)
284
- reset_callback_vars()
285
- except Exception as e:
286
- logger.debug(f"ConfigChangeCheckThread error: {e}")
287
- time.sleep(0.25) # Lowered to 0.25s for more responsiveness
288
-
289
- def add_config_callback(self, callback):
290
- self.config_callbacks.append(callback)
291
-
292
- def add_area_callback(self, callback):
293
- self.area_callbacks.append(callback)
294
-
295
- def reset_callback_vars():
296
- 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
297
- previous_text = None
298
- previous_orig_text = ""
299
- previous_img = None
300
- text_stable_start_time = None
301
- last_oneocr_time = None
302
- force_stable = False
303
- previous_ocr1_result = ""
304
- previous_text_list = []
305
- last_ocr2_result = ""
306
- run.set_last_image(None)
307
-
308
- # class TwoPassOCRHandler:
309
- # def __init__(self, ocr1, ocr2):
310
- # self.ocr1 = ocr1
311
- # self.ocr2 = ocr2
312
- # self.second_ocr_queue = queue.Queue()
313
- # self.previous_text = None
314
- # self.previous_orig_text = ""
315
- # self.previous_img = None
316
- # self.text_stable_start_time = None
317
- # self.last_oneocr_time = None
318
- # self.force_stable = False
319
- # self.previous_ocr1_result = ""
320
- # self.last_sent_result = ""
321
- # self.previous_text_list = []
322
-
323
- # def check_first_ocr(self, text, orig_text, time, img=None, came_from_ss=False, filtering=None, crop_coords=None):
324
- # if not twopassocr or not ocr2:
325
- # return text_callback(text, orig_text, time, img=img, came_from_ss=came_from_ss, filtering=filtering, crop_coords=crop_coords)
326
-
327
- # text_callback(text, orig_text, time, img=img, came_from_ss=came_from_ss, filtering=filtering, crop_coords=crop_coords)
328
-
329
- # def set_ocr_ocr1(self, ocr1):
330
- # self.ocr1 = ocr1
331
- # def set_ocr_ocr2(self, ocr2):
332
- # self.ocr2 = ocr2
333
-
334
- # def get_ocr_ocr1(self):
335
- # return self.ocr1
336
-
337
- # def get_ocr_ocr2(self):
338
- # return self.ocr2
339
-
340
- def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering=None, crop_coords=None):
341
- 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, last_sent_result
342
- orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
343
- if came_from_ss:
344
- save_result_image(img)
345
- asyncio.run(send_result(text, time))
346
- return
347
-
348
- if not text:
349
- run.set_last_image(img)
350
-
351
- line_start_time = time if time else datetime.now()
352
-
353
- if manual or not get_ocr_two_pass_ocr():
354
- if compare_ocr_results(last_sent_result, text, 80):
355
- if text:
356
- logger.info("Seems like Text we already sent, not doing anything.")
357
- return
358
- save_result_image(img)
359
- asyncio.run(send_result(text, line_start_time))
360
- last_sent_result = text
361
- previous_orig_text = orig_text_string
362
- previous_text = None
363
- previous_img = None
364
- text_stable_start_time = None
365
- last_oneocr_time = None
366
- return
367
- if not text or force_stable:
368
- force_stable = False
369
- if previous_text and text_stable_start_time:
370
- stable_time = text_stable_start_time
371
- previous_img_local = previous_img
372
- pre_crop_image = previous_img_local
373
- if compare_ocr_results(previous_orig_text, orig_text_string):
374
- if text:
375
- logger.info("Seems like Text we already sent, not doing anything.")
376
- previous_text = None
377
- return
378
- previous_orig_text = orig_text_string
379
- previous_ocr1_result = previous_text
380
- if crop_coords and get_ocr_optimize_second_scan():
381
- x1, y1, x2, y2 = crop_coords
382
- x1 = max(0, min(x1, img.width))
383
- y1 = max(0, min(y1, img.height))
384
- x2 = max(x1, min(x2, img.width))
385
- y2 = max(y1, min(y2, img.height))
386
- previous_img_local.save(os.path.join(get_temporary_directory(), "pre_oneocrcrop.png"))
387
- try:
388
- previous_img_local = previous_img_local.crop((x1, y1, x2, y2))
389
- except ValueError:
390
- logger.warning("Error cropping image, using original image")
391
- second_ocr_queue.put((previous_text, stable_time, previous_img_local, filtering, pre_crop_image))
392
- # threading.Thread(target=do_second_ocr, args=(previous_text, stable_time, previous_img_local, filtering), daemon=True).start()
393
- previous_img = None
394
- previous_text = None
395
- text_stable_start_time = None
396
- last_oneocr_time = None
397
- previous_text = None
398
- return
399
-
400
- # Make sure it's an actual new line before starting the timer
401
- if text and compare_ocr_results(orig_text_string, previous_orig_text):
402
- return
403
-
404
- if not text_stable_start_time:
405
- text_stable_start_time = line_start_time
406
- previous_text = text
407
- previous_text_list = orig_text
408
- last_oneocr_time = line_start_time
409
- previous_img = img
410
-
411
- done = False
412
-
413
- # Create a queue for tasks
414
- second_ocr_queue = queue.Queue()
415
-
416
- def process_task_queue():
417
- while True:
418
- try:
419
- task = second_ocr_queue.get()
420
- if task is None: # Exit signal
421
- break
422
- ocr1_text, stable_time, previous_img_local, filtering, pre_crop_image = task
423
- do_second_ocr(ocr1_text, stable_time, previous_img_local, filtering, pre_crop_image)
424
- except Exception as e:
425
- logger.exception(f"Error processing task: {e}")
426
- finally:
427
- second_ocr_queue.task_done()
428
-
429
-
430
- def run_oneocr(ocr_config: OCRConfig, rectangles, config_check_thread):
431
- global done
432
- print("Running OneOCR")
433
- screen_area = None
434
- screen_areas = [",".join(str(c) for c in rect_config.coordinates) for rect_config in rectangles if not rect_config.is_excluded]
435
- exclusions = list(rect.coordinates for rect in list(filter(lambda x: x.is_excluded, rectangles)))
436
-
437
- run.init_config(False)
438
- try:
439
- read_from = ""
440
- if obs_ocr:
441
- read_from = "obs"
442
- elif window:
443
- read_from = "screencapture"
444
- read_from_secondary = "clipboard" if ss_clipboard else None
445
- run.run(read_from=read_from,
446
- read_from_secondary=read_from_secondary,
447
- write_to="callback",
448
- screen_capture_area=screen_area,
449
- # screen_capture_monitor=monitor_config['index'],
450
- screen_capture_window=ocr_config.window if ocr_config and ocr_config.window else None,
451
- screen_capture_delay_secs=get_ocr_scan_rate(), engine=ocr1,
452
- text_callback=text_callback,
453
- screen_capture_exclusions=exclusions,
454
- monitor_index=None,
455
- ocr1=ocr1,
456
- ocr2=ocr2,
457
- gsm_ocr_config=ocr_config,
458
- screen_capture_areas=screen_areas,
459
- furigana_filter_sensitivity=furigana_filter_sensitivity,
460
- screen_capture_combo=manual_ocr_hotkey if manual_ocr_hotkey and manual else None,
461
- config_check_thread=config_check_thread)
462
- except Exception as e:
463
- logger.exception(f"Error running OneOCR: {e}")
464
- done = True
465
-
466
-
467
-
468
- def add_ss_hotkey(ss_hotkey="ctrl+shift+g"):
469
- import keyboard
470
-
471
- def ocr_secondary_rectangles():
472
- logger.info("Running secondary OCR rectangles...")
473
- ocr_config = get_ocr_config()
474
- img = obs.get_screenshot_PIL(compression=80, img_format="jpg")
475
- ocr_config.scale_to_custom_size(img.width, img.height)
476
- img = run.apply_ocr_config_to_image(img, ocr_config, is_secondary=True)
477
- do_second_ocr("", datetime.now(), img, TextFiltering(lang=get_ocr_language()), ignore_furigana_filter=True, ignore_previous_result=True)
478
-
479
- secret_ss_hotkey = "F14"
480
- filtering = TextFiltering(lang=get_ocr_language())
481
- cropper = ScreenCropper()
482
- def capture():
483
- print("Taking screenshot...")
484
- img = cropper.run()
485
- do_second_ocr("", datetime.now(), img, filtering, ignore_furigana_filter=True, ignore_previous_result=True)
486
- def capture_main_monitor():
487
- print("Taking screenshot of main monitor...")
488
- with mss.mss() as sct:
489
- main_monitor = sct.monitors[1] if len(sct.monitors) > 1 else sct.monitors[0]
490
- img = sct.grab(main_monitor)
491
- img_bytes = mss.tools.to_png(img.rgb, img.size)
492
- do_second_ocr("", datetime.now(), img_bytes, filtering, ignore_furigana_filter=True, ignore_previous_result=True)
493
- hotkey_reg = None
494
- try:
495
- hotkey_reg = keyboard.add_hotkey(ss_hotkey, capture)
496
- if not manual:
497
- secondary_hotkey_reg = keyboard.add_hotkey(get_ocr_manual_ocr_hotkey().lower(), ocr_secondary_rectangles)
498
- if "f13" in ss_hotkey.lower():
499
- secret_hotkey_reg = keyboard.add_hotkey(secret_ss_hotkey, capture_main_monitor)
500
- print(f"Press {ss_hotkey} to take a screenshot.")
501
- except Exception as e:
502
- if hotkey_reg:
503
- keyboard.remove_hotkey(hotkey_reg)
504
- if secondary_hotkey_reg:
505
- keyboard.remove_hotkey(secondary_hotkey_reg)
506
- if secret_hotkey_reg:
507
- keyboard.remove_hotkey(secret_hotkey_reg)
508
- logger.error(f"Error setting up screenshot hotkey with keyboard, Attempting Backup: {e}")
509
- logger.debug(e)
510
- pynput_hotkey = ss_hotkey.replace("ctrl", "<ctrl>").replace("shift", "<shift>").replace("alt", "<alt>")
511
- secret_ss_hotkey = secret_hotkey_reg.replace("ctrl", "<ctrl>").replace("shift", "<shift>").replace("alt", "<alt>")
512
- secondary_ss_hotkey = secondary_hotkey_reg.replace("ctrl", "<ctrl>").replace("shift", "<shift>").replace("alt", "<alt>")
513
- try:
514
- from pynput import keyboard as pynput_keyboard
515
- listener = pynput_keyboard.GlobalHotKeys({
516
- pynput_hotkey: capture,
517
- secondary_ss_hotkey: ocr_secondary_rectangles,
518
- secret_ss_hotkey: capture_main_monitor
519
- })
520
- listener.start()
521
- print(f"Press {pynput_hotkey} to take a screenshot.")
522
- except Exception as e:
523
- logger.error(f"Error setting up screenshot hotkey with pynput, Screenshot Hotkey Will not work: {e}")
524
-
525
- def set_force_stable_hotkey():
526
- import keyboard
527
- global force_stable
528
- def toggle_force_stable():
529
- global force_stable
530
- force_stable = not force_stable
531
- if force_stable:
532
- print("Force stable mode enabled.")
533
- else:
534
- print("Force stable mode disabled.")
535
- keyboard.add_hotkey('p', toggle_force_stable)
536
- print("Press Ctrl+Shift+F to toggle force stable mode.")
537
-
538
- if __name__ == "__main__":
539
- try:
540
- global ocr1, ocr2, twopassocr, language, ss_clipboard, ss, ocr_config, furigana_filter_sensitivity, area_select_ocr_hotkey, window, optimize_second_scan, use_window_for_config, keep_newline, obs_ocr
541
- import sys
542
-
543
- import argparse
544
-
545
- parser = argparse.ArgumentParser(description="OCR Configuration")
546
- parser.add_argument("--language", type=str, default="ja", help="Language for OCR (default: ja)")
547
- parser.add_argument("--ocr1", type=str, default="oneocr", help="Primary OCR engine (default: oneocr)")
548
- parser.add_argument("--ocr2", type=str, default="glens", help="Secondary OCR engine (default: glens)")
549
- parser.add_argument("--twopassocr", type=int, choices=[0, 1], default=1,
550
- help="Enable two-pass OCR (default: 1)")
551
- parser.add_argument("--manual", action="store_true", help="Use screenshot-only mode")
552
- parser.add_argument("--clipboard", action="store_true", help="Use clipboard for input")
553
- parser.add_argument("--clipboard-output", action="store_true", default=False, help="Use clipboard for output")
554
- parser.add_argument("--window", type=str, help="Specify the window name for OCR")
555
- parser.add_argument("--furigana_filter_sensitivity", type=float, default=0,
556
- help="Furigana Filter Sensitivity for OCR (default: 0)")
557
- parser.add_argument("--manual_ocr_hotkey", type=str, default=None, help="Hotkey for manual OCR (default: None)")
558
- parser.add_argument("--area_select_ocr_hotkey", type=str, default="ctrl+shift+o",
559
- help="Hotkey for area selection OCR (default: ctrl+shift+o)")
560
- parser.add_argument("--optimize_second_scan", action="store_true",
561
- help="Optimize second scan by cropping based on first scan results")
562
- parser.add_argument("--use_window_for_config", action="store_true",
563
- help="Use the specified window for loading OCR configuration")
564
- parser.add_argument("--keep_newline", action="store_true", help="Keep new lines in OCR output")
565
- parser.add_argument('--obs_ocr', action='store_true', help='Use OBS for Picture Source (not implemented)')
566
-
567
- args = parser.parse_args()
568
-
569
- language = args.language
570
- ocr1 = args.ocr1
571
- ocr2 = args.ocr2 if args.ocr2 else None
572
- twopassocr = bool(args.twopassocr)
573
- manual = args.manual
574
- ss_clipboard = args.clipboard
575
- window_name = args.window
576
- furigana_filter_sensitivity = args.furigana_filter_sensitivity
577
- ss_hotkey = args.area_select_ocr_hotkey.lower()
578
- manual_ocr_hotkey = args.manual_ocr_hotkey.lower().replace("ctrl", "<ctrl>").replace("shift",
579
- "<shift>").replace(
580
- "alt", "<alt>") if args.manual_ocr_hotkey else None
581
- clipboard_output = args.clipboard_output
582
- optimize_second_scan = args.optimize_second_scan
583
- use_window_for_config = args.use_window_for_config
584
- keep_newline = args.keep_newline
585
- obs_ocr = args.obs_ocr
586
-
587
- obs.connect_to_obs_sync(retry=0)
588
-
589
- # Start config change checker thread
590
- config_check_thread = ConfigChangeCheckThread()
591
- config_check_thread.start()
592
- # Example: add a callback to config_check_thread if needed
593
- # config_check_thread.add_callback(lambda: print("Config changed!"))
594
-
595
- window = None
596
- logger.info(f"Received arguments: {vars(args)}")
597
- # set_force_stable_hotkey()
598
- ocr_config: OCRConfig = get_ocr_config(window=window_name, use_window_for_config=use_window_for_config)
599
- if ocr_config and not obs_ocr:
600
- if ocr_config.window:
601
- start_time = time.time()
602
- while time.time() - start_time < 30:
603
- window = get_window(ocr_config.window)
604
- if window or manual:
605
- if window:
606
- ocr_config.scale_coords()
607
- break
608
- logger.info(f"Window: {ocr_config.window} Could not be found, retrying in 1 second...")
609
- time.sleep(1)
610
- else:
611
- logger.error(f"Window '{ocr_config.window}' not found within 30 seconds.")
612
- sys.exit(1)
613
- logger.info(
614
- f"Starting OCR with configuration: Window: {ocr_config.window}, Rectangles: {ocr_config.rectangles}, Engine 1: {ocr1}, Engine 2: {ocr2}, Two-pass OCR: {twopassocr}")
615
- set_dpi_awareness()
616
- if manual or ocr_config:
617
- rectangles = ocr_config.rectangles if ocr_config and ocr_config.rectangles else []
618
- oneocr_threads = []
619
- ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config, rectangles, config_check_thread), daemon=True)
620
- ocr_thread.start()
621
- if not manual:
622
- worker_thread = threading.Thread(target=process_task_queue, daemon=True)
623
- worker_thread.start()
624
- websocket_server_thread = WebsocketServerThread(read=True)
625
- websocket_server_thread.start()
626
- add_ss_hotkey(ss_hotkey)
627
- try:
628
- while not done:
629
- time.sleep(1)
630
- except KeyboardInterrupt as e:
631
- pass
632
- else:
633
- print("Failed to load OCR configuration. Please check the logs.")
634
- except Exception as e:
635
- logger.info(e, exc_info=True)
636
- logger.debug(e, exc_info=True)
637
- logger.info("Closing in 5 seconds...")
638
- time.sleep(5)