GameSentenceMiner 2.3.7__tar.gz → 2.3.8__tar.gz

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.
Files changed (31) hide show
  1. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/anki.py +11 -13
  2. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/config_gui.py +2 -5
  3. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/ffmpeg.py +14 -7
  4. gamesentenceminer-2.3.8/GameSentenceMiner/gametext.py +274 -0
  5. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/gsm.py +27 -12
  6. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/utility_gui.py +23 -22
  7. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  8. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/PKG-INFO +1 -1
  9. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/pyproject.toml +1 -1
  10. gamesentenceminer-2.3.7/GameSentenceMiner/gametext.py +0 -197
  11. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/__init__.py +0 -0
  12. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/configuration.py +0 -0
  13. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
  14. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/downloader/__init__.py +0 -0
  15. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/downloader/download_tools.py +0 -0
  16. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/model.py +0 -0
  17. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/notification.py +0 -0
  18. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/obs.py +0 -0
  19. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/package_updater.py +0 -0
  20. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/util.py +0 -0
  21. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/vad/__init__.py +0 -0
  22. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/vad/silero_trim.py +0 -0
  23. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/vad/vosk_helper.py +0 -0
  24. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner/vad/whisper_helper.py +0 -0
  25. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  26. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  27. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  28. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/requires.txt +0 -0
  29. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  30. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/README.md +0 -0
  31. {gamesentenceminer-2.3.7 → gamesentenceminer-2.3.8}/setup.cfg +0 -0
@@ -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)
@@ -0,0 +1,274 @@
1
+ import asyncio
2
+ import re
3
+ import threading
4
+ import time
5
+ from datetime import datetime
6
+ from typing import Callable
7
+
8
+ import pyperclip
9
+ import websockets
10
+
11
+ from GameSentenceMiner import util
12
+ from GameSentenceMiner.configuration import *
13
+ from GameSentenceMiner.configuration import get_config, logger
14
+ from GameSentenceMiner.util import remove_html_tags
15
+ from difflib import SequenceMatcher
16
+
17
+
18
+ initial_time = datetime.now()
19
+ current_line = ''
20
+ current_line_after_regex = ''
21
+ current_line_time = datetime.now()
22
+
23
+ reconnecting = False
24
+ multi_mine_event_bus: Callable[[str, datetime], None] = None
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()
78
+
79
+ class ClipboardMonitor(threading.Thread):
80
+
81
+ def __init__(self):
82
+ threading.Thread.__init__(self)
83
+ self.daemon = True
84
+
85
+ def run(self):
86
+ global current_line_time, current_line, line_history
87
+
88
+ # Initial clipboard content
89
+ current_line = pyperclip.paste()
90
+
91
+ while True:
92
+ current_clipboard = pyperclip.paste()
93
+
94
+ if current_clipboard != current_line:
95
+ handle_new_text_event(current_clipboard)
96
+
97
+ time.sleep(0.05)
98
+
99
+
100
+ async def listen_websocket():
101
+ global current_line, current_line_time, line_history, reconnecting
102
+ while True:
103
+ try:
104
+ async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
105
+ if reconnecting:
106
+ logger.info(f"Texthooker WebSocket connected Successfully!")
107
+ reconnecting = False
108
+ while True:
109
+ message = await websocket.recv()
110
+
111
+ try:
112
+ data = json.loads(message)
113
+ if "sentence" in data:
114
+ current_clipboard = data["sentence"]
115
+ except json.JSONDecodeError:
116
+ current_clipboard = message
117
+ if current_clipboard != current_line:
118
+ handle_new_text_event(current_clipboard)
119
+ except (websockets.ConnectionClosed, ConnectionError) as e:
120
+ if not reconnecting:
121
+ logger.warning(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
122
+ reconnecting = True
123
+ await asyncio.sleep(5)
124
+
125
+ def handle_new_text_event(current_clipboard):
126
+ global current_line, current_line_time, line_history, current_line_after_regex
127
+ current_line = current_clipboard
128
+ if get_config().general.texthook_replacement_regex:
129
+ current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
130
+ else:
131
+ current_line_after_regex = current_line
132
+ current_line_time = datetime.now()
133
+ line_history.add_line(current_line_after_regex)
134
+ multi_mine_event_bus(line_history[-1])
135
+ logger.debug(f"New Line: {current_clipboard}")
136
+
137
+
138
+ def reset_line_hotkey_pressed():
139
+ global current_line_time
140
+ logger.info("LINE RESET HOTKEY PRESSED")
141
+ current_line_time = datetime.now()
142
+ util.set_last_mined_line("")
143
+
144
+
145
+ def run_websocket_listener():
146
+ asyncio.run(listen_websocket())
147
+
148
+
149
+ def start_text_monitor(send_to_mine_event_bus):
150
+ global multi_mine_event_bus
151
+ multi_mine_event_bus = send_to_mine_event_bus
152
+ if get_config().general.use_websocket:
153
+ text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
154
+ else:
155
+ text_thread = ClipboardMonitor()
156
+ text_thread.start()
157
+
158
+
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
225
+
226
+ if not last_note:
227
+ return lines[-1]
228
+
229
+ sentence = last_note['fields'][get_config().anki.sentence_field]['value']
230
+ if not sentence:
231
+ return lines[-1]
232
+
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
238
+
239
+ logger.debug("Couldn't find a match in history, using last event")
240
+ return lines[-1]
241
+
242
+
243
+ def get_line_and_future_lines(last_note):
244
+ if not last_note:
245
+ return []
246
+
247
+ sentence = last_note['fields'][get_config().anki.sentence_field]['value']
248
+ found_lines = []
249
+ if sentence:
250
+ found = False
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}")
254
+ if found:
255
+ found_lines.append(line.text)
256
+ if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
257
+ found = True
258
+ found_lines.append(line.text)
259
+ return found_lines
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
+
272
+
273
+ def get_time_of_line(line):
274
+ return line_history.get_time(line)
@@ -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,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
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GameSentenceMiner"
10
- version = "2.3.7"
10
+ version = "2.3.8"
11
11
  description = "A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -1,197 +0,0 @@
