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 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
- util.use_previous_audio = True
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:
@@ -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,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
@@ -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,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
- previous_line = ''
18
- previous_line_time = datetime.now()
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 previous_line_time, previous_line, line_history
34
+ global current_line_time, current_line, line_history
32
35
 
33
36
  # Initial clipboard content
34
- previous_line = pyperclip.paste()
37
+ current_line = pyperclip.paste()
35
38
 
36
39
  while True:
37
40
  current_clipboard = pyperclip.paste()
38
41
 
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
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 previous_line, previous_line_time, line_history, reconnecting
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
- 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
-
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 previous_line_time
87
+ global current_line_time
81
88
  logger.info("LINE RESET HOTKEY PRESSED")
82
- previous_line_time = datetime.now()
83
- line_history[previous_line] = previous_line_time
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 previous_line_time, 0
112
+ return current_line_time, 0
105
113
 
106
- line_time = previous_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.previous_line)
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
- obs_process = obs.start_obs()
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.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),
@@ -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
- close_obs()
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
- settings_window.window.after(0, settings_window.check_update)
406
+ root.after(0, settings_window.check_update)
391
407
  if get_config().general.open_config_on_startup:
392
- settings_window.window.after(0, settings_window.show)
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
- settings_window.window.mainloop()
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.2.4
4
- Summary: A tool for mining sentences from games. Update: Fix Previous Sentence When Mining from History
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=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=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=cgKpPfRpouWI6tjE_35MWp8nXqRzXs3LvsYXWm5_DOg,4584
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.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.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,,