GameSentenceMiner 2.16.8__py3-none-any.whl → 2.16.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -363,6 +363,8 @@ class ConfigApp:
363
363
  self.vad_trim_beginning_value = tk.BooleanVar(value=self.settings.vad.trim_beginning)
364
364
  self.vad_beginning_offset_value = tk.StringVar(value=str(self.settings.vad.beginning_offset))
365
365
  self.add_audio_on_no_results_value = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
366
+ self.use_tts_as_fallback_value = tk.BooleanVar(value=self.settings.vad.use_tts_as_fallback)
367
+ self.tts_url_value = tk.StringVar(value=self.settings.vad.tts_url)
366
368
  self.language_value = tk.StringVar(value=self.settings.vad.language)
367
369
  self.cut_and_splice_segments_value = tk.BooleanVar(value=self.settings.vad.cut_and_splice_segments)
368
370
  self.splice_padding_value = tk.StringVar(value=str(self.settings.vad.splice_padding) if self.settings.vad.splice_padding else "")
@@ -397,6 +399,8 @@ class ConfigApp:
397
399
  self.overlay_websocket_port_value = tk.StringVar(value=str(self.settings.overlay.websocket_port))
398
400
  self.overlay_websocket_send_value = tk.BooleanVar(value=self.settings.overlay.monitor_to_capture)
399
401
  self.overlay_engine_value = tk.StringVar(value=self.settings.overlay.engine)
402
+ self.periodic_value = tk.BooleanVar(value=self.settings.overlay.periodic)
403
+ self.periodic_interval_value = tk.StringVar(value=str(self.settings.overlay.periodic_interval))
400
404
 
401
405
  # Master Config Settings
402
406
  self.switch_to_default_if_not_found_value = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
@@ -595,6 +599,8 @@ class ConfigApp:
595
599
  trim_beginning=self.vad_trim_beginning_value.get(),
596
600
  beginning_offset=float(self.vad_beginning_offset_value.get()),
597
601
  add_audio_on_no_results=self.add_audio_on_no_results_value.get(),
602
+ use_tts_as_fallback=self.use_tts_as_fallback_value.get(),
603
+ tts_url=self.tts_url_value.get(),
598
604
  language=self.language_value.get(),
599
605
  cut_and_splice_segments=self.cut_and_splice_segments_value.get(),
600
606
  splice_padding=float(self.splice_padding_value.get()) if self.splice_padding_value.get() else 0.0,
@@ -630,7 +636,9 @@ class ConfigApp:
630
636
  overlay=Overlay(
631
637
  websocket_port=int(self.overlay_websocket_port_value.get()),
632
638
  monitor_to_capture=self.overlay_monitor.current() if self.monitors else 0,
633
- engine=OverlayEngine(self.overlay_engine_value.get()).value if self.overlay_engine_value.get() else OverlayEngine.LENS.value
639
+ engine=OverlayEngine(self.overlay_engine_value.get()).value if self.overlay_engine_value.get() else OverlayEngine.LENS.value,
640
+ periodic=self.periodic_value.get(),
641
+ periodic_interval=self.periodic_interval_value.get(),
634
642
  )
635
643
  # wip=WIP(
636
644
  # overlay_websocket_port=int(self.overlay_websocket_port_value.get()),
@@ -1111,6 +1119,17 @@ class ConfigApp:
1111
1119
  row=self.current_row, column=1, sticky='W', pady=2)
1112
1120
  self.current_row += 1
1113
1121
 
