GameSentenceMiner 2.4.12__py3-none-any.whl → 2.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/anki.py +40 -34
- GameSentenceMiner/config_gui.py +54 -10
- GameSentenceMiner/configuration.py +28 -3
- GameSentenceMiner/downloader/download_tools.py +30 -1
- GameSentenceMiner/ffmpeg.py +4 -2
- GameSentenceMiner/gametext.py +23 -23
- GameSentenceMiner/gsm.py +35 -31
- GameSentenceMiner/model.py +72 -0
- GameSentenceMiner/notification.py +9 -0
- GameSentenceMiner/util.py +3 -3
- GameSentenceMiner/utility_gui.py +51 -12
- GameSentenceMiner/vad/silero_trim.py +1 -1
- GameSentenceMiner/vad/vosk_helper.py +1 -1
- GameSentenceMiner/vad/whisper_helper.py +1 -1
- {gamesentenceminer-2.4.12.dist-info → gamesentenceminer-2.5.1.dist-info}/METADATA +3 -2
- gamesentenceminer-2.5.1.dist-info/RECORD +27 -0
- {gamesentenceminer-2.4.12.dist-info → gamesentenceminer-2.5.1.dist-info}/WHEEL +1 -1
- gamesentenceminer-2.4.12.dist-info/RECORD +0 -27
- {gamesentenceminer-2.4.12.dist-info → gamesentenceminer-2.5.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.4.12.dist-info → gamesentenceminer-2.5.1.dist-info/licenses}/LICENSE +0 -0
- {gamesentenceminer-2.4.12.dist-info → gamesentenceminer-2.5.1.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)}")
|
@@ -228,8 +233,9 @@ def update_new_card():
|
|
228
233
|
if get_config().obs.get_game_from_scene:
|
229
234
|
obs.update_current_game()
|
230
235
|
if use_prev_audio:
|
236
|
+
lines = get_utility_window().get_selected_lines()
|
231
237
|
with util.lock:
|
232
|
-
update_anki_card(last_card, note=get_initial_card_info(last_card,
|
238
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
|
233
239
|
else:
|
234
240
|
logger.info("New card(s) detected! Added to Processing Queue!")
|
235
241
|
card_queue.append(last_card)
|
@@ -239,20 +245,20 @@ def update_new_card():
|
|
239
245
|
def sentence_is_same_as_previous(last_card):
|
240
246
|
if not util.get_last_mined_line():
|
241
247
|
return False
|
242
|
-
return
|
248
|
+
return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(util.get_last_mined_line())
|
243
249
|
|
244
250
|
def get_sentence(card):
|
245
|
-
return card
|
251
|
+
return card.get_field(get_config().anki.sentence_field)
|
246
252
|
|
247
253
|
def check_tags_for_should_update(last_card):
|
248
254
|
if get_config().anki.tags_to_check:
|
249
255
|
found = False
|
250
|
-
for tag in last_card
|
256
|
+
for tag in last_card.tags:
|
251
257
|
if tag.lower() in get_config().anki.tags_to_check:
|
252
258
|
found = True
|
253
259
|
break
|
254
260
|
if not found:
|
255
|
-
logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card
|
261
|
+
logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card.tags}, Tags_To_Check {get_config().anki.tags_to_check}")
|
256
262
|
return found
|
257
263
|
else:
|
258
264
|
return True
|
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:
|
@@ -106,6 +108,7 @@ class ConfigApp:
|
|
106
108
|
use_clipboard=self.clipboard_enabled.get(),
|
107
109
|
websocket_uri=self.websocket_uri.get(),
|
108
110
|
open_config_on_startup=self.open_config_on_startup.get(),
|
111
|
+
open_multimine_on_startup=self.open_multimine_on_startup.get(),
|
109
112
|
texthook_replacement_regex=self.texthook_replacement_regex.get()
|
110
113
|
),
|
111
114
|
paths=Paths(
|
@@ -160,7 +163,6 @@ class ConfigApp:
|
|
160
163
|
ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
|
161
164
|
external_tool = self.external_tool.get(),
|
162
165
|
anki_media_collection=self.anki_media_collection.get(),
|
163
|
-
mining_from_history_grab_all_audio=self.mining_from_history_grab_all_audio.get()
|
164
166
|
),
|
165
167
|
obs=OBS(
|
166
168
|
enabled=self.obs_enabled.get(),
|
@@ -184,7 +186,9 @@ class ConfigApp:
|
|
184
186
|
vosk_url='https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' if self.vosk_url.get() == VOSK_BASE else "https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip",
|
185
187
|
selected_vad_model=self.selected_vad_model.get(),
|
186
188
|
backup_vad_model=self.backup_vad_model.get(),
|
187
|
-
trim_beginning=self.vad_trim_beginning.get()
|
189
|
+
trim_beginning=self.vad_trim_beginning.get(),
|
190
|
+
beginning_offset=float(self.vad_beginning_offset.get()),
|
191
|
+
add_audio_on_no_results=self.add_audio_on_no_results.get(),
|
188
192
|
)
|
189
193
|
)
|
190
194
|
|
@@ -215,6 +219,7 @@ class ConfigApp:
|
|
215
219
|
signal_restart_settings_change()
|
216
220
|
settings_saved = True
|
217
221
|
configuration.reload_config()
|
222
|
+
self.settings = get_config()
|
218
223
|
for func in on_save:
|
219
224
|
func()
|
220
225
|
|
@@ -225,8 +230,9 @@ class ConfigApp:
|
|
225
230
|
|
226
231
|
self.window.title("GameSentenceMiner Configuration - " + current_config.name)
|
227
232
|
|
228
|
-
|
229
|
-
|
233
|
+
|
234
|
+
if current_config.name != self.settings.name or self.settings.config_changed(current_config):
|
235
|
+
logger.info("Config changed, reloading settings.")
|
230
236
|
self.master_config = new_config
|
231
237
|
self.settings = current_config
|
232
238
|
for frame in self.notebook.winfo_children():
|
@@ -296,6 +302,13 @@ class ConfigApp:
|
|
296
302
|
self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
|
297
303
|
row=self.current_row, column=2)
|
298
304
|
|
305
|
+
ttk.Label(general_frame, text="Open Multimine on Startup:").grid(row=self.current_row, column=0, sticky='W')
|
306
|
+
self.open_multimine_on_startup = tk.BooleanVar(value=self.settings.general.open_multimine_on_startup)
|
307
|
+
ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup).grid(row=self.current_row, column=1,
|
308
|
+
sticky='W')
|
309
|
+
self.add_label_and_increment_row(general_frame, "Whether to open multimining window when the script starts.",
|
310
|
+
row=self.current_row, column=2)
|
311
|
+
|
299
312
|
ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
|
300
313
|
self.current_version = ttk.Label(general_frame, text=get_current_version())
|
301
314
|
self.current_version.grid(row=self.current_row, column=1)
|
@@ -365,6 +378,17 @@ class ConfigApp:
|
|
365
378
|
self.add_label_and_increment_row(vad_frame, "Trim the beginning of the audio based on Voice Detection Results",
|
366
379
|
row=self.current_row, column=2)
|
367
380
|
|
381
|
+
ttk.Label(vad_frame, text="Beginning Offset After Beginning Trim:").grid(row=self.current_row, column=0, sticky='W')
|
382
|
+
self.vad_beginning_offset = ttk.Entry(vad_frame)
|
383
|
+
self.vad_beginning_offset.insert(0, str(self.settings.vad.beginning_offset))
|
384
|
+
self.vad_beginning_offset.grid(row=self.current_row, column=1)
|
385
|
+
self.add_label_and_increment_row(vad_frame, 'Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning', row=self.current_row, column=2)
|
386
|
+
|
387
|
+
ttk.Label(vad_frame, text="Add Audio on No Results:").grid(row=self.current_row, column=0, sticky='W')
|
388
|
+
self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
|
389
|
+
ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results).grid(row=self.current_row, column=1, sticky='W')
|
390
|
+
self.add_label_and_increment_row(vad_frame, "Add audio even if no results are found by VAD.", row=self.current_row, column=2)
|
391
|
+
|
368
392
|
|
369
393
|
@new_tab
|
370
394
|
def create_paths_tab(self):
|
@@ -740,12 +764,14 @@ class ConfigApp:
|
|
740
764
|
row=self.current_row,
|
741
765
|
column=2)
|
742
766
|
|
743
|
-
ttk.
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
self.add_label_and_increment_row(audio_frame,
|
748
|
-
|
767
|
+
ttk.Button(audio_frame, text="Install Ocenaudio", command=self.download_and_install_ocen).grid(
|
768
|
+
row=self.current_row, column=0, pady=5)
|
769
|
+
ttk.Button(audio_frame, text="Get Anki Media Collection",
|
770
|
+
command=self.set_default_anki_media_collection).grid(row=self.current_row, column=1, pady=5)
|
771
|
+
self.add_label_and_increment_row(audio_frame,
|
772
|
+
"These Two buttons both help set up the External Audio Editing Tool. The first one downloads and installs OcenAudio, a free audio editing software. The second one sets the default Anki media collection path.",
|
773
|
+
row=self.current_row,
|
774
|
+
column=3)
|
749
775
|
|
750
776
|
@new_tab
|
751
777
|
def create_obs_tab(self):
|
@@ -898,6 +924,24 @@ class ConfigApp:
|
|
898
924
|
def show_error_box(self, title, message):
|
899
925
|
messagebox.showerror(title, message)
|
900
926
|
|
927
|
+
def download_and_install_ocen(self):
|
928
|
+
confirm = messagebox.askyesno("Download OcenAudio?", "Would you like to download and install OcenAudio? It is a free audio editing software that works extremely well with GSM.")
|
929
|
+
if confirm:
|
930
|
+
exe_path = download_ocenaudio_if_needed()
|
931
|
+
messagebox.showinfo("OcenAudio Downloaded", f"OcenAudio has been downloaded and installed. You can find it at {exe_path}.")
|
932
|
+
self.external_tool.delete(0, tk.END)
|
933
|
+
self.external_tool.insert(0, exe_path)
|
934
|
+
self.save_settings()
|
935
|
+
|
936
|
+
def set_default_anki_media_collection(self):
|
937
|
+
confirm = messagebox.askyesno("Set Default Anki Media Collection?", "Would you like to set the default Anki media collection path? This will help the script find the media collection for external trimming.\n\nDefault: %APPDATA%/Anki2/User 1/collection.media")
|
938
|
+
if confirm:
|
939
|
+
default_path = get_default_anki_media_collection_path()
|
940
|
+
if default_path != self.settings.audio.external_tool:
|
941
|
+
self.anki_media_collection.delete(0, tk.END)
|
942
|
+
self.anki_media_collection.insert(0, default_path)
|
943
|
+
self.save_settings()
|
944
|
+
|
901
945
|
|
902
946
|
if __name__ == '__main__':
|
903
947
|
root = ttk.Window(themename='darkly')
|
@@ -42,6 +42,7 @@ class General:
|
|
42
42
|
use_clipboard: bool = True
|
43
43
|
websocket_uri: str = 'localhost:6677'
|
44
44
|
open_config_on_startup: bool = False
|
45
|
+
open_multimine_on_startup: bool = False
|
45
46
|
texthook_replacement_regex: str = ""
|
46
47
|
|
47
48
|
|
@@ -116,7 +117,6 @@ class Audio:
|
|
116
117
|
ffmpeg_reencode_options: str = ''
|
117
118
|
external_tool: str = ""
|
118
119
|
anki_media_collection: str = ""
|
119
|
-
mining_from_history_grab_all_audio: bool = False
|
120
120
|
|
121
121
|
|
122
122
|
@dataclass_json
|
@@ -150,6 +150,8 @@ class VAD:
|
|
150
150
|
selected_vad_model: str = SILERO
|
151
151
|
backup_vad_model: str = OFF
|
152
152
|
trim_beginning: bool = False
|
153
|
+
beginning_offset: float = -0.25
|
154
|
+
add_audio_on_no_results: bool = False
|
153
155
|
|
154
156
|
|
155
157
|
@dataclass_json
|
@@ -244,9 +246,13 @@ class ProfileConfig:
|
|
244
246
|
previous.obs.host != self.obs.host,
|
245
247
|
previous.obs.port != self.obs.port
|
246
248
|
]):
|
249
|
+
logger.info("Restart Required for Some Settings that were Changed")
|
247
250
|
return True
|
248
251
|
return False
|
249
252
|
|
253
|
+
def config_changed(self, new: 'ProfileConfig') -> bool:
|
254
|
+
return self != new
|
255
|
+
|
250
256
|
|
251
257
|
@dataclass_json
|
252
258
|
@dataclass
|
@@ -276,6 +282,17 @@ class Config:
|
|
276
282
|
return self.configs[DEFAULT_CONFIG]
|
277
283
|
|
278
284
|
|
285
|
+
def get_default_anki_path():
|
286
|
+
if platform == 'win32': # Windows
|
287
|
+
base_dir = os.getenv('APPDATA')
|
288
|
+
else: # macOS and Linux
|
289
|
+
base_dir = '~/.local/share/'
|
290
|
+
config_dir = os.path.join(base_dir, 'Anki2')
|
291
|
+
return config_dir
|
292
|
+
|
293
|
+
def get_default_anki_media_collection_path():
|
294
|
+
return os.path.join(get_default_anki_path(), 'User 1', 'collection.media')
|
295
|
+
|
279
296
|
def get_app_directory():
|
280
297
|
if platform == 'win32': # Windows
|
281
298
|
appdata_dir = os.getenv('APPDATA')
|
@@ -377,11 +394,19 @@ def reload_config():
|
|
377
394
|
def get_master_config():
|
378
395
|
return config_instance
|
379
396
|
|
397
|
+
def save_full_config(config):
|
398
|
+
with open(get_config_path(), 'w') as file:
|
399
|
+
json.dump(config.to_dict(), file, indent=4)
|
400
|
+
|
401
|
+
def save_current_config(config):
|
402
|
+
global config_instance
|
403
|
+
config_instance.set_config_for_profile(config_instance.current_profile, config)
|
404
|
+
save_full_config(config_instance)
|
405
|
+
|
380
406
|
def switch_profile_and_save(profile_name):
|
381
407
|
global config_instance
|
382
408
|
config_instance.current_profile = profile_name
|
383
|
-
|
384
|
-
json.dump(config_instance.to_dict(), file, indent=4)
|
409
|
+
save_full_config(config_instance)
|
385
410
|
return config_instance.get_config()
|
386
411
|
|
387
412
|
|
@@ -149,10 +149,39 @@ def download_ffmpeg_if_needed():
|
|
149
149
|
with source, target:
|
150
150
|
shutil.copyfileobj(source, target)
|
151
151
|
logger.info(f"FFmpeg extracted to {ffmpeg_dir}.")
|
152
|
+
|
153
|
+
def download_ocenaudio_if_needed():
|
154
|
+
ocenaudio_dir = os.path.join(get_app_directory(), 'ocenaudio', 'ocenaudio')
|
155
|
+
ocenaudio_exe_path = os.path.join(ocenaudio_dir, 'ocenaudio.exe')
|
156
|
+
if os.path.exists(ocenaudio_dir) and os.path.exists(ocenaudio_exe_path):
|
157
|
+
logger.info(f"Ocenaudio already installed at {ocenaudio_dir}.")
|
158
|
+
return ocenaudio_exe_path
|
159
|
+
|
160
|
+
if os.path.exists(ocenaudio_dir) and not os.path.exists(ocenaudio_exe_path):
|
161
|
+
logger.info("Ocenaudio directory exists but executable is missing. Re-downloading Ocenaudio...")
|
162
|
+
shutil.rmtree(ocenaudio_dir)
|
163
|
+
|
164
|
+
ocenaudio_url = "https://www.ocenaudio.com/downloads/ocenaudio_windows64.zip"
|
165
|
+
|
166
|
+
download_dir = os.path.join(get_app_directory(), "downloads")
|
167
|
+
os.makedirs(download_dir, exist_ok=True)
|
168
|
+
ocenaudio_archive = os.path.join(download_dir, "ocenaudio.zip")
|
169
|
+
|
170
|
+
logger.info(f"Downloading Ocenaudio from {ocenaudio_url}...")
|
171
|
+
urllib.request.urlretrieve(ocenaudio_url, ocenaudio_archive)
|
172
|
+
logger.info(f"Ocenaudio downloaded. Extracting to {ocenaudio_dir}...")
|
173
|
+
|
174
|
+
os.makedirs(ocenaudio_dir, exist_ok=True)
|
175
|
+
with zipfile.ZipFile(ocenaudio_archive, 'r') as zip_ref:
|
176
|
+
zip_ref.extractall(ocenaudio_dir)
|
177
|
+
|
178
|
+
logger.info(f"Ocenaudio extracted to {ocenaudio_dir}.")
|
179
|
+
return ocenaudio_exe_path
|
180
|
+
|
152
181
|
def main():
|
153
|
-
# Run dependency checks
|
154
182
|
download_obs_if_needed()
|
155
183
|
download_ffmpeg_if_needed()
|
184
|
+
download_ocenaudio_if_needed()
|
156
185
|
|
157
186
|
if __name__ == "__main__":
|
158
187
|
main()
|
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 = ''
|
@@ -22,7 +23,6 @@ current_line_time = datetime.now()
|
|
22
23
|
|
23
24
|
reconnecting = False
|
24
25
|
websocket_connected = False
|
25
|
-
multi_mine_event_bus: Callable[[str, datetime], None] = None
|
26
26
|
|
27
27
|
@dataclass
|
28
28
|
class GameLine:
|
@@ -89,14 +89,17 @@ class ClipboardMonitor(threading.Thread):
|
|
89
89
|
# Initial clipboard content
|
90
90
|
current_line = pyperclip.paste()
|
91
91
|
|
92
|
+
skip_next_clipboard = False
|
92
93
|
while True:
|
93
94
|
if websocket_connected:
|
94
95
|
time.sleep(1)
|
96
|
+
skip_next_clipboard = True
|
95
97
|
continue
|
96
98
|
current_clipboard = pyperclip.paste()
|
97
99
|
|
98
|
-
if current_clipboard != current_line:
|
100
|
+
if current_clipboard != current_line and not skip_next_clipboard:
|
99
101
|
handle_new_text_event(current_clipboard)
|
102
|
+
skip_next_clipboard = False
|
100
103
|
|
101
104
|
time.sleep(0.05)
|
102
105
|
|
@@ -138,7 +141,7 @@ def handle_new_text_event(current_clipboard):
|
|
138
141
|
logger.info(f"Line Received: {current_line_after_regex}")
|
139
142
|
current_line_time = datetime.now()
|
140
143
|
line_history.add_line(current_line_after_regex)
|
141
|
-
|
144
|
+
get_utility_window().add_text(line_history[-1])
|
142
145
|
|
143
146
|
|
144
147
|
def reset_line_hotkey_pressed():
|
@@ -152,9 +155,7 @@ def run_websocket_listener():
|
|
152
155
|
asyncio.run(listen_websocket())
|
153
156
|
|
154
157
|
|
155
|
-
def start_text_monitor(
|
156
|
-
global multi_mine_event_bus
|
157
|
-
multi_mine_event_bus = send_to_mine_event_bus
|
158
|
+
def start_text_monitor():
|
158
159
|
if get_config().general.use_websocket:
|
159
160
|
threading.Thread(target=run_websocket_listener, daemon=True).start()
|
160
161
|
if get_config().general.use_clipboard:
|
@@ -169,24 +170,26 @@ def similar(a, b):
|
|
169
170
|
def one_contains_the_other(a, b):
|
170
171
|
return a in b or b in a
|
171
172
|
|
173
|
+
def lines_match(a, b):
|
174
|
+
similarity = similar(a, b)
|
175
|
+
logger.debug(f"Comparing: {a} with {b} - Similarity: {similarity}, Or One contains the other: {one_contains_the_other(a, b)}")
|
176
|
+
return similar(a, b) >= 0.60 or one_contains_the_other(a, b)
|
172
177
|
|
173
178
|
def get_text_event(last_note) -> GameLine:
|
174
179
|
lines = line_history.values
|
175
180
|
|
176
|
-
if not last_note:
|
177
|
-
return lines[-1]
|
178
|
-
|
179
181
|
if not lines:
|
180
182
|
raise Exception("No lines in history. Text is required from either clipboard or websocket for GSM to work. Please check your setup/config.")
|
181
183
|
|
182
|
-
|
184
|
+
if not last_note:
|
185
|
+
return lines[-1]
|
186
|
+
|
187
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
183
188
|
if not sentence:
|
184
189
|
return lines[-1]
|
185
190
|
|
186
191
|
for line in reversed(lines):
|
187
|
-
|
188
|
-
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
|
189
|
-
if similarity >= 0.60 or one_contains_the_other(line.text, remove_html_tags(sentence)):
|
192
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
|
190
193
|
return line
|
191
194
|
|
192
195
|
logger.debug("Couldn't find a match in history, using last event")
|
@@ -197,28 +200,25 @@ def get_line_and_future_lines(last_note):
|
|
197
200
|
if not last_note:
|
198
201
|
return []
|
199
202
|
|
200
|
-
sentence = last_note
|
203
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
201
204
|
found_lines = []
|
202
205
|
if sentence:
|
203
206
|
found = False
|
204
207
|
for line in line_history.values:
|
205
|
-
similarity = similar(remove_html_tags(sentence), line.text)
|
206
|
-
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
|
207
208
|
if found:
|
208
209
|
found_lines.append(line.text)
|
209
|
-
if
|
210
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
|
210
211
|
found = True
|
211
212
|
found_lines.append(line.text)
|
212
213
|
return found_lines
|
213
214
|
|
214
|
-
def get_mined_line(last_note, lines):
|
215
|
+
def get_mined_line(last_note: AnkiCard, lines):
|
215
216
|
if not last_note:
|
216
217
|
return lines[-1]
|
217
218
|
|
218
|
-
sentence = last_note
|
219
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
219
220
|
for line in lines:
|
220
|
-
|
221
|
-
if similarity >= 0.60 or one_contains_the_other(line.text, remove_html_tags(sentence)):
|
221
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
|
222
222
|
return line
|
223
223
|
return lines[-1]
|
224
224
|
|
GameSentenceMiner/gsm.py
CHANGED
@@ -18,13 +18,13 @@ from GameSentenceMiner import gametext
|
|
18
18
|
from GameSentenceMiner import notification
|
19
19
|
from GameSentenceMiner import obs
|
20
20
|
from GameSentenceMiner import util
|
21
|
-
from GameSentenceMiner import utility_gui
|
22
21
|
from GameSentenceMiner.configuration import *
|
23
22
|
from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
|
24
23
|
from GameSentenceMiner.electron_messaging import signal_restart_settings_change
|
25
24
|
from GameSentenceMiner.ffmpeg import get_audio_and_trim
|
26
25
|
from GameSentenceMiner.gametext import get_text_event, get_mined_line
|
27
26
|
from GameSentenceMiner.util import *
|
27
|
+
from GameSentenceMiner.utility_gui import init_utility_window, get_utility_window
|
28
28
|
from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
|
29
29
|
|
30
30
|
if is_windows():
|
@@ -33,7 +33,6 @@ if is_windows():
|
|
33
33
|
obs_process = None
|
34
34
|
procs_to_close = []
|
35
35
|
settings_window: config_gui.ConfigApp = None
|
36
|
-
utility_window: utility_gui.UtilityApp = None
|
37
36
|
obs_paused = False
|
38
37
|
icon: Icon
|
39
38
|
menu: Menu
|
@@ -91,7 +90,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
91
90
|
if get_config().anki.update_anki:
|
92
91
|
last_note = anki.get_last_anki_card()
|
93
92
|
if get_config().features.backfill_audio:
|
94
|
-
last_note = anki.get_cards_by_sentence(gametext.
|
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
@@ -1,13 +1,14 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
1
3
|
import tkinter as tk
|
2
4
|
from tkinter import ttk
|
3
5
|
|
4
|
-
from GameSentenceMiner.configuration import logger
|
6
|
+
from GameSentenceMiner.configuration import logger, get_app_directory
|
5
7
|
|
6
8
|
|
7
9
|
class UtilityApp:
|
8
10
|
def __init__(self, root):
|
9
11
|
self.root = root
|
10
|
-
|
11
12
|
self.items = []
|
12
13
|
self.checkboxes = []
|
13
14
|
self.multi_mine_window = None # Store the multi-mine window reference
|
@@ -15,32 +16,60 @@ class UtilityApp:
|
|
15
16
|
|
16
17
|
style = ttk.Style()
|
17
18
|
style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
|
19
|
+
self.config_file = os.path.join(get_app_directory(), "multi-mine-window-config.json")
|
20
|
+
self.load_window_config()
|
21
|
+
|
22
|
+
|
23
|
+
def save_window_config(self):
|
24
|
+
if self.multi_mine_window:
|
25
|
+
config = {
|
26
|
+
"x": self.multi_mine_window.winfo_x(),
|
27
|
+
"y": self.multi_mine_window.winfo_y(),
|
28
|
+
"width": self.multi_mine_window.winfo_width(),
|
29
|
+
"height": self.multi_mine_window.winfo_height()
|
30
|
+
}
|
31
|
+
print(config)
|
32
|
+
with open(self.config_file, "w") as f:
|
33
|
+
json.dump(config, f)
|
34
|
+
|
35
|
+
def load_window_config(self):
|
36
|
+
if os.path.exists(self.config_file):
|
37
|
+
with open(self.config_file, "r") as f:
|
38
|
+
config = json.load(f)
|
39
|
+
self.window_x = config.get("x", 100)
|
40
|
+
self.window_y = config.get("y", 100)
|
41
|
+
self.window_width = config.get("width", 800)
|
42
|
+
self.window_height = config.get("height", 400)
|
43
|
+
else:
|
44
|
+
self.window_x = 100
|
45
|
+
self.window_y = 100
|
46
|
+
self.window_width = 800
|
47
|
+
self.window_height = 400
|
18
48
|
|
19
|
-
# def show(self):
|
20
|
-
# if self.multi_mine_window is None or not tk.Toplevel.winfo_exists(self.multi_mine_window):
|
21
|
-
# self.multi_mine_window = tk.Toplevel(self.root)
|
22
|
-
# self.multi_mine_window.title("Multi-Mine Window")
|
23
|
-
# self.update_multi_mine_window()
|
24
|
-
#
|
25
49
|
def show(self):
|
26
|
-
""" Open the multi-mine window only if it doesn't exist. """
|
27
50
|
if not self.multi_mine_window or not tk.Toplevel.winfo_exists(self.multi_mine_window):
|
28
|
-
logger.info("opening multi-mine_window")
|
29
51
|
self.multi_mine_window = tk.Toplevel(self.root)
|
30
52
|
self.multi_mine_window.title("Multi Mine Window")
|
31
53
|
|
32
|
-
self.multi_mine_window.
|
54
|
+
self.multi_mine_window.geometry(f"{self.window_width}x{self.window_height}+{self.window_x}+{self.window_y}")
|
55
|
+
|
56
|
+
self.multi_mine_window.minsize(800, 400)
|
33
57
|
|
34
58
|
self.checkbox_frame = ttk.Frame(self.multi_mine_window)
|
35
59
|
self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
36
60
|
|
37
|
-
# Add existing items
|
38
61
|
for line, var in self.items:
|
39
62
|
self.add_checkbox_to_gui(line, var)
|
63
|
+
|
64
|
+
self.multi_mine_window.protocol("WM_DELETE_WINDOW", self.on_close)
|
40
65
|
else:
|
41
66
|
self.multi_mine_window.deiconify()
|
42
67
|
self.multi_mine_window.lift()
|
43
68
|
|
69
|
+
def on_close(self):
|
70
|
+
self.save_window_config()
|
71
|
+
self.multi_mine_window.withdraw()
|
72
|
+
|
44
73
|
def add_text(self, line):
|
45
74
|
if line.text:
|
46
75
|
var = tk.BooleanVar()
|
@@ -121,6 +150,16 @@ class UtilityApp:
|
|
121
150
|
# for checkbox in self.checkboxes:
|
122
151
|
# checkbox.set(False)
|
123
152
|
|
153
|
+
def init_utility_window(root):
|
154
|
+
global utility_window
|
155
|
+
utility_window = UtilityApp(root)
|
156
|
+
return utility_window
|
157
|
+
|
158
|
+
def get_utility_window():
|
159
|
+
return utility_window
|
160
|
+
|
161
|
+
utility_window: UtilityApp = None
|
162
|
+
|
124
163
|
|
125
164
|
if __name__ == "__main__":
|
126
165
|
root = tk.Tk()
|
@@ -38,6 +38,6 @@ def process_audio_with_silero(input_audio, output_audio):
|
|
38
38
|
end_time = voice_activity[-1]['end'] if voice_activity else 0
|
39
39
|
|
40
40
|
# Trim the audio using FFmpeg
|
41
|
-
ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
|
41
|
+
ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
|
42
42
|
logger.info(f"Trimmed audio saved to: {output_audio}")
|
43
43
|
return True
|
@@ -140,7 +140,7 @@ def process_audio_with_vosk(input_audio, output_audio):
|
|
140
140
|
logger.info(f"Trimmed End of Audio to {end_time} seconds:")
|
141
141
|
|
142
142
|
# Trim the audio using FFmpeg
|
143
|
-
ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
|
143
|
+
ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
|
144
144
|
logger.info(f"Trimmed audio saved to: {output_audio}")
|
145
145
|
return True
|
146
146
|
|
@@ -87,7 +87,7 @@ def process_audio_with_whisper(input_audio, output_audio):
|
|
87
87
|
logger.info(f"Trimmed End of Audio to {end_time} seconds:")
|
88
88
|
|
89
89
|
# Trim the audio using FFmpeg
|
90
|
-
ffmpeg.trim_audio(input_audio, start_time, end_time + get_config().audio.end_offset, output_audio)
|
90
|
+
ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
|
91
91
|
logger.info(f"Trimmed audio saved to: {output_audio}")
|
92
92
|
return True
|
93
93
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: GameSentenceMiner
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.5.1
|
4
4
|
Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
|
5
5
|
Author-email: Beangate <bpwhelan95@gmail.com>
|
6
6
|
License: MIT License
|
@@ -32,6 +32,7 @@ Requires-Dist: dataclasses_json~=0.6.7
|
|
32
32
|
Requires-Dist: numpy
|
33
33
|
Requires-Dist: pystray
|
34
34
|
Requires-Dist: pywin32; sys_platform == "win32"
|
35
|
+
Dynamic: license-file
|
35
36
|
|
36
37
|
# Game Sentence Miner
|
37
38
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
GameSentenceMiner/anki.py,sha256=_jr6q3tOSP-7YGaY9cVz5v3508T_V2JINJjDYbp1YS0,11495
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=PzzLX-OwK71U5JSFaxup0ec0oWBWaF6AeCXs0m13HK0,56762
|
4
|
+
GameSentenceMiner/configuration.py,sha256=kyvNCkZSZcqXbGjak8lb_GyhhjN8tIfb7eEfusz_M8A,16038
|
5
|
+
GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
|
6
|
+
GameSentenceMiner/ffmpeg.py,sha256=2dwKbKxw_9sUXud67pPAx_6dGd1j-D99CdqLFVtbhSk,11479
|
7
|
+
GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
|
8
|
+
GameSentenceMiner/gsm.py,sha256=cWHJwWHyKsdzo0TXTXREG549MUtlQLyect9S37UK56g,20309
|
9
|
+
GameSentenceMiner/model.py,sha256=WYLjIS9IEPxaLPwG5c-edfcfBKYzBFpdR3V3Da8MuLg,5277
|
10
|
+
GameSentenceMiner/notification.py,sha256=WeFodBsshhbOagcEfjAJ3kxjUGvBuUAQKEJ8Zf0YO04,2267
|
11
|
+
GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
|
12
|
+
GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
|
13
|
+
GameSentenceMiner/util.py,sha256=nmp2cmuBN0Azoc91f3zN_dZSh4nK9z21VTfpIbn8Tk4,4761
|
14
|
+
GameSentenceMiner/utility_gui.py,sha256=VYuY8Br47AFak_zOVx9R1oUCBvoRjonDyE0oE6FVwJ0,5742
|
15
|
+
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
16
|
+
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
|
18
|
+
GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
|
20
|
+
GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
|
21
|
+
GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
|
22
|
+
gamesentenceminer-2.5.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
23
|
+
gamesentenceminer-2.5.1.dist-info/METADATA,sha256=OjwqqaJ_DG7FR6OLc3z1XlUX6WH6SqsOc4EDXUjbzbQ,5409
|
24
|
+
gamesentenceminer-2.5.1.dist-info/WHEEL,sha256=tTnHoFhvKQHCh4jz3yCn0WPTYIy7wXx3CJtJ7SJGV7c,91
|
25
|
+
gamesentenceminer-2.5.1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
26
|
+
gamesentenceminer-2.5.1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
27
|
+
gamesentenceminer-2.5.1.dist-info/RECORD,,
|
@@ -1,27 +0,0 @@
|
|
1
|
-
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=YSz5gUTsKOdbogwHKtgFM7v7pREjdAwl7A0Wa_CXnKg,10918
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=s0U0Sj_WvrJ8GecSyj33tWMh-OyOcZ5yY1ddEc1cgwg,53456
|
4
|
-
GameSentenceMiner/configuration.py,sha256=_VxzRHCJPW2iJTW6gheD1d6-oaUbUQQnLsIiHjgjNcY,15186
|
5
|
-
GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
|
6
|
-
GameSentenceMiner/ffmpeg.py,sha256=vkRvhsuXCL8-tGynobdLBnw4qNHUhTC33ITCCnjfZLM,11468
|
7
|
-
GameSentenceMiner/gametext.py,sha256=YXjaZF130lkzU-I2dpZ5HoNrj3GSCG-7X9B04IFnetI,7551
|
8
|
-
GameSentenceMiner/gsm.py,sha256=E9Hpbyzrv8FEFGBs-4Hf60m3sk0ps3OQdvB1n42AhRU,20164
|
9
|
-
GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
|
10
|
-
GameSentenceMiner/notification.py,sha256=WBaQWoPNhW4XqdPBUmxPBgjk0ngzH_4v9zMQ-XQAKC8,2010
|
11
|
-
GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
|
12
|
-
GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
|
13
|
-
GameSentenceMiner/util.py,sha256=MITweiFYaefWQF5nR8tZ9yE6vd_b-fLuP0MP1Y1U4K0,4720
|
14
|
-
GameSentenceMiner/utility_gui.py,sha256=EtQUnCgTTdzKJE0iCJiHjjc_c6tc7JtI09LRg4_iy8Y,4555
|
15
|
-
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
16
|
-
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
GameSentenceMiner/downloader/download_tools.py,sha256=1x57nzktdlsUruTJxCPNpmHDDlhZ589NwRyAMP0Pq7E,6818
|
18
|
-
GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
|
20
|
-
GameSentenceMiner/vad/vosk_helper.py,sha256=HifeXKbEMrs81ZuuGxS67yAghu8TMXUP6Oan9i9dTxw,5938
|
21
|
-
GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
|
22
|
-
gamesentenceminer-2.4.12.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
23
|
-
gamesentenceminer-2.4.12.dist-info/METADATA,sha256=fqK1YCT402ptWC4idyZaIqjbCQ38jGDAZGJCuVJ1TAE,5388
|
24
|
-
gamesentenceminer-2.4.12.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
25
|
-
gamesentenceminer-2.4.12.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
26
|
-
gamesentenceminer-2.4.12.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
27
|
-
gamesentenceminer-2.4.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|