GameSentenceMiner 2.2.4__py3-none-any.whl → 2.3.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 +23 -6
- GameSentenceMiner/config_gui.py +53 -7
- GameSentenceMiner/configuration.py +6 -0
- GameSentenceMiner/ffmpeg.py +2 -2
- GameSentenceMiner/gametext.py +57 -26
- GameSentenceMiner/gsm.py +29 -13
- GameSentenceMiner/util.py +7 -1
- GameSentenceMiner/utility_gui.py +127 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.1.dist-info}/METADATA +2 -2
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.1.dist-info}/RECORD +13 -12
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.1.dist-info}/WHEEL +0 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.1.dist-info}/entry_points.txt +0 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.1.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -6,12 +6,13 @@ import urllib.request
|
|
6
6
|
|
7
7
|
import requests as req
|
8
8
|
|
9
|
-
from GameSentenceMiner import obs, util, notification, ffmpeg
|
9
|
+
from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
|
10
10
|
|
11
11
|
from GameSentenceMiner.configuration import *
|
12
12
|
from GameSentenceMiner.configuration import get_config
|
13
13
|
from GameSentenceMiner.gametext import get_last_two_sentences
|
14
14
|
from GameSentenceMiner.obs import get_current_game
|
15
|
+
from util import remove_html_tags
|
15
16
|
|
16
17
|
audio_in_anki = None
|
17
18
|
screenshot_in_anki = None
|
@@ -69,6 +70,8 @@ def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='
|
|
69
70
|
if get_config().features.open_anki_edit:
|
70
71
|
notification.open_anki_card(last_note['noteId'])
|
71
72
|
|
73
|
+
util.set_last_mined_line(get_sentence(last_note))
|
74
|
+
|
72
75
|
if get_config().audio.external_tool:
|
73
76
|
open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
|
74
77
|
|
@@ -103,14 +106,21 @@ def add_image_to_card(last_note, image_path):
|
|
103
106
|
logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note['noteId']}")
|
104
107
|
|
105
108
|
|
106
|
-
def get_initial_card_info(last_note):
|
109
|
+
def get_initial_card_info(last_note, selected_lines):
|
107
110
|
note = {'id': last_note['noteId'], 'fields': {}}
|
108
111
|
if not last_note:
|
109
112
|
return note
|
110
113
|
current_line, previous_line = get_last_two_sentences(last_note)
|
111
114
|
logger.debug(f"Previous Sentence {previous_line}")
|
112
115
|
logger.debug(f"Current Sentence {current_line}")
|
113
|
-
|
116
|
+
|
117
|
+
if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
|
118
|
+
lines = gametext.get_line_and_future_lines(last_note)
|
119
|
+
if lines:
|
120
|
+
note['fields'][get_config().anki.sentence_field] = "".join(lines)
|
121
|
+
|
122
|
+
if selected_lines and get_config().anki.multi_overwrites_sentence:
|
123
|
+
note['fields'][get_config().anki.sentence_field] = "".join(selected_lines)
|
114
124
|
|
115
125
|
logger.debug(
|
116
126
|
f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and previous_line and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
|
@@ -202,20 +212,27 @@ def update_new_card():
|
|
202
212
|
if not check_tags_for_should_update(last_card):
|
203
213
|
return
|
204
214
|
|
205
|
-
use_prev_audio = util.use_previous_audio
|
206
215
|
if util.lock.locked():
|
207
216
|
logger.info("Audio still being Trimmed, Card Queued!")
|
208
|
-
use_prev_audio = True
|
209
217
|
with util.lock:
|
218
|
+
use_prev_audio = sentence_is_same_as_previous(last_card)
|
219
|
+
logger.info(f"last mined line: {util.get_last_mined_line()}, current sentence: {get_sentence(last_card)}")
|
210
220
|
logger.info(f"use previous audio: {use_prev_audio}")
|
211
221
|
if get_config().obs.get_game_from_scene:
|
212
222
|
obs.update_current_game()
|
213
223
|
if use_prev_audio:
|
214
|
-
update_anki_card(last_card, note=get_initial_card_info(last_card), reuse_audio=True)
|
224
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
|
215
225
|
else:
|
216
226
|
logger.info("New card(s) detected!")
|
217
227
|
obs.save_replay_buffer()
|
218
228
|
|
229
|
+
def sentence_is_same_as_previous(last_card):
|
230
|
+
if not util.get_last_mined_line():
|
231
|
+
return False
|
232
|
+
return remove_html_tags(get_sentence(last_card)) == remove_html_tags(util.get_last_mined_line())
|
233
|
+
|
234
|
+
def get_sentence(card):
|
235
|
+
return card['fields'][get_config().anki.sentence_field]['value']
|
219
236
|
|
220
237
|
def check_tags_for_should_update(last_card):
|
221
238
|
if get_config().anki.tags_to_check:
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -49,8 +49,9 @@ class HoverInfoWidget:
|
|
49
49
|
|
50
50
|
|
51
51
|
class ConfigApp:
|
52
|
-
def __init__(self):
|
53
|
-
self.window =
|
52
|
+
def __init__(self, root):
|
53
|
+
self.window = root
|
54
|
+
# self.window = ttk.Window(themename='darkly')
|
54
55
|
self.window.title('GameSentenceMiner Configuration')
|
55
56
|
self.window.protocol("WM_DELETE_WINDOW", self.hide)
|
56
57
|
|
@@ -125,7 +126,8 @@ class ConfigApp:
|
|
125
126
|
use_websocket=self.websocket_enabled.get(),
|
126
127
|
websocket_uri=self.websocket_uri.get(),
|
127
128
|
open_config_on_startup=self.open_config_on_startup.get(),
|
128
|
-
check_for_update_on_startup=self.check_for_update_on_startup.get()
|
129
|
+
check_for_update_on_startup=self.check_for_update_on_startup.get(),
|
130
|
+
texthook_replacement_regex=self.texthook_replacement_regex.get()
|
129
131
|
),
|
130
132
|
paths=Paths(
|
131
133
|
folder_to_watch=self.folder_to_watch.get(),
|
@@ -150,6 +152,7 @@ class ConfigApp:
|
|
150
152
|
polling_rate=int(self.polling_rate.get()),
|
151
153
|
overwrite_audio=self.overwrite_audio.get(),
|
152
154
|
overwrite_picture=self.overwrite_picture.get(),
|
155
|
+
multi_overwrites_sentence=self.multi_overwrites_sentence.get(),
|
153
156
|
anki_custom_fields={
|
154
157
|
key_entry.get(): value_entry.get() for key_entry, value_entry, delete_button in
|
155
158
|
self.custom_field_entries if key_entry.get()
|
@@ -176,10 +179,13 @@ class ConfigApp:
|
|
176
179
|
end_offset=float(self.end_offset.get()),
|
177
180
|
ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
|
178
181
|
external_tool = self.external_tool.get(),
|
179
|
-
anki_media_collection=self.anki_media_collection.get()
|
182
|
+
anki_media_collection=self.anki_media_collection.get(),
|
183
|
+
mining_from_history_grab_all_audio=self.mining_from_history_grab_all_audio.get()
|
180
184
|
),
|
181
185
|
obs=OBS(
|
182
186
|
enabled=self.obs_enabled.get(),
|
187
|
+
open_obs=self.open_obs.get(),
|
188
|
+
close_obs=self.close_obs.get(),
|
183
189
|
host=self.obs_host.get(),
|
184
190
|
port=int(self.obs_port.get()),
|
185
191
|
password=self.obs_password.get(),
|
@@ -189,7 +195,8 @@ class ConfigApp:
|
|
189
195
|
),
|
190
196
|
hotkeys=Hotkeys(
|
191
197
|
reset_line=self.reset_line_hotkey.get(),
|
192
|
-
take_screenshot=self.take_screenshot_hotkey.get()
|
198
|
+
take_screenshot=self.take_screenshot_hotkey.get(),
|
199
|
+
open_utility=self.open_utility_hotkey.get()
|
193
200
|
),
|
194
201
|
vad=VAD(
|
195
202
|
whisper_model=self.whisper_model.get(),
|
@@ -262,11 +269,11 @@ class ConfigApp:
|
|
262
269
|
general_frame = ttk.Frame(self.notebook)
|
263
270
|
self.notebook.add(general_frame, text='General')
|
264
271
|
|
265
|
-
ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
|
272
|
+
ttk.Label(general_frame, text="Websocket Enabled (Clipboard Disabled):").grid(row=self.current_row, column=0, sticky='W')
|
266
273
|
self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
|
267
274
|
ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
|
268
275
|
sticky='W')
|
269
|
-
self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication.",
|
276
|
+
self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor. RESTART REQUIRED.",
|
270
277
|
row=self.current_row, column=2)
|
271
278
|
|
272
279
|
ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
|
@@ -276,6 +283,13 @@ class ConfigApp:
|
|
276
283
|
self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting.", row=self.current_row,
|
277
284
|
column=2)
|
278
285
|
|
286
|
+
ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
|
287
|
+
self.texthook_replacement_regex = ttk.Entry(general_frame)
|
288
|
+
self.texthook_replacement_regex.insert(0, self.settings.general.texthook_replacement_regex)
|
289
|
+
self.texthook_replacement_regex.grid(row=self.current_row, column=1)
|
290
|
+
self.add_label_and_increment_row(general_frame, "Regex to run replacement on texthook input, set this to the same as what you may have in your texthook page.", row=self.current_row,
|
291
|
+
column=2)
|
292
|
+
|
279
293
|
ttk.Label(general_frame, text="Open Config on Startup:").grid(row=self.current_row, column=0, sticky='W')
|
280
294
|
self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
|
281
295
|
ttk.Checkbutton(general_frame, variable=self.open_config_on_startup).grid(row=self.current_row, column=1,
|
@@ -532,6 +546,13 @@ class ConfigApp:
|
|
532
546
|
self.add_label_and_increment_row(anki_frame, "Overwrite existing pictures in Anki cards.", row=self.current_row,
|
533
547
|
column=2)
|
534
548
|
|
549
|
+
ttk.Label(anki_frame, text="Multi-line Mining Overwrite Sentence:").grid(row=self.current_row, column=0, sticky='W')
|
550
|
+
self.multi_overwrites_sentence = tk.BooleanVar(
|
551
|
+
value=self.settings.anki.multi_overwrites_sentence)
|
552
|
+
ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence).grid(row=self.current_row, column=1, sticky='W')
|
553
|
+
self.add_label_and_increment_row(anki_frame, "When using Multi-line Mining, overrwrite the sentence with a concatenation of the lines selected.", row=self.current_row,
|
554
|
+
column=2)
|
555
|
+
|
535
556
|
self.anki_custom_fields = self.settings.anki.anki_custom_fields
|
536
557
|
self.custom_field_entries = []
|
537
558
|
|
@@ -729,6 +750,13 @@ class ConfigApp:
|
|
729
750
|
row=self.current_row,
|
730
751
|
column=2)
|
731
752
|
|
753
|
+
ttk.Label(audio_frame, text="Grab all Future Audio when Mining from History:").grid(row=self.current_row, column=0, sticky='W')
|
754
|
+
self.mining_from_history_grab_all_audio = tk.BooleanVar(
|
755
|
+
value=self.settings.audio.mining_from_history_grab_all_audio)
|
756
|
+
ttk.Checkbutton(audio_frame, variable=self.mining_from_history_grab_all_audio).grid(row=self.current_row, column=1, sticky='W')
|
757
|
+
self.add_label_and_increment_row(audio_frame, "When mining from History, this option will allow the script to get all audio from that line to the current time.", row=self.current_row,
|
758
|
+
column=2)
|
759
|
+
|
732
760
|
@new_tab
|
733
761
|
def create_obs_tab(self):
|
734
762
|
obs_frame = ttk.Frame(self.notebook)
|
@@ -740,6 +768,18 @@ class ConfigApp:
|
|
740
768
|
self.add_label_and_increment_row(obs_frame, "Enable or disable OBS integration.", row=self.current_row,
|
741
769
|
column=2)
|
742
770
|
|
771
|
+
ttk.Label(obs_frame, text="Open OBS:").grid(row=self.current_row, column=0, sticky='W')
|
772
|
+
self.open_obs = tk.BooleanVar(value=self.settings.obs.open_obs)
|
773
|
+
ttk.Checkbutton(obs_frame, variable=self.open_obs).grid(row=self.current_row, column=1, sticky='W')
|
774
|
+
self.add_label_and_increment_row(obs_frame, "Open OBS when the GSM starts.", row=self.current_row,
|
775
|
+
column=2)
|
776
|
+
|
777
|
+
ttk.Label(obs_frame, text="Close OBS:").grid(row=self.current_row, column=0, sticky='W')
|
778
|
+
self.close_obs = tk.BooleanVar(value=self.settings.obs.close_obs)
|
779
|
+
ttk.Checkbutton(obs_frame, variable=self.close_obs).grid(row=self.current_row, column=1, sticky='W')
|
780
|
+
self.add_label_and_increment_row(obs_frame, "Close OBS when the GSM closes.", row=self.current_row,
|
781
|
+
column=2)
|
782
|
+
|
743
783
|
ttk.Label(obs_frame, text="Host:").grid(row=self.current_row, column=0, sticky='W')
|
744
784
|
self.obs_host = ttk.Entry(obs_frame)
|
745
785
|
self.obs_host.insert(0, self.settings.obs.host)
|
@@ -800,6 +840,12 @@ class ConfigApp:
|
|
800
840
|
self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
|
801
841
|
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
|
802
842
|
|
843
|
+
ttk.Label(hotkeys_frame, text="Open Utility Hotkey:").grid(row=self.current_row, column=0, sticky='W')
|
844
|
+
self.open_utility_hotkey = ttk.Entry(hotkeys_frame)
|
845
|
+
self.open_utility_hotkey.insert(0, self.settings.hotkeys.open_utility)
|
846
|
+
self.open_utility_hotkey.grid(row=self.current_row, column=1)
|
847
|
+
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to open the text utility.", row=self.current_row, column=2)
|
848
|
+
|
803
849
|
|
804
850
|
@new_tab
|
805
851
|
def create_profiles_tab(self):
|
@@ -41,6 +41,7 @@ class General:
|
|
41
41
|
websocket_uri: str = 'localhost:6677'
|
42
42
|
open_config_on_startup: bool = False
|
43
43
|
check_for_update_on_startup: bool = False
|
44
|
+
texthook_replacement_regex: str = ""
|
44
45
|
|
45
46
|
|
46
47
|
@dataclass_json
|
@@ -71,6 +72,7 @@ class Anki:
|
|
71
72
|
polling_rate: int = 200
|
72
73
|
overwrite_audio: bool = False
|
73
74
|
overwrite_picture: bool = True
|
75
|
+
multi_overwrites_sentence: bool = True
|
74
76
|
anki_custom_fields: Dict[str, str] = None # Initialize to None and set it in __post_init__
|
75
77
|
|
76
78
|
def __post_init__(self):
|
@@ -112,12 +114,15 @@ class Audio:
|
|
112
114
|
ffmpeg_reencode_options: str = ''
|
113
115
|
external_tool: str = ""
|
114
116
|
anki_media_collection: str = ""
|
117
|
+
mining_from_history_grab_all_audio: bool = False
|
115
118
|
|
116
119
|
|
117
120
|
@dataclass_json
|
118
121
|
@dataclass
|
119
122
|
class OBS:
|
120
123
|
enabled: bool = True
|
124
|
+
open_obs: bool = True
|
125
|
+
close_obs: bool = False
|
121
126
|
host: str = "localhost"
|
122
127
|
port: int = 4455
|
123
128
|
password: str = "your_password"
|
@@ -131,6 +136,7 @@ class OBS:
|
|
131
136
|
class Hotkeys:
|
132
137
|
reset_line: str = 'f5'
|
133
138
|
take_screenshot: str = 'f6'
|
139
|
+
open_utility: str = 'ctrl+m'
|
134
140
|
|
135
141
|
|
136
142
|
@dataclass_json
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -185,14 +185,14 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_l
|
|
185
185
|
ffmpeg_command = ffmpeg_base_command_list + [
|
186
186
|
"-i", untrimmed_audio,
|
187
187
|
"-ss", start_trim_time]
|
188
|
-
if next_line and next_line > line_time:
|
188
|
+
if next_line and next_line > line_time and not get_config().audio.mining_from_history_grab_all_audio:
|
189
189
|
end_total_seconds = total_seconds + (next_line - line_time).total_seconds() + 1
|
190
190
|
hours, remainder = divmod(end_total_seconds, 3600)
|
191
191
|
minutes, seconds = divmod(remainder, 60)
|
192
192
|
end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
193
193
|
ffmpeg_command.extend(['-to', end_trim_time])
|
194
194
|
logger.info(
|
195
|
-
f"Looks
|
195
|
+
f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time}")
|
196
196
|
|
197
197
|
ffmpeg_command.extend([
|
198
198
|
"-c", "copy", # Using copy to avoid re-encoding, adjust if needed
|
GameSentenceMiner/gametext.py
CHANGED
@@ -1,24 +1,27 @@
|
|
1
1
|
import asyncio
|
2
|
+
import re
|
2
3
|
import threading
|
3
4
|
import time
|
4
5
|
from collections import OrderedDict
|
5
6
|
from datetime import datetime
|
7
|
+
from typing import Callable
|
6
8
|
|
7
9
|
import pyperclip
|
8
10
|
import websockets
|
9
11
|
|
10
|
-
from GameSentenceMiner import util
|
11
12
|
from GameSentenceMiner.configuration import *
|
12
13
|
from GameSentenceMiner.configuration import get_config, logger
|
13
14
|
from GameSentenceMiner.util import remove_html_tags
|
14
15
|
from difflib import SequenceMatcher
|
15
16
|
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
current_line = ''
|
19
|
+
current_line_after_regex = ''
|
20
|
+
current_line_time = datetime.now()
|
19
21
|
|
20
22
|
line_history = OrderedDict()
|
21
23
|
reconnecting = False
|
24
|
+
multi_mine_event_bus: Callable[[str, datetime], None] = None
|
22
25
|
|
23
26
|
|
24
27
|
class ClipboardMonitor(threading.Thread):
|
@@ -28,25 +31,22 @@ class ClipboardMonitor(threading.Thread):
|
|
28
31
|
self.daemon = True
|
29
32
|
|
30
33
|
def run(self):
|
31
|
-
global
|
34
|
+
global current_line_time, current_line, line_history
|
32
35
|
|
33
36
|
# Initial clipboard content
|
34
|
-
|
37
|
+
current_line = pyperclip.paste()
|
35
38
|
|
36
39
|
while True:
|
37
40
|
current_clipboard = pyperclip.paste()
|
38
41
|
|
39
|
-
if current_clipboard !=
|
40
|
-
|
41
|
-
previous_line_time = datetime.now()
|
42
|
-
line_history[previous_line] = previous_line_time
|
43
|
-
util.use_previous_audio = False
|
42
|
+
if current_clipboard != current_line:
|
43
|
+
handle_new_text_event(current_clipboard)
|
44
44
|
|
45
45
|
time.sleep(0.05)
|
46
46
|
|
47
47
|
|
48
48
|
async def listen_websocket():
|
49
|
-
global
|
49
|
+
global current_line, current_line_time, line_history, reconnecting
|
50
50
|
while True:
|
51
51
|
try:
|
52
52
|
async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
|
@@ -62,33 +62,41 @@ async def listen_websocket():
|
|
62
62
|
current_clipboard = data["sentence"]
|
63
63
|
except json.JSONDecodeError:
|
64
64
|
current_clipboard = message
|
65
|
-
|
66
|
-
|
67
|
-
previous_line = current_clipboard
|
68
|
-
previous_line_time = datetime.now()
|
69
|
-
line_history[previous_line] = previous_line_time
|
70
|
-
util.use_previous_audio = False
|
71
|
-
|
65
|
+
if current_clipboard != current_line:
|
66
|
+
handle_new_text_event(current_clipboard)
|
72
67
|
except (websockets.ConnectionClosed, ConnectionError) as e:
|
73
68
|
if not reconnecting:
|
74
69
|
logger.warning(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
|
75
70
|
reconnecting = True
|
76
71
|
await asyncio.sleep(5)
|
77
72
|
|
73
|
+
def handle_new_text_event(current_clipboard):
|
74
|
+
global current_line, current_line_time, line_history, current_line_after_regex
|
75
|
+
current_line = current_clipboard
|
76
|
+
if get_config().general.texthook_replacement_regex:
|
77
|
+
current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
|
78
|
+
else:
|
79
|
+
current_line_after_regex = current_line
|
80
|
+
current_line_time = datetime.now()
|
81
|
+
line_history[current_line_after_regex] = current_line_time
|
82
|
+
multi_mine_event_bus(current_line_after_regex, current_line_time)
|
83
|
+
logger.debug(f"New Line: {current_clipboard}")
|
84
|
+
|
78
85
|
|
79
86
|
def reset_line_hotkey_pressed():
|
80
|
-
global
|
87
|
+
global current_line_time
|
81
88
|
logger.info("LINE RESET HOTKEY PRESSED")
|
82
|
-
|
83
|
-
line_history[
|
84
|
-
util.use_previous_audio = False
|
89
|
+
current_line_time = datetime.now()
|
90
|
+
line_history[current_line_after_regex] = current_line_time
|
85
91
|
|
86
92
|
|
87
93
|
def run_websocket_listener():
|
88
94
|
asyncio.run(listen_websocket())
|
89
95
|
|
90
96
|
|
91
|
-
def start_text_monitor():
|
97
|
+
def start_text_monitor(send_to_mine_event_bus):
|
98
|
+
global multi_mine_event_bus
|
99
|
+
multi_mine_event_bus = send_to_mine_event_bus
|
92
100
|
if get_config().general.use_websocket:
|
93
101
|
text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
|
94
102
|
else:
|
@@ -101,9 +109,9 @@ def get_line_timing(last_note):
|
|
101
109
|
return SequenceMatcher(None, a, b).ratio()
|
102
110
|
|
103
111
|
if not last_note:
|
104
|
-
return
|
112
|
+
return current_line_time, 0
|
105
113
|
|
106
|
-
line_time =
|
114
|
+
line_time = current_line_time
|
107
115
|
next_line = 0
|
108
116
|
prev_clip_time = 0
|
109
117
|
|
@@ -154,4 +162,27 @@ def get_last_two_sentences(last_note):
|
|
154
162
|
logger.debug("Couldn't find lines in history, using last two lines")
|
155
163
|
return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
|
156
164
|
|
157
|
-
return current_line, prev_line
|
165
|
+
return current_line, prev_line
|
166
|
+
|
167
|
+
|
168
|
+
def get_line_and_future_lines(last_note):
|
169
|
+
def similar(a, b):
|
170
|
+
return SequenceMatcher(None, a, b).ratio()
|
171
|
+
lines = list(line_history.items())
|
172
|
+
|
173
|
+
if not last_note:
|
174
|
+
return []
|
175
|
+
|
176
|
+
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
177
|
+
found_lines = []
|
178
|
+
if sentence:
|
179
|
+
found = False
|
180
|
+
for i, (line, clip_time) in enumerate(lines):
|
181
|
+
similarity = similar(remove_html_tags(sentence), line)
|
182
|
+
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
|
183
|
+
if found:
|
184
|
+
found_lines.append(line)
|
185
|
+
if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
|
186
|
+
found = True
|
187
|
+
found_lines.append(line)
|
188
|
+
return found_lines
|
GameSentenceMiner/gsm.py
CHANGED
@@ -2,6 +2,7 @@ import signal
|
|
2
2
|
import subprocess
|
3
3
|
import sys
|
4
4
|
import time
|
5
|
+
import ttkbootstrap as ttk
|
5
6
|
from subprocess import Popen
|
6
7
|
|
7
8
|
import keyboard
|
@@ -11,6 +12,7 @@ from pystray import Icon, Menu, MenuItem
|
|
11
12
|
from watchdog.events import FileSystemEventHandler
|
12
13
|
from watchdog.observers import Observer
|
13
14
|
|
15
|
+
from GameSentenceMiner import utility_gui
|
14
16
|
from GameSentenceMiner import anki
|
15
17
|
from GameSentenceMiner import config_gui
|
16
18
|
from GameSentenceMiner import configuration
|
@@ -32,9 +34,11 @@ if is_windows():
|
|
32
34
|
obs_process: Popen = None
|
33
35
|
procs_to_close = []
|
34
36
|
settings_window: config_gui.ConfigApp = None
|
37
|
+
utility_window: utility_gui.UtilityApp = None
|
35
38
|
obs_paused = False
|
36
39
|
icon: Icon
|
37
40
|
menu: Menu
|
41
|
+
root = None
|
38
42
|
|
39
43
|
|
40
44
|
class VideoToAudioHandler(FileSystemEventHandler):
|
@@ -60,21 +64,22 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
60
64
|
logger.error(
|
61
65
|
f"Video was unusually small, potentially empty! Check OBS for Correct Scene Settings! Path: {video_path}")
|
62
66
|
return
|
63
|
-
util.use_previous_audio = True
|
64
67
|
last_note = None
|
65
68
|
logger.debug("Attempting to get last anki card")
|
66
69
|
if get_config().anki.update_anki:
|
67
70
|
last_note = anki.get_last_anki_card()
|
68
71
|
if get_config().features.backfill_audio:
|
69
|
-
last_note = anki.get_cards_by_sentence(gametext.
|
72
|
+
last_note = anki.get_cards_by_sentence(gametext.current_line)
|
70
73
|
line_time, next_line_time = get_line_timing(last_note)
|
74
|
+
if utility_window.lines_selected():
|
75
|
+
line_time, next_line_time = utility_window.get_selected_times()
|
71
76
|
ss_timing = 0
|
72
77
|
if line_time and next_line_time:
|
73
78
|
ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
|
74
79
|
if last_note:
|
75
80
|
logger.debug(json.dumps(last_note))
|
76
81
|
|
77
|
-
note = anki.get_initial_card_info(last_note)
|
82
|
+
note = anki.get_initial_card_info(last_note, utility_window.get_selected_lines())
|
78
83
|
|
79
84
|
tango = last_note['fields'][get_config().anki.word_field]['value'] if last_note else ''
|
80
85
|
|
@@ -106,6 +111,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
106
111
|
os.remove(video_path) # Optionally remove the video after conversion
|
107
112
|
if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
108
113
|
os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
|
114
|
+
utility_window.reset_checkboxes()
|
109
115
|
|
110
116
|
@staticmethod
|
111
117
|
def get_audio(line_time, next_line_time, video_path):
|
@@ -150,10 +156,11 @@ def initialize(reloading=False):
|
|
150
156
|
download_obs_if_needed()
|
151
157
|
download_ffmpeg_if_needed()
|
152
158
|
if get_config().obs.enabled:
|
153
|
-
|
159
|
+
if get_config().obs.open_obs:
|
160
|
+
obs_process = obs.start_obs()
|
154
161
|
obs.connect_to_obs(start_replay=True)
|
155
162
|
anki.start_monitoring_anki()
|
156
|
-
gametext.start_text_monitor()
|
163
|
+
gametext.start_text_monitor(utility_window.add_text)
|
157
164
|
os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
|
158
165
|
os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
|
159
166
|
os.makedirs(get_config().paths.audio_destination, exist_ok=True)
|
@@ -175,6 +182,7 @@ def initial_checks():
|
|
175
182
|
def register_hotkeys():
|
176
183
|
keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
|
177
184
|
keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
|
185
|
+
keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
|
178
186
|
|
179
187
|
|
180
188
|
def get_screenshot():
|
@@ -189,7 +197,7 @@ def get_screenshot():
|
|
189
197
|
if last_note:
|
190
198
|
logger.debug(json.dumps(last_note))
|
191
199
|
if get_config().features.backfill_audio:
|
192
|
-
last_note = anki.get_cards_by_sentence(gametext.
|
200
|
+
last_note = anki.get_cards_by_sentence(gametext.current_line)
|
193
201
|
if last_note:
|
194
202
|
anki.add_image_to_card(last_note, encoded_image)
|
195
203
|
notification.send_screenshot_updated(last_note['fields'][get_config().anki.word_field]['value'])
|
@@ -219,11 +227,14 @@ def create_image():
|
|
219
227
|
|
220
228
|
return image
|
221
229
|
|
222
|
-
|
223
230
|
def open_settings():
|
224
231
|
obs.update_current_game()
|
225
232
|
settings_window.show()
|
226
233
|
|
234
|
+
def open_multimine():
|
235
|
+
obs.update_current_game()
|
236
|
+
utility_window.show()
|
237
|
+
|
227
238
|
|
228
239
|
def open_log():
|
229
240
|
"""Function to handle opening log."""
|
@@ -267,6 +278,7 @@ def update_icon():
|
|
267
278
|
|
268
279
|
menu = Menu(
|
269
280
|
MenuItem("Open Settings", open_settings),
|
281
|
+
MenuItem("Open Multi-Mine GUI", open_multimine),
|
270
282
|
MenuItem("Open Log", open_log),
|
271
283
|
MenuItem("Toggle Replay Buffer", play_pause),
|
272
284
|
MenuItem("Restart OBS", restart_obs),
|
@@ -299,6 +311,7 @@ def run_tray():
|
|
299
311
|
|
300
312
|
menu = Menu(
|
301
313
|
MenuItem("Open Settings", open_settings),
|
314
|
+
MenuItem("Open Multi-Mine GUI", open_multimine),
|
302
315
|
MenuItem("Open Log", open_log),
|
303
316
|
MenuItem("Toggle Replay Buffer", play_pause),
|
304
317
|
MenuItem("Restart OBS", restart_obs),
|
@@ -331,7 +344,8 @@ def cleanup():
|
|
331
344
|
if get_config().obs.start_buffer:
|
332
345
|
obs.stop_replay_buffer()
|
333
346
|
obs.disconnect_from_obs()
|
334
|
-
|
347
|
+
if get_config().obs.close_obs:
|
348
|
+
close_obs()
|
335
349
|
|
336
350
|
proc: Popen
|
337
351
|
for proc in procs_to_close:
|
@@ -363,8 +377,11 @@ def handle_exit():
|
|
363
377
|
|
364
378
|
|
365
379
|
def main(reloading=False, do_config_input=True):
|
366
|
-
global settings_window
|
380
|
+
global root, settings_window, utility_window
|
367
381
|
logger.info("Script started.")
|
382
|
+
root = ttk.Window(themename='darkly')
|
383
|
+
settings_window = config_gui.ConfigApp(root)
|
384
|
+
utility_window = utility_gui.UtilityApp(root)
|
368
385
|
initialize(reloading)
|
369
386
|
initial_checks()
|
370
387
|
event_handler = VideoToAudioHandler()
|
@@ -385,14 +402,13 @@ def main(reloading=False, do_config_input=True):
|
|
385
402
|
util.run_new_thread(run_tray)
|
386
403
|
|
387
404
|
try:
|
388
|
-
settings_window = config_gui.ConfigApp()
|
389
405
|
if get_config().general.check_for_update_on_startup:
|
390
|
-
|
406
|
+
root.after(0, settings_window.check_update)
|
391
407
|
if get_config().general.open_config_on_startup:
|
392
|
-
|
408
|
+
root.after(0, settings_window.show)
|
393
409
|
settings_window.add_save_hook(update_icon)
|
394
410
|
settings_window.on_exit = exit_program
|
395
|
-
|
411
|
+
root.mainloop()
|
396
412
|
except KeyboardInterrupt:
|
397
413
|
cleanup()
|
398
414
|
|
GameSentenceMiner/util.py
CHANGED
@@ -14,10 +14,16 @@ from GameSentenceMiner.configuration import logger
|
|
14
14
|
SCRIPTS_DIR = r"E:\Japanese Stuff\agent-v0.1.4-win32-x64\data\scripts"
|
15
15
|
|
16
16
|
# Global variables to control script execution
|
17
|
-
use_previous_audio = False
|
18
17
|
keep_running = True
|
19
18
|
lock = threading.Lock()
|
19
|
+
last_mined_line = None
|
20
20
|
|
21
|
+
def get_last_mined_line():
|
22
|
+
return last_mined_line
|
23
|
+
|
24
|
+
def set_last_mined_line(line):
|
25
|
+
global last_mined_line
|
26
|
+
last_mined_line = line
|
21
27
|
|
22
28
|
def run_new_thread(func):
|
23
29
|
thread = threading.Thread(target=func, daemon=True)
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import tkinter as tk
|
2
|
+
from tkinter import ttk
|
3
|
+
|
4
|
+
from GameSentenceMiner.configuration import logger
|
5
|
+
|
6
|
+
|
7
|
+
class UtilityApp:
|
8
|
+
def __init__(self, root):
|
9
|
+
self.root = root
|
10
|
+
|
11
|
+
self.items = []
|
12
|
+
self.checkboxes = []
|
13
|
+
self.multi_mine_window = None # Store the multi-mine window reference
|
14
|
+
self.checkbox_frame = None
|
15
|
+
|
16
|
+
style = ttk.Style()
|
17
|
+
style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
|
18
|
+
|
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
|
+
def show(self):
|
26
|
+
""" Open the multi-mine window only if it doesn't exist. """
|
27
|
+
if not self.multi_mine_window or not tk.Toplevel.winfo_exists(self.multi_mine_window):
|
28
|
+
logger.info("opening multi-mine_window")
|
29
|
+
self.multi_mine_window = tk.Toplevel(self.root)
|
30
|
+
self.multi_mine_window.title("Multi Mine Window")
|
31
|
+
|
32
|
+
self.multi_mine_window.minsize(800, 400) # Set a minimum size to prevent shrinking too
|
33
|
+
|
34
|
+
self.checkbox_frame = ttk.Frame(self.multi_mine_window)
|
35
|
+
self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
36
|
+
|
37
|
+
# Add existing items
|
38
|
+
for text, var, time in self.items:
|
39
|
+
self.add_checkbox_to_gui(text, var, time)
|
40
|
+
else:
|
41
|
+
self.multi_mine_window.deiconify()
|
42
|
+
self.multi_mine_window.lift()
|
43
|
+
|
44
|
+
def add_text(self, text, time):
|
45
|
+
if text:
|
46
|
+
var = tk.BooleanVar()
|
47
|
+
self.items.append((text, var, time))
|
48
|
+
|
49
|
+
# Remove the first checkbox if there are more than 10
|
50
|
+
if len(self.items) > 10:
|
51
|
+
self.checkboxes[0].destroy()
|
52
|
+
self.checkboxes.pop(0)
|
53
|
+
self.items.pop(0)
|
54
|
+
|
55
|
+
if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
|
56
|
+
self.add_checkbox_to_gui(text, var, time)
|
57
|
+
|
58
|
+
def add_checkbox_to_gui(self, text, var, time):
|
59
|
+
""" Add a single checkbox without repainting everything. """
|
60
|
+
if self.checkbox_frame:
|
61
|
+
chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
|
62
|
+
chk.pack(anchor='w')
|
63
|
+
self.checkboxes.append(chk)
|
64
|
+
|
65
|
+
|
66
|
+
# def update_multi_mine_window(self):
|
67
|
+
# for widget in self.multi_mine_window.winfo_children():
|
68
|
+
# widget.destroy()
|
69
|
+
#
|
70
|
+
# for i, (text, var, time) in enumerate(self.items):
|
71
|
+
# time: datetime
|
72
|
+
# chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
|
73
|
+
# chk.pack(anchor='w')
|
74
|
+
|
75
|
+
def get_selected_lines(self):
|
76
|
+
filtered_items = [text for text, var, _ in self.items if var.get()]
|
77
|
+
return filtered_items if len(filtered_items) >= 2 else []
|
78
|
+
|
79
|
+
def get_selected_times(self):
|
80
|
+
filtered_times = [time for _, var, time in self.items if var.get()]
|
81
|
+
|
82
|
+
if len(filtered_times) >= 2:
|
83
|
+
logger.info(filtered_times)
|
84
|
+
# Find the index of the last checked checkbox
|
85
|
+
last_checked_index = max(i for i, (_, var, _) in enumerate(self.items) if var.get())
|
86
|
+
|
87
|
+
# Get the time AFTER the last checked checkbox, if it exists
|
88
|
+
if last_checked_index + 1 < len(self.items):
|
89
|
+
next_time = self.items[last_checked_index + 1][2]
|
90
|
+
else:
|
91
|
+
next_time = 0
|
92
|
+
|
93
|
+
return filtered_times[0], next_time
|
94
|
+
|
95
|
+
return None
|
96
|
+
|
97
|
+
def lines_selected(self):
|
98
|
+
filter_times = [time for _, var, time in self.items if var.get()]
|
99
|
+
if len(filter_times) >= 2:
|
100
|
+
return True
|
101
|
+
return False
|
102
|
+
|
103
|
+
# def validate_checkboxes(self, *args):
|
104
|
+
# logger.debug("Validating checkboxes")
|
105
|
+
# found_checked = False
|
106
|
+
# found_unchecked = False
|
107
|
+
# for _, var in self.items:
|
108
|
+
# if var.get():
|
109
|
+
# if found_unchecked:
|
110
|
+
# messagebox.showinfo("Invalid", "Can only select neighboring checkboxes.")
|
111
|
+
# break
|
112
|
+
# found_checked = True
|
113
|
+
# if found_checked and not var.get():
|
114
|
+
# found_unchecked = True
|
115
|
+
|
116
|
+
def reset_checkboxes(self):
|
117
|
+
for _, var, _ in self.items:
|
118
|
+
var.set(False)
|
119
|
+
# if self.multi_mine_window:
|
120
|
+
# for checkbox in self.checkboxes:
|
121
|
+
# checkbox.set(False)
|
122
|
+
|
123
|
+
|
124
|
+
if __name__ == "__main__":
|
125
|
+
root = tk.Tk()
|
126
|
+
app = UtilityApp(root)
|
127
|
+
root.mainloop()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: GameSentenceMiner
|
3
|
-
Version: 2.
|
4
|
-
Summary: A tool for mining sentences from games. Update:
|
3
|
+
Version: 2.3.1
|
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
|
7
7
|
Project-URL: Homepage, https://github.com/bpwhelan/GameSentenceMiner
|
@@ -1,15 +1,16 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=
|
4
|
-
GameSentenceMiner/configuration.py,sha256=
|
5
|
-
GameSentenceMiner/ffmpeg.py,sha256=
|
6
|
-
GameSentenceMiner/gametext.py,sha256=
|
7
|
-
GameSentenceMiner/gsm.py,sha256=
|
2
|
+
GameSentenceMiner/anki.py,sha256=gpd_af7zrwuGyIfMAPvunZW1I0mVG2MBemVnikTVp5c,10151
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=B37GPeGtGNTJgFTor3M01Tw2iZYRRtHLYck6Q83-JRg,53460
|
4
|
+
GameSentenceMiner/configuration.py,sha256=x50-InaGl70dHpWhdOAZJTIKj84EtVfTqEmIFCnDcvw,14348
|
5
|
+
GameSentenceMiner/ffmpeg.py,sha256=VExJYWSFhYuWukIXgOiHufsoSROEDA8LnVQFG8srRGc,10924
|
6
|
+
GameSentenceMiner/gametext.py,sha256=ckZGOHpGuvFE2zDNdaASSaMYi4JCFsyG2kYKCbZIQPo,6463
|
7
|
+
GameSentenceMiner/gsm.py,sha256=l92vu9P06v5crBfka-TGjlSyqM7WG2ND4ZzWQVTY7ws,17103
|
8
8
|
GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
|
9
9
|
GameSentenceMiner/notification.py,sha256=WBaQWoPNhW4XqdPBUmxPBgjk0ngzH_4v9zMQ-XQAKC8,2010
|
10
10
|
GameSentenceMiner/obs.py,sha256=3Flcjxy812VpF78EPI7sxlGx6yyM3GfqzlinW17SK20,6231
|
11
11
|
GameSentenceMiner/package_updater.py,sha256=0uaLAp0WrWqostNTBWRS0laITjI9aN9Yt_6GXosS4NQ,1278
|
12
|
-
GameSentenceMiner/util.py,sha256=
|
12
|
+
GameSentenceMiner/util.py,sha256=MITweiFYaefWQF5nR8tZ9yE6vd_b-fLuP0MP1Y1U4K0,4720
|
13
|
+
GameSentenceMiner/utility_gui.py,sha256=-T5b14Nx6KvNnBEmdVz0mWkXoYi-bZzHIce6ADwfVEQ,4701
|
13
14
|
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
14
15
|
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
16
|
GameSentenceMiner/downloader/download_tools.py,sha256=M7vLo6_0QMuk1Ji4CsZqk1C2g7Bq6PyM29r7XNoP6Rw,6406
|
@@ -17,8 +18,8 @@ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
17
18
|
GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
|
18
19
|
GameSentenceMiner/vad/vosk_helper.py,sha256=-AAwK0cgOC5rK3_gL0sQgrPJ75E49g_PxZR4d5ckwc4,5826
|
19
20
|
GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
|
20
|
-
GameSentenceMiner-2.
|
21
|
-
GameSentenceMiner-2.
|
22
|
-
GameSentenceMiner-2.
|
23
|
-
GameSentenceMiner-2.
|
24
|
-
GameSentenceMiner-2.
|
21
|
+
GameSentenceMiner-2.3.1.dist-info/METADATA,sha256=UN9Pn6JrUpTuIeMRBT3CAizAUNmVM9aTKw6DCBwxTOg,10120
|
22
|
+
GameSentenceMiner-2.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
23
|
+
GameSentenceMiner-2.3.1.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
24
|
+
GameSentenceMiner-2.3.1.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
25
|
+
GameSentenceMiner-2.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|