GameSentenceMiner 2.3.7__py3-none-any.whl → 2.3.8__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
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import queue
3
2
  import subprocess
4
3
  import threading
5
4
  import time
@@ -11,7 +10,7 @@ from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
11
10
 
12
11
  from GameSentenceMiner.configuration import *
13
12
  from GameSentenceMiner.configuration import get_config
14
- from GameSentenceMiner.gametext import get_last_two_sentences
13
+ from GameSentenceMiner.gametext import get_text_event
15
14
  from GameSentenceMiner.obs import get_current_game
16
15
  from GameSentenceMiner.util import remove_html_tags
17
16
 
@@ -26,7 +25,7 @@ card_queue = []
26
25
 
27
26
 
28
27
  def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
29
- should_update_audio=True, ss_time=0):
28
+ should_update_audio=True, ss_time=0, game_line=None):
30
29
  global audio_in_anki, screenshot_in_anki, prev_screenshot_in_anki
31
30
  update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
32
31
  last_note['fields'][get_config().anki.sentence_audio_field][
@@ -44,12 +43,10 @@ def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='
44
43
  if get_config().paths.remove_screenshot:
45
44
  os.remove(screenshot)
46
45
  if get_config().anki.previous_image_field:
47
- _, previous_sentence = get_last_two_sentences(last_note)
48
- prev_screenshot = ffmpeg.get_screenshot(video_path, ffmpeg.get_screenshot_time(video_path, gametext.get_time_of_line(previous_sentence)))
46
+ prev_screenshot = ffmpeg.get_screenshot(video_path, ffmpeg.get_screenshot_time(video_path, game_line.prev))
49
47
  prev_screenshot_in_anki = store_media_file(prev_screenshot)
50
48
  if get_config().paths.remove_screenshot:
51
49
  os.remove(prev_screenshot)
52
- util.set_last_mined_line(get_sentence(last_note))
53
50
  audio_html = f"[sound:{audio_in_anki}]"
54
51
  image_html = f"<img src=\"{screenshot_in_anki}\">"
55
52
  prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
@@ -122,7 +119,7 @@ def get_initial_card_info(last_note, selected_lines):
122
119
  note = {'id': last_note['noteId'], 'fields': {}}
123
120
  if not last_note:
124
121
  return note
125
- current_line, previous_line = get_last_two_sentences(last_note)
122
+ game_line = get_text_event(last_note)
126
123
 
127
124
  if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
128
125
  lines = gametext.get_line_and_future_lines(last_note)
@@ -130,13 +127,13 @@ def get_initial_card_info(last_note, selected_lines):
130
127
  note['fields'][get_config().anki.sentence_field] = "".join(lines)
131
128
 
132
129
  if selected_lines and get_config().anki.multi_overwrites_sentence:
133
- note['fields'][get_config().anki.sentence_field] = "".join(selected_lines)
130
+ note['fields'][get_config().anki.sentence_field] = "".join([line.text for line in selected_lines])
134
131
 
135
- logger.debug(
136
- 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']}")
137
- if get_config().anki.previous_sentence_field and previous_line and not \
132
+ if get_config().anki.previous_sentence_field and game_line.prev and not \
138
133
  last_note['fields'][get_config().anki.previous_sentence_field]['value']:
139
- note['fields'][get_config().anki.previous_sentence_field] = previous_line
134
+ logger.debug(
135
+ f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and game_line.prev.text and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
136
+ note['fields'][get_config().anki.previous_sentence_field] = game_line.prev.text
140
137
  return note
141
138
 
142
139
 
@@ -227,7 +224,8 @@ def update_new_card():
227
224
  if get_config().obs.get_game_from_scene:
228
225
  obs.update_current_game()
229
226
  if use_prev_audio:
230
- update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
227
+ with util.lock:
228
+ update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
231
229
  else:
232
230
  logger.info("New card(s) detected! Added to Processing Queue!")
233
231
  card_queue.append(last_card)
@@ -51,6 +51,7 @@ class HoverInfoWidget:
51
51
  class ConfigApp:
52
52
  def __init__(self, root):
53
53
  self.window = root
54
+ self.on_exit = None
54
55
  # self.window = ttk.Window(themename='darkly')
55
56
  self.window.title('GameSentenceMiner Configuration')
56
57
  self.window.protocol("WM_DELETE_WINDOW", self.hide)
@@ -82,10 +83,6 @@ class ConfigApp:
82
83
  def add_save_hook(self, func):
83
84
  on_save.append(func)
84
85
 
85
- def add_exit_hook(self, func):
86
- global exit_func
87
- exit_func = func
88
-
89
86
  def show(self):
90
87
  obs.update_current_game()
91
88
  self.reload_settings()
@@ -103,7 +100,7 @@ class ConfigApp:
103
100
  if update_available:
104
101
  messagebox.showinfo("Update", "GSM Will Copy the Update Command to your clipboard, please run it in a terminal.")
105
102
  pyperclip.copy("pip install --upgrade GameSentenceMiner")
106
- exit_func(None, None)
103
+ self.on_exit(None, None)
107
104
  else:
108
105
  messagebox.showinfo("No Update Found", "No update found.")
109
106
 
@@ -5,6 +5,8 @@ import time
5
5
  from GameSentenceMiner import obs, util, configuration
6
6
  from GameSentenceMiner.configuration import *
7
7
  from GameSentenceMiner.util import *
8
+ from GameSentenceMiner.gametext import initial_time
9
+
8
10
 
9
11
  def get_ffmpeg_path():
10
12
  return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if util.is_windows() else "ffmpeg"
@@ -47,7 +49,12 @@ def get_screenshot(video_file, time_from_end):
47
49
  return output_image
48
50
 
49
51
 
50
- def get_screenshot_time(video_path, line_time):
52
+ def get_screenshot_time(video_path, game_line):
53
+ if game_line:
54
+ line_time = game_line.time
55
+ else:
56
+ line_time = initial_time
57
+
51
58
  file_length = get_video_duration(video_path)
52
59
  file_mod_time = get_file_modification_time(video_path)
53
60
 
@@ -115,7 +122,7 @@ def get_audio_codec(video_path):
115
122
  return None
116
123
 
117
124
 
118
- def get_audio_and_trim(video_path, line_time, next_line_time):
125
+ def get_audio_and_trim(video_path, game_line, next_line_time):
119
126
  supported_formats = {
120
127
  'opus': 'libopus',
121
128
  'mp3': 'libmp3lame',
@@ -149,7 +156,7 @@ def get_audio_and_trim(video_path, line_time, next_line_time):
149
156
 
150
157
  subprocess.run(command)
151
158
 
152
- return trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_line_time)
159
+ return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time)
153
160
 
154
161
 
155
162
  def get_video_duration(file_path):
@@ -167,12 +174,12 @@ def get_video_duration(file_path):
167
174
  return float(duration_info["format"]["duration"]) # Return the duration in seconds
168
175
 
169
176
 
170
- def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_line):
177
+ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line):
171
178
  trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
