GameSentenceMiner 2.18.9__py3-none-any.whl → 2.18.11__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.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/owocr/owocr/ocr.py +44 -15
- GameSentenceMiner/tools/furigana_filter_preview.py +101 -21
- GameSentenceMiner/ui/config_gui.py +53 -1
- GameSentenceMiner/ui/furigana_filter_preview.py +410 -0
- GameSentenceMiner/util/configuration.py +16 -3
- GameSentenceMiner/util/get_overlay_coords.py +12 -5
- GameSentenceMiner/web/texthooking_page.py +3 -0
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/RECORD +13 -12
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.9.dist-info → gamesentenceminer-2.18.11.dist-info}/top_level.txt +0 -0
|
@@ -277,11 +277,12 @@ class GoogleLens:
|
|
|
277
277
|
key = 'l'
|
|
278
278
|
available = False
|
|
279
279
|
|
|
280
|
-
def __init__(self, lang='ja'):
|
|
280
|
+
def __init__(self, lang='ja', get_furigana_sens_from_file=True):
|
|
281
281
|
import regex
|
|
282
282
|
self.regex = get_regex(lang)
|
|
283
283
|
self.initial_lang = lang
|
|
284
284
|
self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
|
|
285
|
+
self.get_furigana_sens_from_file = get_furigana_sens_from_file
|
|
285
286
|
if 'betterproto' not in sys.modules:
|
|
286
287
|
logger.warning('betterproto not available, Google Lens will not work!')
|
|
287
288
|
else:
|
|
@@ -289,10 +290,10 @@ class GoogleLens:
|
|
|
289
290
|
logger.info('Google Lens ready')
|
|
290
291
|
|
|
291
292
|
def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False):
|
|
292
|
-
if
|
|
293
|
+
if self.get_furigana_sens_from_file:
|
|
293
294
|
furigana_filter_sensitivity = get_furigana_filter_sensitivity()
|
|
294
295
|
else:
|
|
295
|
-
furigana_filter_sensitivity =
|
|
296
|
+
furigana_filter_sensitivity = furigana_filter_sensitivity
|
|
296
297
|
lang = get_ocr_language()
|
|
297
298
|
img, is_path = input_to_pil_image(img)
|
|
298
299
|
if lang != self.initial_lang:
|
|
@@ -362,6 +363,12 @@ class GoogleLens:
|
|
|
362
363
|
text = response_dict['objects_response']['text']
|
|
363
364
|
skipped = []
|
|
364
365
|
previous_line = None
|
|
366
|
+
filtered_response_dict = response_dict
|
|
367
|
+
if furigana_filter_sensitivity:
|
|
368
|
+
import copy
|
|
369
|
+
filtered_response_dict = copy.deepcopy(response_dict)
|
|
370
|
+
filtered_paragraphs = []
|
|
371
|
+
|
|
365
372
|
if 'text_layout' in text:
|
|
366
373
|
for paragraph in text['text_layout']['paragraphs']:
|
|
367
374
|
if previous_line:
|
|
@@ -376,25 +383,40 @@ class GoogleLens:
|
|
|
376
383
|
# logger.info(avg_height * 2)
|
|
377
384
|
if vertical_space > avg_height * 2:
|
|
378
385
|
res += 'BLANK_LINE\n'
|
|
386
|
+
passed_furigana_filter_lines = []
|
|
379
387
|
for line in paragraph['lines']:
|
|
380
388
|
if furigana_filter_sensitivity:
|
|
381
389
|
line_width = line['geometry']['bounding_box']['width'] * img.width
|
|
382
390
|
line_height = line['geometry']['bounding_box']['height'] * img.height
|
|
391
|
+
passes = False
|
|
383
392
|
for word in line['words']:
|
|
384
393
|
if self.punctuation_regex.findall(word['plain_text']):
|
|
385
394
|
res += word['plain_text'] + word['text_separator']
|
|
386
395
|
continue
|
|
387
396
|
if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
|
|
388
397
|
res += word['plain_text'] + word['text_separator']
|
|
398
|
+
passes = True
|
|
389
399
|
else:
|
|
390
400
|
skipped.extend(word['plain_text'])
|
|
391
401
|
continue
|
|
402
|
+
if passes:
|
|
403
|
+
passed_furigana_filter_lines.append(line)
|
|
392
404
|
else:
|
|
393
405
|
for word in line['words']:
|
|
394
406
|
res += word['plain_text'] + word['text_separator']
|
|
395
407
|
res += '\n'
|
|
396
408
|
|
|
409
|
+
if furigana_filter_sensitivity and passed_furigana_filter_lines:
|
|
410
|
+
# Create a filtered paragraph with only the passing lines
|
|
411
|
+
filtered_paragraph = paragraph.copy()
|
|
412
|
+
filtered_paragraph['lines'] = passed_furigana_filter_lines
|
|
413
|
+
filtered_paragraphs.append(filtered_paragraph)
|
|
414
|
+
|
|
397
415
|
previous_line = paragraph
|
|
416
|
+
|
|
417
|
+
if furigana_filter_sensitivity:
|
|
418
|
+
filtered_response_dict['objects_response']['text']['text_layout']['paragraphs'] = filtered_paragraphs
|
|
419
|
+
|
|
398
420
|
res += '\n'
|
|
399
421
|
# logger.info(
|
|
400
422
|
# f"Skipped {len(skipped)} chars due to furigana filter sensitivity: {furigana_filter_sensitivity}")
|
|
@@ -438,7 +460,7 @@ class GoogleLens:
|
|
|
438
460
|
# res += '\n'
|
|
439
461
|
|
|
440
462
|
if return_coords:
|
|
441
|
-
x = (True, res,
|
|
463
|
+
x = (True, res, filtered_response_dict)
|
|
442
464
|
else:
|
|
443
465
|
x = (True, res)
|
|
444
466
|
|
|
@@ -869,11 +891,12 @@ class OneOCR:
|
|
|
869
891
|
key = 'z'
|
|
870
892
|
available = False
|
|
871
893
|
|
|
872
|
-
def __init__(self, config={}, lang='ja'):
|
|
894
|
+
def __init__(self, config={}, lang='ja', get_furigana_sens_from_file=True):
|
|
873
895
|
import regex
|
|
874
896
|
self.initial_lang = lang
|
|
875
897
|
self.regex = get_regex(lang)
|
|
876
898
|
self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
|
|
899
|
+
self.get_furigana_sens_from_file = get_furigana_sens_from_file
|
|
877
900
|
if sys.platform == 'win32':
|
|
878
901
|
if int(platform.release()) < 10:
|
|
879
902
|
logger.warning('OneOCR is not supported on Windows older than 10!')
|
|
@@ -921,10 +944,10 @@ class OneOCR:
|
|
|
921
944
|
|
|
922
945
|
def __call__(self, img, furigana_filter_sensitivity=0, return_coords=False, multiple_crop_coords=False, return_one_box=True, return_dict=False):
|
|
923
946
|
lang = get_ocr_language()
|
|
924
|
-
if
|
|
947
|
+
if self.get_furigana_sens_from_file:
|
|
925
948
|
furigana_filter_sensitivity = get_furigana_filter_sensitivity()
|
|
926
949
|
else:
|
|
927
|
-
furigana_filter_sensitivity =
|
|
950
|
+
furigana_filter_sensitivity = furigana_filter_sensitivity
|
|
928
951
|
if lang != self.initial_lang:
|
|
929
952
|
self.initial_lang = lang
|
|
930
953
|
self.regex = get_regex(lang)
|
|
@@ -958,6 +981,7 @@ class OneOCR:
|
|
|
958
981
|
skipped = []
|
|
959
982
|
boxes = []
|
|
960
983
|
if furigana_filter_sensitivity > 0:
|
|
984
|
+
passing_lines = []
|
|
961
985
|
for line in filtered_lines:
|
|
962
986
|
line_x1, line_x2, line_x3, line_x4 = line['bounding_rect']['x1'], line['bounding_rect']['x2'], \
|
|
963
987
|
line['bounding_rect']['x3'], line['bounding_rect']['x4']
|
|
@@ -965,16 +989,20 @@ class OneOCR:
|
|
|
965
989
|
line['bounding_rect']['y3'], line['bounding_rect']['y4']
|
|
966
990
|
line_width = max(line_x2 - line_x1, line_x3 - line_x4)
|
|
967
991
|
line_height = max(line_y3 - line_y1, line_y4 - line_y2)
|
|
968
|
-
|
|
969
|
-
|
|
992
|
+
|
|
993
|
+
# Check if the line passes the size filter
|
|
994
|
+
if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
|
|
995
|
+
# Line passes - include all its text and add to passing_lines
|
|
996
|
+
for char in line['words']:
|
|
970
997
|
res += char['text']
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
998
|
+
passing_lines.append(line)
|
|
999
|
+
else:
|
|
1000
|
+
# Line fails - only include punctuation, skip the rest
|
|
1001
|
+
for char in line['words']:
|
|
975
1002
|
skipped.extend(char for char in line['text'])
|
|
976
|
-
continue
|
|
977
1003
|
res += '\n'
|
|
1004
|
+
filtered_lines = passing_lines
|
|
1005
|
+
return_resp = {'text': res, 'text_angle': ocr_resp['text_angle'], 'lines': passing_lines}
|
|
978
1006
|
# logger.info(
|
|
979
1007
|
# f"Skipped {len(skipped)} chars due to furigana filter sensitivity: {furigana_filter_sensitivity}")
|
|
980
1008
|
# widths, heights = [], []
|
|
@@ -1012,6 +1040,7 @@ class OneOCR:
|
|
|
1012
1040
|
# res += '\n'
|
|
1013
1041
|
else:
|
|
1014
1042
|
res = ocr_resp['text']
|
|
1043
|
+
return_resp = ocr_resp
|
|
1015
1044
|
|
|
1016
1045
|
if multiple_crop_coords:
|
|
1017
1046
|
for line in filtered_lines:
|
|
@@ -1042,7 +1071,7 @@ class OneOCR:
|
|
|
1042
1071
|
if return_one_box:
|
|
1043
1072
|
x.append(crop_coords)
|
|
1044
1073
|
if return_dict:
|
|
1045
|
-
x.append(
|
|
1074
|
+
x.append(return_resp)
|
|
1046
1075
|
if is_path:
|
|
1047
1076
|
img.close()
|
|
1048
1077
|
return x
|
|
@@ -6,9 +6,42 @@ import threading
|
|
|
6
6
|
import regex
|
|
7
7
|
|
|
8
8
|
from GameSentenceMiner import obs
|
|
9
|
-
from GameSentenceMiner.util.configuration import logger
|
|
9
|
+
from GameSentenceMiner.util.configuration import logger, get_overlay_config
|
|
10
10
|
from GameSentenceMiner.owocr.owocr.ocr import GoogleLens, OneOCR
|
|
11
11
|
|
|
12
|
+
def get_overlay_screenshot() -> Image.Image:
|
|
13
|
+
"""
|
|
14
|
+
Captures a screenshot from the configured overlay monitor using mss.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A PIL Image object of the screenshot from the overlay monitor.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
import mss
|
|
21
|
+
overlay_config = get_overlay_config()
|
|
22
|
+
monitor_index = overlay_config.monitor_to_capture
|
|
23
|
+
|
|
24
|
+
with mss.mss() as sct:
|
|
25
|
+
# mss.monitors[0] is all monitors combined, mss.monitors[1] is the first monitor
|
|
26
|
+
# So we need to add 1 to the monitor_index to get the correct monitor
|
|
27
|
+
monitor = sct.monitors[monitor_index + 1]
|
|
28
|
+
screenshot = sct.grab(monitor)
|
|
29
|
+
|
|
30
|
+
# Convert to PIL Image
|
|
31
|
+
img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
|
|
32
|
+
logger.info(f"Screenshot captured from monitor {monitor_index + 1} ({img.width}x{img.height})")
|
|
33
|
+
return img
|
|
34
|
+
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.error("mss library not found. Please install it to use overlay functionality.")
|
|
37
|
+
raise
|
|
38
|
+
except IndexError:
|
|
39
|
+
logger.error(f"Monitor index {monitor_index + 1} not found. Available monitors: {len(sct.monitors) - 1}")
|
|
40
|
+
raise
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Failed to capture overlay screenshot: {e}")
|
|
43
|
+
raise
|
|
44
|
+
|
|
12
45
|
def get_ocr_results_from_image(image_obj: Image.Image) -> tuple:
|
|
13
46
|
"""
|
|
14
47
|
This is the function where you will plug in your OCR logic.
|
|
@@ -34,7 +67,8 @@ class FuriganaFilterVisualizer:
|
|
|
34
67
|
self.ocr1_result = None
|
|
35
68
|
self.ocr2_result = None
|
|
36
69
|
self.current_ocr = 1
|
|
37
|
-
self.
|
|
70
|
+
self.title_prefix = "Furigana Filter Visualizer"
|
|
71
|
+
self.master.title(f"{self.title_prefix} - Lens")
|
|
38
72
|
|
|
39
73
|
self.words_data = []
|
|
40
74
|
self.lines_data = []
|
|
@@ -84,6 +118,12 @@ class FuriganaFilterVisualizer:
|
|
|
84
118
|
|
|
85
119
|
self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
|
|
86
120
|
self.master.protocol("WM_DELETE_WINDOW", self.on_ok)
|
|
121
|
+
|
|
122
|
+
def set_title_prefix(self, prefix: str):
|
|
123
|
+
"""Set the title prefix and update the current title."""
|
|
124
|
+
self.title_prefix = prefix
|
|
125
|
+
ocr_name = "Lens" if self.current_ocr == 1 else "OneOCR"
|
|
126
|
+
self.master.title(f"{self.title_prefix} - {ocr_name}")
|
|
87
127
|
|
|
88
128
|
def update_with_ocr_data(self, ocr1_result, ocr2_result):
|
|
89
129
|
"""Called by the background thread to populate the GUI with OCR data."""
|
|
@@ -123,10 +163,10 @@ class FuriganaFilterVisualizer:
|
|
|
123
163
|
# Change to oneocr or lens, in title too
|
|
124
164
|
if self.current_ocr == 1:
|
|
125
165
|
self.swap_button.config(text="Switch to OneOCR")
|
|
126
|
-
self.master.title("
|
|
166
|
+
self.master.title(f"{self.title_prefix} - Lens")
|
|
127
167
|
else:
|
|
128
168
|
self.swap_button.config(text="Switch to Lens")
|
|
129
|
-
self.master.title("
|
|
169
|
+
self.master.title(f"{self.title_prefix} - OneOCR")
|
|
130
170
|
self.pre_process_word_geometries()
|
|
131
171
|
self.update_filter_visualization(self.slider.get())
|
|
132
172
|
|
|
@@ -243,17 +283,22 @@ class FuriganaFilterVisualizer:
|
|
|
243
283
|
for words that pass the sensitivity filter.
|
|
244
284
|
"""
|
|
245
285
|
sensitivity = float(slider_value)
|
|
246
|
-
|
|
286
|
+
# Only update the label if it exists (GUI is fully initialized)
|
|
287
|
+
if hasattr(self, 'slider_value_label'):
|
|
288
|
+
self.slider_value_label.config(text=f"{sensitivity:.0f} px")
|
|
247
289
|
|
|
248
290
|
for rect_id in self.drawn_rects:
|
|
249
291
|
self.canvas.delete(rect_id)
|
|
250
292
|
self.drawn_rects.clear()
|
|
251
293
|
|
|
294
|
+
# Set color based on current OCR: green for Lens (OCR 1), blue for OneOCR (OCR 2)
|
|
295
|
+
outline_color = 'green' if self.current_ocr == 1 else 'blue'
|
|
296
|
+
|
|
252
297
|
for line_data in self.lines_data:
|
|
253
298
|
if line_data['px_w'] > sensitivity and line_data['px_h'] > sensitivity:
|
|
254
299
|
x1, y1, x2, y2 = line_data['coords']
|
|
255
300
|
rect_id = self.canvas.create_rectangle(
|
|
256
|
-
x1, y1, x2, y2, outline=
|
|
301
|
+
x1, y1, x2, y2, outline=outline_color, width=2
|
|
257
302
|
)
|
|
258
303
|
self.drawn_rects.append(rect_id)
|
|
259
304
|
|
|
@@ -289,29 +334,64 @@ def scale_down_width_height(width, height):
|
|
|
289
334
|
|
|
290
335
|
def main():
|
|
291
336
|
import sys
|
|
292
|
-
|
|
337
|
+
|
|
338
|
+
# Parse command line arguments
|
|
339
|
+
current_furigana_sensitivity = 0
|
|
340
|
+
use_overlay = False
|
|
341
|
+
|
|
342
|
+
if len(sys.argv) > 1:
|
|
343
|
+
# Check if any argument is "overlay" or "--overlay"
|
|
344
|
+
args = sys.argv[1:]
|
|
345
|
+
if "overlay" in args or "--overlay" in args:
|
|
346
|
+
use_overlay = True
|
|
347
|
+
# Remove overlay flags and use remaining numeric argument as sensitivity
|
|
348
|
+
numeric_args = [arg for arg in args if arg not in ["overlay", "--overlay"] and arg.isdigit()]
|
|
349
|
+
if numeric_args:
|
|
350
|
+
current_furigana_sensitivity = int(numeric_args[0])
|
|
351
|
+
else:
|
|
352
|
+
# Assume first argument is sensitivity
|
|
353
|
+
try:
|
|
354
|
+
current_furigana_sensitivity = int(args[0])
|
|
355
|
+
except ValueError:
|
|
356
|
+
logger.warning(f"Invalid sensitivity value: {args[0]}. Using default value 0.")
|
|
293
357
|
|
|
294
358
|
"""Main execution function."""
|
|
295
|
-
|
|
296
|
-
logger.info("
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
if use_overlay:
|
|
360
|
+
logger.info("Using overlay mode - capturing from configured monitor...")
|
|
361
|
+
try:
|
|
362
|
+
screenshot_img = get_overlay_screenshot()
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Failed to get overlay screenshot: {e}")
|
|
365
|
+
return
|
|
366
|
+
else:
|
|
367
|
+
try:
|
|
368
|
+
logger.info("Connecting to OBS...")
|
|
369
|
+
obs.connect_to_obs_sync()
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Failed to connect to OBS. Please ensure OBS is running and the WebSocket server is enabled. Error: {e}")
|
|
372
|
+
return
|
|
304
373
|
|
|
305
|
-
|
|
374
|
+
logger.info("Taking OBS screenshot...")
|
|
375
|
+
screenshot_img = obs.get_screenshot_PIL(compression=90, img_format='jpg')
|
|
306
376
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
377
|
+
if not screenshot_img:
|
|
378
|
+
logger.error("Failed to get screenshot from OBS.")
|
|
379
|
+
return
|
|
310
380
|
|
|
311
|
-
|
|
381
|
+
# Scale down the image for performance
|
|
382
|
+
screenshot_img = screenshot_img.resize(scale_down_width_height(screenshot_img.width, screenshot_img.height), Image.LANCZOS)
|
|
383
|
+
|
|
384
|
+
source_type = "overlay monitor" if use_overlay else "OBS"
|
|
385
|
+
logger.info(f"Screenshot received from {source_type} ({screenshot_img.width}x{screenshot_img.height}).")
|
|
312
386
|
|
|
313
387
|
root = tk.Tk()
|
|
314
388
|
app = FuriganaFilterVisualizer(root, screenshot_img, current_furigana_sensitivity)
|
|
389
|
+
|
|
390
|
+
# Update window title to reflect source
|
|
391
|
+
if use_overlay:
|
|
392
|
+
overlay_config = get_overlay_config()
|
|
393
|
+
monitor_num = overlay_config.monitor_to_capture + 1
|
|
394
|
+
app.set_title_prefix(f"Furigana Filter Visualizer - Overlay Monitor {monitor_num}")
|
|
315
395
|
|
|
316
396
|
def ocr_worker():
|
|
317
397
|
logger.info("Starting OCR process in background thread...")
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
|
+
import re
|
|
8
9
|
import tkinter as tk
|
|
9
10
|
from tkinter import filedialog, messagebox, simpledialog, scrolledtext, font
|
|
10
11
|
from PIL import Image, ImageTk
|
|
@@ -318,7 +319,39 @@ class ConfigApp:
|
|
|
318
319
|
|
|
319
320
|
print(dialog.selected_path)
|
|
320
321
|
return dialog.selected_path
|
|
321
|
-
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# IMPLEMENT LATER,FOR NOW JUST RUN THE FILE
|
|
325
|
+
# def show_furigana_filter_selector(self, current_sensitivity):
|
|
326
|
+
# """
|
|
327
|
+
# Displays a modal dialog for the user to select the furigana filter sensitivity.
|
|
328
|
+
|
|
329
|
+
# Args:
|
|
330
|
+
# current_sensitivity (int): The current sensitivity setting.
|
|
331
|
+
# Returns: int: The selected sensitivity setting, or None if canceled.
|
|
332
|
+
# """
|
|
333
|
+
# dialog = FuriganaFilterSelectorDialog(self.window,
|
|
334
|
+
# self,
|
|
335
|
+
# current_sensitivity=current_sensitivity)
|
|
336
|
+
# return dialog.selected_sensitivity
|
|
337
|
+
|
|
338
|
+
def show_minimum_character_size_selector(self, current_size):
|
|
339
|
+
"""
|
|
340
|
+
Opens an external tool for selecting the minimum character size.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
current_sensitivity (int): The current sensitivity setting.
|
|
344
|
+
Returns: int: The selected sensitivity setting, or None if canceled.
|
|
345
|
+
"""
|
|
346
|
+
# Run the file directly with the current python interpreter and get stdout
|
|
347
|
+
result = subprocess.run([sys.executable, '-m', 'GameSentenceMiner.tools.furigana_filter_preview', str(current_size), 'overlay'], capture_output=True, text=True)
|
|
348
|
+
if result.returncode == 0:
|
|
349
|
+
# Parse the output for RESULT:[value]
|
|
350
|
+
match = re.search(r"RESULT:\[(\d+)\]", result.stdout)
|
|
351
|
+
if match:
|
|
352
|
+
return int(match.group(1))
|
|
353
|
+
return None
|
|
354
|
+
|
|
322
355
|
def create_vars(self):
|
|
323
356
|
"""
|
|
324
357
|
Initializes all the tkinter variables used in the configuration GUI.
|
|
@@ -462,6 +495,7 @@ class ConfigApp:
|
|
|
462
495
|
self.periodic_value = tk.BooleanVar(value=self.settings.overlay.periodic)
|
|
463
496
|
self.periodic_interval_value = tk.StringVar(value=str(self.settings.overlay.periodic_interval))
|
|
464
497
|
self.scan_delay_value = tk.StringVar(value=str(self.settings.overlay.scan_delay))
|
|
498
|
+
self.overlay_minimum_character_size_value = tk.StringVar(value=str(self.settings.overlay.minimum_character_size))
|
|
465
499
|
|
|
466
500
|
# Master Config Settings
|
|
467
501
|
self.switch_to_default_if_not_found_value = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
|
|
@@ -703,6 +737,7 @@ class ConfigApp:
|
|
|
703
737
|
scan_delay=float(self.scan_delay_value.get()),
|
|
704
738
|
periodic=float(self.periodic_value.get()),
|
|
705
739
|
periodic_interval=float(self.periodic_interval_value.get()),
|
|
740
|
+
minimum_character_size=int(self.overlay_minimum_character_size_value.get()),
|
|
706
741
|
)
|
|
707
742
|
# wip=WIP(
|
|
708
743
|
# overlay_websocket_port=int(self.overlay_websocket_port_value.get()),
|
|
@@ -2387,6 +2422,19 @@ class ConfigApp:
|
|
|
2387
2422
|
ttk.Entry(overlay_frame, textvariable=self.periodic_interval_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
|
|
2388
2423
|
self.current_row += 1
|
|
2389
2424
|
|
|
2425
|
+
# Minimum Character Size
|
|
2426
|
+
minimum_character_size_i18n = overlay_i18n.get('minimum_character_size', {})
|
|
2427
|
+
HoverInfoLabelWidget(overlay_frame, text=minimum_character_size_i18n.get('label', 'Minimum Character Size:'),
|
|
2428
|
+
tooltip=minimum_character_size_i18n.get('tooltip', 'Minimum size of characters to be detected.'),
|
|
2429
|
+
row=self.current_row, column=0)
|
|
2430
|
+
ttk.Entry(overlay_frame, textvariable=self.overlay_minimum_character_size_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
|
|
2431
|
+
# button to open minimum character size Finder
|
|
2432
|
+
ttk.Button(overlay_frame, text=overlay_i18n.get('minimum_character_size_finder_button',
|
|
2433
|
+
'Minimum Character Size Finder'),
|
|
2434
|
+
command=lambda: self.open_minimum_character_size_selector(self.overlay_minimum_character_size_value.get()), bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
|
|
2435
|
+
|
|
2436
|
+
self.current_row += 1
|
|
2437
|
+
|
|
2390
2438
|
if self.monitors:
|
|
2391
2439
|
# Ensure the index is valid
|
|
2392
2440
|
monitor_index = self.settings.overlay.monitor_to_capture
|
|
@@ -2397,6 +2445,10 @@ class ConfigApp:
|
|
|
2397
2445
|
|
|
2398
2446
|
self.add_reset_button(overlay_frame, "overlay", self.current_row, 0, self.create_overlay_tab)
|
|
2399
2447
|
|
|
2448
|
+
def open_minimum_character_size_selector(self, size):
|
|
2449
|
+
new_size = self.show_minimum_character_size_selector(size)
|
|
2450
|
+
self.overlay_minimum_character_size_value.set(new_size)
|
|
2451
|
+
|
|
2400
2452
|
@new_tab
|
|
2401
2453
|
def create_wip_tab(self):
|
|
2402
2454
|
if self.wip_tab is None:
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk
|
|
3
|
+
from PIL import Image, ImageTk
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
import regex
|
|
7
|
+
|
|
8
|
+
from GameSentenceMiner import obs
|
|
9
|
+
from GameSentenceMiner.util.configuration import logger, get_overlay_config
|
|
10
|
+
from GameSentenceMiner.owocr.owocr.ocr import GoogleLens, OneOCR
|
|
11
|
+
|
|
12
|
+
def get_overlay_screenshot() -> Image.Image:
|
|
13
|
+
"""
|
|
14
|
+
Captures a screenshot from the configured overlay monitor using mss.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A PIL Image object of the screenshot from the overlay monitor.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
import mss
|
|
21
|
+
overlay_config = get_overlay_config()
|
|
22
|
+
monitor_index = overlay_config.monitor_to_capture
|
|
23
|
+
|
|
24
|
+
with mss.mss() as sct:
|
|
25
|
+
# mss.monitors[0] is all monitors combined, mss.monitors[1] is the first monitor
|
|
26
|
+
# So we need to add 1 to the monitor_index to get the correct monitor
|
|
27
|
+
monitor = sct.monitors[monitor_index + 1]
|
|
28
|
+
screenshot = sct.grab(monitor)
|
|
29
|
+
|
|
30
|
+
# Convert to PIL Image
|
|
31
|
+
img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
|
|
32
|
+
logger.info(f"Screenshot captured from monitor {monitor_index + 1} ({img.width}x{img.height})")
|
|
33
|
+
return img
|
|
34
|
+
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.error("mss library not found. Please install it to use overlay functionality.")
|
|
37
|
+
raise
|
|
38
|
+
except IndexError:
|
|
39
|
+
logger.error(f"Monitor index {monitor_index + 1} not found. Available monitors: {len(sct.monitors) - 1}")
|
|
40
|
+
raise
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Failed to capture overlay screenshot: {e}")
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
def get_ocr_results_from_image(image_obj: Image.Image) -> tuple:
|
|
46
|
+
"""
|
|
47
|
+
This is the function where you will plug in your OCR logic.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
image_obj: A PIL Image object of the screenshot (used by your actual OCR call).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A tuple containing the OCR results from both engines.
|
|
54
|
+
"""
|
|
55
|
+
lens = GoogleLens()
|
|
56
|
+
oneocr = OneOCR()
|
|
57
|
+
oneocr_res = oneocr(image_obj, return_dict=True)
|
|
58
|
+
res = lens(image_obj, return_coords=True)
|
|
59
|
+
|
|
60
|
+
return res[2], oneocr_res[3]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FuriganaFilterVisualizer:
|
|
64
|
+
def __init__(self, master, image: Image.Image, current_furigana_sensitivity: int = 0):
|
|
65
|
+
self.master = master
|
|
66
|
+
self.image = image
|
|
67
|
+
self.ocr1_result = None
|
|
68
|
+
self.ocr2_result = None
|
|
69
|
+
self.current_ocr = 1
|
|
70
|
+
self.title_prefix = "Furigana Filter Visualizer"
|
|
71
|
+
self.master.title(f"{self.title_prefix} - Lens")
|
|
72
|
+
|
|
73
|
+
self.words_data = []
|
|
74
|
+
self.lines_data = []
|
|
75
|
+
self.drawn_rects = []
|
|
76
|
+
|
|
77
|
+
main_frame = tk.Frame(master)
|
|
78
|
+
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
79
|
+
|
|
80
|
+
self.photo_image = ImageTk.PhotoImage(self.image)
|
|
81
|
+
self.canvas = tk.Canvas(main_frame, width=self.image.width, height=self.image.height)
|
|
82
|
+
self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
83
|
+
self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
|
|
84
|
+
|
|
85
|
+
self.loading_bg = self.canvas.create_rectangle(
|
|
86
|
+
self.image.width/2 - 100, self.image.height/2 - 25,
|
|
87
|
+
self.image.width/2 + 100, self.image.height/2 + 25,
|
|
88
|
+
fill="black", outline="white", width=2
|
|
89
|
+
)
|
|
90
|
+
self.loading_text = self.canvas.create_text(
|
|
91
|
+
self.image.width / 2, self.image.height / 2,
|
|
92
|
+
text="Loading OCR data...", fill="white", font=("Helvetica", 16)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.control_frame = tk.Frame(main_frame, padx=10, pady=10)
|
|
96
|
+
self.control_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
97
|
+
|
|
98
|
+
ttk.Label(self.control_frame, text="Furigana Filter Sensitivity:").pack(side=tk.LEFT, padx=(0, 10))
|
|
99
|
+
|
|
100
|
+
self.slider = ttk.Scale(
|
|
101
|
+
self.control_frame, from_=0, to=100, orient=tk.HORIZONTAL, command=self.update_filter_visualization
|
|
102
|
+
)
|
|
103
|
+
self.slider.set(current_furigana_sensitivity)
|
|
104
|
+
self.slider.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
105
|
+
|
|
106
|
+
self.slider_value_label = ttk.Label(self.control_frame, text=f"{self.slider.get():.0f} px", width=6)
|
|
107
|
+
self.slider_value_label.pack(side=tk.LEFT, padx=(10, 0))
|
|
108
|
+
|
|
109
|
+
self.swap_button = ttk.Button(self.control_frame, text="Switch to OneOCR", command=self.swap_ocr)
|
|
110
|
+
self.swap_button.pack(side=tk.LEFT, padx=(10, 0))
|
|
111
|
+
|
|
112
|
+
self.ok_button = ttk.Button(self.control_frame, text="OK", command=self.on_ok)
|
|
113
|
+
self.ok_button.pack(side=tk.LEFT, padx=(10, 0))
|
|
114
|
+
|
|
115
|
+
self.slider.config(state=tk.DISABLED)
|
|
116
|
+
self.swap_button.config(state=tk.DISABLED)
|
|
117
|
+
self.ok_button.config(state=tk.DISABLED)
|
|
118
|
+
|
|
119
|
+
self.punctuation_regex = regex.compile(r'[\p{P}\p{S}]')
|
|
120
|
+
self.master.protocol("WM_DELETE_WINDOW", self.on_ok)
|
|
121
|
+
|
|
122
|
+
def set_title_prefix(self, prefix: str):
|
|
123
|
+
"""Set the title prefix and update the current title."""
|
|
124
|
+
self.title_prefix = prefix
|
|
125
|
+
ocr_name = "Lens" if self.current_ocr == 1 else "OneOCR"
|
|
126
|
+
self.master.title(f"{self.title_prefix} - {ocr_name}")
|
|
127
|
+
|
|
128
|
+
def update_with_ocr_data(self, ocr1_result, ocr2_result):
|
|
129
|
+
"""Called by the background thread to populate the GUI with OCR data."""
|
|
130
|
+
self.ocr1_result = ocr1_result
|
|
131
|
+
self.ocr2_result = ocr2_result
|
|
132
|
+
|
|
133
|
+
# Remove loading message
|
|
134
|
+
self.canvas.delete(self.loading_bg)
|
|
135
|
+
self.canvas.delete(self.loading_text)
|
|
136
|
+
|
|
137
|
+
if not self.ocr1_result:
|
|
138
|
+
logger.error("OCR processing failed or returned no data.")
|
|
139
|
+
self.canvas.create_text(
|
|
140
|
+
self.image.width / 2, self.image.height / 2,
|
|
141
|
+
text="OCR Failed!", fill="red", font=("Helvetica", 16)
|
|
142
|
+
)
|
|
143
|
+
# Still enable OK button to allow closing
|
|
144
|
+
self.ok_button.config(state=tk.NORMAL)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Enable controls
|
|
148
|
+
self.slider.config(state=tk.NORMAL)
|
|
149
|
+
self.ok_button.config(state=tk.NORMAL)
|
|
150
|
+
if self.ocr2_result:
|
|
151
|
+
self.swap_button.config(state=tk.NORMAL)
|
|
152
|
+
|
|
153
|
+
# Process and display initial data
|
|
154
|
+
self.pre_process_word_geometries()
|
|
155
|
+
self.update_filter_visualization(self.slider.get())
|
|
156
|
+
|
|
157
|
+
def on_ok(self):
|
|
158
|
+
print(f"RESULT:[{self.slider.get():.0f}]")
|
|
159
|
+
self.master.destroy()
|
|
160
|
+
|
|
161
|
+
def swap_ocr(self):
|
|
162
|
+
self.current_ocr = 2 if self.current_ocr == 1 else 1
|
|
163
|
+
# Change to oneocr or lens, in title too
|
|
164
|
+
if self.current_ocr == 1:
|
|
165
|
+
self.swap_button.config(text="Switch to OneOCR")
|
|
166
|
+
self.master.title(f"{self.title_prefix} - Lens")
|
|
167
|
+
else:
|
|
168
|
+
self.swap_button.config(text="Switch to Lens")
|
|
169
|
+
self.master.title(f"{self.title_prefix} - OneOCR")
|
|
170
|
+
self.pre_process_word_geometries()
|
|
171
|
+
self.update_filter_visualization(self.slider.get())
|
|
172
|
+
|
|
173
|
+
def pre_process_word_geometries(self):
|
|
174
|
+
"""
|
|
175
|
+
Parses the OCR result structure (supports both original and new JSON formats),
|
|
176
|
+
calculates absolute pixel values, and stores them for high-performance updates.
|
|
177
|
+
"""
|
|
178
|
+
img_w, img_h = self.image.size
|
|
179
|
+
logger.info(f"Processing word geometries for image size {img_w}x{img_h}...")
|
|
180
|
+
|
|
181
|
+
# Select the current OCR result
|
|
182
|
+
ocr_result = self.ocr1_result if self.current_ocr == 1 else self.ocr2_result
|
|
183
|
+
if not ocr_result:
|
|
184
|
+
return
|
|
185
|
+
self.words_data.clear()
|
|
186
|
+
self.lines_data.clear()
|
|
187
|
+
|
|
188
|
+
# Try to detect the format: oneocr has 'lines' as a top-level key
|
|
189
|
+
if 'lines' in ocr_result:
|
|
190
|
+
for line in ocr_result.get('lines', []):
|
|
191
|
+
for word in line.get('words', []):
|
|
192
|
+
try:
|
|
193
|
+
bbox = word['bounding_rect']
|
|
194
|
+
x1 = bbox['x1']
|
|
195
|
+
y1 = bbox['y1']
|
|
196
|
+
x2 = bbox['x3']
|
|
197
|
+
y2 = bbox['y3']
|
|
198
|
+
px_w = abs(x2 - x1)
|
|
199
|
+
px_h = abs(y2 - y1)
|
|
200
|
+
self.words_data.append({
|
|
201
|
+
'text': word.get('text', ''),
|
|
202
|
+
'px_w': px_w,
|
|
203
|
+
'px_h': px_h,
|
|
204
|
+
'coords': (x1, y1, x2, y2)
|
|
205
|
+
})
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning(f"Skipping malformed word data (new format): {e}. Data: {word}")
|
|
208
|
+
continue
|
|
209
|
+
try:
|
|
210
|
+
bbox = line['bounding_rect']
|
|
211
|
+
x1 = bbox['x1']
|
|
212
|
+
y1 = bbox['y1']
|
|
213
|
+
x2 = bbox['x3']
|
|
214
|
+
y2 = bbox['y3']
|
|
215
|
+
px_w = abs(x2 - x1)
|
|
216
|
+
px_h = abs(y2 - y1)
|
|
217
|
+
self.lines_data.append({
|
|
218
|
+
'text': line.get('text', ''),
|
|
219
|
+
'px_w': px_w,
|
|
220
|
+
'px_h': px_h,
|
|
221
|
+
'coords': (x1, y1, x2, y2)
|
|
222
|
+
})
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.warning(f"Skipping malformed line data (new format): {e}. Data: {line}")
|
|
225
|
+
continue
|
|
226
|
+
else:
|
|
227
|
+
# Lens format (nested paragraphs/lines/words)
|
|
228
|
+
text_layout = ocr_result.get('objects_response', {}).get('text', {}).get('text_layout', {})
|
|
229
|
+
if not text_layout:
|
|
230
|
+
logger.error("Could not find 'text_layout' in the OCR response.")
|
|
231
|
+
return
|
|
232
|
+
for paragraph in text_layout.get('paragraphs', []):
|
|
233
|
+
for line in paragraph.get('lines', []):
|
|
234
|
+
for word in line.get('words', []):
|
|
235
|
+
try:
|
|
236
|
+
bbox_pct = word['geometry']['bounding_box']
|
|
237
|
+
width_pct = bbox_pct['width']
|
|
238
|
+
height_pct = bbox_pct['height']
|
|
239
|
+
top_left_x_pct = bbox_pct['center_x'] - (width_pct / 2)
|
|
240
|
+
top_left_y_pct = bbox_pct['center_y'] - (height_pct / 2)
|
|
241
|
+
px_w = width_pct * img_w
|
|
242
|
+
px_h = height_pct * img_h
|
|
243
|
+
x1 = top_left_x_pct * img_w
|
|
244
|
+
y1 = top_left_y_pct * img_h
|
|
245
|
+
x2 = x1 + px_w
|
|
246
|
+
y2 = y1 + px_h
|
|
247
|
+
self.words_data.append({
|
|
248
|
+
'text': word.get('plain_text', ''),
|
|
249
|
+
'px_w': px_w,
|
|
250
|
+
'px_h': px_h,
|
|
251
|
+
'coords': (x1, y1, x2, y2)
|
|
252
|
+
})
|
|
253
|
+
except (KeyError, TypeError) as e:
|
|
254
|
+
logger.warning(f"Skipping malformed word data (orig format): {e}. Data: {word}")
|
|
255
|
+
continue
|
|
256
|
+
try:
|
|
257
|
+
line_bbox = line['geometry']['bounding_box']
|
|
258
|
+
width_pct = line_bbox['width']
|
|
259
|
+
height_pct = line_bbox['height']
|
|
260
|
+
top_left_x_pct = line_bbox['center_x'] - (width_pct / 2)
|
|
261
|
+
top_left_y_pct = line_bbox['center_y'] - (height_pct / 2)
|
|
262
|
+
px_w = width_pct * img_w
|
|
263
|
+
px_h = height_pct * img_h
|
|
264
|
+
x1 = top_left_x_pct * img_w
|
|
265
|
+
y1 = top_left_y_pct * img_h
|
|
266
|
+
x2 = x1 + px_w
|
|
267
|
+
y2 = y1 + px_h
|
|
268
|
+
self.lines_data.append({
|
|
269
|
+
'text': ''.join([w.get('plain_text', '') for w in line.get('words', [])]),
|
|
270
|
+
'px_w': px_w,
|
|
271
|
+
'px_h': px_h,
|
|
272
|
+
'coords': (x1, y1, x2, y2)
|
|
273
|
+
})
|
|
274
|
+
except (KeyError, TypeError) as e:
|
|
275
|
+
logger.warning(f"Skipping malformed line data (orig format): {e}. Data: {line}")
|
|
276
|
+
continue
|
|
277
|
+
logger.info(f"Successfully pre-processed {len(self.lines_data)} lines.")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def update_filter_visualization(self, slider_value):
|
|
281
|
+
"""
|
|
282
|
+
Called on every slider move. Clears old rectangles and draws new ones
|
|
283
|
+
for words that pass the sensitivity filter.
|
|
284
|
+
"""
|
|
285
|
+
sensitivity = float(slider_value)
|
|
286
|
+
# Only update the label if it exists (GUI is fully initialized)
|
|
287
|
+
if hasattr(self, 'slider_value_label'):
|
|
288
|
+
self.slider_value_label.config(text=f"{sensitivity:.0f} px")
|
|
289
|
+
|
|
290
|
+
for rect_id in self.drawn_rects:
|
|
291
|
+
self.canvas.delete(rect_id)
|
|
292
|
+
self.drawn_rects.clear()
|
|
293
|
+
|
|
294
|
+
# Set color based on current OCR: green for Lens (OCR 1), blue for OneOCR (OCR 2)
|
|
295
|
+
outline_color = 'green' if self.current_ocr == 1 else 'blue'
|
|
296
|
+
|
|
297
|
+
for line_data in self.lines_data:
|
|
298
|
+
if line_data['px_w'] > sensitivity and line_data['px_h'] > sensitivity:
|
|
299
|
+
x1, y1, x2, y2 = line_data['coords']
|
|
300
|
+
rect_id = self.canvas.create_rectangle(
|
|
301
|
+
x1, y1, x2, y2, outline=outline_color, width=2
|
|
302
|
+
)
|
|
303
|
+
self.drawn_rects.append(rect_id)
|
|
304
|
+
|
|
305
|
+
def scale_down_width_height(width, height):
|
|
306
|
+
if width == 0 or height == 0:
|
|
307
|
+
return width, height
|
|
308
|
+
aspect_ratio = width / height
|
|
309
|
+
if aspect_ratio > 2.66:
|
|
310
|
+
# Ultra-wide (32:9) - use 1920x540
|
|
311
|
+
return 1920, 540
|
|
312
|
+
elif aspect_ratio > 2.33:
|
|
313
|
+
# 21:9 - use 1920x800
|
|
314
|
+
return 1920, 800
|
|
315
|
+
elif aspect_ratio > 1.77:
|
|
316
|
+
# 16:9 - use 1280x720
|
|
317
|
+
return 1280, 720
|
|
318
|
+
elif aspect_ratio > 1.6:
|
|
319
|
+
# 16:10 - use 1280x800
|
|
320
|
+
return 1280, 800
|
|
321
|
+
elif aspect_ratio > 1.33:
|
|
322
|
+
# 4:3 - use 960x720
|
|
323
|
+
return 960, 720
|
|
324
|
+
elif aspect_ratio > 1.25:
|
|
325
|
+
# 5:4 - use 900x720
|
|
326
|
+
return 900, 720
|
|
327
|
+
elif aspect_ratio > 1.5:
|
|
328
|
+
# 3:2 - use 1080x720
|
|
329
|
+
return 1080, 720
|
|
330
|
+
else:
|
|
331
|
+
# Default/fallback - use original resolution
|
|
332
|
+
print(f"Unrecognized aspect ratio {aspect_ratio}. Using original resolution.")
|
|
333
|
+
return width, height
|
|
334
|
+
|
|
335
|
+
def main():
|
|
336
|
+
import sys
|
|
337
|
+
|
|
338
|
+
# Parse command line arguments
|
|
339
|
+
current_furigana_sensitivity = 0
|
|
340
|
+
use_overlay = False
|
|
341
|
+
|
|
342
|
+
if len(sys.argv) > 1:
|
|
343
|
+
# Check if any argument is "overlay" or "--overlay"
|
|
344
|
+
args = sys.argv[1:]
|
|
345
|
+
if "overlay" in args or "--overlay" in args:
|
|
346
|
+
use_overlay = True
|
|
347
|
+
# Remove overlay flags and use remaining numeric argument as sensitivity
|
|
348
|
+
numeric_args = [arg for arg in args if arg not in ["overlay", "--overlay"] and arg.isdigit()]
|
|
349
|
+
if numeric_args:
|
|
350
|
+
current_furigana_sensitivity = int(numeric_args[0])
|
|
351
|
+
else:
|
|
352
|
+
# Assume first argument is sensitivity
|
|
353
|
+
try:
|
|
354
|
+
current_furigana_sensitivity = int(args[0])
|
|
355
|
+
except ValueError:
|
|
356
|
+
logger.warning(f"Invalid sensitivity value: {args[0]}. Using default value 0.")
|
|
357
|
+
|
|
358
|
+
"""Main execution function."""
|
|
359
|
+
if use_overlay:
|
|
360
|
+
logger.info("Using overlay mode - capturing from configured monitor...")
|
|
361
|
+
try:
|
|
362
|
+
screenshot_img = get_overlay_screenshot()
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Failed to get overlay screenshot: {e}")
|
|
365
|
+
return
|
|
366
|
+
else:
|
|
367
|
+
try:
|
|
368
|
+
logger.info("Connecting to OBS...")
|
|
369
|
+
obs.connect_to_obs_sync()
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Failed to connect to OBS. Please ensure OBS is running and the WebSocket server is enabled. Error: {e}")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
logger.info("Taking OBS screenshot...")
|
|
375
|
+
screenshot_img = obs.get_screenshot_PIL(compression=90, img_format='jpg')
|
|
376
|
+
|
|
377
|
+
if not screenshot_img:
|
|
378
|
+
logger.error("Failed to get screenshot from OBS.")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
# Scale down the image for performance
|
|
382
|
+
screenshot_img = screenshot_img.resize(scale_down_width_height(screenshot_img.width, screenshot_img.height), Image.LANCZOS)
|
|
383
|
+
|
|
384
|
+
source_type = "overlay monitor" if use_overlay else "OBS"
|
|
385
|
+
logger.info(f"Screenshot received from {source_type} ({screenshot_img.width}x{screenshot_img.height}).")
|
|
386
|
+
|
|
387
|
+
root = tk.Tk()
|
|
388
|
+
app = FuriganaFilterVisualizer(root, screenshot_img, current_furigana_sensitivity)
|
|
389
|
+
|
|
390
|
+
# Update window title to reflect source
|
|
391
|
+
if use_overlay:
|
|
392
|
+
overlay_config = get_overlay_config()
|
|
393
|
+
monitor_num = overlay_config.monitor_to_capture + 1
|
|
394
|
+
app.set_title_prefix(f"Furigana Filter Visualizer - Overlay Monitor {monitor_num}")
|
|
395
|
+
|
|
396
|
+
def ocr_worker():
|
|
397
|
+
logger.info("Starting OCR process in background thread...")
|
|
398
|
+
try:
|
|
399
|
+
ocr1_data, ocr2_data = get_ocr_results_from_image(screenshot_img)
|
|
400
|
+
root.after(0, app.update_with_ocr_data, ocr1_data, ocr2_data)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.error(f"Error in OCR background thread: {e}")
|
|
403
|
+
root.after(0, app.update_with_ocr_data, None, None)
|
|
404
|
+
|
|
405
|
+
threading.Thread(target=ocr_worker, daemon=True).start()
|
|
406
|
+
|
|
407
|
+
root.mainloop()
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
main()
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import threading
|
|
7
7
|
import inspect
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from logging.handlers import RotatingFileHandler
|
|
@@ -409,7 +410,6 @@ class General:
|
|
|
409
410
|
except ValueError:
|
|
410
411
|
return "Unknown"
|
|
411
412
|
|
|
412
|
-
|
|
413
413
|
@dataclass_json
|
|
414
414
|
@dataclass
|
|
415
415
|
class Paths:
|
|
@@ -424,9 +424,11 @@ class Paths:
|
|
|
424
424
|
|
|
425
425
|
def __post_init__(self):
|
|
426
426
|
if self.folder_to_watch:
|
|
427
|
-
self.folder_to_watch = os.path.normpath(self.folder_to_watch)
|
|
427
|
+
self.folder_to_watch = os.path.normpath(self.folder_to_watch).replace("\\", "/")
|
|
428
|
+
self.folder_to_watch = re.sub(r'/+', '/', self.folder_to_watch)
|
|
428
429
|
if self.output_folder:
|
|
429
|
-
self.output_folder = os.path.normpath(self.output_folder)
|
|
430
|
+
self.output_folder = os.path.normpath(self.output_folder).replace("\\", "/")
|
|
431
|
+
self.output_folder = re.sub(r'/+', '/', self.output_folder)
|
|
430
432
|
|
|
431
433
|
|
|
432
434
|
@dataclass_json
|
|
@@ -647,6 +649,16 @@ class Ai:
|
|
|
647
649
|
# Change Legacy Model Name
|
|
648
650
|
if self.gemini_model == 'gemini-2.5-flash-lite-preview-06-17':
|
|
649
651
|
self.gemini_model = 'gemini-2.5-flash-lite'
|
|
652
|
+
|
|
653
|
+
def is_configured(self) -> bool:
|
|
654
|
+
if self.enabled:
|
|
655
|
+
if self.provider == AI_GEMINI and self.gemini_api_key and self.gemini_model:
|
|
656
|
+
return True
|
|
657
|
+
if self.provider == AI_GROQ and self.groq_api_key and self.groq_model:
|
|
658
|
+
return True
|
|
659
|
+
if self.provider == AI_OPENAI and self.open_ai_api_key and self.open_ai_model and self.open_ai_url:
|
|
660
|
+
return True
|
|
661
|
+
return False
|
|
650
662
|
|
|
651
663
|
|
|
652
664
|
class OverlayEngine(str, Enum):
|
|
@@ -662,6 +674,7 @@ class Overlay:
|
|
|
662
674
|
periodic: bool = False
|
|
663
675
|
periodic_interval: float = 1.0
|
|
664
676
|
scan_delay: float = 0.25
|
|
677
|
+
minimum_character_size: int = 0
|
|
665
678
|
|
|
666
679
|
def __post_init__(self):
|
|
667
680
|
if self.monitor_to_capture == -1:
|
|
@@ -141,8 +141,8 @@ class OverlayProcessor:
|
|
|
141
141
|
if self.config.overlay.websocket_port and all([GoogleLens, get_regex]):
|
|
142
142
|
logger.info("Initializing OCR engines...")
|
|
143
143
|
if OneOCR:
|
|
144
|
-
self.oneocr = OneOCR(lang=get_ocr_language())
|
|
145
|
-
self.lens = GoogleLens(lang=get_ocr_language())
|
|
144
|
+
self.oneocr = OneOCR(lang=get_ocr_language(), get_furigana_sens_from_file=False)
|
|
145
|
+
self.lens = GoogleLens(lang=get_ocr_language(), get_furigana_sens_from_file=False)
|
|
146
146
|
self.ocr_language = get_ocr_language()
|
|
147
147
|
self.regex = get_regex(self.ocr_language)
|
|
148
148
|
logger.info("OCR engines initialized.")
|
|
@@ -253,7 +253,6 @@ class OverlayProcessor:
|
|
|
253
253
|
if not mss:
|
|
254
254
|
raise RuntimeError("MSS screenshot library is not installed.")
|
|
255
255
|
with mss.mss() as sct:
|
|
256
|
-
logger.info(get_overlay_config())
|
|
257
256
|
monitor = self.get_monitor_workarea(get_overlay_config().monitor_to_capture) # Get primary monitor work area
|
|
258
257
|
sct_img = sct.grab(monitor)
|
|
259
258
|
img = Image.frombytes('RGB', sct_img.size, sct_img.bgra, 'raw', 'BGRX')
|
|
@@ -338,9 +337,12 @@ class OverlayProcessor:
|
|
|
338
337
|
return_coords=True,
|
|
339
338
|
multiple_crop_coords=True,
|
|
340
339
|
return_one_box=False,
|
|
341
|
-
furigana_filter_sensitivity=
|
|
340
|
+
furigana_filter_sensitivity=get_overlay_config().minimum_character_size,
|
|
342
341
|
)
|
|
343
342
|
|
|
343
|
+
if not crop_coords_list:
|
|
344
|
+
return
|
|
345
|
+
|
|
344
346
|
# Check for cancellation after OneOCR
|
|
345
347
|
if asyncio.current_task().cancelled():
|
|
346
348
|
raise asyncio.CancelledError()
|
|
@@ -388,7 +390,7 @@ class OverlayProcessor:
|
|
|
388
390
|
res = self.lens(
|
|
389
391
|
composite_image,
|
|
390
392
|
return_coords=True,
|
|
391
|
-
furigana_filter_sensitivity=
|
|
393
|
+
furigana_filter_sensitivity=get_overlay_config().minimum_character_size
|
|
392
394
|
)
|
|
393
395
|
|
|
394
396
|
# Check for cancellation after Google Lens
|
|
@@ -455,6 +457,8 @@ class OverlayProcessor:
|
|
|
455
457
|
|
|
456
458
|
for para in paragraphs:
|
|
457
459
|
for line in para.get("lines", []):
|
|
460
|
+
# if not self.regex.match(line.get("plain_text", "")):
|
|
461
|
+
# continue
|
|
458
462
|
line_text_parts = []
|
|
459
463
|
word_list = []
|
|
460
464
|
|
|
@@ -541,6 +545,9 @@ class OverlayProcessor:
|
|
|
541
545
|
"""
|
|
542
546
|
converted_results = []
|
|
543
547
|
for item in oneocr_results:
|
|
548
|
+
# Check Regex
|
|
549
|
+
# if not self.regex.match(item.get("text", "")):
|
|
550
|
+
# continue
|
|
544
551
|
bbox = item.get("bounding_rect", {})
|
|
545
552
|
if not bbox:
|
|
546
553
|
continue
|
|
@@ -217,6 +217,9 @@ def translate_line():
|
|
|
217
217
|
text = data.get('text', '').strip()
|
|
218
218
|
if event_id is None:
|
|
219
219
|
return jsonify({'error': 'Missing id'}), 400
|
|
220
|
+
|
|
221
|
+
if not get_config().ai.is_configured():
|
|
222
|
+
return jsonify({'error': 'AI translation is not properly configured. Please check your settings in the "AI" Tab.'}), 400
|
|
220
223
|
line = get_line_by_id(event_id)
|
|
221
224
|
if line is None:
|
|
222
225
|
return jsonify({'error': 'Invalid id'}), 400
|
|
@@ -27,25 +27,26 @@ GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9
|
|
|
27
27
|
GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
|
|
28
28
|
GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
|
|
29
29
|
GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
|
|
30
|
-
GameSentenceMiner/owocr/owocr/ocr.py,sha256=
|
|
30
|
+
GameSentenceMiner/owocr/owocr/ocr.py,sha256=8cqZEUF90UlV3jBIvxKBga6YBFGjNBCVu1UiBcwISG0,72215
|
|
31
31
|
GameSentenceMiner/owocr/owocr/run.py,sha256=Z7VkoFrsoQbMTHc6CmwpcMzsOROK9A_RJRwhlxw15oA,81871
|
|
32
32
|
GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
|
|
33
33
|
GameSentenceMiner/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
GameSentenceMiner/tools/audio_offset_selector.py,sha256=8Stk3BP-XVIuzRv9nl9Eqd2D-1yD3JrgU-CamBywJmY,8542
|
|
35
|
-
GameSentenceMiner/tools/furigana_filter_preview.py,sha256=
|
|
35
|
+
GameSentenceMiner/tools/furigana_filter_preview.py,sha256=DAT2-j6vSDHr9ufk6PiaLikEsbIp56B_OHIEeYLMwlk,17135
|
|
36
36
|
GameSentenceMiner/tools/ss_selector.py,sha256=ob2oJdiYreDMMau7CvsglpnhZ1CDnJqop3lV54-PjRo,4782
|
|
37
37
|
GameSentenceMiner/tools/window_transparency.py,sha256=GtbxbmZg0-UYPXhfHff-7IKZyY2DKe4B9GdyovfmpeM,8166
|
|
38
38
|
GameSentenceMiner/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
GameSentenceMiner/ui/anki_confirmation.py,sha256=ohpWlPTvKn-_5_lpINdKZciR0k8RWRfnDrfuzyJgItc,15242
|
|
40
|
-
GameSentenceMiner/ui/config_gui.py,sha256=
|
|
40
|
+
GameSentenceMiner/ui/config_gui.py,sha256=9Ak6DocxTuHWrNXYGd2W-82oUGTgaD8F25Ibge_HWSc,155064
|
|
41
|
+
GameSentenceMiner/ui/furigana_filter_preview.py,sha256=DAT2-j6vSDHr9ufk6PiaLikEsbIp56B_OHIEeYLMwlk,17135
|
|
41
42
|
GameSentenceMiner/ui/screenshot_selector.py,sha256=AKML87MpgYQeSuj1F10GngpNrn9qp06zLLzNRwrQWM8,8900
|
|
42
43
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
44
|
GameSentenceMiner/util/audio_player.py,sha256=-yFsf0qoTSS1ga5rCmEJZJGUSJzXCvfZHY3t0NxycDk,7896
|
|
44
|
-
GameSentenceMiner/util/configuration.py,sha256=
|
|
45
|
+
GameSentenceMiner/util/configuration.py,sha256=txJKUVCwLgIzg0JobxiKnpzTGlCpM3rVdVimVWau6Zw,48178
|
|
45
46
|
GameSentenceMiner/util/db.py,sha256=1DjGjlwWnPefmQfzvMqqFPW0a0qeO-fIXE1YqKiok18,32000
|
|
46
47
|
GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
|
|
47
48
|
GameSentenceMiner/util/ffmpeg.py,sha256=cAzztfY36Xf2WvsJDjavoiMOvA9ac2GVdCrSB4LzHk4,29007
|
|
48
|
-
GameSentenceMiner/util/get_overlay_coords.py,sha256=
|
|
49
|
+
GameSentenceMiner/util/get_overlay_coords.py,sha256=TJz3iVimiwhRLwkYNB1uOZnDR4BEWWUWYjoJEe7uSQs,25160
|
|
49
50
|
GameSentenceMiner/util/gsm_utils.py,sha256=mASECTmN10c2yPL4NEfLg0Y0YWwFso1i6r_hhJPR3MY,10974
|
|
50
51
|
GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
|
|
51
52
|
GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
|
|
@@ -65,7 +66,7 @@ GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZ
|
|
|
65
66
|
GameSentenceMiner/web/gsm_websocket.py,sha256=B0VKpxmsRu0WRh5nFWlpDPBQ6-K2ed7TEIa0O6YWeoo,4166
|
|
66
67
|
GameSentenceMiner/web/service.py,sha256=6cgUmDgtp3ZKzuPFszowjPoq-BDtC1bS3ux6sykeaqo,6662
|
|
67
68
|
GameSentenceMiner/web/stats.py,sha256=zIK0ZzyInvvlJh87KhAKYl2CCuMJWW6Wyv7UssURFbE,22366
|
|
68
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
|
69
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=fyMz74cfv4qPlA29UVJH0YhsuXtUL2aTEWNh4PwVaP4,15876
|
|
69
70
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
71
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
|
71
72
|
GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
|
|
@@ -125,9 +126,9 @@ GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic
|
|
|
125
126
|
GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json,sha256=8wjnnaYQqmho6t5tMxrIAc03512A2tYhQh5dfsQnfAM,11372
|
|
126
127
|
GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json,sha256=wRkqZNPzz6DT9OTPHpXwfqW96Qb96stCQNNgOL-ZdKk,17535
|
|
127
128
|
GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
128
|
-
gamesentenceminer-2.18.
|
|
129
|
-
gamesentenceminer-2.18.
|
|
130
|
-
gamesentenceminer-2.18.
|
|
131
|
-
gamesentenceminer-2.18.
|
|
132
|
-
gamesentenceminer-2.18.
|
|
133
|
-
gamesentenceminer-2.18.
|
|
129
|
+
gamesentenceminer-2.18.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
130
|
+
gamesentenceminer-2.18.11.dist-info/METADATA,sha256=x2IbmM6IgBD1ASP88yxHCsXhaweR2ZFrs8r6ZnVnICk,7488
|
|
131
|
+
gamesentenceminer-2.18.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
132
|
+
gamesentenceminer-2.18.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
|
133
|
+
gamesentenceminer-2.18.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
|
134
|
+
gamesentenceminer-2.18.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|