GameSentenceMiner 2.13.7.post1__py3-none-any.whl → 2.13.8__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 +50 -9
- GameSentenceMiner/config_gui.py +59 -75
- GameSentenceMiner/gametext.py +1 -2
- GameSentenceMiner/gsm.py +64 -52
- GameSentenceMiner/locales/en_us.json +22 -14
- GameSentenceMiner/locales/ja_jp.json +14 -6
- GameSentenceMiner/locales/zh_cn.json +14 -6
- GameSentenceMiner/obs.py +0 -6
- GameSentenceMiner/util/configuration.py +16 -12
- GameSentenceMiner/util/ffmpeg.py +59 -6
- GameSentenceMiner/util/gsm_utils.py +8 -0
- GameSentenceMiner/web/texthooking_page.py +6 -0
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/RECORD +18 -18
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import copy
|
2
|
+
from pathlib import Path
|
2
3
|
import queue
|
3
4
|
import time
|
4
5
|
|
@@ -10,7 +11,7 @@ from requests import post
|
|
10
11
|
|
11
12
|
from GameSentenceMiner import obs
|
12
13
|
from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
|
13
|
-
from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue, \
|
14
|
+
from GameSentenceMiner.util.gsm_utils import make_unique, sanitize_filename, wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue, \
|
14
15
|
run_new_thread, open_audio_in_external
|
15
16
|
from GameSentenceMiner.util import ffmpeg, notification
|
16
17
|
from GameSentenceMiner.util.configuration import *
|
@@ -20,6 +21,8 @@ from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_m
|
|
20
21
|
from GameSentenceMiner.obs import get_current_game
|
21
22
|
from GameSentenceMiner.web import texthooking_page
|
22
23
|
import re
|
24
|
+
import platform
|
25
|
+
import sys
|
23
26
|
|
24
27
|
# Global variables to track state
|
25
28
|
previous_note_ids = set()
|
@@ -28,7 +31,7 @@ card_queue = []
|
|
28
31
|
|
29
32
|
|
30
33
|
def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
|
31
|
-
should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0):
|
34
|
+
should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0, start_time=None, end_time=None):
|
32
35
|
update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
|
33
36
|
last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
|
34
37
|
update_picture = (get_config().anki.picture_field and get_config().screenshot.enabled
|
@@ -37,6 +40,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
37
40
|
audio_in_anki = ''
|
38
41
|
screenshot_in_anki = ''
|
39
42
|
prev_screenshot_in_anki = ''
|
43
|
+
screenshot = ''
|
44
|
+
prev_screenshot = ''
|
40
45
|
if reuse_audio:
|
41
46
|
logger.info("Reusing Audio from last note")
|
42
47
|
anki_result = anki_results[game_line.id]
|
@@ -53,8 +58,6 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
53
58
|
screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
|
54
59
|
wait_for_stable_file(screenshot)
|
55
60
|
screenshot_in_anki = store_media_file(screenshot)
|
56
|
-
if get_config().paths.remove_screenshot:
|
57
|
-
os.remove(screenshot)
|
58
61
|
if get_config().anki.previous_image_field and game_line.prev:
|
59
62
|
prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev, try_selector=get_config().screenshot.use_screenshot_selector)
|
60
63
|
wait_for_stable_file(prev_screenshot)
|
@@ -64,6 +67,46 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
64
67
|
audio_html = f"[sound:{audio_in_anki}]"
|
65
68
|
image_html = f"<img src=\"{screenshot_in_anki}\">"
|
66
69
|
prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
|
70
|
+
|
71
|
+
|
72
|
+
# Move files to output folder if configured
|
73
|
+
if get_config().paths.output_folder and get_config().paths.copy_temp_files_to_output_folder:
|
74
|
+
word_path = os.path.join(get_config().paths.output_folder, sanitize_filename(tango))
|
75
|
+
os.makedirs(word_path, exist_ok=True)
|
76
|
+
if audio_path:
|
77
|
+
audio_filename = Path(audio_path).name
|
78
|
+
new_audio_path = os.path.join(word_path, audio_filename)
|
79
|
+
if os.path.exists(audio_path):
|
80
|
+
shutil.copy(audio_path, new_audio_path)
|
81
|
+
if screenshot:
|
82
|
+
screenshot_filename = Path(screenshot).name
|
83
|
+
new_screenshot_path = os.path.join(word_path, screenshot_filename)
|
84
|
+
if os.path.exists(screenshot):
|
85
|
+
shutil.copy(screenshot, new_screenshot_path)
|
86
|
+
if prev_screenshot:
|
87
|
+
prev_screenshot_filename = Path(prev_screenshot).name
|
88
|
+
new_prev_screenshot_path = os.path.join(word_path, "prev_" + prev_screenshot_filename)
|
89
|
+
if os.path.exists(prev_screenshot):
|
90
|
+
shutil.copy(prev_screenshot, new_prev_screenshot_path)
|
91
|
+
|
92
|
+
if video_path and get_config().paths.copy_trimmed_replay_to_output_folder:
|
93
|
+
trimmed_video = ffmpeg.trim_replay_for_gameline(video_path, start_time, end_time, accurate=True)
|
94
|
+
new_video_path = os.path.join(word_path, Path(trimmed_video).name)
|
95
|
+
if os.path.exists(trimmed_video):
|
96
|
+
shutil.copy(trimmed_video, new_video_path)
|
97
|
+
|
98
|
+
# Open to word_path if configured
|
99
|
+
if get_config().paths.open_output_folder_on_card_creation:
|
100
|
+
try:
|
101
|
+
if platform.system() == "Windows":
|
102
|
+
subprocess.Popen(f'explorer "{word_path}"')
|
103
|
+
elif platform.system() == "Darwin":
|
104
|
+
subprocess.Popen(["open", word_path])
|
105
|
+
else:
|
106
|
+
subprocess.Popen(["xdg-open", word_path])
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Error opening output folder: {e}")
|
109
|
+
|
67
110
|
|
68
111
|
# note = {'id': last_note.noteId, 'fields': {}}
|
69
112
|
|
@@ -413,9 +456,7 @@ def get_note_ids():
|
|
413
456
|
|
414
457
|
|
415
458
|
def start_monitoring_anki():
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
obs_thread.daemon = True # Ensures the thread will exit when the main program exits
|
420
|
-
obs_thread.start()
|
459
|
+
obs_thread = threading.Thread(target=monitor_anki)
|
460
|
+
obs_thread.daemon = True
|
461
|
+
obs_thread.start()
|
421
462
|
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -143,7 +143,7 @@ class ConfigApp:
|
|
143
143
|
self.current_row = 0
|
144
144
|
|
145
145
|
self.master_config: Config = configuration.load_config()
|
146
|
-
self.i18n = load_localization(self.master_config.
|
146
|
+
self.i18n = load_localization(self.master_config.get_locale())
|
147
147
|
|
148
148
|
self.window.title(self.i18n.get('app', {}).get('title', 'GameSentenceMiner Configuration'))
|
149
149
|
|
@@ -212,7 +212,7 @@ class ConfigApp:
|
|
212
212
|
|
213
213
|
def change_locale(self):
|
214
214
|
"""Change the locale of the application."""
|
215
|
-
if self.locale_value.get() == self.master_config.
|
215
|
+
if self.locale_value.get() == self.master_config.get_locale().name:
|
216
216
|
return
|
217
217
|
self.i18n = load_localization(Locale[self.locale_value.get()])
|
218
218
|
self.save_settings()
|
@@ -245,7 +245,6 @@ class ConfigApp:
|
|
245
245
|
self.native_language_value = tk.StringVar(value=CommonLanguages.from_code(self.settings.general.native_language).name.replace('_', ' ').title())
|
246
246
|
|
247
247
|
# OBS Settings
|
248
|
-
self.obs_enabled_value = tk.BooleanVar(value=self.settings.obs.enabled)
|
249
248
|
self.obs_websocket_port_value = tk.StringVar(value=str(self.settings.obs.port))
|
250
249
|
self.obs_host_value = tk.StringVar(value=self.settings.obs.host)
|
251
250
|
self.obs_port_value = tk.StringVar(value=str(self.settings.obs.port))
|
@@ -257,8 +256,10 @@ class ConfigApp:
|
|
257
256
|
|
258
257
|
# Paths Settings
|
259
258
|
self.folder_to_watch_value = tk.StringVar(value=self.settings.paths.folder_to_watch)
|
260
|
-
self.
|
261
|
-
self.
|
259
|
+
self.output_folder_value = tk.StringVar(value=self.settings.paths.output_folder)
|
260
|
+
self.copy_temp_files_to_output_folder_value = tk.BooleanVar(value=self.settings.paths.copy_temp_files_to_output_folder)
|
261
|
+
self.open_output_folder_on_card_creation_value = tk.BooleanVar(value=self.settings.paths.open_output_folder_on_card_creation)
|
262
|
+
self.copy_trimmed_replay_to_output_folder_value = tk.BooleanVar(value=self.settings.paths.copy_trimmed_replay_to_output_folder)
|
262
263
|
self.remove_video_value = tk.BooleanVar(value=self.settings.paths.remove_video)
|
263
264
|
self.remove_audio_value = tk.BooleanVar(value=self.settings.paths.remove_audio)
|
264
265
|
self.remove_screenshot_value = tk.BooleanVar(value=self.settings.paths.remove_screenshot)
|
@@ -357,7 +358,7 @@ class ConfigApp:
|
|
357
358
|
|
358
359
|
# Master Config Settings
|
359
360
|
self.switch_to_default_if_not_found_value = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
|
360
|
-
self.locale_value = tk.StringVar(value=self.master_config.
|
361
|
+
self.locale_value = tk.StringVar(value=self.master_config.get_locale().name)
|
361
362
|
|
362
363
|
|
363
364
|
def create_tabs(self):
|
@@ -469,11 +470,11 @@ class ConfigApp:
|
|
469
470
|
),
|
470
471
|
paths=Paths(
|
471
472
|
folder_to_watch=self.folder_to_watch_value.get(),
|
472
|
-
|
473
|
-
|
473
|
+
output_folder=self.output_folder_value.get(),
|
474
|
+
open_output_folder_on_card_creation=self.open_output_folder_on_card_creation_value.get(),
|
474
475
|
remove_video=self.remove_video_value.get(),
|
475
|
-
|
476
|
-
|
476
|
+
copy_temp_files_to_output_folder=self.copy_temp_files_to_output_folder_value.get(),
|
477
|
+
copy_trimmed_replay_to_output_folder=self.copy_trimmed_replay_to_output_folder_value.get()
|
477
478
|
),
|
478
479
|
anki=Anki(
|
479
480
|
update_anki=self.update_anki_value.get(),
|
@@ -525,7 +526,6 @@ class ConfigApp:
|
|
525
526
|
pre_vad_end_offset=float(self.pre_vad_audio_offset_value.get()),
|
526
527
|
),
|
527
528
|
obs=OBS(
|
528
|
-
enabled=self.obs_enabled_value.get(),
|
529
529
|
open_obs=self.obs_open_obs_value.get(),
|
530
530
|
close_obs=self.obs_close_obs_value.get(),
|
531
531
|
host=self.obs_host_value.get(),
|
@@ -593,15 +593,6 @@ class ConfigApp:
|
|
593
593
|
dialog_i18n = self.i18n.get('dialogs', {}).get('config_error', {})
|
594
594
|
error_title = dialog_i18n.get('title', 'Configuration Error')
|
595
595
|
|
596
|
-
if config.features.backfill_audio and config.features.full_auto:
|
597
|
-
messagebox.showerror(error_title,
|
598
|
-
dialog_i18n.get('full_auto_and_backfill', 'Cannot have Full Auto and Backfill...'))
|
599
|
-
return
|
600
|
-
|
601
|
-
if not config.general.use_websocket and not config.general.use_clipboard:
|
602
|
-
messagebox.showerror(error_title, dialog_i18n.get('no_input_method', 'Cannot have both...'))
|
603
|
-
return
|
604
|
-
|
605
596
|
current_profile = self.profile_combobox.get()
|
606
597
|
prev_config = self.master_config.get_config()
|
607
598
|
self.master_config.switch_to_default_if_not_found = self.switch_to_default_if_not_found_value.get()
|
@@ -625,8 +616,7 @@ class ConfigApp:
|
|
625
616
|
if sync_changes:
|
626
617
|
self.master_config.sync_changed_fields(prev_config)
|
627
618
|
|
628
|
-
|
629
|
-
file.write(self.master_config.to_json(indent=4))
|
619
|
+
self.master_config.save()
|
630
620
|
|
631
621
|
logger.info("Settings saved successfully!")
|
632
622
|
|
@@ -934,13 +924,6 @@ class ConfigApp:
|
|
934
924
|
row=self.current_row, column=3, sticky='W', pady=2)
|
935
925
|
self.current_row += 1
|
936
926
|
|
937
|
-
# obs_enabled_i18n = simple_i18n.get('obs_enabled', {})
|
938
|
-
# HoverInfoLabelWidget(feature_frame, text=obs_enabled_i18n.get('label', '...'),
|
939
|
-
# tooltip=obs_enabled_i18n.get('tooltip', '...'), row=self.current_row, column=0)
|
940
|
-
# ttk.Checkbutton(feature_frame, variable=self.obs_enabled_value, bootstyle="round-toggle").grid(
|
941
|
-
# row=self.current_row, column=1, sticky='W', pady=2)
|
942
|
-
# self.current_row += 1
|
943
|
-
|
944
927
|
# screenshot_i18n = simple_i18n.get('screenshot_enabled', {})
|
945
928
|
# HoverInfoLabelWidget(feature_frame, text=screenshot_i18n.get('label', '...'),
|
946
929
|
# tooltip=screenshot_i18n.get('tooltip', '...'), row=self.current_row, column=0)
|
@@ -1094,46 +1077,61 @@ class ConfigApp:
|
|
1094
1077
|
ttk.Button(paths_frame, text=browse_text, command=lambda: self.browse_folder(folder_watch_entry),
|
1095
1078
|
bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
|
1096
1079
|
self.current_row += 1
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1080
|
+
|
1081
|
+
# Combine "Copy temp files to output folder" and "Output folder" on one row
|
1082
|
+
copy_to_output_i18n = paths_i18n.get('copy_temp_files_to_output_folder', {})
|
1083
|
+
combined_i18n = paths_i18n.get('output_folder', {})
|
1084
|
+
|
1085
|
+
# Output folder and "Copy temp files to output folder" on one row
|
1086
|
+
HoverInfoLabelWidget(paths_frame, text=combined_i18n.get('label', '...'),
|
1087
|
+
tooltip=combined_i18n.get('tooltip', '...'), foreground="dark orange",
|
1088
|
+
font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
|
1089
|
+
output_folder_entry = ttk.Entry(paths_frame, width=30, textvariable=self.output_folder_value)
|
1090
|
+
output_folder_entry.grid(row=self.current_row, column=1, sticky='EW', pady=2)
|
1091
|
+
ttk.Button(paths_frame, text=browse_text, command=lambda: self.browse_folder(output_folder_entry),
|
1092
|
+
bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
|
1093
|
+
|
1094
|
+
HoverInfoLabelWidget(paths_frame, text=copy_to_output_i18n.get('label', '...'),
|
1095
|
+
tooltip=copy_to_output_i18n.get('tooltip', '...'), row=self.current_row, column=3)
|
1096
|
+
ttk.Checkbutton(paths_frame, variable=self.copy_temp_files_to_output_folder_value, bootstyle="round-toggle").grid(
|
1097
|
+
row=self.current_row, column=4, sticky='W', pady=2)
|
1105
1098
|
self.current_row += 1
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
ttk.Button(paths_frame, text=browse_text, command=lambda: self.browse_folder(ss_dest_entry),
|
1114
|
-
bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
|
1099
|
+
|
1100
|
+
|
1101
|
+
copy_to_output_i18n = paths_i18n.get('copy_trimmed_replay_to_output_folder', {})
|
1102
|
+
HoverInfoLabelWidget(paths_frame, text=copy_to_output_i18n.get('label', '...'),
|
1103
|
+
tooltip=copy_to_output_i18n.get('tooltip', '...'), row=self.current_row, column=0)
|
1104
|
+
ttk.Checkbutton(paths_frame, variable=self.copy_trimmed_replay_to_output_folder_value, bootstyle="round-toggle").grid(
|
1105
|
+
row=self.current_row, column=1, sticky='W', pady=2)
|
1115
1106
|
self.current_row += 1
|
1116
1107
|
|
1117
|
-
|
1118
|
-
HoverInfoLabelWidget(paths_frame, text=
|
1119
|
-
row=self.current_row, column=0)
|
1120
|
-
ttk.Checkbutton(paths_frame, variable=self.
|
1108
|
+
open_output_folder_i18n = paths_i18n.get('open_output_folder_on_card_creation', {})
|
1109
|
+
HoverInfoLabelWidget(paths_frame, text=open_output_folder_i18n.get('label', '...'),
|
1110
|
+
tooltip=open_output_folder_i18n.get('tooltip', '...'), row=self.current_row, column=0)
|
1111
|
+
ttk.Checkbutton(paths_frame, variable=self.open_output_folder_on_card_creation_value, bootstyle="round-toggle").grid(row=self.current_row,
|
1121
1112
|
column=1, sticky='W', pady=2)
|
1122
1113
|
self.current_row += 1
|
1123
|
-
|
1124
|
-
|
1125
|
-
HoverInfoLabelWidget(paths_frame, text=
|
1114
|
+
|
1115
|
+
rm_video_i18n = paths_i18n.get('remove_video', {})
|
1116
|
+
HoverInfoLabelWidget(paths_frame, text=rm_video_i18n.get('label', '...'), tooltip=rm_video_i18n.get('tooltip', '...'),
|
1126
1117
|
row=self.current_row, column=0)
|
1127
|
-
ttk.Checkbutton(paths_frame, variable=self.
|
1128
|
-
|
1118
|
+
ttk.Checkbutton(paths_frame, variable=self.remove_video_value, bootstyle="round-toggle").grid(row=self.current_row,
|
1119
|
+
column=1, sticky='W', pady=2)
|
1129
1120
|
self.current_row += 1
|
1130
1121
|
|
1131
|
-
|
1132
|
-
HoverInfoLabelWidget(paths_frame, text=
|
1133
|
-
|
1134
|
-
ttk.Checkbutton(paths_frame, variable=self.
|
1135
|
-
|
1136
|
-
self.current_row += 1
|
1122
|
+
# rm_audio_i18n = paths_i18n.get('remove_audio', {})
|
1123
|
+
# HoverInfoLabelWidget(paths_frame, text=rm_audio_i18n.get('label', '...'), tooltip=rm_audio_i18n.get('tooltip', '...'),
|
1124
|
+
# row=self.current_row, column=0)
|
1125
|
+
# ttk.Checkbutton(paths_frame, variable=self.remove_audio_value, bootstyle="round-toggle").grid(row=self.current_row,
|
1126
|
+
# column=1, sticky='W', pady=2)
|
1127
|
+
# self.current_row += 1
|
1128
|
+
|
1129
|
+
# rm_ss_i18n = paths_i18n.get('remove_screenshot', {})
|
1130
|
+
# HoverInfoLabelWidget(paths_frame, text=rm_ss_i18n.get('label', '...'), tooltip=rm_ss_i18n.get('tooltip', '...'),
|
1131
|
+
# row=self.current_row, column=0)
|
1132
|
+
# ttk.Checkbutton(paths_frame, variable=self.remove_screenshot_value, bootstyle="round-toggle").grid(
|
1133
|
+
# row=self.current_row, column=1, sticky='W', pady=2)
|
1134
|
+
# self.current_row += 1
|
1137
1135
|
|
1138
1136
|
self.add_reset_button(paths_frame, "paths", self.current_row, 0, self.create_paths_tab)
|
1139
1137
|
|
@@ -1335,13 +1333,6 @@ class ConfigApp:
|
|
1335
1333
|
row=self.current_row, column=1, sticky='W', pady=2)
|
1336
1334
|
self.current_row += 1
|
1337
1335
|
|
1338
|
-
full_auto_i18n = features_i18n.get('full_auto', {})
|
1339
|
-
HoverInfoLabelWidget(features_frame, text=full_auto_i18n.get('label', '...'), tooltip=full_auto_i18n.get('tooltip', '...'),
|
1340
|
-
row=self.current_row, column=0)
|
1341
|
-
ttk.Checkbutton(features_frame, variable=self.full_auto_value, bootstyle="round-toggle").grid(row=self.current_row,
|
1342
|
-
column=1, sticky='W', pady=2)
|
1343
|
-
self.current_row += 1
|
1344
|
-
|
1345
1336
|
self.add_reset_button(features_frame, "features", self.current_row, 0, self.create_features_tab)
|
1346
1337
|
|
1347
1338
|
for col in range(3): features_frame.grid_columnconfigure(col, weight=0)
|
@@ -1607,13 +1598,6 @@ class ConfigApp:
|
|
1607
1598
|
obs_frame = self.obs_tab
|
1608
1599
|
obs_i18n = self.i18n.get('tabs', {}).get('obs', {})
|
1609
1600
|
|
1610
|
-
enabled_i18n = obs_i18n.get('enabled', {})
|
1611
|
-
HoverInfoLabelWidget(obs_frame, text=enabled_i18n.get('label', '...'), tooltip=enabled_i18n.get('tooltip', '...'),
|
1612
|
-
row=self.current_row, column=0)
|
1613
|
-
ttk.Checkbutton(obs_frame, variable=self.obs_enabled_value, bootstyle="round-toggle").grid(row=self.current_row,
|
1614
|
-
column=1, sticky='W', pady=2)
|
1615
|
-
self.current_row += 1
|
1616
|
-
|
1617
1601
|
open_i18n = obs_i18n.get('open_obs', {})
|
1618
1602
|
HoverInfoLabelWidget(obs_frame, text=open_i18n.get('label', '...'), tooltip=open_i18n.get('tooltip', '...'), row=self.current_row,
|
1619
1603
|
column=0)
|
GameSentenceMiner/gametext.py
CHANGED
@@ -149,10 +149,9 @@ async def handle_new_text_event(current_clipboard, line_time=None):
|
|
149
149
|
global current_line, current_line_time, current_line_after_regex, timer, current_sequence_start_time, last_raw_clipboard
|
150
150
|
obs.update_current_game()
|
151
151
|
current_line = current_clipboard
|
152
|
-
logger.info(f"Current Line: {current_line} last raw clipboard: {last_raw_clipboard}")
|
153
152
|
# Only apply this logic if merging is enabled
|
154
153
|
if get_config().general.merge_matching_sequential_text:
|
155
|
-
logger.info(f"
|
154
|
+
logger.info(f"Current Line: {current_line} last raw clipboard: {last_raw_clipboard}")
|
156
155
|
# If no timer is active, this is the start of a new sequence
|
157
156
|
if not timer:
|
158
157
|
logger.info("Starting a new sequence of text lines.")
|
GameSentenceMiner/gsm.py
CHANGED
@@ -151,7 +151,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
151
151
|
|
152
152
|
if get_config().anki.sentence_audio_field and get_config().audio.enabled:
|
153
153
|
logger.debug("Attempting to get audio from video")
|
154
|
-
final_audio_output, vad_result, vad_trimmed_audio = VideoToAudioHandler.get_audio(
|
154
|
+
final_audio_output, vad_result, vad_trimmed_audio, start_time, end_time = VideoToAudioHandler.get_audio(
|
155
155
|
start_line,
|
156
156
|
line_cutoff,
|
157
157
|
video_path,
|
@@ -186,7 +186,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
186
186
|
should_update_audio=vad_result.output_audio,
|
187
187
|
ss_time=ss_timing,
|
188
188
|
game_line=mined_line,
|
189
|
-
selected_lines=selected_lines
|
189
|
+
selected_lines=selected_lines,
|
190
|
+
start_time=start_time,
|
191
|
+
end_time=end_time
|
190
192
|
)
|
191
193
|
elif get_config().features.notify_on_update and vad_result.success:
|
192
194
|
notification.send_audio_generated_notification(
|
@@ -200,23 +202,25 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
200
202
|
f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
|
201
203
|
notification.send_error_no_anki_update()
|
202
204
|
finally:
|
203
|
-
if not skip_delete:
|
204
|
-
|
205
|
-
os.
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
210
214
|
|
211
215
|
@staticmethod
|
212
216
|
def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
|
213
|
-
trimmed_audio = get_audio_and_trim(
|
217
|
+
trimmed_audio, start_time, end_time = get_audio_and_trim(
|
214
218
|
video_path, game_line, next_line_time, anki_card_creation_time)
|
215
219
|
if temporary:
|
216
220
|
return ffmpeg.convert_audio_to_wav_lossless(trimmed_audio)
|
217
221
|
vad_trimmed_audio = make_unique_file_name(
|
218
222
|
f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
219
|
-
final_audio_output = make_unique_file_name(os.path.join(
|
223
|
+
final_audio_output = make_unique_file_name(os.path.join(get_temporary_directory(),
|
220
224
|
f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
|
221
225
|
|
222
226
|
vad_result = vad_processor.trim_audio_with_vad(
|
@@ -230,7 +234,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
230
234
|
get_config().audio.ffmpeg_reencode_options_to_use)
|
231
235
|
elif os.path.exists(vad_trimmed_audio):
|
232
236
|
shutil.move(vad_trimmed_audio, final_audio_output)
|
233
|
-
return final_audio_output, vad_result, vad_trimmed_audio
|
237
|
+
return final_audio_output, vad_result, vad_trimmed_audio, start_time, end_time
|
234
238
|
|
235
239
|
|
236
240
|
def initial_checks():
|
@@ -459,41 +463,52 @@ def restart_obs():
|
|
459
463
|
|
460
464
|
|
461
465
|
def cleanup():
|
462
|
-
|
463
|
-
|
466
|
+
try:
|
467
|
+
logger.info("Performing cleanup...")
|
468
|
+
gsm_state.keep_running = False
|
464
469
|
|
465
|
-
|
470
|
+
if obs.obs_connection_manager and obs.obs_connection_manager.is_alive():
|
471
|
+
obs.obs_connection_manager.stop()
|
466
472
|
obs.stop_replay_buffer()
|
467
473
|
obs.disconnect_from_obs()
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
474
|
+
if get_config().obs.close_obs:
|
475
|
+
close_obs()
|
476
|
+
|
477
|
+
if texthooking_page.websocket_server_threads:
|
478
|
+
for thread in texthooking_page.websocket_server_threads:
|
479
|
+
if thread and isinstance(thread, threading.Thread) and thread.is_alive():
|
480
|
+
thread.stop_server()
|
481
|
+
thread.join()
|
482
|
+
|
483
|
+
proc: Popen
|
484
|
+
for proc in procs_to_close:
|
485
|
+
try:
|
486
|
+
logger.info(f"Terminating process {proc.args[0]}")
|
487
|
+
proc.terminate()
|
488
|
+
proc.wait()
|
489
|
+
logger.info(f"Process {proc.args[0]} terminated.")
|
490
|
+
except psutil.NoSuchProcess:
|
491
|
+
logger.info("PID already closed.")
|
492
|
+
except Exception as e:
|
493
|
+
proc.kill()
|
494
|
+
logger.error(f"Error terminating process {proc}: {e}")
|
495
|
+
|
496
|
+
if icon:
|
497
|
+
icon.stop()
|
498
|
+
|
499
|
+
for video in gsm_state.videos_to_remove:
|
500
|
+
try:
|
501
|
+
if os.path.exists(video):
|
502
|
+
os.remove(video)
|
503
|
+
except Exception as e:
|
504
|
+
logger.error(f"Error removing temporary video file {video}: {e}")
|
505
|
+
|
506
|
+
settings_window.window.destroy()
|
507
|
+
# time.sleep(5)
|
508
|
+
logger.info("Cleanup complete.")
|
509
|
+
except Exception as e:
|
510
|
+
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
511
|
+
sys.exit(1)
|
497
512
|
|
498
513
|
|
499
514
|
def handle_exit():
|
@@ -516,15 +531,13 @@ def initialize(reloading=False):
|
|
516
531
|
if shutil.which("ffmpeg") is None:
|
517
532
|
os.environ["PATH"] += os.pathsep + \
|
518
533
|
os.path.dirname(get_ffmpeg_path())
|
519
|
-
if get_config().obs.
|
520
|
-
|
521
|
-
obs_process = obs.start_obs()
|
534
|
+
if get_config().obs.open_obs:
|
535
|
+
obs_process = obs.start_obs()
|
522
536
|
# obs.connect_to_obs(start_replay=True)
|
523
537
|
# anki.start_monitoring_anki()
|
524
538
|
# gametext.start_text_monitor()
|
525
539
|
os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
|
526
|
-
os.makedirs(get_config().paths.
|
527
|
-
os.makedirs(get_config().paths.audio_destination, exist_ok=True)
|
540
|
+
os.makedirs(get_config().paths.output_folder, exist_ok=True)
|
528
541
|
initial_checks()
|
529
542
|
register_websocket_message_handler(handle_websocket_message)
|
530
543
|
# if get_config().vad.do_vad_postprocessing:
|
@@ -576,9 +589,8 @@ def post_init2():
|
|
576
589
|
def async_loop():
|
577
590
|
async def loop():
|
578
591
|
await obs.connect_to_obs()
|
579
|
-
|
580
|
-
|
581
|
-
await check_obs_folder_is_correct()
|
592
|
+
await register_scene_switcher_callback()
|
593
|
+
await check_obs_folder_is_correct()
|
582
594
|
logger.info("Post-Initialization started.")
|
583
595
|
vad_processor.init()
|
584
596
|
# if is_beangate:
|
@@ -116,28 +116,36 @@
|
|
116
116
|
"paths": {
|
117
117
|
"title": "Paths",
|
118
118
|
"folder_to_watch": {
|
119
|
-
"label": "
|
120
|
-
"tooltip": "Path where the OBS Replays will be saved."
|
119
|
+
"label": "OBS Replay Path:",
|
120
|
+
"tooltip": "Path where the OBS Replays will be saved. This should be configured for you."
|
121
121
|
},
|
122
|
-
"
|
123
|
-
"label": "
|
124
|
-
"tooltip": "Path where
|
122
|
+
"output_folder": {
|
123
|
+
"label": "Output Mirror Folder:",
|
124
|
+
"tooltip": "Path where all exported files (screenshots, audio, etc.) will be saved in addition to Anki. Useful for accessing files outside of Anki. (Optional)"
|
125
125
|
},
|
126
|
-
"
|
127
|
-
"label": "
|
128
|
-
"tooltip": "
|
126
|
+
"copy_temp_files_to_output_folder": {
|
127
|
+
"label": "Enabled:",
|
128
|
+
"tooltip": "Whether to copy temporary files to the output folder."
|
129
|
+
},
|
130
|
+
"open_output_folder_on_card_creation": {
|
131
|
+
"label": "Open Output Folder on Card Creation:",
|
132
|
+
"tooltip": "Open the output folder when a card is created. Useful for quick access to files."
|
133
|
+
},
|
134
|
+
"copy_trimmed_replay_to_output_folder": {
|
135
|
+
"label": "Copy Trimmed Replay to Output Folder:",
|
136
|
+
"tooltip": "Copy a loosely trimmed replay to the output folder."
|
129
137
|
},
|
130
138
|
"remove_video": {
|
131
|
-
"label": "Remove
|
132
|
-
"tooltip": "Remove
|
139
|
+
"label": "Remove Replay:",
|
140
|
+
"tooltip": "Remove replay from the OBS output folder."
|
133
141
|
},
|
134
142
|
"remove_audio": {
|
135
|
-
"label": "Remove Audio:",
|
136
|
-
"tooltip": "Remove audio from the output."
|
143
|
+
"label": "Remove Audio from Folder:",
|
144
|
+
"tooltip": "Remove audio from the output. This does not affect Anki, only the audio destination folder."
|
137
145
|
},
|
138
146
|
"remove_screenshot": {
|
139
|
-
"label": "Remove Screenshot:",
|
140
|
-
"tooltip": "Remove screenshots
|
147
|
+
"label": "Remove Screenshot from Folder:",
|
148
|
+
"tooltip": "Remove screenshots from the screenshot destination folder. This does not affect Anki, only the screenshot destination folder."
|
141
149
|
}
|
142
150
|
},
|
143
151
|
"anki": {
|
@@ -118,13 +118,21 @@
|
|
118
118
|
"label": "監視フォルダ:",
|
119
119
|
"tooltip": "OBSリプレイが保存されるパス。"
|
120
120
|
},
|
121
|
-
"
|
122
|
-
"label": "
|
123
|
-
"tooltip": "
|
121
|
+
"output_folder": {
|
122
|
+
"label": "アウトプットフォルダ:",
|
123
|
+
"tooltip": "スクリーンショットと音声ファイル、その他関連ファイルの保存先パス。Ankiを使用しない場合に便利です。 (オプション)"
|
124
124
|
},
|
125
|
-
"
|
126
|
-
"label": "
|
127
|
-
"tooltip": "
|
125
|
+
"open_output_folder_on_card_creation": {
|
126
|
+
"label": "カード作成時にアウトプットフォルダを開く:",
|
127
|
+
"tooltip": "カードが作成されたときにアウトプットフォルダを開きます。ファイルへの迅速なアクセスに便利です。"
|
128
|
+
},
|
129
|
+
"copy_temp_files_to_output_folder": {
|
130
|
+
"label": "有効:",
|
131
|
+
"tooltip": "一時的なファイルをアウトプットフォルダにコピーします。Anki以外のツールでの使用に便利です。"
|
132
|
+
},
|
133
|
+
"copy_trimmed_replay_to_output_folder": {
|
134
|
+
"label": "トリミングされたリプレイをアウトプットフォルダにコピー:",
|
135
|
+
"tooltip": "トリミングされたリプレイをアウトプットフォルダにコピーします。トリミングされたリプレイへの迅速なアクセスに便利です。"
|
128
136
|
},
|
129
137
|
"remove_video": {
|
130
138
|
"label": "動画を削除:",
|
@@ -119,13 +119,21 @@
|
|
119
119
|
"label": "监视文件夹:",
|
120
120
|
"tooltip": "OBS 录像回放的保存路径。"
|
121
121
|
},
|
122
|
-
"
|
123
|
-
"label": "
|
124
|
-
"tooltip": "
|
122
|
+
"output_folder": {
|
123
|
+
"label": "输出文件夹:",
|
124
|
+
"tooltip": "截图和音频文件以及其他相关文件的保存路径。如果不使用 Anki,这将非常有用。 (可选)"
|
125
125
|
},
|
126
|
-
"
|
127
|
-
"label": "
|
128
|
-
"tooltip": "
|
126
|
+
"copy_temp_files_to_output_folder": {
|
127
|
+
"label": "启用:",
|
128
|
+
"tooltip": "将临时文件(如截图和音频)复制到输出文件夹。"
|
129
|
+
},
|
130
|
+
"open_output_folder_on_card_creation": {
|
131
|
+
"label": "卡片创建时打开输出文件夹:",
|
132
|
+
"tooltip": "在创建卡片时打开输出文件夹。便于快速访问文件。"
|
133
|
+
},
|
134
|
+
"copy_trimmed_replay_to_output_folder": {
|
135
|
+
"label": "复制修剪后的回放到输出文件夹:",
|
136
|
+
"tooltip": "将修剪后的回放复制到输出文件夹。便于快速访问修剪后的回放。"
|
129
137
|
},
|
130
138
|
"remove_video": {
|
131
139
|
"label": "删除视频:",
|
GameSentenceMiner/obs.py
CHANGED
@@ -139,9 +139,6 @@ def get_obs_websocket_config_values():
|
|
139
139
|
|
140
140
|
async def connect_to_obs(retry=5):
|
141
141
|
global client, obs_connection_manager, event_client, connecting
|
142
|
-
if not get_config().obs.enabled:
|
143
|
-
return
|
144
|
-
|
145
142
|
if is_windows():
|
146
143
|
get_obs_websocket_config_values()
|
147
144
|
|
@@ -181,9 +178,6 @@ async def connect_to_obs(retry=5):
|
|
181
178
|
|
182
179
|
def connect_to_obs_sync(retry=2):
|
183
180
|
global client, obs_connection_manager, event_client
|
184
|
-
if not get_config().obs.enabled or client:
|
185
|
-
return
|
186
|
-
|
187
181
|
if is_windows():
|
188
182
|
get_obs_websocket_config_values()
|
189
183
|
|
@@ -366,16 +366,19 @@ class General:
|
|
366
366
|
@dataclass
|
367
367
|
class Paths:
|
368
368
|
folder_to_watch: str = expanduser("~/Videos/GSM")
|
369
|
-
|
370
|
-
|
369
|
+
output_folder: str = expanduser("~/Videos/GSM/Output")
|
370
|
+
copy_temp_files_to_output_folder: bool = False
|
371
|
+
open_output_folder_on_card_creation: bool = True
|
372
|
+
copy_trimmed_replay_to_output_folder: bool = False
|
371
373
|
remove_video: bool = True
|
372
374
|
remove_audio: bool = False
|
373
375
|
remove_screenshot: bool = False
|
374
376
|
|
375
377
|
def __post_init__(self):
|
376
|
-
|
377
|
-
|
378
|
-
|
378
|
+
if self.folder_to_watch:
|
379
|
+
self.folder_to_watch = os.path.normpath(self.folder_to_watch)
|
380
|
+
if self.output_folder:
|
381
|
+
self.output_folder = os.path.normpath(self.output_folder)
|
379
382
|
|
380
383
|
@dataclass_json
|
381
384
|
@dataclass
|
@@ -469,7 +472,6 @@ class Audio:
|
|
469
472
|
@dataclass_json
|
470
473
|
@dataclass
|
471
474
|
class OBS:
|
472
|
-
enabled: bool = True
|
473
475
|
open_obs: bool = True
|
474
476
|
close_obs: bool = True
|
475
477
|
host: str = "127.0.0.1"
|
@@ -609,10 +611,6 @@ class ProfileConfig:
|
|
609
611
|
config_data = toml.load(f)
|
610
612
|
|
611
613
|
self.paths.folder_to_watch = expanduser(config_data['paths'].get('folder_to_watch', self.paths.folder_to_watch))
|
612
|
-
self.paths.audio_destination = expanduser(
|
613
|
-
config_data['paths'].get('audio_destination', self.paths.audio_destination))
|
614
|
-
self.paths.screenshot_destination = expanduser(config_data['paths'].get('screenshot_destination',
|
615
|
-
self.paths.screenshot_destination))
|
616
614
|
|
617
615
|
self.anki.url = config_data['anki'].get('url', self.anki.url)
|
618
616
|
self.anki.sentence_field = config_data['anki'].get('sentence_field', self.anki.sentence_field)
|
@@ -650,7 +648,6 @@ class ProfileConfig:
|
|
650
648
|
self.vad.do_vad_postprocessing)
|
651
649
|
self.vad.trim_beginning = config_data['audio'].get('vosk_trim_beginning', self.vad.trim_beginning)
|
652
650
|
|
653
|
-
self.obs.enabled = config_data['obs'].get('enabled', self.obs.enabled)
|
654
651
|
self.obs.host = config_data['obs'].get('host', self.obs.host)
|
655
652
|
self.obs.port = config_data['obs'].get('port', self.obs.port)
|
656
653
|
self.obs.password = config_data['obs'].get('password', self.obs.password)
|
@@ -689,12 +686,19 @@ class Config:
|
|
689
686
|
configs: Dict[str, ProfileConfig] = field(default_factory=dict)
|
690
687
|
current_profile: str = DEFAULT_CONFIG
|
691
688
|
switch_to_default_if_not_found: bool = True
|
692
|
-
locale:
|
689
|
+
locale: str = Locale.English.value
|
693
690
|
|
694
691
|
@classmethod
|
695
692
|
def new(cls):
|
696
693
|
instance = cls(configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
|
697
694
|
return instance
|
695
|
+
|
696
|
+
def get_locale(self) -> Locale:
|
697
|
+
try:
|
698
|
+
return Locale.from_any(self.locale)
|
699
|
+
except KeyError:
|
700
|
+
logger.warning(f"Locale '{self.locale}' not found. Defaulting to English.")
|
701
|
+
return Locale.English
|
698
702
|
|
699
703
|
@classmethod
|
700
704
|
def load(cls):
|
GameSentenceMiner/util/ffmpeg.py
CHANGED
@@ -82,7 +82,7 @@ def get_screenshot(video_file, screenshot_timing, try_selector=False):
|
|
82
82
|
else:
|
83
83
|
logger.error("Frame extractor script failed to run or returned no output, defaulting")
|
84
84
|
output_image = make_unique_file_name(os.path.join(
|
85
|
-
|
85
|
+
get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
|
86
86
|
# FFmpeg command to extract the last frame of the video
|
87
87
|
ffmpeg_command = ffmpeg_base_command_list + [
|
88
88
|
"-ss", f"{screenshot_timing}",
|
@@ -114,7 +114,7 @@ def get_screenshot(video_file, screenshot_timing, try_selector=False):
|
|
114
114
|
except Exception as e:
|
115
115
|
logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
|
116
116
|
output_image = make_unique_file_name(os.path.join(
|
117
|
-
|
117
|
+
get_temporary_directory(),
|
118
118
|
f"{obs.get_current_game(sanitize=True)}.png"))
|
119
119
|
ffmpeg_command = ffmpeg_base_command_list + [
|
120
120
|
"-ss", f"{screenshot_timing}", # Default to 1 second
|
@@ -192,7 +192,7 @@ def get_screenshot_time(video_path, game_line, default_beginning=False, vad_resu
|
|
192
192
|
|
193
193
|
def process_image(image_file):
|
194
194
|
output_image = make_unique_file_name(
|
195
|
-
os.path.join(
|
195
|
+
os.path.join(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
|
196
196
|
|
197
197
|
# FFmpeg command to process the input image
|
198
198
|
ffmpeg_command = ffmpeg_base_command_list + [
|
@@ -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(
|
225
|
+
output_image = make_unique_file_name(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}")
|
@@ -334,9 +334,10 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
|
|
334
334
|
logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {end_trim_time}")
|
335
335
|
else:
|
336
336
|
logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds")
|
337
|
-
|
337
|
+
|
338
|
+
|
338
339
|
logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
|
339
|
-
return trimmed_audio
|
340
|
+
return trimmed_audio, start_trim_time, end_trim_time
|
340
341
|
|
341
342
|
def get_video_timings(video_path, game_line, anki_card_creation_time=None):
|
342
343
|
if anki_card_creation_time:
|
@@ -500,6 +501,58 @@ def combine_audio_files(audio_files, output_file):
|
|
500
501
|
logger.debug("Combining audio files with command: " + " ".join(command))
|
501
502
|
|
502
503
|
subprocess.run(command)
|
504
|
+
|
505
|
+
|
506
|
+
def trim_replay_for_gameline(video_path, start_time, end_time, accurate=False):
|
507
|
+
"""
|
508
|
+
Trims the video replay based on the start and end times.
|
509
|
+
|
510
|
+
Offers two modes:
|
511
|
+
1. Fast (default): Uses stream copy. Very fast, no quality loss, but may not be
|
512
|
+
frame-accurate (cut starts at the keyframe before start_time).
|
513
|
+
2. Accurate: Re-encodes the video. Slower, but provides frame-perfect cuts.
|
514
|
+
|
515
|
+
:param video_path: Path to the video file.
|
516
|
+
:param start_time: Start time in seconds.
|
517
|
+
:param end_time: End time in seconds.
|
518
|
+
:param accurate: If True, re-encodes for frame-perfect trimming. Defaults to False.
|
519
|
+
:return: Path to the trimmed video file.
|
520
|
+
"""
|
521
|
+
output_name = f"trimmed_{Path(video_path).stem}.mp4"
|
522
|
+
trimmed_video = make_unique_file_name(
|
523
|
+
os.path.join(configuration.get_temporary_directory(), output_name))
|
524
|
+
|
525
|
+
# We use input seeking for accuracy, as it's faster when re-encoding.
|
526
|
+
# We place -ss before -i for fast seeking.
|
527
|
+
command = ffmpeg_base_command_list + [
|
528
|
+
"-ss", str(start_time),
|
529
|
+
"-i", video_path,
|
530
|
+
]
|
531
|
+
|
532
|
+
# The duration is now more reliable to calculate
|
533
|
+
duration = end_time - start_time
|
534
|
+
if duration > 0:
|
535
|
+
command.extend(["-t", str(duration)])
|
536
|
+
|
537
|
+
if accurate:
|
538
|
+
# Re-encode. Slower but frame-accurate.
|
539
|
+
# You can specify encoding parameters here if needed, e.g., -crf 23
|
540
|
+
command.extend(["-c:v", "libx264", "-preset", "veryfast", "-c:a", "aac"])
|
541
|
+
log_msg = f"Accurately trimming video (re-encoding) {video_path}"
|
542
|
+
else:
|
543
|
+
# Stream copy. Fast but not frame-accurate.
|
544
|
+
command.extend(["-c:v", "copy", "-c:a", "copy"])
|
545
|
+
log_msg = f"Fast trimming video (stream copy) {video_path}"
|
546
|
+
|
547
|
+
command.append(trimmed_video)
|
548
|
+
|
549
|
+
video_length = get_video_duration(video_path)
|
550
|
+
logger.info(f"{log_msg} of length {video_length} from {start_time} to {end_time} seconds.")
|
551
|
+
logger.debug(" ".join(command))
|
552
|
+
|
553
|
+
subprocess.run(command)
|
554
|
+
|
555
|
+
return trimmed_video
|
503
556
|
|
504
557
|
|
505
558
|
def is_video_big_enough(file_path, min_size_kb=250):
|
@@ -27,6 +27,14 @@ def make_unique_file_name(path):
|
|
27
27
|
current_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%f')[:-3]
|
28
28
|
return str(path.parent / f"{path.stem}_{current_time}{path.suffix}")
|
29
29
|
|
30
|
+
def make_unique(text):
|
31
|
+
"""
|
32
|
+
Generate a unique string by appending a timestamp to the input text.
|
33
|
+
This is useful for creating unique filenames or identifiers.
|
34
|
+
"""
|
35
|
+
current_time = datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%f')[:-3]
|
36
|
+
return f"{text}_{current_time}"
|
37
|
+
|
30
38
|
def sanitize_filename(filename):
|
31
39
|
return re.sub(r'[ <>:"/\\|?*\x00-\x1F]', '', filename)
|
32
40
|
|
@@ -487,6 +487,12 @@ if get_config().advanced.plaintext_websocket_port:
|
|
487
487
|
overlay_server_thread = WebsocketServerThread(read=False, get_ws_port_func=lambda : get_config().get_field_value('wip', 'overlay_websocket_port'))
|
488
488
|
overlay_server_thread.start()
|
489
489
|
|
490
|
+
websocket_server_threads = [
|
491
|
+
websocket_server_thread,
|
492
|
+
plaintext_websocket_server_thread,
|
493
|
+
overlay_server_thread
|
494
|
+
]
|
495
|
+
|
490
496
|
async def texthooker_page_coro():
|
491
497
|
global websocket_server_thread, plaintext_websocket_server_thread, overlay_server_thread
|
492
498
|
# Run the WebSocket server in the asyncio event loop
|
@@ -1,9 +1,9 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=
|
4
|
-
GameSentenceMiner/gametext.py,sha256=
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
6
|
-
GameSentenceMiner/obs.py,sha256=
|
2
|
+
GameSentenceMiner/anki.py,sha256=zBvKvu-LBaUmG1bUNLq9QXmVoPidqZCxJn4UPw6pUug,21041
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=BeI5Mz9CqDsW08fn1BNzZNUegEmHZsxkDmST8rY2jqU,125738
|
4
|
+
GameSentenceMiner/gametext.py,sha256=h6IOGesK79X8IlvqqMmSzRkSVtkPAXDMHrkQsBwEV1E,10879
|
5
|
+
GameSentenceMiner/gsm.py,sha256=EKt7WFPWLWdf2C8VOELakpgU7y0GXlqdRXtXQmN3Jok,28283
|
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
|
9
9
|
GameSentenceMiner/ai/ai_prompting.py,sha256=iHkEx2pQJ-tEyejOgYy4G0DcZc8qvBugVL6-CQpPSME,26089
|
@@ -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=
|
19
|
-
GameSentenceMiner/locales/ja_jp.json,sha256=
|
20
|
-
GameSentenceMiner/locales/zh_cn.json,sha256=
|
18
|
+
GameSentenceMiner/locales/en_us.json,sha256=9aFJw4vls269_ufnVx03uRLXG8-E-hdG9hJ9RVzM01I,25653
|
19
|
+
GameSentenceMiner/locales/ja_jp.json,sha256=zx5R_DLsdUw9Szg7RrOkRprMcnumM5Bnl4QhsfOj-qM,27190
|
20
|
+
GameSentenceMiner/locales/zh_cn.json,sha256=fwP5GQh0WkftdpXuyyt9IiaEIPK_ZXtEa_3kmoeQMAk,24198
|
21
21
|
GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=Ezj-0k6Wo-una91FvYhMp6KGkRhWYihXzLAoh_Wu2xY,5329
|
23
23
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
@@ -33,10 +33,10 @@ GameSentenceMiner/owocr/owocr/run.py,sha256=asd5RsYRlsN7FhnMgbjDcN_m3QtVCWzysb6P
|
|
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=
|
36
|
+
GameSentenceMiner/util/configuration.py,sha256=OPEt2ZhbJuHjhOMPg1NcYQJmYBIh1eAfHb1e_FI9Y_0,37182
|
37
37
|
GameSentenceMiner/util/electron_config.py,sha256=9CA27nzEFlxezzDqOPHxeD4BdJ093AnSJ9DJTcwWPsM,8762
|
38
|
-
GameSentenceMiner/util/ffmpeg.py,sha256=
|
39
|
-
GameSentenceMiner/util/gsm_utils.py,sha256=
|
38
|
+
GameSentenceMiner/util/ffmpeg.py,sha256=fQakPhT4ZtUojwPVLGFcly1VNRfoth8STdtJLy_9X2g,23527
|
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
|
42
42
|
GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
|
@@ -52,7 +52,7 @@ GameSentenceMiner/util/downloader/download_tools.py,sha256=zR-aEHiFVkyo-9oPoSx6n
|
|
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
54
|
GameSentenceMiner/web/service.py,sha256=S7bYf2kSk08u-8R9Qpv7piM-pxfFjYZUvU825xupmuI,5279
|
55
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
55
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=Mled45nQBh_Eb1vH4bLEqseMtrBmS2Vw9G9XxiPaSEQ,17525
|
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.
|
71
|
-
gamesentenceminer-2.13.
|
72
|
-
gamesentenceminer-2.13.
|
73
|
-
gamesentenceminer-2.13.
|
74
|
-
gamesentenceminer-2.13.
|
75
|
-
gamesentenceminer-2.13.
|
70
|
+
gamesentenceminer-2.13.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
71
|
+
gamesentenceminer-2.13.8.dist-info/METADATA,sha256=DnuBBPXg8p6Qo4eQNbnGNwx1tPBeEqLH6qsEusnmxDc,1463
|
72
|
+
gamesentenceminer-2.13.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
73
|
+
gamesentenceminer-2.13.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
74
|
+
gamesentenceminer-2.13.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
75
|
+
gamesentenceminer-2.13.8.dist-info/RECORD,,
|
File without changes
|
{gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/entry_points.txt
RENAMED
File without changes
|
{gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{gamesentenceminer-2.13.7.post1.dist-info → gamesentenceminer-2.13.8.dist-info}/top_level.txt
RENAMED
File without changes
|