GameSentenceMiner 2.2.4__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/anki.py +13 -3
- GameSentenceMiner/config_gui.py +39 -7
- GameSentenceMiner/configuration.py +4 -0
- GameSentenceMiner/ffmpeg.py +2 -2
- GameSentenceMiner/gametext.py +58 -24
- GameSentenceMiner/gsm.py +25 -10
- GameSentenceMiner/utility_gui.py +127 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.0.dist-info}/METADATA +2 -2
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.0.dist-info}/RECORD +12 -11
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.0.dist-info}/WHEEL +0 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.0.dist-info}/entry_points.txt +0 -0
- {GameSentenceMiner-2.2.4.dist-info → GameSentenceMiner-2.3.0.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -6,7 +6,7 @@ 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
|
@@ -103,7 +103,7 @@ def add_image_to_card(last_note, image_path):
|
|
103
103
|
logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note['noteId']}")
|
104
104
|
|
105
105
|
|
106
|
-
def get_initial_card_info(last_note):
|
106
|
+
def get_initial_card_info(last_note, selected_lines):
|
107
107
|
note = {'id': last_note['noteId'], 'fields': {}}
|
108
108
|
if not last_note:
|
109
109
|
return note
|
@@ -112,6 +112,16 @@ def get_initial_card_info(last_note):
|
|
112
112
|
logger.debug(f"Current Sentence {current_line}")
|
113
113
|
util.use_previous_audio = True
|
114
114
|
|
115
|
+
if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
|
116
|
+
lines = gametext.get_line_and_future_lines(last_note)
|
117
|
+
logger.info(lines)
|
118
|
+
logger.info("".join(lines))
|
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)
|
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']}")
|
117
127
|
if get_config().anki.previous_sentence_field and previous_line and not \
|
@@ -211,7 +221,7 @@ def update_new_card():
|
|
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()
|
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,7 +179,8 @@ 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(),
|
@@ -189,7 +193,8 @@ class ConfigApp:
|
|
189
193
|
),
|
190
194
|
hotkeys=Hotkeys(
|
191
195
|
reset_line=self.reset_line_hotkey.get(),
|
192
|
-
take_screenshot=self.take_screenshot_hotkey.get()
|
196
|
+
take_screenshot=self.take_screenshot_hotkey.get(),
|
197
|
+
open_utility=self.open_utility_hotkey.get()
|
193
198
|
),
|
194
199
|
vad=VAD(
|
195
200
|
whisper_model=self.whisper_model.get(),
|
@@ -262,11 +267,11 @@ class ConfigApp:
|
|
262
267
|
general_frame = ttk.Frame(self.notebook)
|
263
268
|
self.notebook.add(general_frame, text='General')
|
264
269
|
|
265
|
-
ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
|
270
|
+
ttk.Label(general_frame, text="Websocket Enabled (Clipboard Disabled):").grid(row=self.current_row, column=0, sticky='W')
|
266
271
|
self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
|
267
272
|
ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
|
268
273
|
sticky='W')
|
269
|
-
self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication.",
|
274
|
+
self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor. RESTART REQUIRED.",
|
270
275
|
row=self.current_row, column=2)
|
271
276
|
|
272
277
|
ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
|
@@ -276,6 +281,13 @@ class ConfigApp:
|
|
276
281
|
self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting.", row=self.current_row,
|
277
282
|
column=2)
|
278
283
|
|
284
|
+
ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
|
285
|
+
self.texthook_replacement_regex = ttk.Entry(general_frame)
|
286
|
+
self.texthook_replacement_regex.insert(0, self.settings.general.texthook_replacement_regex)
|
287
|
+
self.texthook_replacement_regex.grid(row=self.current_row, column=1)
|
288
|
+
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,
|
289
|
+
column=2)
|
290
|
+
|
279
291
|
ttk.Label(general_frame, text="Open Config on Startup:").grid(row=self.current_row, column=0, sticky='W')
|
280
292
|
self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
|
281
293
|
ttk.Checkbutton(general_frame, variable=self.open_config_on_startup).grid(row=self.current_row, column=1,
|
@@ -532,6 +544,13 @@ class ConfigApp:
|
|
532
544
|
self.add_label_and_increment_row(anki_frame, "Overwrite existing pictures in Anki cards.", row=self.current_row,
|
533
545
|
column=2)
|
534
546
|
|
547
|
+
ttk.Label(anki_frame, text="Multi-line Mining Overwrite Sentence:").grid(row=self.current_row, column=0, sticky='W')
|
548
|
+
self.multi_overwrites_sentence = tk.BooleanVar(
|
549
|
+
value=self.settings.anki.multi_overwrites_sentence)
|
550
|
+
ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence).grid(row=self.current_row, column=1, sticky='W')
|
551
|
+
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,
|
552
|
+
column=2)
|
553
|
+
|
535
554
|
self.anki_custom_fields = self.settings.anki.anki_custom_fields
|
536
555
|
self.custom_field_entries = []
|
537
556
|
|
@@ -729,6 +748,13 @@ class ConfigApp:
|
|
729
748
|
row=self.current_row,
|
730
749
|
column=2)
|
731
750
|
|
751
|
+
ttk.Label(audio_frame, text="Grab all Future Audio when Mining from History:").grid(row=self.current_row, column=0, sticky='W')
|
752
|
+
self.mining_from_history_grab_all_audio = tk.BooleanVar(
|
753
|
+
value=self.settings.audio.mining_from_history_grab_all_audio)
|
754
|
+
ttk.Checkbutton(audio_frame, variable=self.mining_from_history_grab_all_audio).grid(row=self.current_row, column=1, sticky='W')
|
755
|
+
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,
|
756
|
+
column=2)
|
757
|
+
|
732
758
|
@new_tab
|
733
759
|
def create_obs_tab(self):
|
734
760
|
obs_frame = ttk.Frame(self.notebook)
|
@@ -800,6 +826,12 @@ class ConfigApp:
|
|
800
826
|
self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
|
801
827
|
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
|
802
828
|
|
829
|
+
ttk.Label(hotkeys_frame, text="Open Utility Hotkey:").grid(row=self.current_row, column=0, sticky='W')
|
830
|
+
self.open_utility_hotkey = ttk.Entry(hotkeys_frame)
|
831
|
+
self.open_utility_hotkey.insert(0, self.settings.hotkeys.open_utility)
|
832
|
+
self.open_utility_hotkey.grid(row=self.current_row, column=1)
|
833
|
+
self.add_label_and_increment_row(hotkeys_frame, "Hotkey to open the text utility.", row=self.current_row, column=2)
|
834
|
+
|
803
835
|
|
804
836
|
@new_tab
|
805
837
|
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,6 +114,7 @@ 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
|
@@ -131,6 +134,7 @@ class OBS:
|
|
131
134
|
class Hotkeys:
|
132
135
|
reset_line: str = 'f5'
|
133
136
|
take_screenshot: str = 'f6'
|
137
|
+
open_utility: str = 'ctrl+m'
|
134
138
|
|
135
139
|
|
136
140
|
@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,8 +1,10 @@
|
|
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
|
@@ -14,11 +16,13 @@ from GameSentenceMiner.util import remove_html_tags
|
|
14
16
|
from difflib import SequenceMatcher
|
15
17
|
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
+
current_line = ''
|
20
|
+
current_line_after_regex = ''
|
21
|
+
current_line_time = datetime.now()
|
19
22
|
|
20
23
|
line_history = OrderedDict()
|
21
24
|
reconnecting = False
|
25
|
+
multi_mine_event_bus: Callable[[str, datetime], None] = None
|
22
26
|
|
23
27
|
|
24
28
|
class ClipboardMonitor(threading.Thread):
|
@@ -28,25 +32,22 @@ class ClipboardMonitor(threading.Thread):
|
|
28
32
|
self.daemon = True
|
29
33
|
|
30
34
|
def run(self):
|
31
|
-
global
|
35
|
+
global current_line_time, current_line, line_history
|
32
36
|
|
33
37
|
# Initial clipboard content
|
34
|
-
|
38
|
+
current_line = pyperclip.paste()
|
35
39
|
|
36
40
|
while True:
|
37
41
|
current_clipboard = pyperclip.paste()
|
38
42
|
|
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
|
43
|
+
if current_clipboard != current_line:
|
44
|
+
handle_new_text_event(current_clipboard)
|
44
45
|
|
45
46
|
time.sleep(0.05)
|
46
47
|
|
47
48
|
|
48
49
|
async def listen_websocket():
|
49
|
-
global
|
50
|
+
global current_line, current_line_time, line_history, reconnecting
|
50
51
|
while True:
|
51
52
|
try:
|
52
53
|
async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
|
@@ -62,25 +63,33 @@ async def listen_websocket():
|
|
62
63
|
current_clipboard = data["sentence"]
|
63
64
|
except json.JSONDecodeError:
|
64
65
|
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
|
-
|
66
|
+
if current_clipboard != current_line:
|
67
|
+
handle_new_text_event(current_clipboard)
|
72
68
|
except (websockets.ConnectionClosed, ConnectionError) as e:
|
73
69
|
if not reconnecting:
|
74
70
|
logger.warning(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
|
75
71
|
reconnecting = True
|
76
72
|
await asyncio.sleep(5)
|
77
73
|
|
74
|
+
def handle_new_text_event(current_clipboard):
|
75
|
+
global current_line, current_line_time, line_history, current_line_after_regex
|
76
|
+
current_line = current_clipboard
|
77
|
+
if get_config().general.texthook_replacement_regex:
|
78
|
+
current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
|
79
|
+
else:
|
80
|
+
current_line_after_regex = current_line
|
81
|
+
current_line_time = datetime.now()
|
82
|
+
line_history[current_line_after_regex] = current_line_time
|
83
|
+
util.use_previous_audio = False
|
84
|
+
multi_mine_event_bus(current_line_after_regex, current_line_time)
|
85
|
+
logger.debug(f"New Line: {current_clipboard}")
|
86
|
+
|
78
87
|
|
79
88
|
def reset_line_hotkey_pressed():
|
80
|
-
global
|
89
|
+
global current_line_time
|
81
90
|
logger.info("LINE RESET HOTKEY PRESSED")
|
82
|
-
|
83
|
-
line_history[
|
91
|
+
current_line_time = datetime.now()
|
92
|
+
line_history[current_line_after_regex] = current_line_time
|
84
93
|
util.use_previous_audio = False
|
85
94
|
|
86
95
|
|
@@ -88,7 +97,9 @@ def run_websocket_listener():
|
|
88
97
|
asyncio.run(listen_websocket())
|
89
98
|
|
90
99
|
|
91
|
-
def start_text_monitor():
|
100
|
+
def start_text_monitor(send_to_mine_event_bus):
|
101
|
+
global multi_mine_event_bus
|
102
|
+
multi_mine_event_bus = send_to_mine_event_bus
|
92
103
|
if get_config().general.use_websocket:
|
93
104
|
text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
|
94
105
|
else:
|
@@ -101,9 +112,9 @@ def get_line_timing(last_note):
|
|
101
112
|
return SequenceMatcher(None, a, b).ratio()
|
102
113
|
|
103
114
|
if not last_note:
|
104
|
-
return
|
115
|
+
return current_line_time, 0
|
105
116
|
|
106
|
-
line_time =
|
117
|
+
line_time = current_line_time
|
107
118
|
next_line = 0
|
108
119
|
prev_clip_time = 0
|
109
120
|
|
@@ -154,4 +165,27 @@ def get_last_two_sentences(last_note):
|
|
154
165
|
logger.debug("Couldn't find lines in history, using last two lines")
|
155
166
|
return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
|
156
167
|
|
157
|
-
return current_line, prev_line
|
168
|
+
return current_line, prev_line
|
169
|
+
|
170
|
+
|
171
|
+
def get_line_and_future_lines(last_note):
|
172
|
+
def similar(a, b):
|
173
|
+
return SequenceMatcher(None, a, b).ratio()
|
174
|
+
lines = list(line_history.items())
|
175
|
+
|
176
|
+
if not last_note:
|
177
|
+
return []
|
178
|
+
|
179
|
+
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
180
|
+
found_lines = []
|
181
|
+
if sentence:
|
182
|
+
found = False
|
183
|
+
for i, (line, clip_time) in enumerate(lines):
|
184
|
+
similarity = similar(remove_html_tags(sentence), line)
|
185
|
+
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
|
186
|
+
if found:
|
187
|
+
found_lines.append(line)
|
188
|
+
if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
|
189
|
+
found = True
|
190
|
+
found_lines.append(line)
|
191
|
+
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):
|
@@ -66,15 +70,17 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
66
70
|
if get_config().anki.update_anki:
|
67
71
|
last_note = anki.get_last_anki_card()
|
68
72
|
if get_config().features.backfill_audio:
|
69
|
-
last_note = anki.get_cards_by_sentence(gametext.
|
73
|
+
last_note = anki.get_cards_by_sentence(gametext.current_line)
|
70
74
|
line_time, next_line_time = get_line_timing(last_note)
|
75
|
+
if utility_window.lines_selected():
|
76
|
+
line_time, next_line_time = utility_window.get_selected_times()
|
71
77
|
ss_timing = 0
|
72
78
|
if line_time and next_line_time:
|
73
79
|
ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
|
74
80
|
if last_note:
|
75
81
|
logger.debug(json.dumps(last_note))
|
76
82
|
|
77
|
-
note = anki.get_initial_card_info(last_note)
|
83
|
+
note = anki.get_initial_card_info(last_note, utility_window.get_selected_lines())
|
78
84
|
|
79
85
|
tango = last_note['fields'][get_config().anki.word_field]['value'] if last_note else ''
|
80
86
|
|
@@ -106,6 +112,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
106
112
|
os.remove(video_path) # Optionally remove the video after conversion
|
107
113
|
if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
108
114
|
os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
|
115
|
+
utility_window.reset_checkboxes()
|
109
116
|
|
110
117
|
@staticmethod
|
111
118
|
def get_audio(line_time, next_line_time, video_path):
|
@@ -153,7 +160,7 @@ def initialize(reloading=False):
|
|
153
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),
|
@@ -363,8 +376,11 @@ def handle_exit():
|
|
363
376
|
|
364
377
|
|
365
378
|
def main(reloading=False, do_config_input=True):
|
366
|
-
global settings_window
|
379
|
+
global root, settings_window, utility_window
|
367
380
|
logger.info("Script started.")
|
381
|
+
root = ttk.Window(themename='darkly')
|
382
|
+
settings_window = config_gui.ConfigApp(root)
|
383
|
+
utility_window = utility_gui.UtilityApp(root)
|
368
384
|
initialize(reloading)
|
369
385
|
initial_checks()
|
370
386
|
event_handler = VideoToAudioHandler()
|
@@ -385,14 +401,13 @@ def main(reloading=False, do_config_input=True):
|
|
385
401
|
util.run_new_thread(run_tray)
|
386
402
|
|
387
403
|
try:
|
388
|
-
settings_window = config_gui.ConfigApp()
|
389
404
|
if get_config().general.check_for_update_on_startup:
|
390
|
-
|
405
|
+
root.after(0, settings_window.check_update)
|
391
406
|
if get_config().general.open_config_on_startup:
|
392
|
-
|
407
|
+
root.after(0, settings_window.show)
|
393
408
|
settings_window.add_save_hook(update_icon)
|
394
409
|
settings_window.on_exit = exit_program
|
395
|
-
|
410
|
+
root.mainloop()
|
396
411
|
except KeyboardInterrupt:
|
397
412
|
cleanup()
|
398
413
|
|
@@ -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.0
|
4
|
+
Summary: A tool for mining sentences from games. Update: Multi-Line Mining!
|
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=895FTnqZCn2AmfUqxWKsSOkzumFoPRNhwPRf00mB7OI,9755
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=IYM_qZV22Hn_eGbXIqrQx2r7UGlTFDLBa0bqu3HqFl0,52491
|
4
|
+
GameSentenceMiner/configuration.py,sha256=X61ks7iuzf0Wu7U4QuhggUGB9-1KLka-V4cduoEOddA,14294
|
5
|
+
GameSentenceMiner/ffmpeg.py,sha256=VExJYWSFhYuWukIXgOiHufsoSROEDA8LnVQFG8srRGc,10924
|
6
|
+
GameSentenceMiner/gametext.py,sha256=ci1ZTwObi-5pxFWHcXp1cDWMrCz_zaPtUsWvTO_jwRU,6570
|
7
|
+
GameSentenceMiner/gsm.py,sha256=GY9T3h2dwY-XofXqESiyZpezRvlfaZrhzo0PDuuE87g,17065
|
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
12
|
GameSentenceMiner/util.py,sha256=cgKpPfRpouWI6tjE_35MWp8nXqRzXs3LvsYXWm5_DOg,4584
|
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.0.dist-info/METADATA,sha256=isZtUWBqPSC2Q-MG3HbL9aUT4uAbjHIiXhJFSYSR4mg,10113
|
22
|
+
GameSentenceMiner-2.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
23
|
+
GameSentenceMiner-2.3.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
24
|
+
GameSentenceMiner-2.3.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
25
|
+
GameSentenceMiner-2.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|