GameSentenceMiner 2.4.12__py3-none-any.whl → 2.5.1__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
@@ -12,8 +12,10 @@ from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.configuration import get_config
14
14
  from GameSentenceMiner.gametext import get_text_event
15
+ from GameSentenceMiner.model import AnkiCard
16
+ from GameSentenceMiner.utility_gui import utility_window, get_utility_window
15
17
  from GameSentenceMiner.obs import get_current_game
16
- from GameSentenceMiner.util import remove_html_tags
18
+ from GameSentenceMiner.util import remove_html_and_cloze_tags
17
19
 
18
20
  audio_in_anki = None
19
21
  screenshot_in_anki = None
@@ -25,15 +27,13 @@ first_run = True
25
27
  card_queue = []
26
28
 
27
29
 
28
- def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
30
+ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
29
31
  should_update_audio=True, ss_time=0, game_line=None):
30
32
  global audio_in_anki, screenshot_in_anki, prev_screenshot_in_anki
31
33
  update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
32
- last_note['fields'][get_config().anki.sentence_audio_field][
33
- 'value'] or get_config().anki.overwrite_audio)
34
+ last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
34
35
  update_picture = (get_config().anki.picture_field and get_config().anki.overwrite_picture) or not \
35
- last_note['fields'][get_config().anki.picture_field][
36
- 'value']
36
+ last_note.get_field(get_config().anki.picture_field)
37
37
 
38
38
  if not reuse_audio:
39
39
  if update_audio:
@@ -52,7 +52,7 @@ def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='
52
52
  image_html = f"<img src=\"{screenshot_in_anki}\">"
53
53
  prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
54
54
 
55
- # note = {'id': last_note['noteId'], 'fields': {}}
55
+ # note = {'id': last_note.noteId, 'fields': {}}
56
56
 
57
57
  if update_audio:
58
58
  note['fields'][get_config().anki.sentence_audio_field] = audio_html
@@ -75,12 +75,12 @@ def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='
75
75
  tags.append(get_current_game().replace(" ", ""))
76
76
  if tags:
77
77
  tag_string = " ".join(tags)
78
- invoke("addTags", tags=tag_string, notes=[last_note['noteId']])
79
- logger.info(f"UPDATED ANKI CARD FOR {last_note['noteId']}")
78
+ invoke("addTags", tags=tag_string, notes=[last_note.noteId])
79
+ logger.info(f"UPDATED ANKI CARD FOR {last_note.noteId}")
80
80
  if get_config().features.notify_on_update:
81
81
  notification.send_notification(tango)
82
82
  if get_config().features.open_anki_edit:
83
- notification.open_anki_card(last_note['noteId'])
83
+ notification.open_anki_card(last_note.noteId)
84
84
 
85
85
  if get_config().audio.external_tool:
86
86
  open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
@@ -94,10 +94,9 @@ def open_audio_in_external(fileabspath, shell=False):
94
94
  subprocess.Popen([get_config().audio.external_tool, fileabspath])
95
95
 
96
96
 
97
- def add_image_to_card(last_note, image_path):
97
+ def add_image_to_card(last_note: AnkiCard, image_path):
98
98
  global screenshot_in_anki
99
- update_picture = get_config().anki.overwrite_picture or not last_note['fields'][get_config().anki.picture_field][
100
- 'value']
99
+ update_picture = get_config().anki.overwrite_picture or not last_note.get_field(get_config().anki.picture_field)
101
100
 
102
101
  if update_picture:
103
102
  screenshot_in_anki = store_media_file(image_path)
@@ -106,34 +105,37 @@ def add_image_to_card(last_note, image_path):
106
105
 
107
106
  image_html = f"<img src=\"{screenshot_in_anki}\">"
108
107
 
109
- note = {'id': last_note['noteId'], 'fields': {}}
108
+ note = {'id': last_note.noteId, 'fields': {}}
110
109
 
111
110
  if update_picture:
112
111
  note['fields'][get_config().anki.picture_field] = image_html
113
112
 
114
113
  invoke("updateNoteFields", note=note)
115
114
 
116
- logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note['noteId']}")
115
+ logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note.noteId}")
117
116
 
118
117
 
119
- def get_initial_card_info(last_note, selected_lines):
120
- note = {'id': last_note['noteId'], 'fields': {}}
118
+ def get_initial_card_info(last_note: AnkiCard, selected_lines):
119
+ note = {'id': last_note.noteId, 'fields': {}}
121
120
  if not last_note:
122
121
  return note
123
122
  game_line = get_text_event(last_note)
124
123
 
125
- if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
126
- lines = gametext.get_line_and_future_lines(last_note)
127
- if lines:
128
- note['fields'][get_config().anki.sentence_field] = "".join(lines)
129
-
130
124
  if selected_lines and get_config().anki.multi_overwrites_sentence:
131
- note['fields'][get_config().anki.sentence_field] = "".join([line.text for line in selected_lines])
125
+ sentences = "".join([line.text for line in selected_lines])
126
+ try:
127
+ sentence_in_anki = last_note.get_field(get_config().anki.sentence_field)
128
+ logger.info(f"Attempting Preserve HTML for multi-line: {remove_html_and_cloze_tags(sentence_in_anki)}, {sentences}, {remove_html_and_cloze_tags(sentence_in_anki) in sentences}")
129
+ sentences = sentences.replace(remove_html_and_cloze_tags(sentence_in_anki), sentence_in_anki)
130
+ except Exception as e:
131
+ logger.debug(f"Error preserving HTML for multi-line: {e}")
132
+ pass
133
+ note['fields'][get_config().anki.sentence_field] = sentences
132
134
 
133
135
  if get_config().anki.previous_sentence_field and game_line.prev and not \
134
- last_note['fields'][get_config().anki.previous_sentence_field]['value']:
136
+ last_note.get_field(get_config().anki.previous_sentence_field):
135
137
  logger.debug(
136
- f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and game_line.prev.text and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
138
+ f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and game_line.prev.text and not last_note.get_field(get_config().anki.previous_sentence_field)}")
137
139
  note['fields'][get_config().anki.previous_sentence_field] = game_line.prev.text
138
140
  return note
139
141
 
@@ -168,11 +170,11 @@ def invoke(action, **params):
168
170
  return response['result']
169
171
 
170
172
 
171
- def get_last_anki_card():
173
+ def get_last_anki_card() -> AnkiCard | dict:
172
174
  added_ids = invoke('findNotes', query='added:1')
173
175
  if not added_ids:
174
176
  return {}
175
- last_note = invoke('notesInfo', notes=[added_ids[-1]])[0]
177
+ last_note = AnkiCard.from_dict(invoke('notesInfo', notes=[added_ids[-1]])[0])
176
178
  return last_note
177
179
 
178
180
 
@@ -213,14 +215,17 @@ def check_for_new_cards():
213
215
  return
214
216
  new_card_ids = current_note_ids - previous_note_ids
215
217
  if new_card_ids and not first_run:
216
- update_new_card()
218
+ try:
219
+ update_new_card()
220
+ except Exception as e:
221
+ logger.error("Error updating new card, Reason:", e)
217
222
  first_run = False
218
223
  previous_note_ids = current_note_ids # Update the list of known notes
219
224
 
220
225
 
221
226
  def update_new_card():
222
227
  last_card = get_last_anki_card()
223
- if not check_tags_for_should_update(last_card):
228
+ if not last_card or not check_tags_for_should_update(last_card):
224
229
  return
225
230
  use_prev_audio = sentence_is_same_as_previous(last_card)
226
231
  logger.info(f"last mined line: {util.get_last_mined_line()}, current sentence: {get_sentence(last_card)}")
@@ -228,8 +233,9 @@ def update_new_card():
228
233
  if get_config().obs.get_game_from_scene:
229
234
  obs.update_current_game()
230
235
  if use_prev_audio:
236
+ lines = get_utility_window().get_selected_lines()
231
237
  with util.lock:
232
- update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
238
+ update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
233
239
  else:
234
240
  logger.info("New card(s) detected! Added to Processing Queue!")
235
241
  card_queue.append(last_card)
@@ -239,20 +245,20 @@ def update_new_card():
239
245
  def sentence_is_same_as_previous(last_card):
240
246
  if not util.get_last_mined_line():
241
247
  return False
242
- return remove_html_tags(get_sentence(last_card)) == remove_html_tags(util.get_last_mined_line())
248
+ return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(util.get_last_mined_line())
243
249
 
244
250
  def get_sentence(card):
245
- return card['fields'][get_config().anki.sentence_field]['value']
251
+ return card.get_field(get_config().anki.sentence_field)
246
252
 
247
253
  def check_tags_for_should_update(last_card):
248
254
  if get_config().anki.tags_to_check:
249
255
  found = False
250
- for tag in last_card['tags']:
256
+ for tag in last_card.tags:
251
257
  if tag.lower() in get_config().anki.tags_to_check:
252
258
  found = True
253
259
  break
254
260
  if not found:
255
- logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card['tags']}, Tags_To_Check {get_config().anki.tags_to_check}")
261
+ logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card.tags}, Tags_To_Check {get_config().anki.tags_to_check}")
256
262
  return found
257
263
  else:
258
264
  return True
@@ -6,6 +6,7 @@ import ttkbootstrap as ttk
6
6
 
7
7
  from GameSentenceMiner import obs, configuration
8
8
  from GameSentenceMiner.configuration import *
9
+ from GameSentenceMiner.downloader.download_tools import download_ocenaudio_if_needed
9
10
  from GameSentenceMiner.electron_messaging import signal_restart_settings_change
10
11
  from GameSentenceMiner.package import get_current_version, get_latest_version
11
12
 
@@ -85,6 +86,7 @@ class ConfigApp:
85
86
  on_save.append(func)
86
87
 
87
88
  def show(self):
89
+ logger.info("Showing Configuration Window")
88
90
  obs.update_current_game()
89
91
  self.reload_settings()
90
92
  if self.window is not None:
@@ -106,6 +108,7 @@ class ConfigApp:
106
108
  use_clipboard=self.clipboard_enabled.get(),
107
109
  websocket_uri=self.websocket_uri.get(),
108
110
  open_config_on_startup=self.open_config_on_startup.get(),
111
+ open_multimine_on_startup=self.open_multimine_on_startup.get(),
109
112
  texthook_replacement_regex=self.texthook_replacement_regex.get()
110
113
  ),
111
114
  paths=Paths(
@@ -160,7 +163,6 @@ class ConfigApp:
160
163
  ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
161
164
  external_tool = self.external_tool.get(),
162
165
  anki_media_collection=self.anki_media_collection.get(),
163
- mining_from_history_grab_all_audio=self.mining_from_history_grab_all_audio.get()
164
166
  ),
165
167
  obs=OBS(
166
168
  enabled=self.obs_enabled.get(),
@@ -184,7 +186,9 @@ class ConfigApp:
184
186
  vosk_url='https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' if self.vosk_url.get() == VOSK_BASE else "https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip",
185
187
  selected_vad_model=self.selected_vad_model.get(),
186
188
  backup_vad_model=self.backup_vad_model.get(),
187
- trim_beginning=self.vad_trim_beginning.get()
189
+ trim_beginning=self.vad_trim_beginning.get(),
190
+ beginning_offset=float(self.vad_beginning_offset.get()),
191
+ add_audio_on_no_results=self.add_audio_on_no_results.get(),
188
192
  )
189
193
  )
190
194
 
@@ -215,6 +219,7 @@ class ConfigApp:
215
219
  signal_restart_settings_change()
216
220
  settings_saved = True
217
221
  configuration.reload_config()
222
+ self.settings = get_config()
218
223
  for func in on_save:
219
224
  func()
220
225
 
@@ -225,8 +230,9 @@ class ConfigApp:
225
230
 
226
231
  self.window.title("GameSentenceMiner Configuration - " + current_config.name)
227
232
 
228
- if current_config.name != self.settings.name:
229
- logger.info("Profile changed, reloading settings.")
233
+
234
+ if current_config.name != self.settings.name or self.settings.config_changed(current_config):
235
+ logger.info("Config changed, reloading settings.")
230
236
  self.master_config = new_config
231
237
  self.settings = current_config
232
238
  for frame in self.notebook.winfo_children():
@@ -296,6 +302,13 @@ class ConfigApp:
296
302
  self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
297
303
  row=self.current_row, column=2)
298
304
 
305
+ ttk.Label(general_frame, text="Open Multimine on Startup:").grid(row=self.current_row, column=0, sticky='W')
306
+ self.open_multimine_on_startup = tk.BooleanVar(value=self.settings.general.open_multimine_on_startup)
307
+ ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup).grid(row=self.current_row, column=1,
308
+ sticky='W')
309
+ self.add_label_and_increment_row(general_frame, "Whether to open multimining window when the script starts.",
310
+ row=self.current_row, column=2)
311
+
299
312
  ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
300
313
  self.current_version = ttk.Label(general_frame, text=get_current_version())