1122
+ # TODO ADD LOCALIZATION
1123
+ tts_fallback_i18n = vad_i18n.get('use_tts_as_fallback', {})
1124
+ HoverInfoLabelWidget(vad_frame, text=tts_fallback_i18n.get('label', 'Use TTS as Fallback.'), tooltip=tts_fallback_i18n.get('tooltip', 'Use TTS if no audio is detected'), row=self.current_row, column=0)
1125
+ ttk.Checkbutton(vad_frame, variable=self.use_tts_as_fallback_value, bootstyle="round-toggle").grid(row=self.current_row, column=1, sticky='W', pady=2)
1126
+ self.current_row += 1
1127
+
1128
+ tts_url_i18n = vad_i18n.get('tts_url', {})
1129
+ HoverInfoLabelWidget(vad_frame, text=tts_url_i18n.get('label', 'TTS URL'), tooltip=tts_url_i18n.get('tooltip', 'The URL for the TTS service'), row=self.current_row, column=0)
1130
+ ttk.Entry(vad_frame, textvariable=self.tts_url_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1131
+ self.current_row += 1
1132
+
1114
1133
  end_offset_i18n = vad_i18n.get('audio_end_offset', {})
1115
1134
  HoverInfoLabelWidget(vad_frame, text=end_offset_i18n.get('label', '...'),
1116
1135
  tooltip=end_offset_i18n.get('tooltip', '...'), foreground="dark orange",
@@ -1161,6 +1180,13 @@ class ConfigApp:
1161
1180
  # Add Reset Button
1162
1181
  self.add_reset_button(vad_frame, "vad", self.current_row, column=0, recreate_tab=self.create_vad_tab)
1163
1182
 
1183
+ for col in range(3):
1184
+ vad_frame.grid_columnconfigure(col, weight=0)
1185
+ for row in range(self.current_row):
1186
+ vad_frame.grid_rowconfigure(row, minsize=30)
1187
+
1188
+ return vad_frame
1189
+
1164
1190
  @new_tab
1165
1191
  def create_paths_tab(self):
1166
1192
  if self.paths_tab is None:
@@ -1183,7 +1209,7 @@ class ConfigApp:
1183
1209
  ttk.Button(paths_frame, text=browse_text, command=lambda: self.browse_folder(folder_watch_entry),
1184
1210
  bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
1185
1211
  self.current_row += 1
1186
-
1212
+
1187
1213
  # Combine "Copy temp files to output folder" and "Output folder" on one row
1188
1214
  copy_to_output_i18n = paths_i18n.get('copy_temp_files_to_output_folder', {})
1189
1215
  combined_i18n = paths_i18n.get('output_folder', {})
@@ -2058,7 +2084,7 @@ class ConfigApp:
2058
2084
  entry = ttk.Entry(ai_frame, textvariable=self.open_ai_url_value)
2059
2085
  entry.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2060
2086
  self.current_row += 1
2061
-
2087
+
2062
2088
  entry.bind("<FocusOut>", lambda e, row=self.current_row: self.update_models_element(ai_frame, row))
2063
2089
  entry.bind("<Return>", lambda e, row=self.current_row: self.update_models_element(ai_frame, row))
2064
2090
 
@@ -2253,6 +2279,21 @@ class ConfigApp:
2253
2279
  textvariable=self.overlay_engine_value)
2254
2280
  self.overlay_engine.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2255
2281
  self.current_row += 1
2282
+
2283
+ # Periodic Settings
2284
+ periodic_i18n = overlay_i18n.get('periodic', {})
2285
+ HoverInfoLabelWidget(overlay_frame, text=periodic_i18n.get('label', 'Periodic:'),
2286
+ tooltip=periodic_i18n.get('tooltip', 'Enable periodic Scanning.'),
2287
+ row=self.current_row, column=0)
2288
+ ttk.Checkbutton(overlay_frame, variable=self.periodic_value, bootstyle="round-toggle").grid(
2289
+ row=self.current_row, column=1, sticky='W', pady=2)
2290
+ self.current_row += 1
2291
+ periodic_interval_i18n = overlay_i18n.get('periodic_interval', {})
2292
+ HoverInfoLabelWidget(overlay_frame, text=periodic_interval_i18n.get('label', 'Periodic Interval:'),
2293
+ tooltip=periodic_interval_i18n.get('tooltip', 'Interval for periodic scanning.'),
2294
+ row=self.current_row, column=0)
2295
+ ttk.Entry(overlay_frame, textvariable=self.periodic_interval_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2296
+ self.current_row += 1
2256
2297
 
2257
2298
  if self.monitors:
2258
2299
  # Ensure the index is valid
@@ -2293,7 +2334,7 @@ class ConfigApp:
2293
2334
  # self.controller_hotkey_entry.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2294
2335
 
2295
2336
  # listen_for_input_button = ttk.Button(wip_frame, text="Listen for Input", command=lambda: self.listen_for_controller_input())
2296
- # listen_for_input_button.grid(row=self.current_row, column=2, sticky='EW', pady=2)
2337
+ # listen_for_input_button.grid(row=self.current_row, column=2, sticky='EW', pady=2, padx=5)
2297
2338
  # self.current_row += 1
2298
2339
 
2299
2340
  except Exception as e:
@@ -2406,6 +2447,7 @@ class ConfigApp:
2406
2447
  default_path = get_default_anki_media_collection_path()
2407
2448
  if default_path != self.anki_media_collection_value.get():
2408
2449
  self.anki_media_collection_value.set(default_path)
2450
+
2409
2451
  self.save_settings()
2410
2452
 
2411
2453
 
GameSentenceMiner/gsm.py CHANGED
@@ -1,3 +1,4 @@
1
+ import tempfile
1
2
  import time
2
3
  import asyncio
3
4
  import subprocess
@@ -6,6 +7,11 @@ import sys
6
7
  import os
7
8
  import warnings
8
9
 
10
+ import requests
11
+
12
+ from GameSentenceMiner.util.get_overlay_coords import OverlayThread
13
+ from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
14
+
9
15
  os.environ.pop('TCL_LIBRARY', None)
10
16
 
11
17
 
@@ -21,6 +27,7 @@ def handle_error_in_initialization(e):
21
27
  logger.info("Exiting due to initialization error.")
22
28
  sys.exit(1)
23
29
 
30
+
24
31
  try:
25
32
  import os.path
26
33
  import signal
@@ -48,15 +55,18 @@ try:
48
55
 
49
56
  start_time = time.time()
50
57
  from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
51
- logger.debug(f"[Import] download_tools (download_obs_if_needed, download_ffmpeg_if_needed): {time.time() - start_time:.3f}s")
58
+ logger.debug(
59
+ f"[Import] download_tools (download_obs_if_needed, download_ffmpeg_if_needed): {time.time() - start_time:.3f}s")
52
60
 
53
61
  start_time = time.time()
54
62
  from GameSentenceMiner.util.communication.send import send_restart_signal
55
- logger.debug(f"[Import] send_restart_signal: {time.time() - start_time:.3f}s")
63
+ logger.debug(
64
+ f"[Import] send_restart_signal: {time.time() - start_time:.3f}s")
56
65
 
57
66
  start_time = time.time()
58
67
  from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
59
- logger.debug(f"[Import] gsm_utils (wait_for_stable_file, make_unique_file_name, run_new_thread): {time.time() - start_time:.3f}s")
68
+ logger.debug(
69
+ f"[Import] gsm_utils (wait_for_stable_file, make_unique_file_name, run_new_thread): {time.time() - start_time:.3f}s")
60
70
 
61
71
  start_time = time.time()
62
72
  from GameSentenceMiner import anki
@@ -68,7 +78,8 @@ try:
68
78
 
69
79
  start_time = time.time()
70
80
  from GameSentenceMiner.util import configuration, notification, ffmpeg
71
- logger.debug(f"[Import] util (configuration, notification, ffmpeg): {time.time() - start_time:.3f}s")
81
+ logger.debug(
82
+ f"[Import] util (configuration, notification, ffmpeg): {time.time() - start_time:.3f}s")
72
83
 
73
84
  start_time = time.time()
74
85
  from GameSentenceMiner import gametext
@@ -84,19 +95,23 @@ try:
84
95
 
85
96
  start_time = time.time()
86
97
  from GameSentenceMiner.util.communication.websocket import connect_websocket, register_websocket_message_handler, FunctionName
87
- logger.debug(f"[Import] websocket (connect_websocket, register_websocket_message_handler, FunctionName): {time.time() - start_time:.3f}s")
98
+ logger.debug(
99
+ f"[Import] websocket (connect_websocket, register_websocket_message_handler, FunctionName): {time.time() - start_time:.3f}s")
88
100
 
89
101
  start_time = time.time()
90
102
  from GameSentenceMiner.util.ffmpeg import get_audio_and_trim, get_video_timings, get_ffmpeg_path
91
- logger.debug(f"[Import] util.ffmpeg (get_audio_and_trim, get_video_timings, get_ffmpeg_path): {time.time() - start_time:.3f}s")
103
+ logger.debug(
104
+ f"[Import] util.ffmpeg (get_audio_and_trim, get_video_timings, get_ffmpeg_path): {time.time() - start_time:.3f}s")
92
105
 
93
106
  start_time = time.time()
94
107
  from GameSentenceMiner.obs import check_obs_folder_is_correct
95
- logger.debug(f"[Import] obs.check_obs_folder_is_correct: {time.time() - start_time:.3f}s")
108
+ logger.debug(
109
+ f"[Import] obs.check_obs_folder_is_correct: {time.time() - start_time:.3f}s")
96
110
 
97
111
  start_time = time.time()
98
112
  from GameSentenceMiner.util.text_log import GameLine, get_text_event, get_mined_line, get_all_lines, game_log
99
- logger.debug(f"[Import] util.text_log (GameLine, get_text_event, get_mined_line, get_all_lines, game_log): {time.time() - start_time:.3f}s")
113
+ logger.debug(
114
+ f"[Import] util.text_log (GameLine, get_text_event, get_mined_line, get_all_lines, game_log): {time.time() - start_time:.3f}s")
100
115
 
101
116
  start_time = time.time()
102
117
  from GameSentenceMiner.util import *
@@ -104,15 +119,18 @@ try:
104
119
 
105
120
  start_time = time.time()
106
121
  from GameSentenceMiner.web import texthooking_page
107
- logger.debug(f"[Import] web.texthooking_page: {time.time() - start_time:.3f}s")
122
+ logger.debug(
123
+ f"[Import] web.texthooking_page: {time.time() - start_time:.3f}s")
108
124
 
109
125
  start_time = time.time()
110
126
  from GameSentenceMiner.web.service import handle_texthooker_button, set_get_audio_from_video_callback
111
- logger.debug(f"[Import] web.service (handle_texthooker_button, set_get_audio_from_video_callback): {time.time() - start_time:.3f}s")
127
+ logger.debug(
128
+ f"[Import] web.service (handle_texthooker_button, set_get_audio_from_video_callback): {time.time() - start_time:.3f}s")
112
129
 
113
130
  start_time = time.time()
114
131
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
115
- logger.debug(f"[Import] web.texthooking_page.run_text_hooker_page: {time.time() - start_time:.3f}s")
132
+ logger.debug(
133
+ f"[Import] web.texthooking_page.run_text_hooker_page: {time.time() - start_time:.3f}s")
116
134
  except Exception as e:
117
135
  from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
118
136
  handle_error_in_initialization(e)
@@ -172,8 +190,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
172
190
  if get_config().features.backfill_audio:
173
191
  last_note = anki.get_cards_by_sentence(
174
192
  gametext.current_line_after_regex)
175
-
176
- note, last_note = anki.get_initial_card_info(last_note, selected_lines)
193
+
194
+ note, last_note = anki.get_initial_card_info(
195
+ last_note, selected_lines)
177
196
  tango = last_note.get_field(
178
197
  get_config().anki.word_field) if last_note else ''
179
198
 
@@ -184,12 +203,15 @@ class VideoToAudioHandler(FileSystemEventHandler):
184
203
  start_line = selected_lines[0]
185
204
  mined_line = get_mined_line(last_note, selected_lines)
186
205
  line_cutoff = selected_lines[-1].get_next_time()
206
+ full_text = remove_html_and_cloze_tags(note['fields'][get_config().anki.sentence_field])
187
207
  else:
188
208
  mined_line = get_text_event(last_note)
189
209
  if mined_line:
190
210
  start_line = mined_line
191
211
  if mined_line.next:
192
212
  line_cutoff = mined_line.next.time
213
+ full_text = mined_line.text
214
+
193
215
  gsm_state.last_mined_line = mined_line
194
216
 
195
217
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
@@ -213,7 +235,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
213
235
  line_cutoff,
214
236
  video_path,
215
237
  anki_card_creation_time,
216
- mined_line=mined_line)
238
+ mined_line=mined_line,
239
+ full_text=full_text)
217
240
  else:
218
241
  final_audio_output = ""
219
242
  vad_result = VADResult(True, 0, 0, '')
@@ -269,11 +292,13 @@ class VideoToAudioHandler(FileSystemEventHandler):
269
292
  f"Error removing video file {video_path}: {e}", exc_info=True)