1
- import asyncio
2
- import re
3
- import threading
4
- import time
5
- from collections import OrderedDict
6
- from datetime import datetime
7
- from typing import Callable
8
-
9
- import pyperclip
10
- import websockets
11
-
12
- from GameSentenceMiner import util
13
- from GameSentenceMiner.configuration import *
14
- from GameSentenceMiner.configuration import get_config, logger
15
- from GameSentenceMiner.util import remove_html_tags
16
- from difflib import SequenceMatcher
17
-
18
-
19
- initial_time = datetime.now()
20
- current_line = ''
21
- current_line_after_regex = ''
22
- current_line_time = datetime.now()
23
-
24
- line_history = OrderedDict()
25
- reconnecting = False
26
- multi_mine_event_bus: Callable[[str, datetime], None] = None
27
-
28
-
29
- class ClipboardMonitor(threading.Thread):
30
-
31
- def __init__(self):
32
- threading.Thread.__init__(self)
33
- self.daemon = True
34
-
35
- def run(self):
36
- global current_line_time, current_line, line_history
37
-
38
- # Initial clipboard content
39
- current_line = pyperclip.paste()
40
-
41
- while True:
42
- current_clipboard = pyperclip.paste()
43
-
44
- if current_clipboard != current_line:
45
- handle_new_text_event(current_clipboard)
46
-
47
- time.sleep(0.05)
48
-
49
-
50
- async def listen_websocket():
51
- global current_line, current_line_time, line_history, reconnecting
52
- while True:
53
- try:
54
- async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
55
- if reconnecting:
56
- logger.info(f"Texthooker WebSocket connected Successfully!")
57
- reconnecting = False
58
- while True:
59
- message = await websocket.recv()
60
-
61
- try:
62
- data = json.loads(message)
63
- if "sentence" in data:
64
- current_clipboard = data["sentence"]
65
- except json.JSONDecodeError:
66
- current_clipboard = message
67
- if current_clipboard != current_line:
68
- handle_new_text_event(current_clipboard)
69
- except (websockets.ConnectionClosed, ConnectionError) as e:
70
- if not reconnecting:
71
- logger.warning(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
72
- reconnecting = True
73
- await asyncio.sleep(5)
74
-
75
- def handle_new_text_event(current_clipboard):
76
- global current_line, current_line_time, line_history, current_line_after_regex
77
- current_line = current_clipboard
78
- if get_config().general.texthook_replacement_regex:
79
- current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
80
- else:
81
- current_line_after_regex = current_line
82
- 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)
85
- logger.debug(f"New Line: {current_clipboard}")
86
-
87
-
88
- def reset_line_hotkey_pressed():
89
- global current_line_time
90
- logger.info("LINE RESET HOTKEY PRESSED")
91
- current_line_time = datetime.now()
92
- line_history[current_line_after_regex] = current_line_time
93
- util.set_last_mined_line("")
94
-
95
-
96
- def run_websocket_listener():
97
- asyncio.run(listen_websocket())
98
-
99
-
100
- def start_text_monitor(send_to_mine_event_bus):
101
- global multi_mine_event_bus
102
- multi_mine_event_bus = send_to_mine_event_bus
103
- if get_config().general.use_websocket:
104
- text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
105
- else:
106
- text_thread = ClipboardMonitor()
107
- text_thread.start()
108
-
109
-
110
- def get_line_timing(last_note):
111
- def similar(a, b):
112
- return SequenceMatcher(None, a, b).ratio()
113
-
114
- 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 = ""
147
-
148
- 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}")
163
-
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 ''
167
-
168
- return current, previous
169
-
170
-
171
- def get_line_and_future_lines(last_note):
172
- def similar(a, b):
173
- return SequenceMatcher(None, a, b).ratio()
174
- lines = list(line_history.items())
175
-
176
- if not last_note:
177
- return []
178
-
179
- sentence = last_note['fields'][get_config().anki.sentence_field]['value']
180
- found_lines = []
181
- if sentence:
182
- found = False
183
- for i, (line, clip_time) in enumerate(lines):
184
- similarity = similar(remove_html_tags(sentence), line)
185
- logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
186
- if found:
187
- found_lines.append(line)
188
- if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
189
- found = True
190
- found_lines.append(line)
191
- return found_lines
192
-
193
-
194
- def get_time_of_line(line):
195
- if line and line in line_history:
196
- return line_history[line]
197
- return initial_time