GameSentenceMiner 2.13.9__py3-none-any.whl → 2.13.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.
GameSentenceMiner/gsm.py CHANGED
@@ -53,7 +53,7 @@ try:
53
53
  from GameSentenceMiner.util.text_log import GameLine, get_text_event, get_mined_line, get_all_lines, game_log
54
54
  from GameSentenceMiner.util import *
55
55
  from GameSentenceMiner.web import texthooking_page
56
- from GameSentenceMiner.web.service import handle_texthooker_button
56
+ from GameSentenceMiner.web.service import handle_texthooker_button, set_get_audio_from_video_callback
57
57
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
58
58
  except Exception as e:
59
59
  from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
@@ -91,10 +91,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
91
91
  selected_lines = []
92
92
  anki_card_creation_time = None
93
93
  mined_line = None
94
- gsm_state.previous_replay = video_path
95
94
  if gsm_state.line_for_audio or gsm_state.line_for_screenshot:
96
- handle_texthooker_button(
97
- video_path, get_audio_from_video=VideoToAudioHandler.get_audio)
95
+ handle_texthooker_button(video_path)
98
96
  return
99
97
  try:
100
98
  if anki.card_queue and len(anki.card_queue) > 0:
@@ -201,16 +199,14 @@ class VideoToAudioHandler(FileSystemEventHandler):
201
199
  logger.debug(
202
200
  f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
203
201
  notification.send_error_no_anki_update()
204
- finally:
205
- if get_config().paths.remove_video and video_path and not skip_delete:
206
- try:
207
- if os.path.exists(video_path):
208
- logger.debug(f"Removing video: {video_path}")
209
- os.remove(video_path)
210
- except Exception as e:
211
- logger.error(
212
- f"Error removing video file {video_path}: {e}", exc_info=True)
213
- pass
202
+ if get_config().paths.remove_video and video_path and not skip_delete:
203
+ try:
204
+ if os.path.exists(video_path):
205
+ logger.debug(f"Removing video: {video_path}")
206
+ os.remove(video_path)
207
+ except Exception as e:
208
+ logger.error(
209
+ f"Error removing video file {video_path}: {e}", exc_info=True)
214
210
 
215
211
  @staticmethod
216
212
  def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
@@ -525,6 +521,7 @@ def handle_exit():
525
521
  def initialize(reloading=False):
526
522
  global obs_process
527
523
  if not reloading:
524
+ get_temporary_directory(delete=True)
528
525
  if is_windows():
529
526
  download_obs_if_needed()
530
527
  download_ffmpeg_if_needed()
@@ -538,6 +535,7 @@ def initialize(reloading=False):
538
535
  # gametext.start_text_monitor()
539
536
  os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
540
537
  os.makedirs(get_config().paths.output_folder, exist_ok=True)
538
+ set_get_audio_from_video_callback(VideoToAudioHandler.get_audio)
541
539
  initial_checks()
542
540
  register_websocket_message_handler(handle_websocket_message)
543
541
  # if get_config().vad.do_vad_postprocessing:
@@ -33,6 +33,7 @@ class Rectangle:
33
33
  monitor: Monitor
34
34
  coordinates: List[Union[float, int]]
35
35
  is_excluded: bool
36
+ is_secondary: bool = False
36
37
 
37
38
  @dataclass_json
38
39
  @dataclass
@@ -41,7 +42,8 @@ class WindowGeometry:
41
42
  top: int
42
43
  width: int
43
44
  height: int
44
-
45
+
46
+
45
47
  @dataclass_json
46
48
  @dataclass
47
49
  class OCRConfig:
@@ -34,7 +34,7 @@ class ScreenSelector:
34
34
  def __init__(self, result, window_name, use_window_as_config, use_obs_screenshot=False):
35
35
  if not selector_available:
36
36
  raise RuntimeError("tkinter is not available.")
37
- if not window_name:
37
+ if not window_name and not use_obs_screenshot:
38
38
  raise ValueError("A target window name is required for configuration.")
39
39
 
40
40
  obs.connect_to_obs_sync()
@@ -185,7 +185,7 @@ class ScreenSelector:
185
185
  monitor_index = rect_data["monitor"]['index']
186
186
  target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
187
187
  if target_monitor:
188
- self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"]))
188
+ self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"], rect_data.get("is_secondary", False)))
189
189
  loaded_count += 1
190
190
  except (KeyError, ValueError, TypeError) as e:
191
191
  print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
@@ -204,7 +204,7 @@ class ScreenSelector:
204
204
  print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
205
205
 
206
206
  serializable_rects = []
207
- for monitor_dict, abs_coords, is_excluded in self.rectangles:
207
+ for monitor_dict, abs_coords, is_excluded, is_secondary in self.rectangles:
208
208
  x_abs, y_abs, w_abs, h_abs = abs_coords
209
209
 
210
210
  # Convert absolute pixel coordinates to percentages
@@ -217,7 +217,8 @@ class ScreenSelector:
217
217
  serializable_rects.append({
218
218
  "monitor": {'index': monitor_dict['index']},
219
219
  "coordinates": coords_to_save,
220
- "is_excluded": is_excluded
220
+ "is_excluded": is_excluded,
221
+ "is_secondary": is_secondary
221
222
  })
222
223
 
223
224
  save_data = {
@@ -254,13 +255,14 @@ class ScreenSelector:
254
255
 
255
256
  def redo_last_rect(self, event=None):
256
257
  if not self.redo_stack: return
257
- monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
258
+ monitor, abs_coords, is_excluded, is_secondary, old_rect_id = self.redo_stack.pop()
258
259
  canvas = event.widget.winfo_toplevel().winfo_children()[0]
259
260
  x_abs, y_abs, w_abs, h_abs = abs_coords
260
261
  canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
262
+ outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
261
263
  new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
262
- outline='orange' if is_excluded else 'green', width=2)
263
- self.rectangles.append((monitor, abs_coords, is_excluded))
264
+ outline=outline_color, width=2)
265
+ self.rectangles.append((monitor, abs_coords, is_excluded, is_secondary))
264
266
  self.drawn_rect_ids.append(new_rect_id)