172
179
  suffix=f".{get_config().audio.extension}").name
173
180
  file_mod_time = get_file_modification_time(video_path)
174
181
  file_length = get_video_duration(video_path)
175
- time_delta = file_mod_time - line_time
182
+ time_delta = file_mod_time - game_line.time
176
183
  # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
177
184
  total_seconds = file_length - time_delta.total_seconds() + get_config().audio.beginning_offset
178
185
  if total_seconds < 0 or total_seconds >= file_length:
@@ -186,8 +193,8 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_l
186
193
  ffmpeg_command = ffmpeg_base_command_list + [
187
194
  "-i", untrimmed_audio,
188
195
  "-ss", start_trim_time]
189
- if next_line and next_line > line_time and not get_config().audio.mining_from_history_grab_all_audio:
190
- end_total_seconds = total_seconds + (next_line - line_time).total_seconds() + 1
196
+ if next_line and next_line > game_line.time and not get_config().audio.mining_from_history_grab_all_audio:
197
+ end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
191
198
  hours, remainder = divmod(end_total_seconds, 3600)
192
199
  minutes, seconds = divmod(remainder, 60)
193
200
  end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
@@ -2,7 +2,6 @@ import asyncio
2
2
  import re
3
3
  import threading
4
4
  import time
5
- from collections import OrderedDict
6
5
  from datetime import datetime
