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 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()
@@ -49,8 +49,9 @@ class HoverInfoWidget:
49
49
 
50
50
 
51
51
  class ConfigApp:
52
- def __init__(self):
53
- self.window = ttk.Window(themename='darkly')
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
@@ -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 like Clipboard/Websocket was modified before the script knew about the anki card! Trimming end of video to {end_trim_time}")
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
@@ -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
- previous_line = ''
18
- previous_line_time = datetime.now()
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 previous_line_time, previous_line, line_history
35
+ global current_line_time, current_line, line_history
32
36
 
33
37
  # Initial clipboard content
34
- previous_line = pyperclip.paste()
38
+ current_line = pyperclip.paste()
35
39
 
36
40
  while True:
37
41
  current_clipboard = pyperclip.paste()
38
42
 
39
- if current_clipboard != previous_line:
40
- previous_line = current_clipboard
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 previous_line, previous_line_time, line_history, reconnecting
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
- if current_clipboard != previous_line:
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 previous_line_time
89
+ global current_line_time
81
90
  logger.info("LINE RESET HOTKEY PRESSED")
82
- previous_line_time = datetime.now()
83
- line_history[previous_line] = previous_line_time
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 previous_line_time, 0
115
+ return current_line_time, 0
105
116
 
106
- line_time = previous_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.previous_line)
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.previous_line)
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
- settings_window.window.after(0, settings_window.check_update)
405
+ root.after(0, settings_window.check_update)
391
406
  if get_config().general.open_config_on_startup:
392
- settings_window.window.after(0, settings_window.show)
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
- settings_window.window.mainloop()
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.2.4
4
- Summary: A tool for mining sentences from games. Update: Fix Previous Sentence When Mining from History
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=urgly10zM7etTXfagK327TKWXVq146jqkpTmewoim7s,9238
3
- GameSentenceMiner/config_gui.py,sha256=EBl5TuzyqXUovq4YF-UQsl4W7DAzIpdzANI52AiTyoU,49743
4
- GameSentenceMiner/configuration.py,sha256=Wd4Cozdus_Tl33_Qz4twibBx6p_3_mHMzdZfZ-B1qI4,14124
5
- GameSentenceMiner/ffmpeg.py,sha256=txTpco-IGWtfF8vIiWUzrtgI5TA1xPVIK-WJWxU02mM,10878
6
- GameSentenceMiner/gametext.py,sha256=QQbZnV1eZ1DxwJl9fwfn8p6z1UjpSk6JRpxKmJ4CrUw,5210
7
- GameSentenceMiner/gsm.py,sha256=qDa8Q8bjBNnmVGLDqiEYLTC_ZyBkS3zw2SmP4Tc_h0o,16375
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.2.4.dist-info/METADATA,sha256=-S903oxc3YHCUd7s_F97f0CARKRJgk3E-yZ5AZ2u8pQ,10141
21
- GameSentenceMiner-2.2.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
- GameSentenceMiner-2.2.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
23
- GameSentenceMiner-2.2.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
24
- GameSentenceMiner-2.2.4.dist-info/RECORD,,
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,,