270
293
 
271
294
  @staticmethod
272
- def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
295
+ def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None, full_text=''):
273
296
  trimmed_audio, start_time, end_time = get_audio_and_trim(
274
297
  video_path, game_line, next_line_time, anki_card_creation_time)
275
298
  if temporary:
276
299
  return ffmpeg.convert_audio_to_wav_lossless(trimmed_audio)
300
+ if not get_config().vad.do_vad_postprocessing:
301
+ return trimmed_audio, VADResult(True, start_time, end_time, "No VAD"), trimmed_audio, start_time, end_time
277
302
  vad_trimmed_audio = make_unique_file_name(
278
303
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
279
304
  final_audio_output = make_unique_file_name(os.path.join(get_temporary_directory(),
@@ -283,6 +308,25 @@ class VideoToAudioHandler(FileSystemEventHandler):
283
308
  trimmed_audio, vad_trimmed_audio, game_line)
284
309
  if timing_only:
285
310
  return vad_result
311
+
312
+ if not vad_result.success:
313
+ if get_config().vad.add_audio_on_no_results:
314
+ logger.info("No voice activity detected, using full audio.")
315
+ vad_result.output_audio = trimmed_audio
316
+ elif get_config().vad.use_tts_as_fallback:
317
+ logger.info(
318
+ "No voice activity detected, using TTS as fallback.")
319
+ text_to_tts = full_text if full_text else game_line.text
320
+ url = get_config().vad.tts_url.replace("$s", text_to_tts)
321
+ tts_resp = requests.get(url)
322
+ if not tts_resp.ok:
323
+ logger.error(
324
+ f"Error fetching TTS audio from {url}. Is it running?: {tts_resp.status_code} {tts_resp.text}")
325
+ with tempfile.NamedTemporaryFile(dir=get_temporary_directory(), delete=False, suffix=".opus") as tmpfile:
326
+ tmpfile.write(tts_resp.content)
327
+ vad_result.output_audio = tmpfile.name
328
+ else:
329
+ logger.info(vad_result.trim_successful_string())
286
330
  if vad_result.output_audio:
287
331
  vad_trimmed_audio = vad_result.output_audio
288
332
  if get_config().audio.ffmpeg_reencode_options_to_use and os.path.exists(vad_trimmed_audio):
@@ -404,12 +448,13 @@ def open_multimine(icon, item):
404
448
 
405
449
 
406
450
  def exit_program(passed_icon, item):
407
- """Exit the application."""
408
- if not passed_icon:
409
- passed_icon = icon
410
- logger.info("Exiting...")
411
- passed_icon.stop()
412
- cleanup()
451
+ """Exit the application."""
452
+ if not passed_icon:
453
+ passed_icon = icon
454
+ logger.info("Exiting...")
455
+ passed_icon.stop()
456
+ cleanup()
457
+
413
458
 
414
459
  class GSMTray(threading.Thread):
415
460
  def __init__(self):
@@ -421,12 +466,11 @@ class GSMTray(threading.Thread):
421
466
  def run(self):
422
467
  self.run_tray()
423
468
 
424
-
425
469
  def run_tray(self):
426
470
  self.profile_menu = Menu(
427
471
  *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, self.switch_profile) for
428
- profile in
429
- get_master_config().get_all_profile_names()]
472
+ profile in
473
+ get_master_config().get_all_profile_names()]
430
474
  )
