GameSentenceMiner 2.3.7__py3-none-any.whl → 2.4.0__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
@@ -1,18 +1,15 @@
1
1
  import signal
2
- import subprocess
3
- import sys
4
2
  import time
5
- import ttkbootstrap as ttk
6
3
  from subprocess import Popen
7
4
 
8
5
  import keyboard
9
6
  import psutil
7
+ import ttkbootstrap as ttk
10
8
  from PIL import Image, ImageDraw
11
9
  from pystray import Icon, Menu, MenuItem
12
10
  from watchdog.events import FileSystemEventHandler
13
11
  from watchdog.observers import Observer
14
12
 
15
- from GameSentenceMiner import utility_gui
16
13
  from GameSentenceMiner import anki
17
14
  from GameSentenceMiner import config_gui
18
15
  from GameSentenceMiner import configuration
@@ -21,12 +18,13 @@ from GameSentenceMiner import gametext
21
18
  from GameSentenceMiner import notification
22
19
  from GameSentenceMiner import obs
23
20
  from GameSentenceMiner import util
24
- from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
25
- from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
21
+ from GameSentenceMiner import utility_gui
26
22
  from GameSentenceMiner.configuration import *
23
+ from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
27
24
  from GameSentenceMiner.ffmpeg import get_audio_and_trim
28
- from GameSentenceMiner.gametext import get_line_timing
25
+ from GameSentenceMiner.gametext import get_text_event, get_mined_line
29
26
  from GameSentenceMiner.util import *
27
+ from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
30
28
 
31
29
  if is_windows():
32
30
  import win32api
@@ -57,6 +55,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
57
55
  if anki.card_queue and len(anki.card_queue) > 0:
58
56
  last_note = anki.card_queue.pop(0)
59
57
  with util.lock:
58
+ util.set_last_mined_line(anki.get_sentence(last_note))
60
59
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
61
60
  logger.debug(f"Video found and is readable: {video_path}")
62
61
 
@@ -73,12 +72,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
73
72
  last_note = anki.get_last_anki_card()
74
73
  if get_config().features.backfill_audio:
75
74
  last_note = anki.get_cards_by_sentence(gametext.current_line)
76
- line_time, next_line_time = get_line_timing(last_note)
75
+ line_cutoff = None
76
+ start_line = None
77
+ mined_line = get_text_event(last_note)
78
+ if mined_line:
79
+ start_line = mined_line
80
+ if mined_line.next:
81
+ line_cutoff = mined_line.next.time
82
+
77
83
  if utility_window.lines_selected():
78
- line_time, next_line_time = utility_window.get_selected_times()
84
+ lines = utility_window.get_selected_lines()
85
+ start_line = lines[0]
86
+ mined_line = get_mined_line(last_note, lines)
87
+ line_cutoff = utility_window.get_next_line_timing()
88
+
79
89
  ss_timing = 0
80
- if line_time and next_line_time or line_time and get_config().screenshot.use_beginning_of_line_as_screenshot:
81
- ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
90
+ if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
91
+ ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line)
82
92
  if last_note:
83
93
  logger.debug(json.dumps(last_note))
84
94
 
@@ -89,8 +99,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
89
99
  if get_config().anki.sentence_audio_field:
90
100
  logger.debug("Attempting to get audio from video")
91
101
  final_audio_output, should_update_audio, vad_trimmed_audio = VideoToAudioHandler.get_audio(
92
- line_time,
93
- next_line_time,
102
+ start_line,
103
+ line_cutoff,
94
104
  video_path)
95
105
  else:
96
106
  final_audio_output = ""
@@ -103,7 +113,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
103
113
  anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
104
114
  tango=tango,
105
115
  should_update_audio=should_update_audio,
106
- ss_time=ss_timing)
116
+ ss_time=ss_timing,
117
+ game_line=start_line)
107
118
  elif get_config().features.notify_on_update and should_update_audio:
108
119
  notification.send_audio_generated_notification(vad_trimmed_audio)
109
120
  except Exception as e:
@@ -118,11 +129,12 @@ class VideoToAudioHandler(FileSystemEventHandler):
118
129
  utility_window.reset_checkboxes()
119
130
 
120
131
  @staticmethod