301
314
  self.current_version.grid(row=self.current_row, column=1)
@@ -365,6 +378,17 @@ class ConfigApp:
365
378
  self.add_label_and_increment_row(vad_frame, "Trim the beginning of the audio based on Voice Detection Results",
366
379
  row=self.current_row, column=2)
367
380
 
381
+ ttk.Label(vad_frame, text="Beginning Offset After Beginning Trim:").grid(row=self.current_row, column=0, sticky='W')
382
+ self.vad_beginning_offset = ttk.Entry(vad_frame)
383
+ self.vad_beginning_offset.insert(0, str(self.settings.vad.beginning_offset))
384
+ self.vad_beginning_offset.grid(row=self.current_row, column=1)
385
+ self.add_label_and_increment_row(vad_frame, 'Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning', row=self.current_row, column=2)
386
+
387
+ ttk.Label(vad_frame, text="Add Audio on No Results:").grid(row=self.current_row, column=0, sticky='W')
388
+ self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
389
+ ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results).grid(row=self.current_row, column=1, sticky='W')
390
+ self.add_label_and_increment_row(vad_frame, "Add audio even if no results are found by VAD.", row=self.current_row, column=2)
391
+
368
392
 
369
393
  @new_tab
370
394
  def create_paths_tab(self):
@@ -740,12 +764,14 @@ class ConfigApp:
740
764
  row=self.current_row,
741
765
  column=2)
742
766
 
743
- ttk.Label(audio_frame, text="Grab all Future Audio when Mining from History:").grid(row=self.current_row, column=0, sticky='W')
744
- self.mining_from_history_grab_all_audio = tk.BooleanVar(
745
- value=self.settings.audio.mining_from_history_grab_all_audio)
746
- ttk.Checkbutton(audio_frame, variable=self.mining_from_history_grab_all_audio).grid(row=self.current_row, column=1, sticky='W')
747
- self.add_label_and_increment_row(audio_frame, "When mining from History, this option will allow the script to get all audio from that line to the current time.", row=self.current_row,
748
- column=2)
767
+ ttk.Button(audio_frame, text="Install Ocenaudio", command=self.download_and_install_ocen).grid(
768
+ row=self.current_row, column=0, pady=5)
769
+ ttk.Button(audio_frame, text="Get Anki Media Collection",
770
+ command=self.set_default_anki_media_collection).grid(row=self.current_row, column=1, pady=5)
771
+ self.add_label_and_increment_row(audio_frame,
772
+ "These Two buttons both help set up the External Audio Editing Tool. The first one downloads and installs OcenAudio, a free audio editing software. The second one sets the default Anki media collection path.",
773
+ row=self.current_row,
774
+ column=3)
749
775
 
750
776
  @new_tab
751
777
  def create_obs_tab(self):
@@ -898,6 +924,24 @@ class ConfigApp:
898
924
  def show_error_box(self, title, message):
899
925
  messagebox.showerror(title, message)
900
926
 
927
+ def download_and_install_ocen(self):
928
+ confirm = messagebox.askyesno("Download OcenAudio?", "Would you like to download and install OcenAudio? It is a free audio editing software that works extremely well with GSM.")
929
+ if confirm:
930
+ exe_path = download_ocenaudio_if_needed()
931
+ messagebox.showinfo("OcenAudio Downloaded", f"OcenAudio has been downloaded and installed. You can find it at {exe_path}.")
932
+ self.external_tool.delete(0, tk.END)
933
+ self.external_tool.insert(0, exe_path)
934
+ self.save_settings()
935
+
936
+ def set_default_anki_media_collection(self):
937
+ confirm = messagebox.askyesno("Set Default Anki Media Collection?", "Would you like to set the default Anki media collection path? This will help the script find the media collection for external trimming.\n\nDefault: %APPDATA%/Anki2/User 1/collection.media")
938
+ if confirm:
939
+ default_path = get_default_anki_media_collection_path()
940
+ if default_path != self.settings.audio.external_tool:
941
+ self.anki_media_collection.delete(0, tk.END)
942
+ self.anki_media_collection.insert(0, default_path)
943
+ self.save_settings()
944
+
901
945
 
902
946
  if __name__ == '__main__':
903
947
  root = ttk.Window(themename='darkly')
@@ -42,6 +42,7 @@ class General:
42
42
  use_clipboard: bool = True
43
43
  websocket_uri: str = 'localhost:6677'
44
44
  open_config_on_startup: bool = False
45
+ open_multimine_on_startup: bool = False
45
46
  texthook_replacement_regex: str = ""
46
47
 
47
48
 
@@ -116,7 +117,6 @@ class Audio:
116
117
  ffmpeg_reencode_options: str = ''
117
118
  external_tool: str = ""
118
119
  anki_media_collection: str = ""
119
- mining_from_history_grab_all_audio: bool = False
120
120
 
121
121
 
122
122
  @dataclass_json
@@ -150,6 +150,8 @@ class VAD:
150
150
  selected_vad_model: str = SILERO
151
151
  backup_vad_model: str = OFF
152
152
  trim_beginning: bool = False
153
+ beginning_offset: float = -0.25
154
+ add_audio_on_no_results: bool = False
153
155
 
154
156
 
155
157
  @dataclass_json
@@ -244,9 +246,13 @@ class ProfileConfig:
244
246
  previous.obs.host != self.obs.host,
245
247
  previous.obs.port != self.obs.port
246
248
  ]):
249
+ logger.info("Restart Required for Some Settings that were Changed")
247
250
  return True
248
251
  return False
249
252
 
253
+ def config_changed(self, new: 'ProfileConfig') -> bool:
254
+ return self != new
255
+
250
256
 
251
257
  @dataclass_json
252
258
  @dataclass
@@ -276,6 +282,17 @@ class Config:
276
282
  return self.configs[DEFAULT_CONFIG]
277
283
 
278
284
 
285
+ def get_default_anki_path():
286
+ if platform == 'win32': # Windows
287
+ base_dir = os.getenv('APPDATA')
288
+ else: # macOS and Linux
289
+ base_dir = '~/.local/share/'
290
+ config_dir = os.path.join(base_dir, 'Anki2')
291
+ return config_dir
292
+
293
+ def get_default_anki_media_collection_path():
294
+ return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
295
+
279
296
  def get_app_directory():
280
297
  if platform == 'win32': # Windows
281
298
  appdata_dir = os.getenv('APPDATA')
@@ -377,11 +394,19 @@ def reload_config():
377
394
  def get_master_config():
378
395
  return config_instance