431
475
 
432
476
  menu = Menu(
@@ -447,8 +491,8 @@ class GSMTray(threading.Thread):
447
491
  # Recreate the menu with the updated button text
448
492
  profile_menu = Menu(
449
493
  *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, self.switch_profile) for
450
- profile in
451
- get_master_config().get_all_profile_names()]
494
+ profile in
495
+ get_master_config().get_all_profile_names()]
452
496
  )
453
497
 
454
498
  menu = Menu(
@@ -486,6 +530,7 @@ class GSMTray(threading.Thread):
486
530
  if self.icon:
487
531
  self.icon.stop()
488
532
 
533
+
489
534
  gsm_tray = GSMTray()
490
535
 
491
536
 
@@ -540,13 +585,13 @@ def cleanup():
540
585
  obs.disconnect_from_obs()
541
586
  if get_config().obs.close_obs:
542
587
  close_obs()
543
-
588
+
544
589
  if texthooking_page.websocket_server_threads:
545
590
  for thread in texthooking_page.websocket_server_threads:
546
591
  if thread and isinstance(thread, threading.Thread) and thread.is_alive():
547
592
  thread.stop_server()
548
593
  thread.join()
549
-
594
+
550
595
  proc: Popen
551
596
  for proc in procs_to_close:
552
597
  try:
@@ -568,7 +613,8 @@ def cleanup():
568
613
  if os.path.exists(video):
569
614
  os.remove(video)
570
615
  except Exception as e:
571
- logger.error(f"Error removing temporary video file {video}: {e}")
616
+ logger.error(
617
+ f"Error removing temporary video file {video}: {e}")
572
618
 
573
619
  settings_window.window.destroy()
574
620
  # time.sleep(5)
@@ -668,6 +714,9 @@ def async_loop():
668
714
  await register_scene_switcher_callback()
669
715
  await check_obs_folder_is_correct()
670
716
  vad_processor.init()
717
+ OverlayThread().start()
718
+
719
+ # Keep loop alive
671
720
  # if is_beangate:
672
721
  # await run_test_code()
673
722
 
@@ -713,8 +762,8 @@ async def run_test_code():
713
762
  if boxes:
714
763
  await texthooking_page.send_word_coordinates_to_overlay(boxes)
715
764
  await asyncio.sleep(2)
716
-
717
-
765
+
766
+
718
767
  async def check_if_script_is_running():
719
768
  """Check if the script is already running and kill it if so."""
720
769
  if os.path.exists(os.path.join(get_app_directory(), "current_pid.txt")):
@@ -722,14 +771,15 @@ async def check_if_script_is_running():
722
771
  pid = int(f.read().strip())
723
772
  if psutil.pid_exists(pid) and 'python' in psutil.Process(pid).name().lower():
724
773
  logger.info(f"Script is already running with PID: {pid}")
725
- psutil.Process(pid).terminate() # Attempt to terminate the existing process
774
+ # Attempt to terminate the existing process
775
+ psutil.Process(pid).terminate()
726
776
  logger.info("Sent SIGTERM to the existing process.")
727
777
  notification.send_error_notification(
728
778
  "Script was already running. Terminating the existing process.")
729
779
  return True
730
780
  return False
731
-
732
-
781
+
782
+
733
783
  async def log_current_pid():
734
784
  """Log the current process ID."""
735
785
  current_pid = os.getpid()
@@ -748,17 +798,17 @@ async def async_main(reloading=False):
748
798
  initialize_async()
749
799
  observer = Observer()
750
800
  observer.schedule(VideoToAudioHandler(),
751
- get_config().paths.folder_to_watch, recursive=False)
801
+ get_config().paths.folder_to_watch, recursive=False)
752
802
  observer.start()
753
803
  if is_windows():
754
804
  register_hotkeys()
755
-
805
+
756
806
  run_new_thread(initialize_text_monitor)
757
807
  run_new_thread(run_text_hooker_page)
758
808
  run_new_thread(async_loop).join()
759
-
809
+
760
810
  logger.info("Initialization complete. Happy Mining! がんばれ!")
761
-
811
+
762
812
  # await check_if_script_is_running()
763
813
  # await log_current_pid()
764
814
 
@@ -797,10 +847,9 @@ def main():
797
847
  handle_error_in_initialization(e)
798
848
 
799
849
 
800
-
801
850
  if __name__ == "__main__":
802
851
  logger.info("Starting GSM")
803
852
  try:
804
853
  asyncio.run(async_main())
805
854
  except Exception as e:
806
- handle_error_in_initialization(e)
855
+ handle_error_in_initialization(e)
@@ -268,6 +268,14 @@
268
268
  "use_cpu_for_inference": {
269
269
  "label": "Force CPU:",
270
270
  "tooltip": "Even if CUDA is installed, use CPU for Whisper"
271
+ },
272
+ "use_tts_as_fallback": {
273
+ "label": "Use TTS as Fallback:",
274
+ "tooltip": "Use Text-to-Speech as a fallback when no audio is found."
275
+ },
276
+ "tts_url": {
277
+ "label": "TTS URL:",
278
+ "tooltip": "URL for the Text-to-Speech service. Use $s as a placeholder for the text."
271
279
  }
272
280
  },
273
281
  "features": {
@@ -576,6 +584,14 @@
576
584
  "overlay_engine": {
577
585
  "label": "Overlay Engine:",
578
586
  "tooltip": "Select the OCR engine for the overlay. If you use lens, and are on windows, it will use OneOCR to optimize the scan."
587
+ },
588
+ "periodic": {
589
+ "label": "Periodic Capture:",
590
+ "tooltip": "Enable periodic capture of the screen for Overlay. Note, you still need text flowing into GSM for mining to work."
591
+ },
592
+ "periodic_interval": {
593
+ "label": "Capture Interval (Seconds):",
594
+ "tooltip": "Interval in seconds for periodic screen capture."
579
595
  }
580
596
  },
581
597
  "wip": {
@@ -267,6 +267,14 @@
267
267
  "use_cpu_for_inference": {
268
268
  "label": "CPU強制使用:",
269
269
  "tooltip": "CUDAがインストールされていてもWhisperでCPUを使用します"
270
+ },
271
+ "use_tts_as_fallback": {
272
+ "label": "TTSをフォールバックとして使用:",
273
+ "tooltip": "音声が見つからない場合にテキスト読み上げをフォールバックとして使用します。"
274
+ },
275
+ "tts_url": {
276
+ "label": "TTS URL:",
277
+ "tooltip": "テキスト読み上げサービスのURL。テキストのプレースホルダーとして$sを使用します。"
270
278
  }
271
279
  },