265
267
  print("Redo: Restored rectangle.")
266
268
 
@@ -291,6 +293,7 @@ class ScreenSelector:
291
293
  "How to Use:\n"
292
294
  "• Left Click + Drag: Create a capture area (green).\n"
293
295
  "• Shift + Left Click + Drag: Create an exclusion area (orange).\n"
296
+ "• Ctrl + Left Click + Drag: Create a secondary (menu) area (purple).\n"
294
297
  "• Right-Click on a box: Delete it."
295
298
  )
296
299
  tk.Label(main_frame, text=instructions_text, justify=tk.LEFT, anchor="w").pack(pady=(0, 10), fill=tk.X)
@@ -426,17 +429,25 @@ class ScreenSelector:
426
429
  # --- END MODIFICATION ---
427
430
 
428
431
  # Draw existing rectangles (which were converted to absolute pixels on load)
429
- for _, abs_coords, is_excluded in self.rectangles:
432
+ for _, abs_coords, is_excluded, is_secondary in self.rectangles:
430
433
  x_abs, y_abs, w_abs, h_abs = abs_coords
431
434
  canvas_x = x_abs - self.bounding_box['left']
432
435
  canvas_y = y_abs - self.bounding_box['top']
436
+ outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
433
437
  rect_id = self.canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
434
- outline='orange' if is_excluded else 'green', width=2)
438
+ outline=outline_color, width=2)
435
439
  self.drawn_rect_ids.append(rect_id)
436
440
 
437
441
  def on_click(event):
438
442
  self.start_x, self.start_y = event.x, event.y
439
- outline = 'purple' if bool(event.state & 0x0001) else 'red'
443
+ ctrl_held = bool(event.state & 0x0004)
444
+ shift_held = bool(event.state & 0x0001)
445
+ if ctrl_held:
446
+ outline = 'purple'
447
+ elif shift_held:
448
+ outline = 'orange'
449
+ else:
450
+ outline = 'green'
440
451
  self.current_rect_id = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
441
452
  outline=outline, width=2)
442
453
 
@@ -451,8 +462,12 @@ class ScreenSelector:
451
462
  w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
452
463
 
453
464
  if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
454
- is_excl = bool(event.state & 0x0001)
455
- self.canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
465
+ ctrl_held = bool(event.state & 0x0004)
466
+ shift_held = bool(event.state & 0x0001)
467
+ is_excl = shift_held
468
+ is_secondary = ctrl_held
469
+ outline_color = 'purple' if is_secondary else ('orange' if is_excl else 'green')
470
+ self.canvas.itemconfig(self.current_rect_id, outline=outline_color)
456
471
 
457
472
  center_x, center_y = x_abs + w / 2, y_abs + h / 2
458
473
  target_mon = self.monitors[0]
@@ -462,7 +477,7 @@ class ScreenSelector:
462
477
  target_mon = mon
463
478
  break
464
479
 
465
- self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl))
480
+ self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl, is_secondary))
466
481
  self.drawn_rect_ids.append(self.current_rect_id)
467
482
  self.redo_stack.clear()
468
483
  else:
@@ -472,7 +487,7 @@ class ScreenSelector:
472
487
  def on_right_click(event):
473
488
  # Iterate through our rectangles in reverse to find the topmost one.
474
489
  for i in range(len(self.rectangles) - 1, -1, -1):
475
- _monitor, abs_coords, _is_excluded = self.rectangles[i]
490
+ _monitor, abs_coords, _is_excluded, _is_secondary = self.rectangles[i]
476
491
  x_abs, y_abs, w_abs, h_abs = abs_coords
477
492
  canvas_x1 = x_abs - self.bounding_box['left']
478
493
  canvas_y1 = y_abs - self.bounding_box['top']
@@ -540,7 +555,7 @@ def run_screen_selector(result_dict, window_name, use_window_as_config, use_obs_
540
555
 
541
556
  def get_screen_selection(window_name, use_window_as_config=False, use_obs_screenshot=False):
542
557
  if not selector_available: return None
543
- if not window_name:
558
+ if not window_name and not use_obs_screenshot:
544
559
  print("Error: A target window name must be provided.", file=sys.stderr)
545
560
  return None
546
561
 
@@ -567,7 +582,7 @@ if __name__ == "__main__":
567
582
 
568
583
  parser = argparse.ArgumentParser(description="Screen Selector Arguments")
569
584
  parser.add_argument("window_title", nargs="?", default="", help="Target window title")
570
- parser.add_argument("--obs_ocr", action="store_true", help="Use OBS screenshot")
585
+ parser.add_argument("--obs_ocr", action="store_true", default=True, help="Use OBS screenshot")
571
586
  parser.add_argument("--use_window_for_config", action="store_true", help="Use window for config")
572
587
  args = parser.parse_args()
573
588
 
@@ -52,7 +52,6 @@ logger.addHandler(console_handler)
52
52
  def get_ocr_config(window=None, use_window_for_config=False) -> OCRConfig:
53
53
  """Loads and updates screen capture areas from the corresponding JSON file."""
54
54
  ocr_config_dir = get_ocr_config_path()
55
- obs.connect_to_obs_sync(retry=0)
56
55
  obs.update_current_game()
57
56
  if use_window_for_config and window:
58
57
  scene = sanitize_filename(window)
@@ -202,7 +201,7 @@ def do_second_ocr(ocr1_text, time, img, filtering, pre_crop_image=None, ignore_f
202
201
  try:
203
202
  orig_text, text = run.process_and_write_results(img, None, last_ocr2_result if not ignore_previous_result else None, filtering, None,
204
203
  engine=get_ocr_ocr2(), furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
205
-
204
+
206
205
  if compare_ocr_results(last_sent_result, text, threshold=80):
207
206
  if text:
208
207
  logger.info("Seems like Text we already sent, not doing anything.")
@@ -447,6 +446,17 @@ def run_oneocr(ocr_config: OCRConfig, rectangles, config_check_thread):
447
446
 
448
447
  def add_ss_hotkey(ss_hotkey="ctrl+shift+g"):
449
448
  import keyboard
449
+
450
+ def ocr_secondary_rectangles():
451
+ logger.info("Running secondary OCR rectangles...")
452
+ ocr_config = get_ocr_config()
453
+ img = obs.get_screenshot_PIL(compression=80, img_format="jpg")
454
+ ocr_config.scale_to_custom_size(img.width, img.height)
455
+ img = run.apply_ocr_config_to_image(img, ocr_config, is_secondary=True)
456
+ do_second_ocr("", datetime.now(), img, TextFiltering(lang=get_ocr_language()), ignore_furigana_filter=True, ignore_previous_result=True)
457
+
458
+ if not manual:
459
+ keyboard.add_hotkey(get_ocr_manual_ocr_hotkey().lower(), ocr_secondary_rectangles)
450
460
  secret_ss_hotkey = "F14"
451
461
  filtering = TextFiltering(lang=get_ocr_language())
452
462
  cropper = ScreenCropper()
@@ -544,6 +554,8 @@ if __name__ == "__main__":
544
554
  use_window_for_config = args.use_window_for_config
545
555
  keep_newline = args.keep_newline
546
556
  obs_ocr = args.obs_ocr
557
+
558
+ obs.connect_to_obs_sync(retry=0)
547
559
 
548
560
  # Start config change checker thread
549
561
  config_check_thread = ConfigChangeCheckThread()
@@ -832,7 +832,6 @@ class OBSScreenshotThread(threading.Thread):
832
832
  super().__init__(daemon=True)
833
833
  self.ocr_config = ocr_config
834
834
  self.interval = interval
835
- self.obs_client = None
836
835
  self.websocket = None
837
836
  self.current_source = None
838
837
  self.current_source_name = None
@@ -930,16 +929,6 @@ class OBSScreenshotThread(threading.Thread):
930
929
  init_config()
931
930
  start = time.time()
932
931
  while not terminated:
933
- if time.time() - start > 5:
934
- if not self.obs_client:
935
- self.connect_obs()
936
- else:
937
- try:
938
- self.obs_client.get_version()
939
- except Exception as e:
940
- logger.error(f"Lost connection to OBS: {e}")
941
- self.obs_client = None
942
- self.connect_obs()
943
932
  if not screenshot_event.wait(timeout=0.1):
944
933
  continue
945
934
 
@@ -990,15 +979,14 @@ class OBSScreenshotThread(threading.Thread):
990
979
  continue
991
980
 
992
981
 
993
- def apply_ocr_config_to_image(img, ocr_config):
982
+ def apply_ocr_config_to_image(img, ocr_config, is_secondary=False):
994
983
  for rectangle in ocr_config.rectangles:
995
984
  if rectangle.is_excluded:
996
985
  left, top, width, height = rectangle.coordinates
997
986
  draw = ImageDraw.Draw(img)
998
- draw.rectangle((left, top, left + width, top +
999
- height), fill=(0, 0, 0, 0))
987
+ draw.rectangle((left, top, left + width, top + height), fill=(0, 0, 0, 0))
1000
988
 
1001
- rectangles = [r for r in ocr_config.rectangles if not r.is_excluded]
989
+ rectangles = [r for r in ocr_config.rectangles if not r.is_excluded and r.is_secondary == is_secondary]
1002
990
 
1003
991
  # Sort top to bottom
1004
992
  if rectangles:
@@ -843,7 +843,7 @@ def get_log_path():
843
843
 
844
844
  temp_directory = ''
845
845
 
846
- def get_temporary_directory(delete=True):
846
+ def get_temporary_directory(delete=False):
847
847
  global temp_directory
848
848
  if not temp_directory:
849
849
  temp_directory = os.path.join(get_app_directory(), 'temp')
@@ -117,7 +117,7 @@ def get_screenshot(video_file, screenshot_timing, try_selector=False):
117
117
  get_temporary_directory(),
118
118
  f"{obs.get_current_game(sanitize=True)}.png"))
119
119
  ffmpeg_command = ffmpeg_base_command_list + [
120
- "-ss", f"{screenshot_timing}", # Default to 1 second
120
+ "-ss", f"{screenshot_timing}",
121
121
  "-i", video_file,
122
122
  "-vframes", "1",
123
123
  output_image
@@ -222,7 +222,7 @@ def process_image(image_file):
222
222
  break
223
223
  except Exception as e:
224
224
  logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
225
- output_image = make_unique_file_name(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.png")
225
+ output_image = make_unique_file_name(os.path.join(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.png"))
226
226
  shutil.move(image_file, output_image)
227
227
 
228
228
  logger.info(f"Processed image saved to: {output_image}")
@@ -303,21 +303,21 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
303
303
  suffix=f".{get_config().audio.extension}").name
304
304
  start_trim_time, total_seconds, total_seconds_after_offset, file_length = get_video_timings(video_path, game_line, anki_card_creation_time)
305
305
  end_trim_time = 0
306
+ end_trim_seconds = 0
306
307
 
307
308
  ffmpeg_command = ffmpeg_base_command_list + [
308
309
  "-i", untrimmed_audio,
309
310
  "-ss", str(start_trim_time)]
310
311
  if next_line and next_line > game_line.time:
311
- end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
312
- end_trim_time = f"{end_total_seconds:.3f}"
312
+ end_trim_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
313
+ end_trim_time = f"{end_trim_seconds:.3f}"
313
314
  ffmpeg_command.extend(['-to', end_trim_time])
314
315
  logger.debug(
315
316
  f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time} seconds")
316
317
  elif get_config().audio.pre_vad_end_offset and get_config().audio.pre_vad_end_offset < 0:
317
- end_total_seconds = file_length + get_config().audio.pre_vad_end_offset
318
- end_trim_time = f"{end_total_seconds:.3f}"
319
- ffmpeg_command.extend(['-to', end_trim_time])
320
- logger.debug(f"Trimming end of audio to {end_trim_time} seconds due to pre-vad end offset")
318
+ end_trim_seconds = file_length + get_config().audio.pre_vad_end_offset
319
+ ffmpeg_command.extend(['-to', str(end_trim_seconds)])
320
+ logger.debug(f"Trimming end of audio to {end_trim_seconds} seconds due to pre-vad end offset")
321
321
 
322
322
  ffmpeg_command.extend([
323
323
  "-c", "copy", # Using copy to avoid re-encoding, adjust if needed
@@ -326,18 +326,18 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
326
326
 
327
327
  logger.debug(" ".join(ffmpeg_command))
328
328
  subprocess.run(ffmpeg_command)
329
- gsm_state.previous_trim_args = (untrimmed_audio, start_trim_time, end_trim_time)
329
+ gsm_state.previous_trim_args = (untrimmed_audio, start_trim_time, end_trim_seconds)
330
330
 
331
331
  logger.debug(f"{total_seconds_after_offset} trimmed off of beginning")
332
332
 
333
- if end_trim_time:
334
- logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {end_trim_time}")
333
+ if end_trim_seconds:
334
+ logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {end_trim_seconds} seconds")
335
335
  else:
336
336
  logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds")
337
337
 
338
338
 
339
339
  logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
340
- return trimmed_audio, start_trim_time, end_trim_time
340
+ return trimmed_audio, start_trim_time, end_trim_seconds
341
341
 
342
342
  def get_video_timings(video_path, game_line, anki_card_creation_time=None):
343
343
  if anki_card_creation_time:
@@ -11,7 +11,12 @@ from GameSentenceMiner.util.ffmpeg import get_video_timings
11
11
  from GameSentenceMiner.util.text_log import GameLine
12
12
 
13
13
 
14
- def handle_texthooker_button(video_path='', get_audio_from_video=None):
14
+ def set_get_audio_from_video_callback(func):
15
+ global get_audio_from_video
16
+ get_audio_from_video = func
17
+
18
+
19
+ def handle_texthooker_button(video_path=''):
15
20
  try:
16
21
  if gsm_state.line_for_audio:
17
22
  line: GameLine = gsm_state.line_for_audio
@@ -52,6 +52,7 @@ class EventItem:
52
52
  'history': self.history,
53
53
  }
54
54
 
55
+
55
56
  class EventManager:
56
57
  events: list[EventItem]
57
58
  events_dict: dict[str, EventItem] = {}
@@ -87,7 +88,8 @@ class EventManager:
87
88
  event_id, line_id, text, timestamp = row
88
89
  timestamp = datetime.datetime.fromisoformat(timestamp)
89
90
  line = GameLine(line_id, text, timestamp, None, None, 0)
90
- event = EventItem(line, event_id, text, timestamp, False, timestamp < initial_time)
91
+ event = EventItem(line, event_id, text, timestamp,
92
+ False, timestamp < initial_time)
91
93
  self.events.append(event)
92
94
  self.ids.append(event_id)
93
95
  self.events_dict[event_id] = event
@@ -99,7 +101,8 @@ class EventManager:
99
101
  self.events = new_events
100
102
 
101
103
  def add_gameline(self, line: GameLine):
102
- new_event = EventItem(line, line.id, line.text, line.time, False, False)
104
+ new_event = EventItem(line, line.id, line.text,
105
+ line.time, False, False)
103
106
  self.events_dict[line.id] = new_event
104
107
  self.ids.append(line.id)
105
108
  self.events.append(new_event)
@@ -130,12 +133,16 @@ class EventManager:
130
133
  self.conn.close()
131
134
 
132
135
  def clear_history(self):
133
- self.cursor.execute("DELETE FROM events WHERE time < ?", (initial_time.isoformat(),))
136
+ self.cursor.execute("DELETE FROM events WHERE time < ?",
137
+ (initial_time.isoformat(),))
134
138
  logger.info(f"Cleared history before {initial_time.isoformat()}")
135
139
  self.conn.commit()
136
140
  # Clear the in-memory events as well
137
- event_manager.events = [event for event in event_manager if not event.history]
138
- event_manager.events_dict = {event.id: event for event in event_manager.events}
141
+ event_manager.events = [
142
+ event for event in event_manager if not event.history]
143
+ event_manager.events_dict = {
144
+ event.id: event for event in event_manager.events}
145
+
139
146
 
140
147
  class EventProcessor(threading.Thread):
141
148
  def __init__(self, event_queue, db_path):
@@ -173,6 +180,7 @@ class EventProcessor(threading.Thread):
173
180
  if self.conn:
174
181
  self.conn.close()
175
182
 
183
+
176
184
  event_manager = EventManager()
177
185
  event_queue = queue.Queue()
178
186
 
@@ -185,6 +193,8 @@ server_start_time = datetime.datetime.now().timestamp()
185
193
  app = flask.Flask(__name__)
186
194
 
187
195
  # Load data from the JSON file
196
+
197
+
188
198
  def load_data_from_file():
189
199
  if os.path.exists(TEXT_REPLACEMENTS_FILE):
190
200
  with open(TEXT_REPLACEMENTS_FILE, 'r', encoding='utf-8') as file:
@@ -192,10 +202,13 @@ def load_data_from_file():
192
202
  return {"enabled": True, "args": {"replacements": {}}}
193
203
 
194
204
  # Save data to the JSON file
205
+
206
+
195
207
  def save_data_to_file(data):
196
208
  with open(TEXT_REPLACEMENTS_FILE, 'w', encoding='utf-8') as file:
197
209
  json.dump(data, file, indent=4, ensure_ascii=False)
198
210
 
211
+
199
212
  @app.route('/load-data', methods=['GET'])
200
213
  def load_data():
201
214
  try:
@@ -204,6 +217,7 @@ def load_data():
204
217
  except Exception as e:
205
218
  return jsonify({"error": f"Failed to load data: {str(e)}"}), 500
206
219
 
220
+
207
221
  @app.route('/save-data', methods=['POST'])
208
222
  def save_data():
209
223
  try:
@@ -217,40 +231,49 @@ def save_data():
217
231
  except Exception as e:
218
232
  return jsonify({"error": f"Failed to save data: {str(e)}"}), 500
219
233
 
234
+
220
235
  def inject_server_start_time(html_content, timestamp):
221
236
  placeholder = '<script>'
222
237
  replacement = f'<script>const serverStartTime = {timestamp};'
223
238
  return html_content.replace(placeholder, replacement)
224
239
 
240
+
225
241
  @app.route('/favicon.ico')
226
242
  def favicon():
227
243
  return send_from_directory(os.path.join(app.root_path, 'static'),
228
244
  'favicon.ico', mimetype='image/vnd.microsoft.icon')
229
245
 
246
+
230
247
  @app.route('/<path:filename>')
231
248
  def serve_static(filename):
232
249
  return send_from_directory('pages', filename)
233
250
 
251
+
234
252
  @app.route('/')
235
253
  def index():
236
254
  return send_from_directory('templates', 'index.html')
237
255
 
256
+
238
257
  @app.route('/texthooker')
239
258
  def texthooker():
240
259
  return send_from_directory('templates', 'index.html')
241
260
 
261
+
242
262
  @app.route('/textreplacements')
243
263
  def textreplacements():
244
264
  return flask.render_template('text_replacements.html')
245
265
 
266
+
246
267
  @app.route('/data', methods=['GET'])
247
268
  def get_data():
248
269
  return jsonify([event.to_dict() for event in event_manager])
249
270
 
271
+
250
272
  @app.route('/get_ids', methods=['GET'])
251
273
  def get_ids():
252
274
  return jsonify(event_manager.get_ids())
253
275
 
276
+
254
277
  @app.route('/clear_history', methods=['POST'])
255
278
  def clear_history():
256
279
  temp_em = EventManager()
@@ -268,8 +291,8 @@ async def add_event_to_texthooker(line: GameLine):
268
291
  })
269
292
  if get_config().advanced.plaintext_websocket_port:
270
293
  await plaintext_websocket_server_thread.send_text(line.text)
271
-
272
-
294
+
295
+
273
296
  async def send_word_coordinates_to_overlay(boxes):
274
297
  if boxes and len(boxes) > 0 and overlay_server_thread:
275
298
  await overlay_server_thread.send_text(boxes)
@@ -286,6 +309,7 @@ def update_event():
286
309
  event_manager.get(event_id).checked = not event.checked
287
310
  return jsonify({'message': 'Event updated successfully'}), 200
288
311
 
312
+
289
313
  @app.route('/get-screenshot', methods=['Post'])
290
314
  def get_screenshot():
291
315
  """Endpoint to get a screenshot of the current game screen."""
@@ -294,12 +318,13 @@ def get_screenshot():
294
318
  if event_id is None:
295
319
  return jsonify({'error': 'Missing id'}), 400
296
320
  gsm_state.line_for_screenshot = get_line_by_id(event_id)
297
- if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot.id == gsm_state.previous_line_for_screenshot.id or gsm_state.previous_line_for_audio:
321
+ if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot == gsm_state.previous_line_for_screenshot or gsm_state.previous_line_for_audio and gsm_state.line_for_screenshot == gsm_state.previous_line_for_audio:
298
322
  handle_texthooker_button(gsm_state.previous_replay)
299
323
  else:
300
324
  obs.save_replay_buffer()
301
325
  return jsonify({}), 200
302
326
 
327
+
303
328
  @app.route('/play-audio', methods=['POST'])
304
329
  def play_audio():
305
330
  """Endpoint to play audio for a specific event."""
@@ -307,13 +332,16 @@ def play_audio():
307
332
  event_id = data.get('id')
308
333
  if event_id is None:
309
334
  return jsonify({'error': 'Missing id'}), 400
335
+ print(f"Playing audio for event ID: {event_id}")
310
336
  gsm_state.line_for_audio = get_line_by_id(event_id)
311
- if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio or gsm_state.previous_line_for_screenshot:
337
+ print(f"gsm_state.line_for_audio: {gsm_state.line_for_audio}")
338
+ if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio or gsm_state.previous_line_for_screenshot and gsm_state.line_for_audio == gsm_state.previous_line_for_screenshot:
312
339
  handle_texthooker_button(gsm_state.previous_replay)
313
340
  else:
314
341
  obs.save_replay_buffer()
315
342
  return jsonify({}), 200
316
343
 
344
+
317
345
  @app.route("/translate-line", methods=['POST'])
318
346
  def translate_line():
319
347
  data = request.get_json()
@@ -362,9 +390,11 @@ def get_status():
362
390
  def get_selected_lines():
363
391
  return [item.line for item in event_manager if item.checked]
364
392
 
393
+
365
394
  def are_lines_selected():
366
395
  return any(item.checked for item in event_manager)
367
396
 
397
+
368
398
  def reset_checked_lines():
369
399
  async def send_reset_message():
370
400
  await websocket_server_thread.send_text({
@@ -373,9 +403,11 @@ def reset_checked_lines():
373
403
  event_manager.reset_checked_lines()
374
404
  asyncio.run(send_reset_message())
375
405
 
406
+
376
407
  def open_texthooker():
377
408
  webbrowser.open(url + '/texthooker')
378
409
 
410
+
379
411
  def start_web_server():
380
412
  logger.debug("Starting web server...")
381
413
  import logging
@@ -386,7 +418,8 @@ def start_web_server():
386
418
  if get_config().general.open_multimine_on_startup:
387
419
  open_texthooker()
388
420
 
389
- app.run(host='0.0.0.0', port=port, debug=False) # debug=True provides helpful error messages during development
421
+ # debug=True provides helpful error messages during development
422
+ app.run(host='0.0.0.0', port=port, debug=False)
390
423
 
391
424
 
392
425
  websocket_queue = queue.Queue()
@@ -445,7 +478,7 @@ class WebsocketServerThread(threading.Thread):
445
478
  text = json.dumps(text)
446
479
  return asyncio.run_coroutine_threadsafe(
447
480
  self.send_text_coroutine(text), self.loop)
448
-
481
+
449
482
  def has_clients(self):
450
483
  return len(self.clients) > 0
451
484
 
@@ -467,24 +500,30 @@ class WebsocketServerThread(threading.Thread):
467
500
  await stop_event.wait()
468
501
  return
469
502
  except Exception as e:
470
- logger.warning(f"WebSocket server encountered an error: {e}. Retrying...")
503
+ logger.warning(
504
+ f"WebSocket server encountered an error: {e}. Retrying...")
471
505
  await asyncio.sleep(1)
472
506
 
473
507
  asyncio.run(main())
474
508
 
509
+
475
510
  def handle_exit_signal(loop):
476
511
  logger.info("Received exit signal. Shutting down...")
477
512
  for task in asyncio.all_tasks(loop):
478
513
  task.cancel()
479
-
480
- websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda : get_config().get_field_value('advanced', 'texthooker_communication_websocket_port'))
514
+
515
+
516
+ websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda: get_config(
517
+ ).get_field_value('advanced', 'texthooker_communication_websocket_port'))
481
518
  websocket_server_thread.start()
482
519
 
483
520
  if get_config().advanced.plaintext_websocket_port:
484
- plaintext_websocket_server_thread = WebsocketServerThread(read=False, get_ws_port_func=lambda : get_config().get_field_value('advanced', 'plaintext_websocket_port'))
521
+ plaintext_websocket_server_thread = WebsocketServerThread(
522
+ read=False, get_ws_port_func=lambda: get_config().get_field_value('advanced', 'plaintext_websocket_port'))
485
523
  plaintext_websocket_server_thread.start()
486
-
487
- overlay_server_thread = WebsocketServerThread(read=False, get_ws_port_func=lambda : get_config().get_field_value('wip', 'overlay_websocket_port'))
524
+
525
+ overlay_server_thread = WebsocketServerThread(
526
+ read=False, get_ws_port_func=lambda: get_config().get_field_value('wip', 'overlay_websocket_port'))
488
527
  overlay_server_thread.start()
489
528
 
490
529
  websocket_server_threads = [
@@ -493,6 +532,7 @@ websocket_server_threads = [
493
532
  overlay_server_thread
494
533
  ]
495
534
 
535
+
496
536
  async def texthooker_page_coro():
497
537
  global websocket_server_thread, plaintext_websocket_server_thread, overlay_server_thread
498
538
  # Run the WebSocket server in the asyncio event loop
@@ -502,11 +542,13 @@ async def texthooker_page_coro():
502
542
 
503
543
  # Keep the main asyncio event loop running (for the WebSocket server)
504
544
 
545
+
505
546
  def run_text_hooker_page():
506
547
  try:
507
548
  asyncio.run(texthooker_page_coro())
508
549
  except KeyboardInterrupt:
509
550
  logger.info("Shutting down due to KeyboardInterrupt.")
510
551
 
552
+
511
553
  if __name__ == '__main__':
512
- asyncio.run(texthooker_page_coro())
554
+ asyncio.run(texthooker_page_coro())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.13.9
3
+ Version: 2.13.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
@@ -2,7 +2,7 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
2
2
  GameSentenceMiner/anki.py,sha256=zBvKvu-LBaUmG1bUNLq9QXmVoPidqZCxJn4UPw6pUug,21041
3
3
  GameSentenceMiner/config_gui.py,sha256=BeI5Mz9CqDsW08fn1BNzZNUegEmHZsxkDmST8rY2jqU,125738
4
4
  GameSentenceMiner/gametext.py,sha256=h6IOGesK79X8IlvqqMmSzRkSVtkPAXDMHrkQsBwEV1E,10879
5
- GameSentenceMiner/gsm.py,sha256=EKt7WFPWLWdf2C8VOELakpgU7y0GXlqdRXtXQmN3Jok,28283
5
+ GameSentenceMiner/gsm.py,sha256=MTopfkJB_URJOenXeX_ZAtOoMsM7GFazawNA2QdUc_Q,28254
6
6
  GameSentenceMiner/obs.py,sha256=smlP_BFuuMkASVDEPG3DjxJ6p617kZXuNTeQ0edtH64,18703
7
7
  GameSentenceMiner/vad.py,sha256=zFReBMvNEEaQ_YEozCTCaMdV-o40FwtlxYRb17cYZio,19125
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -19,23 +19,23 @@ GameSentenceMiner/locales/en_us.json,sha256=9aFJw4vls269_ufnVx03uRLXG8-E-hdG9hJ9
19
19
  GameSentenceMiner/locales/ja_jp.json,sha256=zx5R_DLsdUw9Szg7RrOkRprMcnumM5Bnl4QhsfOj-qM,27190
20
20
  GameSentenceMiner/locales/zh_cn.json,sha256=fwP5GQh0WkftdpXuyyt9IiaEIPK_ZXtEa_3kmoeQMAk,24198
21
21
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ezj-0k6Wo-una91FvYhMp6KGkRhWYihXzLAoh_Wu2xY,5329
22
+ GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=7h_nfC7TRQQXW-zkYT0YLvupGBCxlIp62NDOwAASxiI,5369
23
23
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
24
- GameSentenceMiner/ocr/owocr_area_selector.py,sha256=tlzQp0uVT-OdM99IqycA58hBbVb4c8LdTAope2hUXWg,27319
25
- GameSentenceMiner/ocr/owocr_helper.py,sha256=EDKDhmZ0kF1xNmw2obZwAdXj-L-zDK09JGZPTJBja-c,26523
24
+ GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Y505tm7KT18flaIyFUe5DfBcBo99e9P3nQDgF4NTmbI,28209
25
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=_FHwiSNsKvRGWwu9huAepbr2Hpij1ewNxSQC4Gwrd7o,27156
26
26
  GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
27
27
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
28
28
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
29
29
  GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
30
30
  GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
31
31
  GameSentenceMiner/owocr/owocr/ocr.py,sha256=L_Dhy5qoSmVNtzBGVHM8aCJtfFsgqbSdTqXd1Obh1EM,62531
32
- GameSentenceMiner/owocr/owocr/run.py,sha256=asd5RsYRlsN7FhnMgbjDcN_m3QtVCWzysb6P9_rplMY,68523
32
+ GameSentenceMiner/owocr/owocr/run.py,sha256=0rsxeIlUys_bpwKDTfBwjER2FpgXKbfzM6lOxgFzzWU,68086
33
33
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
34
34
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  GameSentenceMiner/util/audio_offset_selector.py,sha256=8Stk3BP-XVIuzRv9nl9Eqd2D-1yD3JrgU-CamBywJmY,8542
36
- GameSentenceMiner/util/configuration.py,sha256=U87lD-e28b_0Rn15y6sLRY0NoHf0_mukyFCIucJyBb8,37183
36
+ GameSentenceMiner/util/configuration.py,sha256=p0ZnrPa0UTFzj59y__pNy0aWOxjlHz1fURZI33qWu74,37184
37
37
  GameSentenceMiner/util/electron_config.py,sha256=9CA27nzEFlxezzDqOPHxeD4BdJ093AnSJ9DJTcwWPsM,8762
38
- GameSentenceMiner/util/ffmpeg.py,sha256=fQakPhT4ZtUojwPVLGFcly1VNRfoth8STdtJLy_9X2g,23527
38
+ GameSentenceMiner/util/ffmpeg.py,sha256=HI2H_prbiZtLtLQgYOj1QNu0Ik_8Du_7gtoHftaz1g4,23520
39
39
  GameSentenceMiner/util/gsm_utils.py,sha256=Piwv88Q9av2LBeN7M6QDi0Mp0_R2lNbkcI6ekK5hd2o,11851
40
40
  GameSentenceMiner/util/model.py,sha256=hmA_seopP2bK40v9T4ulua9TrAeWtbkdCv-sTBPBQDk,6660
41
41
  GameSentenceMiner/util/notification.py,sha256=-qk3kTKEERzmMxx5XMh084HCyFmbfqz0XjY1hTKhCeQ,4202
@@ -51,8 +51,8 @@ GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
51
51
  GameSentenceMiner/util/downloader/download_tools.py,sha256=zR-aEHiFVkyo-9oPoSx6nQ2K-_J8WBHLZyLoOhypsW4,8458
52
52
  GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UTZo4H-Q-zb-JREkMLI,10439
53
53
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- GameSentenceMiner/web/service.py,sha256=S7bYf2kSk08u-8R9Qpv7piM-pxfFjYZUvU825xupmuI,5279
55
- GameSentenceMiner/web/texthooking_page.py,sha256=Mled45nQBh_Eb1vH4bLEqseMtrBmS2Vw9G9XxiPaSEQ,17525
54
+ GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
55
+ GameSentenceMiner/web/texthooking_page.py,sha256=qCNtWtWZAiC91uPrHD2LUWdHOufVuzDhTyTH8Z4oXXk,17927
56
56
  GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
58
58
  GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
@@ -67,9 +67,9 @@ GameSentenceMiner/web/templates/index.html,sha256=Gv3CJvNnhAzIVV_QxhNq4OD-pXDt1v
67
67
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
68
68
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
69
69
  GameSentenceMiner/wip/get_overlay_coords.py,sha256=nJRytHJwUBToXeAIkf45HP7Yv42YO-ILbP5h8GVeE2Q,19791
70
- gamesentenceminer-2.13.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
- gamesentenceminer-2.13.9.dist-info/METADATA,sha256=XDe-B_AOtfP-Kno8LypRFVvCfU80qZZylxnB1N9eWKM,1463
72
- gamesentenceminer-2.13.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- gamesentenceminer-2.13.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
74
- gamesentenceminer-2.13.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
75
- gamesentenceminer-2.13.9.dist-info/RECORD,,
70
+ gamesentenceminer-2.13.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
+ gamesentenceminer-2.13.11.dist-info/METADATA,sha256=Vie4cbRkyravU04SbKb0ra2P-2t2-QZTrmnCdrtMQ8g,1464
72
+ gamesentenceminer-2.13.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ gamesentenceminer-2.13.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
74
+ gamesentenceminer-2.13.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
75
+ gamesentenceminer-2.13.11.dist-info/RECORD,,