GameSentenceMiner 2.4.10__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GameSentenceMiner/anki.py 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)}")
@@ -229,7 +234,7 @@ def update_new_card():
229
234
  obs.update_current_game()
230
235
  if use_prev_audio:
231
236
  with util.lock:
232
- update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
237
+ update_anki_card(last_card, note=get_initial_card_info(last_card, get_utility_window().get_selected_lines()), reuse_audio=True)
233
238
  else:
234
239
  logger.info("New card(s) detected! Added to Processing Queue!")
235
240
  card_queue.append(last_card)
@@ -239,20 +244,20 @@ def update_new_card():
239
244
  def sentence_is_same_as_previous(last_card):
240
245
  if not util.get_last_mined_line():
241
246
  return False
242
- return remove_html_tags(get_sentence(last_card)) == remove_html_tags(util.get_last_mined_line())
247
+ return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(util.get_last_mined_line())
243
248
 
244
249
  def get_sentence(card):
245
- return card['fields'][get_config().anki.sentence_field]['value']
250
+ return card.get_field(get_config().anki.sentence_field)
246
251
 
247
252
  def check_tags_for_should_update(last_card):
248
253
  if get_config().anki.tags_to_check:
249
254
  found = False
250
- for tag in last_card['tags']:
255
+ for tag in last_card.tags:
251
256
  if tag.lower() in get_config().anki.tags_to_check:
252
257
  found = True
253
258
  break
254
259
  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}")
260
+ logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card.tags}, Tags_To_Check {get_config().anki.tags_to_check}")
256
261
  return found
257
262
  else:
258
263
  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:
@@ -103,8 +105,10 @@ class ConfigApp:
103
105
  config = ProfileConfig(
104
106
  general=General(
105
107
  use_websocket=self.websocket_enabled.get(),
108
+ use_clipboard=self.clipboard_enabled.get(),
106
109
  websocket_uri=self.websocket_uri.get(),
107
110
  open_config_on_startup=self.open_config_on_startup.get(),
111
+ open_multimine_on_startup=self.open_multimine_on_startup.get(),
108
112
  texthook_replacement_regex=self.texthook_replacement_regex.get()
109
113
  ),
110
114
  paths=Paths(
@@ -159,7 +163,6 @@ class ConfigApp:
159
163
  ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
160
164
  external_tool = self.external_tool.get(),
161
165
  anki_media_collection=self.anki_media_collection.get(),
162
- mining_from_history_grab_all_audio=self.mining_from_history_grab_all_audio.get()
163
166
  ),
164
167
  obs=OBS(
165
168
  enabled=self.obs_enabled.get(),
@@ -183,7 +186,9 @@ class ConfigApp:
183
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",
184
187
  selected_vad_model=self.selected_vad_model.get(),
185
188
  backup_vad_model=self.backup_vad_model.get(),
186
- 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(),
187
192
  )
188
193
  )
189
194
 
@@ -191,6 +196,10 @@ class ConfigApp:
191
196
  messagebox.showerror("Configuration Error", "Cannot have Full Auto and Backfill mode on at the same time! Note: Backfill is a very niche workflow.")
192
197
  return
193
198
 
199
+ if not config.general.use_websocket and not config.general.use_clipboard:
200
+ messagebox.showerror("Configuration Error", "Cannot have both Clipboard and Websocket Disabled.")
201
+ return
202
+
194
203
  current_profile = self.profile_combobox.get()
195
204
  prev_config = self.master_config.get_config()
196
205
  if profile_change:
@@ -210,6 +219,7 @@ class ConfigApp:
210
219
  signal_restart_settings_change()
211
220
  settings_saved = True
212
221
  configuration.reload_config()
222
+ self.settings = get_config()
213
223
  for func in on_save:
214
224
  func()
215
225
 
@@ -220,8 +230,9 @@ class ConfigApp:
220
230
 