121
- def get_audio(line_time, next_line_time, video_path):
122
- trimmed_audio = get_audio_and_trim(video_path, line_time, next_line_time)
132
+ def get_audio(game_line, next_line_time, video_path):
133
+ trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
123
134
  vad_trimmed_audio = make_unique_file_name(
124
135
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
125
- final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
136
+ final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
137
+ f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
126
138
  should_update_audio = True
127
139
  if get_config().vad.do_vad_postprocessing:
128
140
  match get_config().vad.selected_vad_model:
@@ -138,13 +150,13 @@ class VideoToAudioHandler(FileSystemEventHandler):
138
150
  case configuration.OFF:
139
151
  pass
140
152
  case configuration.SILERO:
141
- should_update_audio = silero_trim.process_audio_with_silero(trimmed_audio,
142
- vad_trimmed_audio)
153
+ should_update_audio = silero_trim.process_audio_with_silero(trimmed_audio,
154
+ vad_trimmed_audio)
143
155
  case configuration.VOSK:
144
- should_update_audio = vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio)
156
+ should_update_audio = vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio)
145
157
  case configuration.WHISPER:
146
- should_update_audio = whisper_helper.process_audio_with_whisper(trimmed_audio,
147
- vad_trimmed_audio)
158
+ should_update_audio = whisper_helper.process_audio_with_whisper(trimmed_audio,
159
+ vad_trimmed_audio)
148
160
  if get_config().audio.ffmpeg_reencode_options and os.path.exists(vad_trimmed_audio):
149
161
  ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
150
162
  get_config().audio.ffmpeg_reencode_options)
@@ -174,6 +186,7 @@ def initialize(reloading=False):
174
186
  if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
175
187
  whisper_helper.initialize_whisper_model()
176
188
 
189
+
177
190
  def initial_checks():
178
191
  try:
179
192
  subprocess.run(ffmpeg.ffmpeg_base_command_list)
@@ -231,10 +244,12 @@ def create_image():
231
244
 
232
245
  return image
233
246
 
247
+
234
248
  def open_settings():
235
249
  obs.update_current_game()
236
250
  settings_window.show()
237
251
 
252
+
238
253
  def open_multimine():
239
254
  obs.update_current_game()
240
255
  utility_window.show()
@@ -259,10 +274,12 @@ def open_log():
259
274
  logger.info("Log opened.")
260
275
 
261
276
 
262
- def exit_program(icon, item):
277
+ def exit_program(passed_icon, item):
263
278
  """Exit the application."""
279
+ if not passed_icon:
280
+ passed_icon = icon
264
281
  logger.info("Exiting...")
265
- icon.stop()
282
+ passed_icon.stop()
266
283
  cleanup()
267
284
 
268
285
 
@@ -276,7 +293,8 @@ def update_icon():
276
293
  global menu, icon
277
294
  # Recreate the menu with the updated button text
278
295
  profile_menu = Menu(
279
- *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for profile in
296
+ *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for
297
+ profile in
280
298
  get_master_config().get_all_profile_names()]
281
299
  )
282
300
 
@@ -293,6 +311,7 @@ def update_icon():
293
311
  icon.menu = menu
294
312
  icon.update_menu()
295
313
 
314
+
296
315
  def switch_profile(icon, item):
297
316
  if "Active:" in item.text:
298
317
  logger.error("You cannot switch to the currently active profile!")
@@ -326,6 +345,7 @@ def run_tray():
326
345
  icon = Icon("TrayApp", create_image(), "Game Sentence Miner", menu)
327
346
  icon.run()
328
347
 
348
+
329
349
  # def close_obs():
330
350
  # if obs_process:
331
351
  # logger.info("Closing OBS")
@@ -354,6 +374,7 @@ def close_obs():
354
374
  else:
355
375
  print("OBS is not running.")
356
376
 
377
+
357
378
  def restart_obs():
358
379
  global obs_process
359
380
  if obs_process:
@@ -362,6 +383,7 @@ def restart_obs():
362
383
  obs_process = obs.start_obs()
363
384
  obs.connect_to_obs(start_replay=True)
364
385
 
386
+
365
387
  def cleanup():
366
388
  logger.info("Performing cleanup...")
367
389
  util.keep_running = False
@@ -386,11 +408,23 @@ def cleanup():
386
408
  proc.kill()
387
409
  logger.error(f"Error terminating process {proc}: {e}")
388
410
 
389
-
390
411
  settings_window.window.destroy()
391
412
  logger.info("Cleanup complete.")
392
413
 
393
414
 
415
+ def check_for_stdin():
416
+ while True:
417
+ for line in sys.stdin:
418
+ match line:
419
+ case "exit":
420
+ cleanup()
421
+ sys.exit(0)
422
+ case "restart_obs":
423
+ restart_obs()
424
+ case "update":
425
+ update_icon()
426
+
427
+
394
428
  def handle_exit():
395
429
  """Signal handler for graceful termination."""
396
430
 
@@ -405,10 +439,12 @@ def handle_exit():
405
439
  def main(reloading=False, do_config_input=True):
406
440
  global root, settings_window, utility_window
407
441
  logger.info("Script started.")