379
396
 
397
+ def save_full_config(config):
398
+ with open(get_config_path(), 'w') as file:
399
+ json.dump(config.to_dict(), file, indent=4)
400
+
401
+ def save_current_config(config):
402
+ global config_instance
403
+ config_instance.set_config_for_profile(config_instance.current_profile, config)
404
+ save_full_config(config_instance)
405
+
380
406
  def switch_profile_and_save(profile_name):
381
407
  global config_instance
382
408
  config_instance.current_profile = profile_name
383
- with open(get_config_path(), 'w') as file:
384
- json.dump(config_instance.to_dict(), file, indent=4)
409
+ save_full_config(config_instance)
385
410
  return config_instance.get_config()
386
411
 
387
412
 
@@ -149,10 +149,39 @@ def download_ffmpeg_if_needed():
149
149
  with source, target:
150
150
  shutil.copyfileobj(source, target)
151
151
  logger.info(f"FFmpeg extracted to {ffmpeg_dir}.")
152
+
153
+ def download_ocenaudio_if_needed():
154
+ ocenaudio_dir = os.path.join(get_app_directory(), 'ocenaudio', 'ocenaudio')
155
+ ocenaudio_exe_path = os.path.join(ocenaudio_dir, 'ocenaudio.exe')
156
+ if os.path.exists(ocenaudio_dir) and os.path.exists(ocenaudio_exe_path):
157
+ logger.info(f"Ocenaudio already installed at {ocenaudio_dir}.")
158
+ return ocenaudio_exe_path
159
+
160
+ if os.path.exists(ocenaudio_dir) and not os.path.exists(ocenaudio_exe_path):
161
+ logger.info("Ocenaudio directory exists but executable is missing. Re-downloading Ocenaudio...")
162
+ shutil.rmtree(ocenaudio_dir)
163
+
164
+ ocenaudio_url = "https://www.ocenaudio.com/downloads/ocenaudio_windows64.zip"
165
+
166
+ download_dir = os.path.join(get_app_directory(), "downloads")
167
+ os.makedirs(download_dir, exist_ok=True)
168
+ ocenaudio_archive = os.path.join(download_dir, "ocenaudio.zip")
169
+
170
+ logger.info(f"Downloading Ocenaudio from {ocenaudio_url}...")
171
+ urllib.request.urlretrieve(ocenaudio_url, ocenaudio_archive)
172
+ logger.info(f"Ocenaudio downloaded. Extracting to {ocenaudio_dir}...")
173
+
174
+ os.makedirs(ocenaudio_dir, exist_ok=True)
175
+ with zipfile.ZipFile(ocenaudio_archive, 'r') as zip_ref:
176
+ zip_ref.extractall(ocenaudio_dir)
177
+
178
+ logger.info(f"Ocenaudio extracted to {ocenaudio_dir}.")
179
+ return ocenaudio_exe_path
180
+
152
181
  def main():
153
- # Run dependency checks
154
182
  download_obs_if_needed()
155
183
  download_ffmpeg_if_needed()
184
+ download_ocenaudio_if_needed()
156
185
 
157
186
  if __name__ == "__main__":
158
187
  main()
@@ -200,7 +200,7 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
200
200
  ffmpeg_command = ffmpeg_base_command_list + [
201
201
  "-i", untrimmed_audio,
202
202
  "-ss", start_trim_time]
203
- if next_line and next_line > game_line.time and not get_config().audio.mining_from_history_grab_all_audio:
203
+ if next_line and next_line > game_line.time:
204
204
  end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
205
205
  hours, remainder = divmod(end_total_seconds, 3600)
206
206
  minutes, seconds = divmod(remainder, 60)
@@ -290,12 +290,14 @@ def convert_audio_to_wav(input_audio, output_wav):
290
290
  def trim_audio(input_audio, start_time, end_time, output_audio):
291
291
  command = ffmpeg_base_command_list.copy()
292
292
 
293
+ command.extend(['-i', input_audio])
294
+
293
295
  if get_config().vad.trim_beginning and start_time > 0:
296
+ logger.info(f"trimming beginning to {start_time}")
294
297
  command.extend(['-ss', f"{start_time:.2f}"])
295
298
 
296
299
  command.extend([
297
300
  '-to', f"{end_time:.2f}",
298
- '-i', input_audio,
299
301
  '-c', 'copy',
300
302
  output_audio
301
303
  ])
@@ -3,17 +3,18 @@ import re
3
3
  import threading
4
4
  import time
5
5
  from datetime import datetime
6
- from typing import Callable
7
6
 
8
7
  import pyperclip
9
8
  import websockets
10
9
 
11
10
  from GameSentenceMiner import util
11
+ from GameSentenceMiner.model import AnkiCard
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.configuration import get_config, logger
14
- from GameSentenceMiner.util import remove_html_tags
14
+ from GameSentenceMiner.util import remove_html_and_cloze_tags
15
15
  from difflib import SequenceMatcher
16
16
 
17
+ from GameSentenceMiner.utility_gui import get_utility_window
17
18
 
18
19
  initial_time = datetime.now()
19
20
  current_line = ''
@@ -22,7 +23,6 @@ current_line_time = datetime.now()
22
23
 
23
24
  reconnecting = False
24
25
  websocket_connected = False
25
- multi_mine_event_bus: Callable[[str, datetime], None] = None
26
26
 
27
27
  @dataclass
28
28
  class GameLine:
@@ -89,14 +89,17 @@ class ClipboardMonitor(threading.Thread):
89
89
  # Initial clipboard content
90
90
  current_line = pyperclip.paste()
91
91
 
92
+ skip_next_clipboard = False
92
93
  while True:
93
94
  if websocket_connected:
94
95
  time.sleep(1)
96
+ skip_next_clipboard = True
95
97
  continue
96
98
  current_clipboard = pyperclip.paste()
97
99
 
98
- if current_clipboard != current_line:
100
+ if current_clipboard != current_line and not skip_next_clipboard:
99
101
  handle_new_text_event(current_clipboard)
102
+ skip_next_clipboard = False
100
103
 
101
104
  time.sleep(0.05)
102
105
 
@@ -138,7 +141,7 @@ def handle_new_text_event(current_clipboard):
138
141
  logger.info(f"Line Received: {current_line_after_regex}")
139
142
  current_line_time = datetime.now()
140
143
  line_history.add_line(current_line_after_regex)
141
- multi_mine_event_bus(line_history[-1])
144
+ get_utility_window().add_text(line_history[-1])
142
145
 
143
146
 
144
147
  def reset_line_hotkey_pressed():
@@ -152,9 +155,7 @@ def run_websocket_listener():
152
155
  asyncio.run(listen_websocket())
153
156
 
154
157
 
155
- def start_text_monitor(send_to_mine_event_bus):
156
- global multi_mine_event_bus
157
- multi_mine_event_bus = send_to_mine_event_bus
158
+ def start_text_monitor():
158
159
  if get_config().general.use_websocket:
159
160
  threading.Thread(target=run_websocket_listener, daemon=True).start()
160
161
  if get_config().general.use_clipboard:
@@ -169,24 +170,26 @@ def similar(a, b):
169
170
  def one_contains_the_other(a, b):
170
171
  return a in b or b in a
171
172
 
173
+ def lines_match(a, b):
174
+ similarity = similar(a, b)
175
+ logger.debug(f"Comparing: {a} with {b} - Similarity: {similarity}, Or One contains the other: {one_contains_the_other(a, b)}")
176
+ return similar(a, b) >= 0.60 or one_contains_the_other(a, b)
172
177
 
173
178
  def get_text_event(last_note) -> GameLine:
174
179
  lines = line_history.values
175
180
 
176
- if not last_note:
177
- return lines[-1]
178
-
179
181
  if not lines:
180
182
  raise Exception("No lines in history. Text is required from either clipboard or websocket for GSM to work. Please check your setup/config.")
181
183
 
182
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
184
+ if not last_note:
185
+ return lines[-1]
186
+
187
+ sentence = last_note.get_field(get_config().anki.sentence_field)
183
188
  if not sentence:
184
189
  return lines[-1]
185
190
 
186
191
  for line in reversed(lines):
187
- similarity = similar(remove_html_tags(sentence), line.text)
188
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
189
- if similarity >= 0.60 or one_contains_the_other(line.text, remove_html_tags(sentence)):
192
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
190
193
  return line
191
194
 
192
195
  logger.debug("Couldn't find a match in history, using last event")
@@ -197,28 +200,25 @@ def get_line_and_future_lines(last_note):
197
200
  if not last_note:
198
201
  return []
199
202
 
200
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
203
+ sentence = last_note.get_field(get_config().anki.sentence_field)
201
204
  found_lines = []
202
205
  if sentence:
203
206
  found = False
204
207
  for line in line_history.values:
205
- similarity = similar(remove_html_tags(sentence), line.text)
206
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
207
208
  if found:
208
209
  found_lines.append(line.text)
209
- if similarity >= 0.60 or one_contains_the_other(line.text, remove_html_tags(sentence)): # 80% similarity threshold
210
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
210
211
  found = True
211
212
  found_lines.append(line.text)
212
213
  return found_lines
213
214
 
214
- def get_mined_line(last_note, lines):
215
+ def get_mined_line(last_note: AnkiCard, lines):
215
216
  if not last_note:
216
217
  return lines[-1]
217
218
 
218
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
219
+ sentence = last_note.get_field(get_config().anki.sentence_field)
219
220
  for line in lines:
220
- similarity = similar(remove_html_tags(sentence), line.text)
221
- if similarity >= 0.60 or one_contains_the_other(line.text, remove_html_tags(sentence)):
221
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
222
222
  return line
223
223
  return lines[-1]
224
224
 
GameSentenceMiner/gsm.py CHANGED
@@ -18,13 +18,13 @@ from GameSentenceMiner import gametext
18
18
  from GameSentenceMiner import notification
19
19
  from GameSentenceMiner import obs
20
20
  from GameSentenceMiner import util
21
- from GameSentenceMiner import utility_gui
22
21
  from GameSentenceMiner.configuration import *
23
22
  from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
24
23
  from GameSentenceMiner.electron_messaging import signal_restart_settings_change
25
24
  from GameSentenceMiner.ffmpeg import get_audio_and_trim
26
25
  from GameSentenceMiner.gametext import get_text_event, get_mined_line
27
26
  from GameSentenceMiner.util import *
27
+ from GameSentenceMiner.utility_gui import init_utility_window, get_utility_window
28
28
  from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
29
29
 
30
30
  if is_windows():
@@ -33,7 +33,6 @@ if is_windows():
33
33
  obs_process = None
34
34
  procs_to_close = []
35
35
  settings_window: config_gui.ConfigApp = None
36
- utility_window: utility_gui.UtilityApp = None
37
36
  obs_paused = False
38
37
  icon: Icon
39
38
  menu: Menu
@@ -91,7 +90,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
91
90
  if get_config().anki.update_anki:
92
91
  last_note = anki.get_last_anki_card()
93
92
  if get_config().features.backfill_audio:
94
- last_note = anki.get_cards_by_sentence(gametext.current_line)
93
+ last_note = anki.get_cards_by_sentence(gametext.current_line_after_regex)
95
94
  line_cutoff = None
96
95
  start_line = None
97
96
  mined_line = get_text_event(last_note)
@@ -100,21 +99,21 @@ class VideoToAudioHandler(FileSystemEventHandler):
100
99
  if mined_line.next:
101
100
  line_cutoff = mined_line.next.time
102
101
 
103
- if utility_window.lines_selected():
104
- lines = utility_window.get_selected_lines()
102
+ if get_utility_window().lines_selected():
103
+ lines = get_utility_window().get_selected_lines()
105
104
  start_line = lines[0]
106
105
  mined_line = get_mined_line(last_note, lines)
107
- line_cutoff = utility_window.get_next_line_timing()
106
+ line_cutoff = get_utility_window().get_next_line_timing()
108
107
 
109
108
  ss_timing = 0
110
109
  if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
111
110
  ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line)
112
111
  if last_note:
113
- logger.debug(json.dumps(last_note))
112
+ logger.debug(last_note.to_json())
114
113
 
115
- note = anki.get_initial_card_info(last_note, utility_window.get_selected_lines())
114
+ note = anki.get_initial_card_info(last_note, get_utility_window().get_selected_lines())
116
115
 
117
- tango = last_note['fields'][get_config().anki.word_field]['value'] if last_note else ''
116
+ tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
118
117
 
119
118
  if get_config().anki.sentence_audio_field:
120
119
  logger.debug("Attempting to get audio from video")
@@ -127,26 +126,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
127
126
  should_update_audio = False
128
127
  vad_trimmed_audio = ""
129
128
  logger.info("No SentenceAudio Field in config, skipping audio processing!")
130
- # Only update sentenceaudio if it's not present. Want to avoid accidentally overwriting sentence audio
131
- try:
132
- if get_config().anki.update_anki and last_note:
133
- anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
134
- tango=tango,
135
- should_update_audio=should_update_audio,
136
- ss_time=ss_timing,
137
- game_line=start_line)
138
- elif get_config().features.notify_on_update and should_update_audio:
139
- notification.send_audio_generated_notification(vad_trimmed_audio)
140
- except Exception as e:
141
- logger.exception(f"Card failed to update! Maybe it was removed? {e}")
129
+ if get_config().anki.update_anki and last_note:
130
+ anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
131
+ tango=tango,
132
+ should_update_audio=should_update_audio,
133
+ ss_time=ss_timing,
134
+ game_line=start_line)
135
+ elif get_config().features.notify_on_update and should_update_audio:
136
+ notification.send_audio_generated_notification(vad_trimmed_audio)
142
137
  except Exception as e:
143
- logger.exception(f"Some error was hit catching to allow further work to be done: {e}")
144
- # settings_window.show_error_box("Error", f"Some error was hit, check logs for more info: {e}")
138
+ logger.error(f"Failed Processing and/or adding to Anki: Reason {e}")
139
+ logger.debug(f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
140
+ notification.send_error_no_anki_update()
145
141
  if get_config().paths.remove_video and os.path.exists(video_path):
146
142
  os.remove(video_path) # Optionally remove the video after conversion
147
143
  if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
148
144
  os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
149
- utility_window.reset_checkboxes()
145
+ get_utility_window().reset_checkboxes()
150
146
 
151
147
  @staticmethod
152
148
  def get_audio(game_line, next_line_time, video_path):
@@ -177,6 +173,10 @@ class VideoToAudioHandler(FileSystemEventHandler):
177
173
  case configuration.WHISPER:
178
174
  should_update_audio = whisper_helper.process_audio_with_whisper(trimmed_audio,
179
175
  vad_trimmed_audio)
176
+ if not should_update_audio and get_config().vad.add_audio_on_no_results:
177
+ logger.info("No voice activity detected, using full audio.")
178
+ vad_trimmed_audio = trimmed_audio
179
+ should_update_audio = True
180
180
  if get_config().audio.ffmpeg_reencode_options and os.path.exists(vad_trimmed_audio):
181
181
  ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
182
182
  get_config().audio.ffmpeg_reencode_options)
@@ -196,7 +196,7 @@ def initialize(reloading=False):
196
196
  obs_process = obs.start_obs()
197
197
  obs.connect_to_obs(start_replay=True)
198
198
  anki.start_monitoring_anki()
199
- gametext.start_text_monitor(utility_window.add_text)
199
+ gametext.start_text_monitor()
200
200
  os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
201
201
  os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
202
202
  os.makedirs(get_config().paths.audio_destination, exist_ok=True)
@@ -237,9 +237,9 @@ def get_screenshot():
237
237
  last_note = anki.get_cards_by_sentence(gametext.current_line)
238
238
  if last_note:
239
239
  anki.add_image_to_card(last_note, encoded_image)
240
- notification.send_screenshot_updated(last_note['fields'][get_config().anki.word_field]['value'])
240
+ notification.send_screenshot_updated(last_note.get_field(get_config().anki.word_field))
241
241
  if get_config().features.open_anki_edit:
242
- notification.open_anki_card(last_note['noteId'])
242
+ notification.open_anki_card(last_note.noteId)
243
243
  else:
244
244
  notification.send_screenshot_saved(encoded_image)
245
245
  else:
@@ -272,7 +272,7 @@ def open_settings():
272
272
 
273
273
  def open_multimine():
274
274
  obs.update_current_game()
275
- utility_window.show()
275
+ get_utility_window().show()
276
276
 
277
277
 
278
278
  def open_log():
@@ -337,11 +337,13 @@ def switch_profile(icon, item):
337
337
  logger.error("You cannot switch to the currently active profile!")
338
338
  return
339
339
  logger.info(f"Switching to profile: {item.text}")
340
+ prev_config = get_config()
340
341
  get_master_config().current_profile = item.text
341
342
  switch_profile_and_save(item.text)
342
343
  settings_window.reload_settings()
343
344
  update_icon()
344
- signal_restart_settings_change()
345
+ if get_config().restart_required(prev_config):
346
+ signal_restart_settings_change()
345
347
 
346
348
 
347
349
  def run_tray():
@@ -459,12 +461,12 @@ def handle_exit():
459
461
 
460
462
 
461
463
  def main(reloading=False, do_config_input=True):
462
- global root, settings_window, utility_window
464
+ global root, settings_window
463
465
  logger.info("Script started.")
464
466
  util.run_new_thread(check_for_stdin)
465
467
  root = ttk.Window(themename='darkly')
466
468
  settings_window = config_gui.ConfigApp(root)
467
- utility_window = utility_gui.UtilityApp(root)
469
+ init_utility_window(root)
468
470
  initialize(reloading)
469
471
  util.run_new_thread(run_tray)
470
472
  initial_checks()
@@ -486,6 +488,8 @@ def main(reloading=False, do_config_input=True):
486
488
  try:
487
489
  if get_config().general.open_config_on_startup:
488
490
  root.after(0, settings_window.show)
491
+ if get_config().general.open_multimine_on_startup:
492
+ root.after(0, get_utility_window().show)
489
493
  settings_window.add_save_hook(update_icon)
490
494
  settings_window.on_exit = exit_program
491
495
  root.mainloop()
@@ -3,6 +3,8 @@ from typing import Optional, List
3
3
 
4
4
  from dataclasses_json import dataclass_json
5
5
 
6
+ from GameSentenceMiner.configuration import get_config, logger, save_current_config
7
+
6
8
 
7
9
  # OBS
8
10
  @dataclass_json
@@ -82,3 +84,73 @@ class SceneItemsResponse:
82
84
  # videoActive: bool
83
85
  # videoShowing: bool
84
86
 
87
+ @dataclass_json
88
+ @dataclass
89
+ class AnkiCard:
90
+ noteId: int
91
+ profile: str
92
+ tags: list[str]
93
+ fields: dict[str, dict[str, str]]
94
+ modelName: str
95
+ mod: int
96
+ cards: list[int]
97
+ alternatives = {
98
+ "word_field": ["Front", "Word", "TargetWord", "Expression"],
99
+ "sentence_field": ["Example", "Context", "Back", "Sentence"],
100
+ "picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
101
+ "sentence_audio_field": ["SentenceAudio"]
102
+ }
103
+
104
+ def get_field(self, field_name: str) -> str:
105
+ if self.has_field(field_name):
106
+ return self.fields[field_name]['value']
107
+ else:
108
+ raise ValueError(f"Field '{field_name}' not found in AnkiCard. Please make sure your Anki Field Settings in GSM Match your fields in your Anki Note!")
109
+
110
+ def has_field (self, field_name: str) -> bool:
111
+ return field_name in self.fields
112
+
113
+ def __post_init__(self):
114
+ config = get_config()
115
+ changes_found = False
116
+ if not self.has_field(config.anki.word_field):
117
+ found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
118
+ if found_alternative_field:
119
+ logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
120
+ config.anki.word_field = field
121
+ changes_found = True
122
+
123
+ if not self.has_field(config.anki.sentence_field):
124
+ found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
125
+ if found_alternative_field:
126
+ logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
127
+ config.anki.sentence_field = field
128
+ changes_found = True
129
+
130
+ if not self.has_field(config.anki.picture_field):
131
+ found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
132
+ if found_alternative_field:
133
+ logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
134
+ config.anki.picture_field = field
135
+ changes_found = True
136
+
137
+ if not self.has_field(config.anki.sentence_audio_field):
138
+ found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
139
+ if found_alternative_field:
140
+ logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
141
+ config.anki.sentence_audio_field = field
142
+ changes_found = True
143
+
144
+ if changes_found:
145
+ save_current_config(config)
146
+
147
+ def find_field(self, field, field_type):
148
+ if field in self.fields:
149
+ return False, field
150
+
151
+ for alt_field in self.alternatives[field_type]:
152
+ for key in self.fields:
153
+ if alt_field.lower() == key.lower():
154
+ return True, key
155
+
156
+ return False, None
@@ -69,3 +69,12 @@ def send_check_obs_notification(reason):
69
69
  app_name="GameSentenceMiner",
70
70
  timeout=5 # Notification disappears after 5 seconds
71
71
  )
72
+
73
+
74
+ def send_error_no_anki_update():
75
+ notification.notify(
76
+ title="Error",
77
+ message=f"Anki Card not updated, Check Console for Reason!",
78
+ app_name="GameSentenceMiner",
79
+ timeout=5 # Notification disappears after 5 seconds
80
+ )
GameSentenceMiner/util.py CHANGED
@@ -143,6 +143,6 @@ def is_windows():
143
143
  # else:
144
144
  # return subprocess.run(command, shell=shell, input=input, capture_output=capture_output, timeout=timeout,
145
145
  # check=check, **kwargs)
146
- def remove_html_tags(text):
147
- clean_text = re.sub(r'<.*?>', '', text)
148
- return clean_text
146
+ def remove_html_and_cloze_tags(text):
147
+ text = re.sub(r'<.*?>', '', re.sub(r'{{c\d+::(.*?)(::.*?)?}}', r'\1', text))
148
+ return text
@@ -1,13 +1,14 @@
1
+ import json
2
+ import os
1
3
  import tkinter as tk
2
4
  from tkinter import ttk
3
5
 
4
- from GameSentenceMiner.configuration import logger
6
+ from GameSentenceMiner.configuration import logger, get_app_directory
5
7
 
6
8
 
7
9
  class UtilityApp:
8
10
  def __init__(self, root):
9
11
  self.root = root
10
-
11
12
  self.items = []
12
13
  self.checkboxes = []
13
14
  self.multi_mine_window = None # Store the multi-mine window reference
@@ -15,32 +16,60 @@ class UtilityApp:
15
16
 
16
17
  style = ttk.Style()
17
18
  style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
19
+ self.config_file = os.path.join(get_app_directory(), "multi-mine-window-config.json")
20
+ self.load_window_config()
21
+
22
+
23
+ def save_window_config(self):
24
+ if self.multi_mine_window:
25
+ config = {
26
+ "x": self.multi_mine_window.winfo_x(),
27
+ "y": self.multi_mine_window.winfo_y(),
28
+ "width": self.multi_mine_window.winfo_width(),
29
+ "height": self.multi_mine_window.winfo_height()
30
+ }
31
+ print(config)
32
+ with open(self.config_file, "w") as f:
33
+ json.dump(config, f)
34
+
35
+ def load_window_config(self):
36
+ if os.path.exists(self.config_file):
37
+ with open(self.config_file, "r") as f:
38
+ config = json.load(f)
39
+ self.window_x = config.get("x", 100)
40
+ self.window_y = config.get("y", 100)
41
+ self.window_width = config.get("width", 800)
42
+ self.window_height = config.get("height", 400)
43
+ else:
44
+ self.window_x = 100
45
+ self.window_y = 100
46
+ self.window_width = 800
47
+ self.window_height = 400
18
48
 
19
- # def show(self):
20
- # if self.multi_mine_window is None or not tk.Toplevel.winfo_exists(self.multi_mine_window):
21
- # self.multi_mine_window = tk.Toplevel(self.root)
22
- # self.multi_mine_window.title("Multi-Mine Window")
23
- # self.update_multi_mine_window()
24
- #
25
49
  def show(self):
26
- """ Open the multi-mine window only if it doesn't exist. """
27
50
  if not self.multi_mine_window or not tk.Toplevel.winfo_exists(self.multi_mine_window):
28
- logger.info("opening multi-mine_window")
29
51
  self.multi_mine_window = tk.Toplevel(self.root)
30
52
  self.multi_mine_window.title("Multi Mine Window")
31
53
 
32
- self.multi_mine_window.minsize(800, 400) # Set a minimum size to prevent shrinking too
54
+ self.multi_mine_window.geometry(f"{self.window_width}x{self.window_height}+{self.window_x}+{self.window_y}")
55
+
56
+ self.multi_mine_window.minsize(800, 400)
33
57
 
34
58
  self.checkbox_frame = ttk.Frame(self.multi_mine_window)
35
59
  self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
36
60
 
37
- # Add existing items
38
61
  for line, var in self.items:
39
62
  self.add_checkbox_to_gui(line, var)
63
+
64
+ self.multi_mine_window.protocol("WM_DELETE_WINDOW", self.on_close)
40
65
  else:
41
66
  self.multi_mine_window.deiconify()
42
67
  self.multi_mine_window.lift()
43
68
 
69
+ def on_close(self):
70
+ self.save_window_config()
71
+ self.multi_mine_window.withdraw()
72
+
44
73
  def add_text(self, line):
45
74
  if line.text:
46
75
  var = tk.BooleanVar()
@@ -121,6 +150,16 @@ class UtilityApp:
121
150
  # for checkbox in self.checkboxes:
122
151
  # checkbox.set(False)