221
231
  self.window.title("GameSentenceMiner Configuration - " + current_config.name)
222
232
 
223
- if current_config.name != self.settings.name:
224
- 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.")
225
236
  self.master_config = new_config
226
237
  self.settings = current_config
227
238
  for frame in self.notebook.winfo_children():
@@ -256,13 +267,20 @@ class ConfigApp:
256
267
  general_frame = ttk.Frame(self.notebook)
257
268
  self.notebook.add(general_frame, text='General')
258
269
 
259
- ttk.Label(general_frame, text="Websocket Enabled (Clipboard Disabled):").grid(row=self.current_row, column=0, sticky='W')
270
+ ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
260
271
  self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
261
272
  ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
262
273
  sticky='W')
263
274
  self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor. RESTART REQUIRED.",
264
275
  row=self.current_row, column=2)
265
276
 
277
+ ttk.Label(general_frame, text="Clipboard Enabled:").grid(row=self.current_row, column=0, sticky='W')
278
+ self.clipboard_enabled = tk.BooleanVar(value=self.settings.general.use_clipboard)
279
+ ttk.Checkbutton(general_frame, variable=self.clipboard_enabled).grid(row=self.current_row, column=1,
280
+ sticky='W')
281
+ self.add_label_and_increment_row(general_frame, "Enable to allow GSM to see clipboard for text and line timing.",
282
+ row=self.current_row, column=2)
283
+
266
284
  ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
267
285
  self.websocket_uri = ttk.Entry(general_frame)
268
286
  self.websocket_uri.insert(0, self.settings.general.websocket_uri)
@@ -284,6 +302,13 @@ class ConfigApp:
284
302
  self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
285
303
  row=self.current_row, column=2)
286
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
+
287
312
  ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
288
313
  self.current_version = ttk.Label(general_frame, text=get_current_version())
289
314
  self.current_version.grid(row=self.current_row, column=1)
@@ -353,6 +378,17 @@ class ConfigApp:
353
378
  self.add_label_and_increment_row(vad_frame, "Trim the beginning of the audio based on Voice Detection Results",
354
379
  row=self.current_row, column=2)
355
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
+
356
392
 
357
393
  @new_tab
358
394
  def create_paths_tab(self):
@@ -728,12 +764,14 @@ class ConfigApp:
728
764
  row=self.current_row,
729
765
  column=2)
730
766
 
731
- ttk.Label(audio_frame, text="Grab all Future Audio when Mining from History:").grid(row=self.current_row, column=0, sticky='W')
732
- self.mining_from_history_grab_all_audio = tk.BooleanVar(
733
- value=self.settings.audio.mining_from_history_grab_all_audio)
734
- ttk.Checkbutton(audio_frame, variable=self.mining_from_history_grab_all_audio).grid(row=self.current_row, column=1, sticky='W')
735
- 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,
736
- 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)
737
775
 
738
776
  @new_tab
739
777
  def create_obs_tab(self):
@@ -886,6 +924,24 @@ class ConfigApp:
886
924
  def show_error_box(self, title, message):
887
925
  messagebox.showerror(title, message)
888
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
+
889
945
 
890
946
  if __name__ == '__main__':
891
947
  root = ttk.Window(themename='darkly')
@@ -39,8 +39,10 @@ current_game = ''
39
39
  @dataclass
40
40
  class General:
41
41
  use_websocket: bool = True
42
+ use_clipboard: bool = True
42
43
  websocket_uri: str = 'localhost:6677'
43
44
  open_config_on_startup: bool = False
45
+ open_multimine_on_startup: bool = False
44
46
  texthook_replacement_regex: str = ""
45
47
 
46
48
 
@@ -63,7 +65,7 @@ class Anki:
63
65
  sentence_field: str = "Sentence"
64
66
  sentence_audio_field: str = "SentenceAudio"
65
67
  picture_field: str = "Picture"