442
+ util.run_new_thread(check_for_stdin)
408
443
  root = ttk.Window(themename='darkly')
409
444
  settings_window = config_gui.ConfigApp(root)
410
445
  utility_window = utility_gui.UtilityApp(root)
411
446
  initialize(reloading)
447
+ util.run_new_thread(run_tray)
412
448
  initial_checks()
413
449
  event_handler = VideoToAudioHandler()
414
450
  observer = Observer()
@@ -425,8 +461,6 @@ def main(reloading=False, do_config_input=True):
425
461
  if is_windows():
426
462
  win32api.SetConsoleCtrlHandler(handle_exit())
427
463
 
428
- util.run_new_thread(run_tray)
429
-
430
464
  try:
431
465
  if get_config().general.check_for_update_on_startup:
432
466
  root.after(0, settings_window.check_update)
@@ -35,16 +35,16 @@ class UtilityApp:
35
35
  self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
36
36
 
37
37
  # Add existing items
38
- for text, var, time in self.items:
39
- self.add_checkbox_to_gui(text, var, time)
38
+ for line, var in self.items:
39
+ self.add_checkbox_to_gui(line, var)
40
40
  else:
41
41
  self.multi_mine_window.deiconify()
42
42
  self.multi_mine_window.lift()
43
43
 
44
- def add_text(self, text, time):
45
- if text:
44
+ def add_text(self, line):
45
+ if line.text:
46
46
  var = tk.BooleanVar()
47
- self.items.append((text, var, time))
47
+ self.items.append((line, var))
48
48
 
49
49
  if len(self.items) > 10:
50
50
  if self.checkboxes:
@@ -53,12 +53,12 @@ class UtilityApp:
53
53
  self.items.pop(0)
54
54
 
55
55
  if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
56
- self.add_checkbox_to_gui(text, var, time)
56
+ self.add_checkbox_to_gui(line, var)
57
57
 
58
- def add_checkbox_to_gui(self, text, var, time):
58
+ def add_checkbox_to_gui(self, line, var):
59
59
  """ Add a single checkbox without repainting everything. """
60
60
  if self.checkbox_frame:
61
- chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
61
+ chk = ttk.Checkbutton(self.checkbox_frame, text=f"{line.time.strftime('%H:%M:%S')} - {line.text}", variable=var)
62
62
  chk.pack(anchor='w')
63
63
  self.checkboxes.append(chk)
64
64
 
@@ -73,30 +73,31 @@ class UtilityApp:
73
73
  # chk.pack(anchor='w')
74
74
 
75
75
  def get_selected_lines(self):
76
- filtered_items = [text for text, var, _ in self.items if var.get()]
77
- return filtered_items if len(filtered_items) >= 2 else []
76
+ filtered_items = [line for line, var in self.items if var.get()]
77
+ return filtered_items if len(filtered_items) > 0 else []
78
78
 
79
- def get_selected_times(self):
80
- filtered_times = [time for _, var, time in self.items if var.get()]
81
79
 
82
- if len(filtered_times) >= 2:
83
- logger.info(filtered_times)
84
- # Find the index of the last checked checkbox
85
- last_checked_index = max(i for i, (_, var, _) in enumerate(self.items) if var.get())
80
+ def get_next_line_timing(self):
81
+ selected_lines = [line for line, var in self.items if var.get()]
82
+
83
+ if len(selected_lines) >= 2:
84
+ last_checked_index = max(i for i, (_, var) in enumerate(self.items) if var.get())
86
85
 
87
- # Get the time AFTER the last checked checkbox, if it exists
88
86
  if last_checked_index + 1 < len(self.items):
89
- next_time = self.items[last_checked_index + 1][2]
87
+ next_time = self.items[last_checked_index + 1][0].time
90
88
  else:
91
89
  next_time = 0
92
90
 
93
- return filtered_times[0], next_time
91
+ return next_time
92
+ if len(selected_lines) == 1:
93
+ return selected_lines[0].get_next_time()
94
94
 
95
95
  return None
96
96
 
97
+
97
98
  def lines_selected(self):
98
- filter_times = [time for _, var, time in self.items if var.get()]
99
- if len(filter_times) >= 2:
99
+ filter_times = [line.time for line, var in self.items if var.get()]
100
+ if len(filter_times) > 0:
100
101
  return True
101
102
  return False
102
103
 
@@ -114,7 +115,7 @@ class UtilityApp:
114
115
  # found_unchecked = True
115
116
 
116
117
  def reset_checkboxes(self):
117
- for _, var, _ in self.items:
118
+ for _, var in self.items:
118
119
  var.set(False)
119
120
  # if self.multi_mine_window:
120
121
  # for checkbox in self.checkboxes: