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 +39 -34
- GameSentenceMiner/config_gui.py +67 -11
- GameSentenceMiner/configuration.py +31 -4
- GameSentenceMiner/downloader/download_tools.py +30 -1
- GameSentenceMiner/ffmpeg.py +4 -2
- GameSentenceMiner/gametext.py +45 -32
- GameSentenceMiner/gsm.py +35 -31
- GameSentenceMiner/model.py +72 -0
- GameSentenceMiner/notification.py +9 -0
- GameSentenceMiner/util.py +3 -3
- GameSentenceMiner/utility_gui.py +10 -1
- GameSentenceMiner/vad/silero_trim.py +1 -1
- GameSentenceMiner/vad/vosk_helper.py +1 -1
- GameSentenceMiner/vad/whisper_helper.py +1 -1
- {gamesentenceminer-2.4.10.dist-info → gamesentenceminer-2.5.0.dist-info}/METADATA +1 -1
- gamesentenceminer-2.5.0.dist-info/RECORD +27 -0
- gamesentenceminer-2.4.10.dist-info/RECORD +0 -27
- {gamesentenceminer-2.4.10.dist-info → gamesentenceminer-2.5.0.dist-info}/LICENSE +0 -0
- {gamesentenceminer-2.4.10.dist-info → gamesentenceminer-2.5.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.4.10.dist-info → gamesentenceminer-2.5.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.4.10.dist-info → gamesentenceminer-2.5.0.dist-info}/top_level.txt +0 -0
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
|
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
|
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
|
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
|
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
|
79
|
-
logger.info(f"UPDATED ANKI CARD FOR {last_note
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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,
|
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
|
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
|
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
|
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
|
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
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -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
|
-
|
224
|
-
|
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
|
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.
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
self.add_label_and_increment_row(audio_frame,
|
736
|
-
|
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 = '
|
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
|
-
|
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()
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -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
|
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
|
])
|
GameSentenceMiner/gametext.py
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
-
|
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(
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
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
|
-
|
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
|
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
|
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[
|
204
|
-
|
205
|
-
sentence = last_note
|
206
|
-
for
|
207
|
-
|
208
|
-
|
209
|
-
|
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.
|
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
|
104
|
-
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 =
|
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(
|
112
|
+
logger.debug(last_note.to_json())
|
114
113
|
|
115
|
-
note = anki.get_initial_card_info(last_note,
|
114
|
+
note = anki.get_initial_card_info(last_note, get_utility_window().get_selected_lines())
|
116
115
|
|
117
|
-
tango = last_note
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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.
|
144
|
-
|
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
|
-
|
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(
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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()
|
GameSentenceMiner/model.py
CHANGED
@@ -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
|
147
|
-
|
148
|
-
return
|
146
|
+
def remove_html_and_cloze_tags(text):
|
147
|
+
text = re.sub(r'<.*?>', '', re.sub(r'{{c\d+::(.*?)(::.*?)?}}', r'\1', text))
|
148
|
+
return text
|
GameSentenceMiner/utility_gui.py
CHANGED
@@ -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
|
|
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|