GameSentenceMiner 2.13.7__tar.gz → 2.13.8__tar.gz

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.
Files changed (79) hide show
  1. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/anki.py +50 -9
  2. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/config_gui.py +59 -75
  3. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/gametext.py +1 -2
  4. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/gsm.py +81 -70
  5. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/locales/en_us.json +22 -14
  6. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/locales/ja_jp.json +14 -6
  7. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/locales/zh_cn.json +14 -6
  8. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/obs.py +0 -6
  9. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/configuration.py +24 -19
  10. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/ffmpeg.py +59 -6
  11. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/gsm_utils.py +8 -0
  12. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/texthooking_page.py +6 -0
  13. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  14. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/PKG-INFO +1 -1
  15. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/pyproject.toml +1 -1
  16. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/__init__.py +0 -0
  17. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ai/__init__.py +0 -0
  18. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  19. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/__init__.py +0 -0
  20. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon.png +0 -0
  21. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon128.png +0 -0
  22. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon256.png +0 -0
  23. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon32.png +0 -0
  24. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon512.png +0 -0
  25. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/icon64.png +0 -0
  26. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/assets/pickaxe.png +0 -0
  27. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/__init__.py +0 -0
  28. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  29. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  30. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  31. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  32. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  33. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  34. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  35. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  36. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  37. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  38. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  39. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  40. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/__init__.py +0 -0
  41. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  42. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/communication/__init__.py +0 -0
  43. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/communication/send.py +0 -0
  44. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/communication/websocket.py +0 -0
  45. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  46. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  47. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  48. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  49. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/electron_config.py +0 -0
  50. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/model.py +0 -0
  51. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/notification.py +0 -0
  52. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/package.py +0 -0
  53. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/ss_selector.py +0 -0
  54. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/text_log.py +0 -0
  55. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/util/window_transparency.py +0 -0
  56. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/vad.py +0 -0
  57. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/__init__.py +0 -0
  58. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/service.py +0 -0
  59. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/__init__.py +0 -0
  60. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  61. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  62. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/favicon.ico +0 -0
  63. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/favicon.svg +0 -0
  64. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  65. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/style.css +0 -0
  66. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  67. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  68. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/templates/__init__.py +0 -0
  69. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/templates/index.html +0 -0
  70. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  71. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/web/templates/utility.html +0 -0
  72. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner/wip/get_overlay_coords.py +0 -0
  73. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  74. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  75. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  76. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/requires.txt +0 -0
  77. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  78. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/LICENSE +0 -0
  79. {gamesentenceminer-2.13.7 → gamesentenceminer-2.13.8}/setup.cfg +0 -0
@@ -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.")
@@ -9,6 +9,18 @@ import warnings
9
9
  os.environ.pop('TCL_LIBRARY', None)
10
10
 
11
11
 
12
+ def handle_error_in_initialization(e):
13
+ """Handle errors that occur during initialization."""
14
+ logger.exception(e, exc_info=True)
15
+ logger.info(
16
+ "An error occurred during initialization, Maybe try updating GSM from the menu or if running manually, try installing `pip install --update GameSentenceMiner`")
17
+ try:
18
+ while True:
19
+ time.sleep(1)
20
+ except KeyboardInterrupt:
21
+ logger.info("Exiting due to initialization error.")
22
+ sys.exit(1)
23
+
12
24
  try:
13
25
  import os.path
14
26
  import signal
@@ -45,11 +57,7 @@ try:
45
57
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
46
58
  except Exception as e:
47
59
  from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
48
- logger.info(
49
- "Something bad happened during import/initialization, closing in 5 seconds")
50
- logger.exception(e)
51
- time.sleep(5)
52
- sys.exit(1)
60
+ handle_error_in_initialization(e)
53
61
 
54
62
  if is_windows():
55
63
  import win32api
@@ -143,7 +151,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
143
151
 
144
152
  if get_config().anki.sentence_audio_field and get_config().audio.enabled:
145
153
  logger.debug("Attempting to get audio from video")
146
- 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(
147
155
  start_line,
148
156
  line_cutoff,
149
157
  video_path,
@@ -178,7 +186,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
178
186
  should_update_audio=vad_result.output_audio,
179
187
  ss_time=ss_timing,
180
188
  game_line=mined_line,
181
- selected_lines=selected_lines
189
+ selected_lines=selected_lines,
190
+ start_time=start_time,
191
+ end_time=end_time
182
192
  )
183
193
  elif get_config().features.notify_on_update and vad_result.success:
184
194
  notification.send_audio_generated_notification(
@@ -192,23 +202,25 @@ class VideoToAudioHandler(FileSystemEventHandler):
192
202
  f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
193
203
  notification.send_error_no_anki_update()
194
204
  finally:
195
- if not skip_delete:
196
- if video_path and get_config().paths.remove_video and os.path.exists(video_path):
197
- os.remove(video_path)
198
- if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
199
- os.remove(vad_trimmed_audio)
200
- if final_audio_output and get_config().paths.remove_audio and os.path.exists(final_audio_output):
201
- 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
202
214
 
203
215
  @staticmethod
204
216
  def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
205
- trimmed_audio = get_audio_and_trim(
217
+ trimmed_audio, start_time, end_time = get_audio_and_trim(
206
218
  video_path, game_line, next_line_time, anki_card_creation_time)
207
219
  if temporary:
208
220
  return ffmpeg.convert_audio_to_wav_lossless(trimmed_audio)
209
221
  vad_trimmed_audio = make_unique_file_name(
210
222
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
211
- 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(),
212
224
  f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
213
225
 
214
226
  vad_result = vad_processor.trim_audio_with_vad(
@@ -222,7 +234,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
222
234
  get_config().audio.ffmpeg_reencode_options_to_use)
223
235
  elif os.path.exists(vad_trimmed_audio):
224
236
  shutil.move(vad_trimmed_audio, final_audio_output)
225
- return final_audio_output, vad_result, vad_trimmed_audio
237
+ return final_audio_output, vad_result, vad_trimmed_audio, start_time, end_time
226
238
 
227
239
 
228
240
  def initial_checks():
@@ -451,41 +463,52 @@ def restart_obs():
451
463
 
452
464
 
453
465
  def cleanup():
454
- logger.info("Performing cleanup...")
455
- gsm_state.keep_running = False
466
+ try:
467
+ logger.info("Performing cleanup...")
468
+ gsm_state.keep_running = False
456
469
 
457
- if get_config().obs.enabled:
470
+ if obs.obs_connection_manager and obs.obs_connection_manager.is_alive():
471
+ obs.obs_connection_manager.stop()
458
472
  obs.stop_replay_buffer()
459
473
  obs.disconnect_from_obs()
460
- if get_config().obs.close_obs:
461
- close_obs()
462
-
463
- proc: Popen
464
- for proc in procs_to_close:
465
- try:
466
- logger.info(f"Terminating process {proc.args[0]}")
467
- proc.terminate()
468
- proc.wait() # Wait for OBS to fully close
469
- logger.info(f"Process {proc.args[0]} terminated.")
470
- except psutil.NoSuchProcess:
471
- logger.info("PID already closed.")
472
- except Exception as e:
473
- proc.kill()
474
- logger.error(f"Error terminating process {proc}: {e}")
475
-
476
- if icon:
477
- icon.stop()
478
-
479
- for video in gsm_state.videos_to_remove:
480
- try:
481
- if os.path.exists(video):
482
- os.remove(video)
483
- except Exception as e:
484
- logger.error(f"Error removing temporary video file {video}: {e}")
485
-
486
- settings_window.window.destroy()
487
- time.sleep(5)
488
- 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)
489
512
 
490
513
 
491
514
  def handle_exit():
@@ -508,15 +531,13 @@ def initialize(reloading=False):
508
531
  if shutil.which("ffmpeg") is None:
509
532
  os.environ["PATH"] += os.pathsep + \
510
533
  os.path.dirname(get_ffmpeg_path())
511
- if get_config().obs.enabled:
512
- if get_config().obs.open_obs:
513
- obs_process = obs.start_obs()
534
+ if get_config().obs.open_obs:
535
+ obs_process = obs.start_obs()
514
536
  # obs.connect_to_obs(start_replay=True)
515
537
  # anki.start_monitoring_anki()
516
538
  # gametext.start_text_monitor()
517
539
  os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
518
- os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
519
- os.makedirs(get_config().paths.audio_destination, exist_ok=True)
540
+ os.makedirs(get_config().paths.output_folder, exist_ok=True)
520
541
  initial_checks()
521
542
  register_websocket_message_handler(handle_websocket_message)
522
543
  # if get_config().vad.do_vad_postprocessing:
@@ -568,9 +589,8 @@ def post_init2():
568
589
  def async_loop():
569
590
  async def loop():
570
591
  await obs.connect_to_obs()
571
- if get_config().obs.enabled:
572
- await register_scene_switcher_callback()
573
- await check_obs_folder_is_correct()
592
+ await register_scene_switcher_callback()
593
+ await check_obs_folder_is_correct()
574
594
  logger.info("Post-Initialization started.")
575
595
  vad_processor.init()
576
596
  # if is_beangate:
@@ -688,11 +708,7 @@ async def async_main(reloading=False):
688
708
  except Exception as e:
689
709
  logger.error(f"Error stopping observer: {e}")
690
710
  except Exception as e:
691
- logger.error(f"An error occurred during initialization: {e}", exc_info=True)
692
- notification.send_error_notification(
693
- "An error occurred during initialization. Check the log for details.")
694
- asyncio.sleep(5)
695
- raise e
711
+ handle_error_in_initialization(e)
696
712
 
697
713
 
698
714
  def main():
@@ -701,10 +717,8 @@ def main():
701
717
  try:
702
718
  asyncio.run(async_main())
703
719
  except Exception as e:
704
- logger.exception(e, exc_info=True)
705
- logger.info(
706
- "An error occurred during initialization, closing in 5 seconds")
707
- time.sleep(5)
720
+ handle_error_in_initialization(e)
721
+
708
722
 
709
723
 
710
724
  if __name__ == "__main__":
@@ -712,7 +726,4 @@ if __name__ == "__main__":
712
726
  try:
713
727
  asyncio.run(async_main())
714
728
  except Exception as e:
715
- logger.exception(e, exc_info=True)
716
- logger.info(
717
- "An error occurred during initialization, closing in 5 seconds")
718
- time.sleep(5)
729
+ handle_error_in_initialization(e)