GameSentenceMiner 2.3.6__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
 
@@ -496,12 +493,12 @@ class ConfigApp:
496
493
  row=self.current_row,
497
494
  column=2)
498
495
 
499
- ttk.Label(anki_frame, text="Previous Image Field:").grid(row=self.current_row, column=0, sticky='W')
496
+ ttk.Label(anki_frame, text="Previous VoiceLine SS Field:").grid(row=self.current_row, column=0, sticky='W')
500
497
  self.previous_image_field = ttk.Entry(anki_frame)
501
498
  self.previous_image_field.insert(0, self.settings.anki.previous_image_field)
502
499
  self.previous_image_field.grid(row=self.current_row, column=1)
503
500
  self.add_label_and_increment_row(anki_frame,
504
- "Field in Anki for the image line of previous Image. If Empty, will not populate",
501
+ "Field in Anki for the screenshot of previous line. If Empty, will not populate",
505
502
  row=self.current_row,
506
503
  column=2)
507
504
 
@@ -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
 
@@ -16,14 +15,66 @@ from GameSentenceMiner.util import remove_html_tags
16
15
  from difflib import SequenceMatcher
17
16
 
18
17
 
18
+ initial_time = datetime.now()
19
19
  current_line = ''
20
20
  current_line_after_regex = ''
21
21
  current_line_time = datetime.now()
22
22
 
23
- line_history = OrderedDict()
24
23
  reconnecting = False
25
24
  multi_mine_event_bus: Callable[[str, datetime], None] = None
26
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()
27
78
 
28
79
  class ClipboardMonitor(threading.Thread):
29
80
 
@@ -79,8 +130,8 @@ def handle_new_text_event(current_clipboard):
79
130
  else:
80
131
  current_line_after_regex = current_line
81
132
  current_line_time = datetime.now()
82
- line_history[current_line_after_regex] = current_line_time
83
- 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])
84
135
  logger.debug(f"New Line: {current_clipboard}")
85
136
 
86
137
 
@@ -88,7 +139,6 @@ def reset_line_hotkey_pressed():
88
139
  global current_line_time
89
140
  logger.info("LINE RESET HOTKEY PRESSED")
90
141
  current_line_time = datetime.now()
91
- line_history[current_line_after_regex] = current_line_time
92
142
  util.set_last_mined_line("")
93
143
 
94
144
 
@@ -106,72 +156,91 @@ def start_text_monitor(send_to_mine_event_bus):
106
156
  text_thread.start()
107
157
 
108
158
 
109
- def get_line_timing(last_note):
110
- def similar(a, b):
111
- 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
112
225
 
113
226
  if not last_note:
114
- return current_line_time, 0
115
-
116
- line_time = current_line_time
117
- next_line = 0
118
- prev_clip_time = 0
119
-
120
- try:
121
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
122
- if sentence:
123
- for i, (line, clip_time) in enumerate(reversed(line_history.items())):
124
- similarity = similar(remove_html_tags(sentence), line)
125
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
126
- line_time = clip_time
127
- next_line = prev_clip_time
128
- break
129
- prev_clip_time = clip_time
130
- except Exception as e:
131
- logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
132
-
133
- return line_time, next_line
134
-
135
-
136
- def get_last_two_sentences(last_note):
137
- def similar(a, b):
138
- return SequenceMatcher(None, a, b).ratio()
139
- lines = list(line_history.items())
140
-
141
- if not last_note:
142
- return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
143
-
144
- current = ""
145
- previous = ""
227
+ return lines[-1]
146
228
 
147
229
  sentence = last_note['fields'][get_config().anki.sentence_field]['value']
148
- if sentence:
149
- found = False
150
- for i, (line, clip_time) in enumerate(reversed(lines)):
151
- similarity = similar(remove_html_tags(sentence), line)
152
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
153
- if found:
154
- previous = line
155
- break
156
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
157
- found = True
158
- current = line
159
-
160
- logger.debug(f"Current Line: {current}")
161
- logger.debug(f"Previous Line: {previous}")
230
+ if not sentence:
231
+ return lines[-1]
162
232
 
163
- if not current or not previous:
164
- logger.debug("Couldn't find lines in history, using last two lines")
165
- 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
166
238
 
167
- return current, previous
239
+ logger.debug("Couldn't find a match in history, using last event")
240
+ return lines[-1]
168
241
 
169
242
 
170
243
  def get_line_and_future_lines(last_note):
171
- def similar(a, b):
172
- return SequenceMatcher(None, a, b).ratio()
173
- lines = list(line_history.items())
174
-
175
244
  if not last_note:
176
245
  return []
177
246
 
@@ -179,16 +248,27 @@ def get_line_and_future_lines(last_note):
179
248
  found_lines = []
180
249
  if sentence:
181
250
  found = False
182
- for i, (line, clip_time) in enumerate(lines):
183
- similarity = similar(remove_html_tags(sentence), line)
184
- 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}")
185
254
  if found:
186
- found_lines.append(line)
187
- 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
188
257
  found = True
189
- found_lines.append(line)
258
+ found_lines.append(line.text)
190
259
  return found_lines
191
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
+
192
272
 
193
273
  def get_time_of_line(line):
194
- return line_history[line]
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.6
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=ImG3Q6Jt5NfCjNBjli8qU83vF3rNs3mKLPptRMqLqdk,54264
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=KOLtc0KjM3RlAoi_OdxSnA8kG9X07dpY905OSTSro4M,6562
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.6.dist-info/METADATA,sha256=wadC72L9Ow_tRXhwcBi1eBvaReSxn5oUAO9NIAEr0tQ,10120
22
- GameSentenceMiner-2.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
- GameSentenceMiner-2.3.6.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
24
- GameSentenceMiner-2.3.6.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
25
- GameSentenceMiner-2.3.6.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