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/anki.py +11 -13
- GameSentenceMiner/config_gui.py +2 -5
- GameSentenceMiner/configuration.py +5 -1
- GameSentenceMiner/ffmpeg.py +14 -7
- GameSentenceMiner/gametext.py +149 -72
- GameSentenceMiner/gsm.py +62 -28
- GameSentenceMiner/utility_gui.py +23 -22
- gamesentenceminer-2.4.0.dist-info/LICENSE +674 -0
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.4.0.dist-info}/METADATA +2 -1
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.4.0.dist-info}/RECORD +13 -12
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.4.0.dist-info}/WHEEL +1 -1
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.4.0.dist-info}/entry_points.txt +0 -0
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.4.0.dist-info}/top_level.txt +0 -0
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
|
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
|
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
|
-
|
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
|
-
|
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
|
81
|
-
ss_timing = ffmpeg.get_screenshot_time(video_path,
|
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
|
-
|
93
|
-
|
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(
|
122
|
-
trimmed_audio = get_audio_and_trim(video_path,
|
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,
|
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
|
142
|
-
|
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
|
156
|
+
should_update_audio = vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio)
|
145
157
|
case configuration.WHISPER:
|
146
|
-
should_update_audio
|
147
|
-
|
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(
|
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
|
-
|
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
|
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)
|
GameSentenceMiner/utility_gui.py
CHANGED
@@ -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
|
39
|
-
self.add_checkbox_to_gui(
|
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,
|
45
|
-
if text:
|
44
|
+
def add_text(self, line):
|
45
|
+
if line.text:
|
46
46
|
var = tk.BooleanVar()
|
47
|
-
self.items.append((
|
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(
|
56
|
+
self.add_checkbox_to_gui(line, var)
|
57
57
|
|
58
|
-
def add_checkbox_to_gui(self,
|
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 = [
|
77
|
-
return filtered_items if len(filtered_items)
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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][
|
87
|
+
next_time = self.items[last_checked_index + 1][0].time
|
90
88
|
else:
|
91
89
|
next_time = 0
|
92
90
|
|
93
|
-
return
|
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
|
99
|
-
if len(filter_times)
|
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
|
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:
|