7
6
  from typing import Callable
8
7
 
@@ -21,10 +20,61 @@ current_line = ''
21
20
  current_line_after_regex = ''
22
21
  current_line_time = datetime.now()
23
22
 
24
- line_history = OrderedDict()
25
23
  reconnecting = False
26
24
  multi_mine_event_bus: Callable[[str, datetime], None] = None
27
25
 
26
+ @dataclass
27
+ class GameLine:
28
+ text: str
29
+ time: datetime
30
+ prev: 'GameLine'
31
+ next: 'GameLine'
32
+
33
+ def get_previous_time(self):
34
+ if self.prev:
35
+ return self.prev.time
36
+ return initial_time
37
+
38
+ def get_next_time(self):
39
+ if self.next:
40
+ return self.next.time
41
+ return 0
42
+
43
+ @dataclass
44
+ class GameText:
45
+ values: list[GameLine]
46
+
47
+ def __init__(self):
48
+ self.values = []
49
+
50
+ def __getitem__(self, key):
51
+ return self.values[key]
52
+
53
+ def get_time(self, line_text: str, occurrence: int = -1) -> datetime:
54
+ matches = [line for line in self.values if line.text == line_text]
55
+ if matches:
56
+ return matches[occurrence].time # Default to latest
57
+ return initial_time
58
+
59
+ def get_event(self, line_text: str, occurrence: int = -1) -> GameLine | None:
60
+ matches = [line for line in self.values if line.text == line_text]
61
+ if matches:
62
+ return matches[occurrence]
63
+ return None
64
+
65
+ def add_line(self, line_text):
66
+ new_line = GameLine(line_text, datetime.now(), self.values[-1] if self.values else None, None)
67
+ if self.values:
68
+ self.values[-1].next = new_line
69
+ self.values.append(new_line)
70
+
71
+ def has_line(self, line_text) -> bool:
72
+ for game_line in self.values:
73
+ if game_line.text == line_text:
74
+ return True
75
+ return False
76
+
77
+ line_history = GameText()
28
78
 
29
79
  class ClipboardMonitor(threading.Thread):
30
80
 
@@ -80,8 +130,8 @@ def handle_new_text_event(current_clipboard):
80
130
  else:
81
131
  current_line_after_regex = current_line
82
132
  current_line_time = datetime.now()
83
- line_history[current_line_after_regex] = current_line_time
84
- multi_mine_event_bus(current_line_after_regex, current_line_time)
133
+ line_history.add_line(current_line_after_regex)
134
+ multi_mine_event_bus(line_history[-1])
85
135
  logger.debug(f"New Line: {current_clipboard}")
86
136
 
87
137
 
@@ -89,7 +139,6 @@ def reset_line_hotkey_pressed():
89
139
  global current_line_time
90
140
  logger.info("LINE RESET HOTKEY PRESSED")
91
141
  current_line_time = datetime.now()
92
- line_history[current_line_after_regex] = current_line_time
93
142
  util.set_last_mined_line("")
94
143
 
95
144
 
@@ -107,72 +156,91 @@ def start_text_monitor(send_to_mine_event_bus):
107
156
  text_thread.start()
108
157
 
109
158
 