66
- word_field: str = 'Word'
68
+ word_field: str = 'Expression'
67
69
  previous_sentence_field: str = ''
68
70
  previous_image_field: str = ''
69
71
  custom_tags: List[str] = None # Initialize to None and set it in __post_init__
@@ -115,7 +117,6 @@ class Audio:
115
117
  ffmpeg_reencode_options: str = ''
116
118
  external_tool: str = ""
117
119
  anki_media_collection: str = ""
118
- mining_from_history_grab_all_audio: bool = False
119
120
 
120
121
 
121
122
  @dataclass_json
@@ -149,6 +150,8 @@ class VAD:
149
150
  selected_vad_model: str = SILERO
150
151
  backup_vad_model: str = OFF
151
152
  trim_beginning: bool = False
153
+ beginning_offset: float = -0.5
154
+ add_audio_on_no_results: bool = False
152
155
 
153
156
 
154
157
  @dataclass_json
@@ -236,15 +239,20 @@ class ProfileConfig:
236
239
  def restart_required(self, previous):
237
240
  previous: ProfileConfig
238
241
  if any([previous.general.use_websocket != self.general.use_websocket,
242
+ previous.general.use_clipboard != self.general.use_clipboard,
239
243
  previous.general.websocket_uri != self.general.websocket_uri,
240
244
  previous.paths.folder_to_watch != self.paths.folder_to_watch,
241
245
  previous.obs.open_obs != self.obs.open_obs,
242
246
  previous.obs.host != self.obs.host,
243
247
  previous.obs.port != self.obs.port
244
248
  ]):
249
+ logger.info("Restart Required for Some Settings that were Changed")
245
250
  return True
246
251
  return False
247
252
 
253
+ def config_changed(self, new: 'ProfileConfig') -> bool:
254
+ return self != new
255
+
248
256
 
249
257
  @dataclass_json
250
258
  @dataclass
@@ -274,6 +282,17 @@ class Config:
274
282
  return self.configs[DEFAULT_CONFIG]
275
283
 
276
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
+
277
296
  def get_app_directory():
278
297
  if platform == 'win32': # Windows
279
298
  appdata_dir = os.getenv('APPDATA')
@@ -375,11 +394,19 @@ def reload_config():
375
394
  def get_master_config():
376
395
  return config_instance
377
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
+
378
406
  def switch_profile_and_save(profile_name):
379
407
  global config_instance
380
408
  config_instance.current_profile = profile_name
381
- with open(get_config_path(), 'w') as file:
382
- json.dump(config_instance.to_dict(), file, indent=4)
409
+ save_full_config(config_instance)
383
410
  return config_instance.get_config()
384
411
 
385
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 = ''
@@ -21,7 +22,7 @@ current_line_after_regex = ''
21
22
  current_line_time = datetime.now()
22
23
 
23
24
  reconnecting = False
24
- multi_mine_event_bus: Callable[[str, datetime], None] = None
25
+ websocket_connected = False
25
26
 
26
27
  @dataclass
27
28
  class GameLine:
@@ -88,23 +89,30 @@ class ClipboardMonitor(threading.Thread):
88
89
  # Initial clipboard content
89
90
  current_line = pyperclip.paste()
90
91
 
92
+ skip_next_clipboard = False
91
93
  while True:
94
+ if websocket_connected:
95
+ time.sleep(1)
96
+ skip_next_clipboard = True
97
+ continue
92
98
  current_clipboard = pyperclip.paste()
93
99
 
94
- if current_clipboard != current_line:
100
+ if current_clipboard != current_line and not skip_next_clipboard:
95
101
  handle_new_text_event(current_clipboard)
102
+ skip_next_clipboard = False
96
103
 
97
104
  time.sleep(0.05)
98
105
 
99
106
 
100
107
  async def listen_websocket():
101
- global current_line, current_line_time, line_history, reconnecting
108
+ global current_line, current_line_time, line_history, reconnecting, websocket_connected
102
109
  while True:
103
110
  try:
104
111
  async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
105
112
  if reconnecting:
106
- logger.info(f"Texthooker WebSocket connected Successfully!")
113
+ logger.info(f"Texthooker WebSocket connected Successfully!" + " Disabling Clipboard Monitor." if get_config().general.use_clipboard else "")
107
114
  reconnecting = False
115
+ websocket_connected = True
108
116
  while True:
109
117
  message = await websocket.recv()
110
118
 
@@ -117,8 +125,9 @@ async def listen_websocket():
117
125
  if current_clipboard != current_line:
118
126
  handle_new_text_event(current_clipboard)
119
127
  except (websockets.ConnectionClosed, ConnectionError) as e:
128
+ websocket_connected = False
120
129
  if not reconnecting:
121
- logger.warning(f"Texthooker WebSocket connection lost: {e}. IF USING CLIPBOARD, WEBSOCKET NEEDS TO BE TURNED OFF IN SETTINGS. Attempting to Reconnect...")
130
+ logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
122
131
  reconnecting = True
123
132
  await asyncio.sleep(5)
124
133
 
@@ -132,7 +141,7 @@ def handle_new_text_event(current_clipboard):
132
141
  logger.info(f"Line Received: {current_line_after_regex}")
133
142
  current_line_time = datetime.now()
134
143
  line_history.add_line(current_line_after_regex)
135
- multi_mine_event_bus(line_history[-1])
144
+ get_utility_window().add_text(line_history[-1])
136
145
 
137
146
 
138
147
  def reset_line_hotkey_pressed():
@@ -146,34 +155,41 @@ def run_websocket_listener():
146
155
  asyncio.run(listen_websocket())
147
156
 
148
157
 
149
- def start_text_monitor(send_to_mine_event_bus):
150
- global multi_mine_event_bus
151
- multi_mine_event_bus = send_to_mine_event_bus
158
+ def start_text_monitor():
152
159
  if get_config().general.use_websocket:
153
- text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
154
- else:
155
- text_thread = ClipboardMonitor()
156
- text_thread.start()
160
+ threading.Thread(target=run_websocket_listener, daemon=True).start()
161
+ if get_config().general.use_clipboard:
162
+ if get_config().general.use_websocket:
163
+ logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
164
+ ClipboardMonitor().start()
157
165
 
158
166
 
159
167
  def similar(a, b):
160
168
  return SequenceMatcher(None, a, b).ratio()
161
169
 
170
+ def one_contains_the_other(a, b):
171
+ return a in b or b in a
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)
162
177
 
163
178
  def get_text_event(last_note) -> GameLine:
164
179
  lines = line_history.values
165
180
 
181
+ if not lines:
182
+ raise Exception("No lines in history. Text is required from either clipboard or websocket for GSM to work. Please check your setup/config.")
183
+
166
184
  if not last_note:
167
185
  return lines[-1]
168
186
 
169
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
187
+ sentence = last_note.get_field(get_config().anki.sentence_field)
170
188
  if not sentence:
171
189
  return lines[-1]
172
190
 
173
191
  for line in reversed(lines):
174
- similarity = similar(remove_html_tags(sentence), line.text)
175
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
176
- if similarity >= 0.60 or line.text in remove_html_tags(sentence):
192
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
177
193
  return line
178
194
 
179
195
  logger.debug("Couldn't find a match in history, using last event")
@@ -184,30 +200,27 @@ def get_line_and_future_lines(last_note):
184
200
  if not last_note:
185
201
  return []
186
202
 
187
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
203
+ sentence = last_note.get_field(get_config().anki.sentence_field)
188
204
  found_lines = []
189
205
  if sentence:
190
206
  found = False
191
207
  for line in line_history.values:
192
- similarity = similar(remove_html_tags(sentence), line.text)
193
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
194
208
  if found:
195
209
  found_lines.append(line.text)
196
- if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
210
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
197
211
  found = True