272
280
  "features": {
@@ -575,6 +583,14 @@
575
583
  "overlay_engine": {
576
584
  "label": "オーバーレイエンジン:",
577
585
  "tooltip": "オーバーレイのOCRエンジンを選択します。Lensを使用していてWindowsの場合、スキャンを最適化するためにOneOCRを使用します。"
586
+ },
587
+ "periodic": {
588
+ "label": "定期キャプチャ:",
589
+ "tooltip": "OCR処理のために画面を定期的にキャプチャするかどうか。"
590
+ },
591
+ "periodic_interval": {
592
+ "label": "キャプチャ間隔(秒):",
593
+ "tooltip": "定期的な画面キャプチャの間隔(秒単位)。"
578
594
  }
579
595
  },
580
596
  "wip": {
@@ -268,6 +268,14 @@
268
268
  "use_cpu_for_inference": {
269
269
  "label": "强制使用 CPU:",
270
270
  "tooltip": "即使已安装 CUDA,也强制使用 CPU 运行 Whisper"
271
+ },
272
+ "use_tts_as_fallback": {
273
+ "label": "使用 TTS 作为后备:",
274
+ "tooltip": "在未找到音频时使用文本转语音作为后备。"
275
+ },
276
+ "tts_url": {
277
+ "label": "TTS URL:",
278
+ "tooltip": "文本转语音服务的 URL。使用 $s 作为文本的占位符。"
271
279
  }
272
280
  },
273
281
  "features": {
@@ -564,6 +572,14 @@
564
572
  "overlay_engine": {
565
573
  "label": "覆盖层引擎:",
566
574
  "tooltip": "为覆盖层选择 OCR 引擎。如果您使用的是 lens,并且在 windows 上,它将使用 OneOCR 来优化扫描。"
575
+ },
576
+ "periodic": {
577
+ "label": "定期捕获:",
578
+ "tooltip": "启用定期屏幕捕获以进行 OCR 处理。"
579
+ },
580
+ "periodic_interval": {
581
+ "label": "捕获间隔(秒):",
582
+ "tooltip": "定期屏幕捕获的时间间隔(秒)。"
567
583
  }
568
584
  },