110
- def get_line_timing(last_note):
111
- def similar(a, b):
112
- return SequenceMatcher(None, a, b).ratio()
159
+ # def get_line_timing(last_note):
160
+ # def similar(a, b):
161
+ # return SequenceMatcher(None, a, b).ratio()
162
+ #
163
+ # if not last_note:
164
+ # return current_line_time, 0
165
+ #
166
+ # lines = line_history.values
167
+ #
168
+ # line_time = current_line_time
169
+ # next_line = 0
170
+ # prev_clip_time = 0
171
+ #
172
+ # try:
173
+ # sentence = last_note['fields'][get_config().anki.sentence_field]['value']
174
+ # if sentence:
175
+ # for line in reversed(lines):
176
+ # similarity = similar(remove_html_tags(sentence), line.text)
177
+ # if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
178
+ # line_time = line.time
179
+ # next_line = prev_clip_time
180
+ # break
181
+ # prev_clip_time = line.time
182
+ # except Exception as e:
183
+ # logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
184
+ #
185
+ # return line_time, next_line
186
+ #
187
+ #
188
+ # def get_last_two_sentences(last_note) -> (str, str):
189
+ # def similar(a, b):
190
+ # return SequenceMatcher(None, a, b).ratio()
191
+ #
192
+ # lines = line_history.values
193
+ #
194
+ # if not last_note:
195
+ # return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
196
+ #
197
+ # sentence = last_note['fields'][get_config().anki.sentence_field]['value']
198
+ # if not sentence:
199
+ # return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
200
+ #
201
+ # current, previous = "", ""
202
+ # found = False
203
+ #
204
+ # for line in reversed(lines):
205
+ # similarity = similar(remove_html_tags(sentence), line.text)
206
+ # logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
207
+ # if found:
208
+ # previous = line.text
209
+ # break
210
+ # if similarity >= 0.60 or line.text in remove_html_tags(sentence):
211
+ # found = True
212
+ # current = line.text
213
+ #
214
+ # if not current or not previous:
215
+ # logger.debug("Couldn't find lines in history, using last two lines")
216
+ # return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
217
+ #
218
+ # return current, previous
219
+
220
+ def similar(a, b):
221
+ return SequenceMatcher(None, a, b).ratio()
222
+
223
+ def get_text_event(last_note) -> GameLine:
224
+ lines = line_history.values
113
225
 
114
226
  if not last_note:
115
- return current_line_time, 0
116
-
117
- line_time = current_line_time
118
- next_line = 0
119
- prev_clip_time = 0
120
-
121
- try:
122
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
123
- if sentence:
124
- for i, (line, clip_time) in enumerate(reversed(line_history.items())):
125
- similarity = similar(remove_html_tags(sentence), line)
126
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
127
- line_time = clip_time
128
- next_line = prev_clip_time
129
- break
130
- prev_clip_time = clip_time
131
- except Exception as e:
132
- logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
133
-
134
- return line_time, next_line
135
-
136
-
137
- def get_last_two_sentences(last_note):
138
- def similar(a, b):
139
- return SequenceMatcher(None, a, b).ratio()
140
- lines = list(line_history.items())
141
-
142
- if not last_note:
143
- return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
144
-
145
- current = ""
146
- previous = ""
227
+ return lines[-1]
147
228
 
148
229
  sentence = last_note['fields'][get_config().anki.sentence_field]['value']
149
- if sentence:
150
- found = False
151
- for i, (line, clip_time) in enumerate(reversed(lines)):
152
- similarity = similar(remove_html_tags(sentence), line)
153
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
154
- if found:
155
- previous = line
156
- break
157
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
158
- found = True
159
- current = line
160
-
161
- logger.debug(f"Current Line: {current}")
162
- logger.debug(f"Previous Line: {previous}")
230
+ if not sentence:
231
+ return lines[-1]
163
232
 
164
- if not current or not previous:
165
- logger.debug("Couldn't find lines in history, using last two lines")
166
- return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
233
+ for line in reversed(lines):
234
+ similarity = similar(remove_html_tags(sentence), line.text)
235
+ logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
236
+ if similarity >= 0.60 or line.text in remove_html_tags(sentence):
237
+ return line
167
238
 
168
- return current, previous
239
+ logger.debug("Couldn't find a match in history, using last event")
240
+ return lines[-1]
169
241
 
170
242
 
171
243
  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
244
  if not last_note:
177
245
  return []
178
246
 
@@ -180,18 +248,27 @@ def get_line_and_future_lines(last_note):
180
248
  found_lines = []
181
249
  if sentence:
182
250
  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}")
251
+ for line in line_history.values:
252
+ similarity = similar(remove_html_tags(sentence), line.text)
253
+ logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
186
254
  if found:
187
- found_lines.append(line)
188
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
255
+ found_lines.append(line.text)
256
+ if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
189
257
  found = True
190
- found_lines.append(line)
258
+ found_lines.append(line.text)
191
259
  return found_lines
192
260
 
261
+ def get_mined_line(last_note, lines):
262
+ if not last_note:
263
+ return lines[0]
264
+
265
+ sentence = last_note['fields'][get_config().anki.sentence_field]['value']
266
+ for line2 in lines:
267
+ similarity = similar(remove_html_tags(sentence), line2.text)
268
+ if similarity >= 0.60 or line2.text in remove_html_tags(sentence):
269
+ return line2
270
+ return lines[0]
271
+
193
272
 
194
273
  def get_time_of_line(line):
195
- if line and line in line_history:
196
- return line_history[line]
197
- return initial_time
274
+ return line_history.get_time(line)
GameSentenceMiner/gsm.py CHANGED
@@ -25,7 +25,7 @@ from GameSentenceMiner.downloader.download_tools import download_obs_if_needed,
25
25
  from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
26
26
  from GameSentenceMiner.configuration import *
27
27
  from GameSentenceMiner.ffmpeg import get_audio_and_trim
28
- from GameSentenceMiner.gametext import get_line_timing
28
+ from GameSentenceMiner.gametext import get_text_event, get_mined_line
29
29
  from GameSentenceMiner.util import *
30
30
 
31
31
  if is_windows():
@@ -57,6 +57,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
57
57
  if anki.card_queue and len(anki.card_queue) > 0:
58
58
  last_note = anki.card_queue.pop(0)
59
59
  with util.lock:
60
+ util.set_last_mined_line(anki.get_sentence(last_note))
60
61
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
61
62
  logger.debug(f"Video found and is readable: {video_path}")
62
63
 
@@ -73,12 +74,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
73
74
  last_note = anki.get_last_anki_card()
74
75
  if get_config().features.backfill_audio:
75
76
  last_note = anki.get_cards_by_sentence(gametext.current_line)
76
- line_time, next_line_time = get_line_timing(last_note)
77
+ line_cutoff = None
78
+ start_line = None
79
+ mined_line = get_text_event(last_note)
80
+ if mined_line:
81
+ start_line = mined_line
82
+ if mined_line.next:
83
+ line_cutoff = mined_line.next.time
84
+
77
85
  if utility_window.lines_selected():
78
- line_time, next_line_time = utility_window.get_selected_times()
86
+ lines = utility_window.get_selected_lines()
87
+ start_line = lines[0]
88
+ mined_line = get_mined_line(last_note, lines)
89
+ line_cutoff = utility_window.get_next_line_timing()
90
+
79
91
  ss_timing = 0
80
- if line_time and next_line_time or line_time and get_config().screenshot.use_beginning_of_line_as_screenshot:
81
- ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
92
+ if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
93
+ ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line)
82
94
  if last_note:
83
95
  logger.debug(json.dumps(last_note))
84
96
 
@@ -89,8 +101,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
89
101
  if get_config().anki.sentence_audio_field:
90
102
  logger.debug("Attempting to get audio from video")
91
103
  final_audio_output, should_update_audio, vad_trimmed_audio = VideoToAudioHandler.get_audio(
92
- line_time,
93
- next_line_time,
104
+ start_line,
105
+ line_cutoff,
94
106
  video_path)
95
107
  else:
96
108
  final_audio_output = ""
@@ -103,7 +115,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
103
115
  anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
104
116
  tango=tango,
105
117
  should_update_audio=should_update_audio,
106
- ss_time=ss_timing)
118
+ ss_time=ss_timing,
119
+ game_line=start_line)
107
120
  elif get_config().features.notify_on_update and should_update_audio:
108
121
  notification.send_audio_generated_notification(vad_trimmed_audio)
109
122
  except Exception as e:
@@ -118,8 +131,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
118
131
  utility_window.reset_checkboxes()
119
132
 
120
133
  @staticmethod