198
212
  found_lines.append(line.text)
199
213
  return found_lines
200
214
 
201
- def get_mined_line(last_note, lines):
215
+ def get_mined_line(last_note: AnkiCard, lines):
202
216
  if not last_note:
203
- return lines[0]
204
-
205
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
206
- for line2 in lines:
207
- similarity = similar(remove_html_tags(sentence), line2.text)
208
- if similarity >= 0.60 or line2.text in remove_html_tags(sentence):
209
- return line2
210
- return lines[0]
217
+ return lines[-1]
218
+
219
+ sentence = last_note.get_field(get_config().anki.sentence_field)
220
+ for line in lines:
221
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
222
+ return line
223
+ return lines[-1]
211
224
 
212
225
 
213
226
  def get_time_of_line(line):
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
@@ -3,7 +3,6 @@ from tkinter import ttk
3
3
 
4
4
  from GameSentenceMiner.configuration import logger
5
5
 
6
-
7
6
  class UtilityApp:
8
7
  def __init__(self, root):
9
8
  self.root = root
@@ -121,6 +120,16 @@ class UtilityApp:
121
120
  # for checkbox in self.checkboxes:
122
121
  # checkbox.set(False)
123
122
 
123
+ def init_utility_window(root):
124
+ global utility_window
125
+ utility_window = UtilityApp(root)
126
+ return utility_window
127
+
128
+ def get_utility_window():
129
+ return utility_window
130
+
131
+ utility_window: UtilityApp = None
132
+
124
133
 
125
134
  if __name__ == "__main__":
126
135
  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
1
  Metadata-Version: 2.2
2
2
  Name: GameSentenceMiner
3
- Version: 2.4.10
3
+ Version: 2.5.0
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
@@ -0,0 +1,27 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=LzjQc2k81-6Bni2B6cVjGUlKxt7noHeUFYSUJGWNs0E,11473
3
+ GameSentenceMiner/config_gui.py,sha256=PzzLX-OwK71U5JSFaxup0ec0oWBWaF6AeCXs0m13HK0,56762
4
+ GameSentenceMiner/configuration.py,sha256=dJi06CTJYJgYZQDBd2xQIRlIFiBhh5KPGk7TX7jNlYM,16037
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=yiacyduj_7AreOF93Dcqy4lSS8VVWv3QZxhxCMsbkgI,4764
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.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
+ gamesentenceminer-2.5.0.dist-info/METADATA,sha256=2xP65ECcypdDl8FLVD-oMJqXRgwEBDcJG0v-l-R_KYE,5387
24
+ gamesentenceminer-2.5.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
25
+ gamesentenceminer-2.5.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
26
+ gamesentenceminer-2.5.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
27
+ gamesentenceminer-2.5.0.dist-info/RECORD,,
@@ -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=5qsYlPHxDABbBRHJjIWMRsiDPLFTmKNMVUnxQO9YLYc,52612
4
- GameSentenceMiner/configuration.py,sha256=qTXZnk0TMPKnSPSkRg4HpDXYFCXBkD43to31Uaf1NZs,15071
5
- GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
6
- GameSentenceMiner/ffmpeg.py,sha256=vkRvhsuXCL8-tGynobdLBnw4qNHUhTC33ITCCnjfZLM,11468
7
- GameSentenceMiner/gametext.py,sha256=VjWNdjHwWXWIwNOfYxud7EwIyg7t6zZ3IkmMhh8Vc0c,6819
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.10.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
- gamesentenceminer-2.4.10.dist-info/METADATA,sha256=b2hnaSPPzfNT_WmHqWZouxpa0rHuqpB3h-D0eMcAMmc,5388
24
- gamesentenceminer-2.4.10.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
25
- gamesentenceminer-2.4.10.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
26
- gamesentenceminer-2.4.10.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
27
- gamesentenceminer-2.4.10.dist-info/RECORD,,