GameSentenceMiner 2.13.7.post1__py3-none-any.whl → 2.13.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.
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
- # Start monitoring anki
417
- if get_config().obs.enabled and get_config().features.full_auto:
418
- obs_thread = threading.Thread(target=monitor_anki)
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
 
@@ -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.locale)
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.locale.name:
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.audio_destination_value = tk.StringVar(value=self.settings.paths.audio_destination)
261
- self.screenshot_destination_value = tk.StringVar(value=self.settings.paths.screenshot_destination)
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.locale.name)
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
- audio_destination=self.audio_destination_value.get(),
473
- screenshot_destination=self.screenshot_destination_value.get(),
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
- remove_audio=self.remove_audio_value.get(),
476
- remove_screenshot=self.remove_screenshot_value.get()
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
- with open(get_config_path(), 'w') as file:
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
- audio_dest_i18n = paths_i18n.get('audio_destination', {})
1099
- HoverInfoLabelWidget(paths_frame, text=audio_dest_i18n.get('label', '...'), tooltip=audio_dest_i18n.get('tooltip', '...'),
1100
- foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
1101
- audio_dest_entry = ttk.Entry(paths_frame, width=50, textvariable=self.audio_destination_value)
1102
- audio_dest_entry.grid(row=self.current_row, column=1, sticky='W', pady=2)
1103
- ttk.Button(paths_frame, text=browse_text, command=lambda: self.browse_folder(audio_dest_entry),
1104
- bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
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
- ss_dest_i18n = paths_i18n.get('screenshot_destination', {})
1108
- HoverInfoLabelWidget(paths_frame, text=ss_dest_i18n.get('label', '...'),
1109
- tooltip=ss_dest_i18n.get('tooltip', '...'), foreground="dark orange",
1110
- font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
1111
- ss_dest_entry = ttk.Entry(paths_frame, width=50, textvariable=self.screenshot_destination_value)
1112
- ss_dest_entry.grid(row=self.current_row, column=1, sticky='W', pady=2)
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
- rm_vid_i18n = paths_i18n.get('remove_video', {})
1118
- HoverInfoLabelWidget(paths_frame, text=rm_vid_i18n.get('label', '...'), tooltip=rm_vid_i18n.get('tooltip', '...'),
1119
- row=self.current_row, column=0)
1120
- ttk.Checkbutton(paths_frame, variable=self.remove_video_value, bootstyle="round-toggle").grid(row=self.current_row,
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
- rm_audio_i18n = paths_i18n.get('remove_audio', {})
1125
- HoverInfoLabelWidget(paths_frame, text=rm_audio_i18n.get('label', '...'), tooltip=rm_audio_i18n.get('tooltip', '...'),
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.remove_audio_value, bootstyle="round-toggle").grid(row=self.current_row,
1128
- column=1, sticky='W', pady=2)
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
- rm_ss_i18n = paths_i18n.get('remove_screenshot', {})
1132
- HoverInfoLabelWidget(paths_frame, text=rm_ss_i18n.get('label', '...'), tooltip=rm_ss_i18n.get('tooltip', '...'),
1133
- row=self.current_row, column=0)
1134
- ttk.Checkbutton(paths_frame, variable=self.remove_screenshot_value, bootstyle="round-toggle").grid(
1135
- row=self.current_row, column=1, sticky='W', pady=2)
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)
@@ -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"Handling new text event: {current_line}")
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
- if video_path and get_config().paths.remove_video and os.path.exists(video_path):
205
- os.remove(video_path)
206
- if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
207
- os.remove(vad_trimmed_audio)
208
- if final_audio_output and get_config().paths.remove_audio and os.path.exists(final_audio_output):
209
- os.remove(final_audio_output)
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(get_config().paths.audio_destination,
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
- logger.info("Performing cleanup...")
463
- gsm_state.keep_running = False
466
+ try:
467
+ logger.info("Performing cleanup...")
468
+ gsm_state.keep_running = False
464
469
 
465
- if get_config().obs.enabled:
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
- if get_config().obs.close_obs:
469
- close_obs()
470
-
471
- proc: Popen
472
- for proc in procs_to_close:
473
- try:
474
- logger.info(f"Terminating process {proc.args[0]}")
475
- proc.terminate()
476
- proc.wait() # Wait for OBS to fully close
477
- logger.info(f"Process {proc.args[0]} terminated.")
478
- except psutil.NoSuchProcess:
479
- logger.info("PID already closed.")
480
- except Exception as e:
481
- proc.kill()
482
- logger.error(f"Error terminating process {proc}: {e}")
483
-
484
- if icon:
485
- icon.stop()
486
-
487
- for video in gsm_state.videos_to_remove:
488
- try:
489
- if os.path.exists(video):
490
- os.remove(video)
491
- except Exception as e:
492
- logger.error(f"Error removing temporary video file {video}: {e}")
493
-
494
- settings_window.window.destroy()
495
- time.sleep(5)
496
- logger.info("Cleanup complete.")
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.enabled:
520
- if get_config().obs.open_obs:
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.screenshot_destination, exist_ok=True)
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
- if get_config().obs.enabled:
580
- await register_scene_switcher_callback()
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": "Folder to Watch:",
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
- "audio_destination": {
123
- "label": "Audio Destination:",
124
- "tooltip": "Path where the cut Audio will be saved."
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
- "screenshot_destination": {
127
- "label": "Screenshot Destination:",
128
- "tooltip": "Path where the Screenshot will be saved."
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 Video:",
132
- "tooltip": "Remove video from the output."
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 after processing."
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
- "audio_destination": {
122
- "label": "音声保存先:",
123
- "tooltip": "切り出した音声の保存先パス。"
121
+ "output_folder": {
122
+ "label": "アウトプットフォルダ:",
123
+ "tooltip": "スクリーンショットと音声ファイル、その他関連ファイルの保存先パス。Ankiを使用しない場合に便利です。 (オプション)"
124
124
  },
125
- "screenshot_destination": {
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
- "audio_destination": {
123
- "label": "音频保存位置:",
124
- "tooltip": "剪切后音频的保存路径。"
122
+ "output_folder": {
123
+ "label": "输出文件夹:",
124
+ "tooltip": "截图和音频文件以及其他相关文件的保存路径。如果不使用 Anki,这将非常有用。 (可选)"
125
125
  },
126
- "screenshot_destination": {
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
- audio_destination: str = expanduser("~/Videos/GSM/Audio/")
370
- screenshot_destination: str = expanduser("~/Videos/GSM/SS/")
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 = False
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
- self.folder_to_watch = os.path.normpath(self.folder_to_watch)
377
- self.audio_destination = os.path.normpath(self.audio_destination)
378
- self.screenshot_destination = os.path.normpath(self.screenshot_destination)
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: Locale = Locale.English.value
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):
@@ -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
- get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
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
- get_config().paths.screenshot_destination,
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(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
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(os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.png"))
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.13.7.post1
3
+ Version: 2.13.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,9 +1,9 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=FUwcWO0-arzfQjejQmDKP7pNNakhboo8InQ4s_jv6AY,19099
3
- GameSentenceMiner/config_gui.py,sha256=x8H3HXoRlnfgiFczAoCe1wiCoQDP8MWV0v7am36q3Co,126479
4
- GameSentenceMiner/gametext.py,sha256=qR32LhXAo1_a4r01zd7Pm2Yj4ByYCw58u78JdFkSxh4,10939
5
- GameSentenceMiner/gsm.py,sha256=F_fiYcpGaHF_xHEmJ0R8oPzoFQHMgKjWX6tbi7DxIuM,27763
6
- GameSentenceMiner/obs.py,sha256=alh8G-0vEWxV46WqgVsgNU5_PC5JNzyXJdmVetjiGRo,18819
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=zv7qfr23G-os29yXfIE61_tYsTOwUjxF8ADH2YyJ0NA,24881
19
- GameSentenceMiner/locales/ja_jp.json,sha256=PgAkkN7qmbtuRj5a3jHd9R9FNztamhmhRZysY6TwETU,26283
20
- GameSentenceMiner/locales/zh_cn.json,sha256=h-YRSz2XN_TkymjkAH1MQ9KqGLE9_7Ru-N4yTqpm8_8,23615
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=M0TqVXHF2hZz2VqLhDFdGALL9IsNH3fZ9nskAMSVVuI,37332
36
+ GameSentenceMiner/util/configuration.py,sha256=U87lD-e28b_0Rn15y6sLRY0NoHf0_mukyFCIucJyBb8,37183
37
37
  GameSentenceMiner/util/electron_config.py,sha256=9CA27nzEFlxezzDqOPHxeD4BdJ093AnSJ9DJTcwWPsM,8762
38
- GameSentenceMiner/util/ffmpeg.py,sha256=t0tflxq170n8PZKkdw8fTZIUQfXD0p_qARa9JTdhBTc,21530
39
- GameSentenceMiner/util/gsm_utils.py,sha256=iRyLVcodMptRhkCzLf3hyqc6_RCktXnwApi6mLju6oQ,11565
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=2ZS89CAI17xVkx64rGmHHbF96eKR8gPWiR_WAoDJ0Mw,17399
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.7.post1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
- gamesentenceminer-2.13.7.post1.dist-info/METADATA,sha256=RzUIvb_zGe6KOHa8qtlYFMBd50r3NCn4q4F9VHnxYBg,1469
72
- gamesentenceminer-2.13.7.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- gamesentenceminer-2.13.7.post1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
74
- gamesentenceminer-2.13.7.post1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
75
- gamesentenceminer-2.13.7.post1.dist-info/RECORD,,
70
+ gamesentenceminer-2.13.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
71
+ gamesentenceminer-2.13.9.dist-info/METADATA,sha256=XDe-B_AOtfP-Kno8LypRFVvCfU80qZZylxnB1N9eWKM,1463
72
+ gamesentenceminer-2.13.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ gamesentenceminer-2.13.9.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
74
+ gamesentenceminer-2.13.9.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
75
+ gamesentenceminer-2.13.9.dist-info/RECORD,,