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.

@@ -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 furigana_filter_sensitivity != None:
293
+ if self.get_furigana_sens_from_file:
293
294
  furigana_filter_sensitivity = get_furigana_filter_sensitivity()
294
295
  else:
295
- furigana_filter_sensitivity = 0
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, response_dict)
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 furigana_filter_sensitivity != None:
947
+ if self.get_furigana_sens_from_file:
925
948
  furigana_filter_sensitivity = get_furigana_filter_sensitivity()
926
949
  else:
927
- furigana_filter_sensitivity = 0
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
- for char in line['words']:
969
- if self.punctuation_regex.findall(char['text']):
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
- continue
972
- if line_width > furigana_filter_sensitivity and line_height > furigana_filter_sensitivity:
973
- res += char['text']
974
- else:
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(ocr_resp)
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.master.title("Furigana Filter Visualizer - Lens")
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("Furigana Filter Visualizer - Lens")
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("Furigana Filter Visualizer - OneOCR")
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
- self.slider_value_label.config(text=f"{sensitivity:.0f} px")
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='blue', width=2
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
- current_furigana_sensitivity = int(sys.argv[1]) if len(sys.argv) > 1 else 0
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
- try:
296
- logger.info("Connecting to OBS...")
297
- obs.connect_to_obs_sync()
298
- except Exception as e:
299
- logger.error(f"Failed to connect to OBS. Please ensure OBS is running and the WebSocket server is enabled. Error: {e}")
300
- return
301
-
302
- logger.info("Taking OBS screenshot...")
303
- screenshot_img = obs.get_screenshot_PIL(compression=90, img_format='jpg')
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
- screenshot_img = screenshot_img.resize(scale_down_width_height(screenshot_img.width, screenshot_img.height), Image.LANCZOS)
374
+ logger.info("Taking OBS screenshot...")
375
+ screenshot_img = obs.get_screenshot_PIL(compression=90, img_format='jpg')
306
376
 
307
- if not screenshot_img:
308
- logger.error("Failed to get screenshot from OBS.")
309
- return
377
+ if not screenshot_img:
378
+ logger.error("Failed to get screenshot from OBS.")
379
+ return
310
380
 
311
- logger.info(f"Screenshot received ({screenshot_img.width}x{screenshot_img.height}).")
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=None, # Disable furigana filtering
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=None # Disable furigana filtering
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.18.9
3
+ Version: 2.18.11
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -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=7VCTBl4-8mO9P73olLMihUFtu3idBdyZIWP6XQhCte4,70484
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=BXv7FChPEJW_VeG5XYt6suAsMKVArsjr3cEduE9KhYg,13642
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=4baqfL33oMshmqm903GZok32Y4JIEV-3K9gf5gxAJDU,152131
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=lwo73S3xnIMPq8lWSWM6N0pd08A4-JvrejypT3ZvTr8,47483
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=MFl_JOjwzD0D0iZBPcq5Dgy32YPMKqRrugL0WsfMEu4,24819
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=IHRy3VTMAwJeGjTNr3TgjHgxe--g5jZcnHmNzxILXQE,15698
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.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
129
- gamesentenceminer-2.18.9.dist-info/METADATA,sha256=4CeXXcMgBMAwMH_61VDFQWr_xyt-5Fg4XDsQ2sfJA6s,7487
130
- gamesentenceminer-2.18.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
131
- gamesentenceminer-2.18.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
132
- gamesentenceminer-2.18.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
133
- gamesentenceminer-2.18.9.dist-info/RECORD,,
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,,