GameSentenceMiner 2.3.7__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GameSentenceMiner/anki.py CHANGED
@@ -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
 
@@ -7,6 +7,7 @@ from logging.handlers import RotatingFileHandler
7
7
  from os.path import expanduser
8
8
  from sys import platform
9
9
  from typing import List, Dict
10
+ import sys
10
11
 
11
12
  import toml
12
13
  from dataclasses_json import dataclass_json
@@ -370,11 +371,14 @@ def switch_profile_and_save(profile_name):
370
371
  return config_instance.get_config()
371
372
 
372
373
 
374
+ sys.stdout.reconfigure(encoding='utf-8')
375
+ sys.stderr.reconfigure(encoding='utf-8')
376
+
373
377
  logger = logging.getLogger("GameSentenceMiner")
374
378
  logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
375
379
 
376
380
  # Create console handler with level INFO
377
- console_handler = logging.StreamHandler()
381
+ console_handler = logging.StreamHandler(sys.stdout)
378
382
  console_handler.setLevel(logging.INFO)
379
383
 
380
384
  # Create rotating file handler with level DEBUG
@@ -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
 
@@ -79,17 +129,16 @@ def handle_new_text_event(current_clipboard):
79
129
  current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
80
130
  else:
81
131
  current_line_after_regex = current_line
132
+ logger.info(f"Line Received: {current_line_after_regex}")
82
133
  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}")
134
+ line_history.add_line(current_line_after_regex)
135
+ multi_mine_event_bus(line_history[-1])
86
136
 
87
137
 
88
138
  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)