121
- def get_audio(line_time, next_line_time, video_path):
122
- trimmed_audio = get_audio_and_trim(video_path, line_time, next_line_time)
134
+ def get_audio(game_line, next_line_time, video_path):
135
+ trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
123
136
  vad_trimmed_audio = make_unique_file_name(
124
137
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
125
138
  final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
@@ -259,10 +272,12 @@ def open_log():
259
272
  logger.info("Log opened.")
260
273
 
261
274
 
262
- def exit_program(icon, item):
275
+ def exit_program(passed_icon, item):
263
276
  """Exit the application."""
277
+ if not passed_icon:
278
+ passed_icon = icon
264
279
  logger.info("Exiting...")
265
- icon.stop()
280
+ passed_icon.stop()
266
281
  cleanup()
267
282
 
268
283
 
@@ -35,16 +35,16 @@ class UtilityApp:
35
35
  self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
36
36
 
37
37
  # Add existing items
38
- for text, var, time in self.items:
39
- self.add_checkbox_to_gui(text, var, time)
38
+ for line, var in self.items:
39
+ self.add_checkbox_to_gui(line, var)
40
40
  else:
41
41
  self.multi_mine_window.deiconify()
42
42
  self.multi_mine_window.lift()
43
43
 
44
- def add_text(self, text, time):
45
- if text:
44
+ def add_text(self, line):
45
+ if line.text:
46
46
  var = tk.BooleanVar()
47
- self.items.append((text, var, time))
47
+ self.items.append((line, var))
48
48
 
49
49
  if len(self.items) > 10:
50
50
  if self.checkboxes:
@@ -53,12 +53,12 @@ class UtilityApp:
53
53
  self.items.pop(0)
54
54
 
55
55
  if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
56
- self.add_checkbox_to_gui(text, var, time)
56
+ self.add_checkbox_to_gui(line, var)
57
57
 
58
- def add_checkbox_to_gui(self, text, var, time):
58
+ def add_checkbox_to_gui(self, line, var):
59
59
  """ Add a single checkbox without repainting everything. """
60
60
  if self.checkbox_frame:
61
- chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
61
+ chk = ttk.Checkbutton(self.checkbox_frame, text=f"{line.time.strftime('%H:%M:%S')} - {line.text}", variable=var)
62
62
  chk.pack(anchor='w')
63
63
  self.checkboxes.append(chk)
64
64
 
@@ -73,30 +73,31 @@ class UtilityApp:
73
73
  # chk.pack(anchor='w')
74
74
 
75
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 []
76
+ filtered_items = [line for line, var in self.items if var.get()]
77
+ return filtered_items if len(filtered_items) > 0 else []
78
78
 
79
- def get_selected_times(self):
80
- filtered_times = [time for _, var, time in self.items if var.get()]
81
79
 
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())
80
+ def get_next_line_timing(self):
81
+ selected_lines = [line for line, var in self.items if var.get()]
82
+
83
+ if len(selected_lines) >= 2:
84
+ last_checked_index = max(i for i, (_, var) in enumerate(self.items) if var.get())
86
85
 
87
- # Get the time AFTER the last checked checkbox, if it exists
88
86
  if last_checked_index + 1 < len(self.items):
89
- next_time = self.items[last_checked_index + 1][2]
87
+ next_time = self.items[last_checked_index + 1][0].time
90
88
  else:
91
89
  next_time = 0
92
90
 
93
- return filtered_times[0], next_time
91
+ return next_time
92
+ if len(selected_lines) == 1:
93
+ return selected_lines[0].get_next_time()
94
94
 
95
95
  return None
96
96
 
97
+
97
98
  def lines_selected(self):
98
- filter_times = [time for _, var, time in self.items if var.get()]
99
- if len(filter_times) >= 2:
99
+ filter_times = [line.time for line, var in self.items if var.get()]
100
+ if len(filter_times) > 0:
100
101
  return True
101
102
  return False
102
103
 
@@ -114,7 +115,7 @@ class UtilityApp:
114
115
  # found_unchecked = True
115
116
 
116
117
  def reset_checkboxes(self):
117
- for _, var, _ in self.items:
118
+ for _, var in self.items:
118
119
  var.set(False)
119
120
  # if self.multi_mine_window:
120
121
  # for checkbox in self.checkboxes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: GameSentenceMiner
3
- Version: 2.3.7
3
+ Version: 2.3.8
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,16 +1,16 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=bg_RKaY8bT19hfQQ6BJoPp5G634-xDqTryONLUoK6Z8,10691
3
- GameSentenceMiner/config_gui.py,sha256=-Jq3ZZZxsLyIaXkQtYVvDeiH3PC4PnrYHxh_wD22174,54270
2
+ GameSentenceMiner/anki.py,sha256=QBrA-WVQfK4KjKB2Wm1WDW7BlVy8MF6-1ByTApVzPVA,10576
3
+ GameSentenceMiner/config_gui.py,sha256=nk0-aFTrqtygbLOqnQMSUqHQFda4bRhFG18Tz3Ansn4,54215
4
4
  GameSentenceMiner/configuration.py,sha256=_nS-9sWNn97Zdv2V4Ypt_hf4PL6oLa5oK2Z7rWrqEmc,14405
5
- GameSentenceMiner/ffmpeg.py,sha256=RN3XlO5uPmD-76wvMZi1CIom0aTq8aNZSZUjLacfJbo,10977
6
- GameSentenceMiner/gametext.py,sha256=qK9GA9Mn081Ih6kGmunvDhG8HZ5x1RhEco81F2sK4g4,6658
7
- GameSentenceMiner/gsm.py,sha256=pUKOiJxxSHBqBR3v3U8YiHdwqMxAwvz5keDAGUKGQQA,18388
5
+ GameSentenceMiner/ffmpeg.py,sha256=4nyl_kdXi_trzDyhhjSmszN0BlFF-McAFyOghvCyl34,11142
6
+ GameSentenceMiner/gametext.py,sha256=BsVECzyh91nuQzBn9ZkdWLJXhtIekoPyKRNgsuB_wP4,8917
7
+ GameSentenceMiner/gsm.py,sha256=dILfQrWQ-cWKRAaKswnzBMNNlBqC7_BEZUSFxe-8PMk,18994
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=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
11
11
  GameSentenceMiner/package_updater.py,sha256=0uaLAp0WrWqostNTBWRS0laITjI9aN9Yt_6GXosS4NQ,1278
12
12
  GameSentenceMiner/util.py,sha256=MITweiFYaefWQF5nR8tZ9yE6vd_b-fLuP0MP1Y1U4K0,4720
13
- GameSentenceMiner/utility_gui.py,sha256=Wa1YLitHzjbzkvvOtt3HbmFnplfajKR3N4aB2fTOlnM,4679
13
+ GameSentenceMiner/utility_gui.py,sha256=EtQUnCgTTdzKJE0iCJiHjjc_c6tc7JtI09LRg4_iy8Y,4555
14
14
  GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
15
15
  GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  GameSentenceMiner/downloader/download_tools.py,sha256=M7vLo6_0QMuk1Ji4CsZqk1C2g7Bq6PyM29r7XNoP6Rw,6406
@@ -18,8 +18,8 @@ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
18
18
  GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
19
19
  GameSentenceMiner/vad/vosk_helper.py,sha256=-AAwK0cgOC5rK3_gL0sQgrPJ75E49g_PxZR4d5ckwc4,5826
20
20
  GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
21
- GameSentenceMiner-2.3.7.dist-info/METADATA,sha256=1O5tVlKwbajXxt2uSpqH1_yOgt8zHMWo4DTyFKKc3iI,10120
22
- GameSentenceMiner-2.3.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
- GameSentenceMiner-2.3.7.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
24
- GameSentenceMiner-2.3.7.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
25
- GameSentenceMiner-2.3.7.dist-info/RECORD,,
21
+ gamesentenceminer-2.3.8.dist-info/METADATA,sha256=PYvBuu_5hcR_2cEbow_2BhBjM93wjIkCl5gxfGaCGhQ,10120
22
+ gamesentenceminer-2.3.8.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
23
+ gamesentenceminer-2.3.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
24
+ gamesentenceminer-2.3.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
25
+ gamesentenceminer-2.3.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5