569
585
  "wip": {
@@ -558,6 +558,8 @@ class VAD:
558
558
  trim_beginning: bool = False
559
559
  beginning_offset: float = -0.25
560
560
  add_audio_on_no_results: bool = False
561
+ use_tts_as_fallback: bool = False
562
+ tts_url: str = 'http://127.0.0.1:5050/?term=$s'
561
563
  cut_and_splice_segments: bool = False
562
564
  splice_padding: float = 0.1
563
565
  use_cpu_for_inference: bool = False
@@ -642,6 +644,8 @@ class Overlay:
642
644
  websocket_port: int = 55499
643
645
  engine: str = OverlayEngine.LENS.value
644
646
  monitor_to_capture: int = 0
647
+ periodic: bool = False
648
+ periodic_interval: float = 1.0
645
649
 
646
650
  def __post_init__(self):
647
651
  if self.monitor_to_capture == -1:
@@ -4,6 +4,7 @@ import base64
4
4
  import json
5
5
  import math
6
6
  import os
7
+ import threading
7
8
  import time
8
9
  from PIL import Image
9
10
  from typing import Dict, Any, List, Tuple
@@ -16,6 +17,7 @@ from GameSentenceMiner.util.configuration import OverlayEngine, get_config, is_w
16
17
  from GameSentenceMiner.util.electron_config import get_ocr_language
17
18
  from GameSentenceMiner.obs import get_screenshot_PIL
18
19
  from GameSentenceMiner.web.texthooking_page import send_word_coordinates_to_overlay
20
+ from GameSentenceMiner.web.gsm_websocket import overlay_server_thread
19
21
 
20
22
  # def align_and_correct(ocr_json, reference_text):
21
23
  # logger.info(f"Starting align_and_correct with reference_text: '{reference_text}'")
@@ -80,6 +82,32 @@ try:
80
82
  import mss
81
83
  except ImportError:
82
84
  mss = None
85
+
86
+ class OverlayThread(threading.Thread):
87
+ """
88
+ A thread to run the overlay processing loop.
89
+ This is a simple wrapper around asyncio to run the overlay processing
90
+ in a separate thread.
91
+ """
92
+ def __init__(self):
93
+ super().__init__()
94
+ self.overlay_processor = OverlayProcessor()
95
+ self.loop = asyncio.new_event_loop()
96
+ self.daemon = True # Ensure thread exits when main program exits
97
+
98
+ def run(self):
99
+ """Runs the overlay processing loop."""
100
+ asyncio.set_event_loop(self.loop)
101
+ self.loop.run_until_complete(self.overlay_loop())
102
+
103
+ async def overlay_loop(self):
104
+ """Main loop to periodically process and send overlay data."""
105
+ while True:
106
+ if get_config().overlay.periodic and overlay_server_thread.has_clients():
107
+ await self.overlay_processor.find_box_and_send_to_overlay('')
108
+ await asyncio.sleep(get_config().overlay.periodic_interval) # Adjust the interval as needed
109
+ else:
110
+ await asyncio.sleep(3) # Sleep briefly when not active
83
111
 
84
112
  class OverlayProcessor:
85
113
  """
@@ -254,7 +282,7 @@ class OverlayProcessor:
254
282
  return_coords=True,
255
283
  multiple_crop_coords=True,
256
284
  return_one_box=False,
257
- furigana_filter_sensitivity=None # Disable furigana filtering
285
+ furigana_filter_sensitivity=None, # Disable furigana filtering
258
286
  )
259
287
 
260
288
  # 3. Create a composite image with only the detected text regions
GameSentenceMiner/vad.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import tempfile
2
2
  import time
3
3
  import warnings
4
+ import requests
4
5
  from abc import abstractmethod, ABC
5
6
 
6
7
  from GameSentenceMiner.util import configuration, ffmpeg
@@ -37,19 +38,7 @@ class VADSystem:
37
38
  if not result.success and get_config().vad.backup_vad_model != configuration.OFF:
38
39
  logger.info("No voice activity detected, using backup VAD model.")
39
40
  result = self._do_vad_processing(get_config().vad.backup_vad_model, input_audio, output_audio, game_line)
40
- if not result.success:
41
- if get_config().vad.add_audio_on_no_results:
42
- logger.info("No voice activity detected, using full audio.")
43
- result.output_audio = input_audio
44
- else:
45
- logger.info("No voice activity detected.")
46
- return result
47
- else:
48
- logger.info(result.trim_successful_string())
49
41
  return result
50
- else:
51
- return VADResult(True, 0, get_audio_length(input_audio), "OFF", [], input_audio)
52
-
53
42
 
54
43
  def _do_vad_processing(self, model, input_audio, output_audio, game_line):
55
44
  match model:
@@ -361,7 +361,7 @@
361
361
  AFK Timer (seconds)
362
362
  </label>
363
363
  <input type="number" id="afkTimer" name="afk_timer_seconds"
364
- style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
364
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
365
365
  placeholder="120">
366
366
  <small
367
367
  style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
@@ -375,7 +375,7 @@
375
375
  Session Gap (seconds)
376
376
  </label>
377
377
  <input type="number" id="sessionGap" name="session_gap_seconds"
378
- style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
378
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
379
379
  placeholder="3600">
380
380
  <small
381
381
  style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
@@ -405,7 +405,7 @@
405
405
  </label>
406
406
  <input type="number" id="streakRequirement" name="streak_requirement_hours" min="0.01"
407
407
  max="24" step="0.01"
408
- style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
408
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
409
409
  placeholder="1.0">
410
410
  <small
411
411
  style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
@@ -430,7 +430,7 @@
430
430
  </label>
431
431
  <input type="number" id="readingHoursTarget" name="reading_hours_target" min="1"
432
432
  max="10000"
433
- style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
433
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
434
434
  placeholder="1500">
435
435
  <small
436
436
  style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
@@ -446,7 +446,7 @@
446
446
  </label>
447
447
  <input type="number" id="characterCountTarget" name="character_count_target" min="1000"
448
448
  max="1000000000"
449
- style="width: 90%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
449
+ style="padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-tertiary); color: var(--text-primary); font-size: 14px;"
450
450
  placeholder="25000000">
451
451
  <small
452
452
  style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.16.8
3
+ Version: 2.16.9
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
@@ -1,10 +1,10 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  GameSentenceMiner/anki.py,sha256=Qq03nxYCA0bXS8IR1vnEB9cv2vxo6Ruy-UuiojC4ad0,26518
3
- GameSentenceMiner/config_gui.py,sha256=o_7zNONJPtbg6k-lRIpT2vUtxnEhBUljriRCb0zRuuI,143350
3
+ GameSentenceMiner/config_gui.py,sha256=iOsi3IVati6JHCnoCIzLAxlrbZt4j4pL5KNFzSq4gJo,146121
4
4
  GameSentenceMiner/gametext.py,sha256=fgBgLchezpauWELE9Y5G3kVCLfAneD0X4lJFoI3FYbs,10351
5
- GameSentenceMiner/gsm.py,sha256=CcXGZI8K7cYB9FJfCRaOujlWDk08sQGQXhTiMewMhFg,31889
5
+ GameSentenceMiner/gsm.py,sha256=bxuP6gez7Yw-iNxNRMHy6qtDUQy0cNmmwJpnv1crL4Y,33627
6
6
  GameSentenceMiner/obs.py,sha256=EyAYhaLvMjoeC-3j7fuvkqZN5logFFanPfb8Wn1C6m0,27296
7
- GameSentenceMiner/vad.py,sha256=MdeovQGrwIlNdIa8wKWBBo0qSBp-Ay1-hZRe0YhZBSU,20257
7
+ GameSentenceMiner/vad.py,sha256=klkxPA1pNbziZWG1MGjWVRRmOIt9UwlcB8ZV-lSnHsQ,19736
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  GameSentenceMiner/ai/ai_prompting.py,sha256=41xdBzE88Jlt12A0D-T_cMfLO5j6MSxfniOptpwNZm0,24068
10
10
  GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,9 +15,9 @@ GameSentenceMiner/assets/icon32.png,sha256=Kww0hU_qke9_22wBuO_Nq0Dv2SfnOLwMhCyGg
15
15
  GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB_7W4sqs,192487
16
16
  GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
17
17
  GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
18
- GameSentenceMiner/locales/en_us.json,sha256=VZ2RblOdRLUaTyofcJtc-urmW88vFL1G7DMG7wKbw9k,27184
19
- GameSentenceMiner/locales/ja_jp.json,sha256=X00lVjbxEhH3NVBxFMqqWMMh388DDGUvDECXGyf8T0k,29014
20
- GameSentenceMiner/locales/zh_cn.json,sha256=EYbNwLffCyrBw9AEiIObo6mZw9G_nsEkRDiN1QrFhoc,25259
18
+ GameSentenceMiner/locales/en_us.json,sha256=pyh8X6BHVny9Rk3fTBcW9wgbyysvpgWT2cebFAzIyv4,27917
19
+ GameSentenceMiner/locales/ja_jp.json,sha256=zqbZM04BV0qWR7Al_vdsL8b2hNq_B6oq2msvJc1AMOA,29854
20
+ GameSentenceMiner/locales/zh_cn.json,sha256=QLkCZF6zomPGT52UpS8x4ntW3YdeJyfvQTqyZgqdcFk,25914
21
21
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ov04c-nKzh3sADxO-5JyZWVe4DlrHM9edM9tc7-97Jo,5970
23
23
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
@@ -37,11 +37,11 @@ GameSentenceMiner/tools/furigana_filter_preview.py,sha256=BXv7FChPEJW_VeG5XYt6su
37
37
  GameSentenceMiner/tools/ss_selector.py,sha256=cbjMxiKOCuOfbRvLR_PCRlykBrGtm1LXd6u5czPqkmc,4793
38
38
  GameSentenceMiner/tools/window_transparency.py,sha256=GtbxbmZg0-UYPXhfHff-7IKZyY2DKe4B9GdyovfmpeM,8166
39
39
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- GameSentenceMiner/util/configuration.py,sha256=U59JYqUfq5ty_sLMNs-LVrWlc6wXB5-fCM8rstb9cR8,42291
40
+ GameSentenceMiner/util/configuration.py,sha256=qATOwZahVQNP8-ZnWiAKuR7UJLW25QNDmFKYNBYzkmE,42443
41
41
  GameSentenceMiner/util/db.py,sha256=B2Qwg7i0Qn_yxov-NhcT9QdFkF218Cqea_V7ZPzYBzM,21365
42
42
  GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
43
43
  GameSentenceMiner/util/ffmpeg.py,sha256=g3v1aJnv3qWekDnO0FdYozB-MG9di4WUvPA3NyXY9Ws,28998
44
- GameSentenceMiner/util/get_overlay_coords.py,sha256=TEMxhrBE8302WQj2K5q7SLKUhgNH_J-RjHpkUTQJdcU,17577
44
+ GameSentenceMiner/util/get_overlay_coords.py,sha256=LDm32E0MG-4vqT7Vv4hhbP9_lK-TJahTkyWes0TPaeM,18753
45
45
  GameSentenceMiner/util/gsm_utils.py,sha256=Piwv88Q9av2LBeN7M6QDi0Mp0_R2lNbkcI6ekK5hd2o,11851
46
46
  GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
47
47
  GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
@@ -85,14 +85,14 @@ GameSentenceMiner/web/templates/anki_stats.html,sha256=FdIMl-kY0-3as9Wn0i-wKIIkl
85
85
  GameSentenceMiner/web/templates/database.html,sha256=Gd54rgZmfcV7ufoJ69COeMncs5Q5u-rSJcsIvROVCEo,13732
86
86
  GameSentenceMiner/web/templates/index.html,sha256=7ChQ1j602MOiYU95wXAKP_Ezsh_JgdlGz2uXIzS2j0g,227894
87
87
  GameSentenceMiner/web/templates/search.html,sha256=nao3M_hAbm5ftzThi91NtQ3ZiINMPUNx4ngFmqLnLQ4,4060
88
- GameSentenceMiner/web/templates/stats.html,sha256=RRhQ8q0Gimt9gO2aa4iMguNYbijjEhHPJdCpXkV837s,31596
88
+ GameSentenceMiner/web/templates/stats.html,sha256=BL7HQLs62RbPbo5Hs8BgdH9CsrGLH583JSn0Uz3PK5Y,31536
89
89
  GameSentenceMiner/web/templates/utility.html,sha256=KtqnZUMAYs5XsEdC9Tlsd40NKAVic0mu6sh-ReMDJpU,16940
90
90
  GameSentenceMiner/web/templates/components/navigation.html,sha256=6y9PvM3nh8LY6JWrZb6zVOm0vqkBLDc6d3gB9X5lT_w,1055
91
91
  GameSentenceMiner/web/templates/components/theme-styles.html,sha256=hiq3zdJljpRjQO1iUA7gfFKwXebltG-IWW-gnKS4GHA,3439
92
92
  GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
- gamesentenceminer-2.16.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
- gamesentenceminer-2.16.8.dist-info/METADATA,sha256=RAXeKbYDIbfz9SU0p_R5Vrgjd-XnyiJ1x3H-WFsns-0,7348
95
- gamesentenceminer-2.16.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
- gamesentenceminer-2.16.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
97
- gamesentenceminer-2.16.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
98
- gamesentenceminer-2.16.8.dist-info/RECORD,,
93
+ gamesentenceminer-2.16.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
94
+ gamesentenceminer-2.16.9.dist-info/METADATA,sha256=e_346ixgOw2sn9kslL0d_bAorQZbwYKGoHZNpZWdE7M,7348
95
+ gamesentenceminer-2.16.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
+ gamesentenceminer-2.16.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
97
+ gamesentenceminer-2.16.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
98
+ gamesentenceminer-2.16.9.dist-info/RECORD,,