123
152
 
153
+ def init_utility_window(root):
154
+ global utility_window
155
+ utility_window = UtilityApp(root)
156
+ return utility_window
157
+
158
+ def get_utility_window():
159
+ return utility_window
160
+
161
+ utility_window: UtilityApp = None
162
+
124
163
 
125
164
  if __name__ == "__main__":
126
165
  root = tk.Tk()
@@ -38,6 +38,6 @@ def process_audio_with_silero(input_audio, output_audio):
38
38
  end_time = voice_activity[-1]['end'] if voice_activity else 0
39
39
 
40
40
  # Trim the audio using FFmpeg
41
- ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
41
+ ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
42
42
  logger.info(f"Trimmed audio saved to: {output_audio}")
43
43
  return True
@@ -140,7 +140,7 @@ def process_audio_with_vosk(input_audio, output_audio):
140
140
  logger.info(f"Trimmed End of Audio to {end_time} seconds:")
141
141
 
142
142
  # Trim the audio using FFmpeg
143
- ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
143
+ ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
144
144
  logger.info(f"Trimmed audio saved to: {output_audio}")
145
145
  return True
146
146
 
@@ -87,7 +87,7 @@ def process_audio_with_whisper(input_audio, output_audio):
87
87
  logger.info(f"Trimmed End of Audio to {end_time} seconds:")
88
88
 
89
89
  # Trim the audio using FFmpeg
90
- ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
90
+ ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
91
91
  logger.info(f"Trimmed audio saved to: {output_audio}")
92
92
  return True
93
93
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.4.12
3
+ Version: 2.5.1
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -32,6 +32,7 @@ Requires-Dist: dataclasses_json~=0.6.7
32
32
  Requires-Dist: numpy
33
33
  Requires-Dist: pystray
34
34
  Requires-Dist: pywin32; sys_platform == "win32"
35
+ Dynamic: license-file
35
36
 
36
37
  # Game Sentence Miner
37
38
 
@@ -0,0 +1,27 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=_jr6q3tOSP-7YGaY9cVz5v3508T_V2JINJjDYbp1YS0,11495
3
+ GameSentenceMiner/config_gui.py,sha256=PzzLX-OwK71U5JSFaxup0ec0oWBWaF6AeCXs0m13HK0,56762
4
+ GameSentenceMiner/configuration.py,sha256=kyvNCkZSZcqXbGjak8lb_GyhhjN8tIfb7eEfusz_M8A,16038
5
+ GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
6
+ GameSentenceMiner/ffmpeg.py,sha256=2dwKbKxw_9sUXud67pPAx_6dGd1j-D99CdqLFVtbhSk,11479
7
+ GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
8
+ GameSentenceMiner/gsm.py,sha256=cWHJwWHyKsdzo0TXTXREG549MUtlQLyect9S37UK56g,20309
9
+ GameSentenceMiner/model.py,sha256=WYLjIS9IEPxaLPwG5c-edfcfBKYzBFpdR3V3Da8MuLg,5277
10
+ GameSentenceMiner/notification.py,sha256=WeFodBsshhbOagcEfjAJ3kxjUGvBuUAQKEJ8Zf0YO04,2267
11
+ GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
12
+ GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
13
+ GameSentenceMiner/util.py,sha256=nmp2cmuBN0Azoc91f3zN_dZSh4nK9z21VTfpIbn8Tk4,4761
14
+ GameSentenceMiner/utility_gui.py,sha256=VYuY8Br47AFak_zOVx9R1oUCBvoRjonDyE0oE6FVwJ0,5742
15
+ GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
16
+ GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
18
+ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
20
+ GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
21
+ GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
22
+ gamesentenceminer-2.5.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
+ gamesentenceminer-2.5.1.dist-info/METADATA,sha256=OjwqqaJ_DG7FR6OLc3z1XlUX6WH6SqsOc4EDXUjbzbQ,5409
24
+ gamesentenceminer-2.5.1.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
25
+ gamesentenceminer-2.5.1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
26
+ gamesentenceminer-2.5.1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
27
+ gamesentenceminer-2.5.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (77.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,27 +0,0 @@
1
- GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=YSz5gUTsKOdbogwHKtgFM7v7pREjdAwl7A0Wa_CXnKg,10918
3
- GameSentenceMiner/config_gui.py,sha256=s0U0Sj_WvrJ8GecSyj33tWMh-OyOcZ5yY1ddEc1cgwg,53456
4
- GameSentenceMiner/configuration.py,sha256=_VxzRHCJPW2iJTW6gheD1d6-oaUbUQQnLsIiHjgjNcY,15186
5
- GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
6
- GameSentenceMiner/ffmpeg.py,sha256=vkRvhsuXCL8-tGynobdLBnw4qNHUhTC33ITCCnjfZLM,11468
7
- GameSentenceMiner/gametext.py,sha256=YXjaZF130lkzU-I2dpZ5HoNrj3GSCG-7X9B04IFnetI,7551
8
- GameSentenceMiner/gsm.py,sha256=E9Hpbyzrv8FEFGBs-4Hf60m3sk0ps3OQdvB1n42AhRU,20164
9
- GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
10
- GameSentenceMiner/notification.py,sha256=WBaQWoPNhW4XqdPBUmxPBgjk0ngzH_4v9zMQ-XQAKC8,2010
11
- GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
12
- GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
13
- GameSentenceMiner/util.py,sha256=MITweiFYaefWQF5nR8tZ9yE6vd_b-fLuP0MP1Y1U4K0,4720
14
- GameSentenceMiner/utility_gui.py,sha256=EtQUnCgTTdzKJE0iCJiHjjc_c6tc7JtI09LRg4_iy8Y,4555
15
- GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
16
- GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- GameSentenceMiner/downloader/download_tools.py,sha256=1x57nzktdlsUruTJxCPNpmHDDlhZ589NwRyAMP0Pq7E,6818
18
- GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
20
- GameSentenceMiner/vad/vosk_helper.py,sha256=HifeXKbEMrs81ZuuGxS67yAghu8TMXUP6Oan9i9dTxw,5938
21
- GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
22
- gamesentenceminer-2.4.12.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
- gamesentenceminer-2.4.12.dist-info/METADATA,sha256=fqK1YCT402ptWC4idyZaIqjbCQ38jGDAZGJCuVJ1TAE,5388
24
- gamesentenceminer-2.4.12.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
25
- gamesentenceminer-2.4.12.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
26
- gamesentenceminer-2.4.12.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
27
- gamesentenceminer-2.4.12.dist-info/RECORD,,