GameSentenceMiner 2.2.3__py3-none-any.whl → 2.3.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
@@ -6,7 +6,7 @@ import urllib.request
6
6
 
7
7
  import requests as req
8
8
 
9
- from GameSentenceMiner import obs, util, notification, ffmpeg
9
+ from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
10
10
 
11
11
  from GameSentenceMiner.configuration import *
12
12
  from GameSentenceMiner.configuration import get_config
@@ -103,15 +103,25 @@ def add_image_to_card(last_note, image_path):
103
103
  logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note['noteId']}")
104
104
 
105
105
 
106
- def get_initial_card_info(last_note):
106
+ def get_initial_card_info(last_note, selected_lines):
107
107
  note = {'id': last_note['noteId'], 'fields': {}}
108
108
  if not last_note:
109
109
  return note
110
- current_line, previous_line = get_last_two_sentences()
110
+ current_line, previous_line = get_last_two_sentences(last_note)
111
111
  logger.debug(f"Previous Sentence {previous_line}")
112
112
  logger.debug(f"Current Sentence {current_line}")
113
113
  util.use_previous_audio = True
114
114
 
115
+ if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
116
+ lines = gametext.get_line_and_future_lines(last_note)
117
+ logger.info(lines)
118
+ logger.info("".join(lines))
119
+ if lines:
120
+ note['fields'][get_config().anki.sentence_field] = "".join(lines)
121
+
122
+ if selected_lines and get_config().anki.multi_overwrites_sentence:
123
+ note['fields'][get_config().anki.sentence_field] = "".join(selected_lines)
124
+
115
125
  logger.debug(
116
126
  f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and previous_line and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
117
127
  if get_config().anki.previous_sentence_field and previous_line and not \
@@ -200,7 +210,6 @@ def check_for_new_cards():
200
210
  def update_new_card():
201
211
  last_card = get_last_anki_card()
202
212
  if not check_tags_for_should_update(last_card):
203
- logger.info("Card not tagged properly! Not updating!")
204
213
  return
205
214
 
206
215
  use_prev_audio = util.use_previous_audio
@@ -212,7 +221,7 @@ def update_new_card():
212
221
  if get_config().obs.get_game_from_scene:
213
222
  obs.update_current_game()
214
223
  if use_prev_audio:
215
- update_anki_card(last_card, note=get_initial_card_info(last_card), reuse_audio=True)
224
+ update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
216
225
  else:
217
226
  logger.info("New card(s) detected!")
218
227
  obs.save_replay_buffer()
@@ -222,11 +231,11 @@ def check_tags_for_should_update(last_card):
222
231
  if get_config().anki.tags_to_check:
223
232
  found = False
224
233
  for tag in last_card['tags']:
225
- logger.info(tag)
226
- logger.info(get_config().anki.tags_to_check)
227
234
  if tag.lower() in get_config().anki.tags_to_check:
228
235
  found = True
229
236
  break
237
+ if not found:
238
+ logger.info(f"Card not tagged properly! Not updating! Note Tags: {last_card['tags']}, Tags_To_Check {get_config().anki.tags_to_check}")
230
239
  return found
231
240
  else:
232
241
  return True
@@ -1,14 +1,16 @@
1
1
  import tkinter as tk
2
2
  from tkinter import filedialog, messagebox, simpledialog
3
3
 
4
+ import pyperclip
4
5
  import ttkbootstrap as ttk
5
6
 
6
- from GameSentenceMiner.package_updater import check_for_updates, get_latest_version, update, get_current_version
7
+ from GameSentenceMiner.package_updater import check_for_updates, get_latest_version, get_current_version
7
8
  from GameSentenceMiner import obs, configuration
8
9
  from GameSentenceMiner.configuration import *
9
10
 
10
11
  settings_saved = False
11
12
  on_save = []
13
+ exit_func = None
12
14
 
13
15
 
14
16
  def new_tab(func):
@@ -45,9 +47,11 @@ class HoverInfoWidget:
45
47
  self.tooltip = None
46
48
 
47
49
 
50
+
48
51
  class ConfigApp:
49
- def __init__(self):
50
- self.window = ttk.Window(themename='darkly')
52
+ def __init__(self, root):
53
+ self.window = root
54
+ # self.window = ttk.Window(themename='darkly')
51
55
  self.window.title('GameSentenceMiner Configuration')
52
56
  self.window.protocol("WM_DELETE_WINDOW", self.hide)
53
57
 
@@ -75,7 +79,12 @@ class ConfigApp:
75
79
 
76
80
  self.window.withdraw()
77
81
 
82
+ def add_save_hook(self, func):
83
+ on_save.append(func)
78
84
 
85
+ def add_exit_hook(self, func):
86
+ global exit_func
87
+ exit_func = func
79
88
 
80
89
  def show(self):
81
90
  obs.update_current_game()
@@ -93,9 +102,8 @@ class ConfigApp:
93
102
  update_available, version = check_for_updates()
94
103
  if update_available:
95
104
  messagebox.showinfo("Update", "GSM Will Copy the Update Command to your clipboard, please run it in a terminal.")
96
- success = update()
97
- if not success:
98
- messagebox.showinfo("Update Unsuccessful", "Couldn't Start Update, please update manually.")
105
+ pyperclip.copy("pip install --upgrade GameSentenceMiner")
106
+ exit_func(None, None)
99
107
  else:
100
108
  messagebox.showinfo("No Update Found", "No update found.")
101
109
 
@@ -109,9 +117,6 @@ class ConfigApp:
109
117
  elif show_no_update:
110
118
  messagebox.showinfo("No Update Found", "No update found.")
111
119
 
112
- def add_save_hook(self, func):
113
- on_save.append(func)
114
-
115
120
  def save_settings(self, profile_change=False):
116
121
  global settings_saved
117
122
 
@@ -121,7 +126,8 @@ class ConfigApp:
121
126
  use_websocket=self.websocket_enabled.get(),
122
127
  websocket_uri=self.websocket_uri.get(),
123
128
  open_config_on_startup=self.open_config_on_startup.get(),
124
- check_for_update_on_startup=self.check_for_update_on_startup.get()
129
+ check_for_update_on_startup=self.check_for_update_on_startup.get(),
130
+ texthook_replacement_regex=self.texthook_replacement_regex.get()
125
131
  ),
126
132
  paths=Paths(
127
133
  folder_to_watch=self.folder_to_watch.get(),
@@ -139,12 +145,14 @@ class ConfigApp:
139
145
  picture_field=self.picture_field.get(),
140
146
  word_field=self.word_field.get(),
141
147
  previous_sentence_field=self.previous_sentence_field.get(),
148
+ previous_image_field=self.previous_image_field.get(),
142
149
  custom_tags=[tag.strip() for tag in self.custom_tags.get().split(',') if tag.strip()],
143
150
  tags_to_check=[tag.strip().lower() for tag in self.tags_to_check.get().split(',') if tag.strip()],
144
151
  add_game_tag=self.add_game_tag.get(),
145
152
  polling_rate=int(self.polling_rate.get()),
146
153
  overwrite_audio=self.overwrite_audio.get(),
147
154
  overwrite_picture=self.overwrite_picture.get(),
155
+ multi_overwrites_sentence=self.multi_overwrites_sentence.get(),
148
156
  anki_custom_fields={
149
157
  key_entry.get(): value_entry.get() for key_entry, value_entry, delete_button in
150
158
  self.custom_field_entries if key_entry.get()
@@ -171,7 +179,8 @@ class ConfigApp:
171
179
  end_offset=float(self.end_offset.get()),
172
180
  ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
173
181
  external_tool = self.external_tool.get(),
174
- anki_media_collection=self.anki_media_collection.get()
182
+ anki_media_collection=self.anki_media_collection.get(),
183
+ mining_from_history_grab_all_audio=self.mining_from_history_grab_all_audio.get()
175
184
  ),
176
185
  obs=OBS(
177
186
  enabled=self.obs_enabled.get(),
@@ -184,7 +193,8 @@ class ConfigApp:
184
193
  ),
185
194
  hotkeys=Hotkeys(
186
195
  reset_line=self.reset_line_hotkey.get(),
187
- take_screenshot=self.take_screenshot_hotkey.get()
196
+ take_screenshot=self.take_screenshot_hotkey.get(),
197
+ open_utility=self.open_utility_hotkey.get()
188
198
  ),
189
199
  vad=VAD(
190
200
  whisper_model=self.whisper_model.get(),
@@ -257,11 +267,11 @@ class ConfigApp:
257
267
  general_frame = ttk.Frame(self.notebook)
258
268
  self.notebook.add(general_frame, text='General')
259
269
 
260
- ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
270
+ ttk.Label(general_frame, text="Websocket Enabled (Clipboard Disabled):").grid(row=self.current_row, column=0, sticky='W')
261
271
  self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
262
272
  ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
263
273
  sticky='W')
264
- self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication.",
274
+ self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor. RESTART REQUIRED.",
265
275
  row=self.current_row, column=2)
266
276
 
267
277
  ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
@@ -271,6 +281,13 @@ class ConfigApp:
271
281
  self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting.", row=self.current_row,
272
282
  column=2)
273
283
 
284
+ ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
285
+ self.texthook_replacement_regex = ttk.Entry(general_frame)
286
+ self.texthook_replacement_regex.insert(0, self.settings.general.texthook_replacement_regex)
287
+ self.texthook_replacement_regex.grid(row=self.current_row, column=1)
288
+ self.add_label_and_increment_row(general_frame, "Regex to run replacement on texthook input, set this to the same as what you may have in your texthook page.", row=self.current_row,
289
+ column=2)
290
+
274
291
  ttk.Label(general_frame, text="Open Config on Startup:").grid(row=self.current_row, column=0, sticky='W')
275
292
  self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
276
293
  ttk.Checkbutton(general_frame, variable=self.open_config_on_startup).grid(row=self.current_row, column=1,
@@ -476,6 +493,15 @@ class ConfigApp:
476
493
  row=self.current_row,
477
494
  column=2)
478
495
 
496
+ ttk.Label(anki_frame, text="Previous Image Field:").grid(row=self.current_row, column=0, sticky='W')
497
+ self.previous_image_field = ttk.Entry(anki_frame)
498
+ self.previous_image_field.insert(0, self.settings.anki.previous_image_field)
499
+ self.previous_image_field.grid(row=self.current_row, column=1)
500
+ self.add_label_and_increment_row(anki_frame,
501
+ "Field in Anki for the image line of previous Image. If Empty, will not populate",
502
+ row=self.current_row,
503
+ column=2)
504
+
479
505
  ttk.Label(anki_frame, text="Custom Tags:").grid(row=self.current_row, column=0, sticky='W')
480
506
  self.custom_tags = ttk.Entry(anki_frame)
481
507
  self.custom_tags.insert(0, ', '.join(self.settings.anki.custom_tags))
@@ -518,6 +544,13 @@ class ConfigApp:
518
544
  self.add_label_and_increment_row(anki_frame, "Overwrite existing pictures in Anki cards.", row=self.current_row,
519
545
  column=2)
520
546
 
547
+ ttk.Label(anki_frame, text="Multi-line Mining Overwrite Sentence:").grid(row=self.current_row, column=0, sticky='W')
548
+ self.multi_overwrites_sentence = tk.BooleanVar(
549
+ value=self.settings.anki.multi_overwrites_sentence)
550
+ ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence).grid(row=self.current_row, column=1, sticky='W')
551
+ self.add_label_and_increment_row(anki_frame, "When using Multi-line Mining, overrwrite the sentence with a concatenation of the lines selected.", row=self.current_row,
552
+ column=2)
553
+
521
554
  self.anki_custom_fields = self.settings.anki.anki_custom_fields
522
555
  self.custom_field_entries = []
523
556
 
@@ -715,6 +748,13 @@ class ConfigApp:
715
748
  row=self.current_row,
716
749
  column=2)
717
750
 
751
+ ttk.Label(audio_frame, text="Grab all Future Audio when Mining from History:").grid(row=self.current_row, column=0, sticky='W')
752
+ self.mining_from_history_grab_all_audio = tk.BooleanVar(
753
+ value=self.settings.audio.mining_from_history_grab_all_audio)
754
+ ttk.Checkbutton(audio_frame, variable=self.mining_from_history_grab_all_audio).grid(row=self.current_row, column=1, sticky='W')
755
+ self.add_label_and_increment_row(audio_frame, "When mining from History, this option will allow the script to get all audio from that line to the current time.", row=self.current_row,
756
+ column=2)
757
+
718
758
  @new_tab
719
759
  def create_obs_tab(self):
720
760
  obs_frame = ttk.Frame(self.notebook)
@@ -786,6 +826,12 @@ class ConfigApp:
786
826
  self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
787
827
  self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
788
828
 
829
+ ttk.Label(hotkeys_frame, text="Open Utility Hotkey:").grid(row=self.current_row, column=0, sticky='W')
830
+ self.open_utility_hotkey = ttk.Entry(hotkeys_frame)
831
+ self.open_utility_hotkey.insert(0, self.settings.hotkeys.open_utility)
832
+ self.open_utility_hotkey.grid(row=self.current_row, column=1)
833
+ self.add_label_and_increment_row(hotkeys_frame, "Hotkey to open the text utility.", row=self.current_row, column=2)
834
+
789
835
 
790
836
  @new_tab
791
837
  def create_profiles_tab(self):
@@ -41,6 +41,7 @@ class General:
41
41
  websocket_uri: str = 'localhost:6677'
42
42
  open_config_on_startup: bool = False
43
43
  check_for_update_on_startup: bool = False
44
+ texthook_replacement_regex: str = ""
44
45
 
45
46
 
46
47
  @dataclass_json
@@ -64,12 +65,14 @@ class Anki:
64
65
  picture_field: str = "Picture"
65
66
  word_field: str = 'Word'
66
67
  previous_sentence_field: str = ''
68
+ previous_image_field: str = ''
67
69
  custom_tags: List[str] = None # Initialize to None and set it in __post_init__
68
70
  tags_to_check: List[str] = None
69
71
  add_game_tag: bool = True
70
72
  polling_rate: int = 200
71
73
  overwrite_audio: bool = False
72
74
  overwrite_picture: bool = True
75
+ multi_overwrites_sentence: bool = True
73
76
  anki_custom_fields: Dict[str, str] = None # Initialize to None and set it in __post_init__
74
77
 
75
78
  def __post_init__(self):
@@ -111,6 +114,7 @@ class Audio:
111
114
  ffmpeg_reencode_options: str = ''
112
115
  external_tool: str = ""
113
116
  anki_media_collection: str = ""
117
+ mining_from_history_grab_all_audio: bool = False
114
118
 
115
119
 
116
120
  @dataclass_json
@@ -130,6 +134,7 @@ class OBS:
130
134
  class Hotkeys:
131
135
  reset_line: str = 'f5'
132
136
  take_screenshot: str = 'f6'
137
+ open_utility: str = 'ctrl+m'
133
138
 
134
139
 
135
140
  @dataclass_json
@@ -185,14 +185,14 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_l
185
185
  ffmpeg_command = ffmpeg_base_command_list + [
186
186
  "-i", untrimmed_audio,
187
187
  "-ss", start_trim_time]
188
- if next_line and next_line > line_time:
188
+ if next_line and next_line > line_time and not get_config().audio.mining_from_history_grab_all_audio:
189
189
  end_total_seconds = total_seconds + (next_line - line_time).total_seconds() + 1
190
190
  hours, remainder = divmod(end_total_seconds, 3600)
191
191
  minutes, seconds = divmod(remainder, 60)
192
192
  end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
193
193
  ffmpeg_command.extend(['-to', end_trim_time])
194
194
  logger.info(
195
- f"Looks like Clipboard/Websocket was modified before the script knew about the anki card! Trimming end of video to {end_trim_time}")
195
+ f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time}")
196
196
 
197
197
  ffmpeg_command.extend([
198
198
  "-c", "copy", # Using copy to avoid re-encoding, adjust if needed
@@ -1,8 +1,10 @@
1
1
  import asyncio
2
+ import re
2
3
  import threading
3
4
  import time
4
5
  from collections import OrderedDict
5
6
  from datetime import datetime
7
+ from typing import Callable
6
8
 
7
9
  import pyperclip
8
10
  import websockets
@@ -14,11 +16,13 @@ from GameSentenceMiner.util import remove_html_tags
14
16
  from difflib import SequenceMatcher
15
17
 
16
18
 
17
- previous_line = ''
18
- previous_line_time = datetime.now()
19
+ current_line = ''
20
+ current_line_after_regex = ''
21
+ current_line_time = datetime.now()
19
22
 
20
23
  line_history = OrderedDict()
21
24
  reconnecting = False
25
+ multi_mine_event_bus: Callable[[str, datetime], None] = None
22
26
 
23
27
 
24
28
  class ClipboardMonitor(threading.Thread):
@@ -28,25 +32,22 @@ class ClipboardMonitor(threading.Thread):
28
32
  self.daemon = True
29
33
 
30
34
  def run(self):
31
- global previous_line_time, previous_line, line_history
35
+ global current_line_time, current_line, line_history
32
36
 
33
37
  # Initial clipboard content
34
- previous_line = pyperclip.paste()
38
+ current_line = pyperclip.paste()
35
39
 
36
40
  while True:
37
41
  current_clipboard = pyperclip.paste()
38
42
 
39
- if current_clipboard != previous_line:
40
- previous_line = current_clipboard
41
- previous_line_time = datetime.now()
42
- line_history[previous_line] = previous_line_time
43
- util.use_previous_audio = False
43
+ if current_clipboard != current_line:
44
+ handle_new_text_event(current_clipboard)
44
45
 
45
46
  time.sleep(0.05)
46
47
 
47
48
 
48
49
  async def listen_websocket():
49
- global previous_line, previous_line_time, line_history, reconnecting
50
+ global current_line, current_line_time, line_history, reconnecting
50
51
  while True:
51
52
  try:
52
53
  async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
@@ -62,25 +63,33 @@ async def listen_websocket():
62
63
  current_clipboard = data["sentence"]
63
64
  except json.JSONDecodeError:
64
65
  current_clipboard = message
65
-
66
- if current_clipboard != previous_line:
67
- previous_line = current_clipboard
68
- previous_line_time = datetime.now()
69
- line_history[previous_line] = previous_line_time
70
- util.use_previous_audio = False
71
-
66
+ if current_clipboard != current_line:
67
+ handle_new_text_event(current_clipboard)
72
68
  except (websockets.ConnectionClosed, ConnectionError) as e:
73
69
  if not reconnecting:
74
70
  logger.warning(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
75
71
  reconnecting = True
76
72
  await asyncio.sleep(5)
77
73
 
74
+ def handle_new_text_event(current_clipboard):
75
+ global current_line, current_line_time, line_history, current_line_after_regex
76
+ current_line = current_clipboard
77
+ if get_config().general.texthook_replacement_regex:
78
+ current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', current_line)
79
+ else:
80
+ current_line_after_regex = current_line
81
+ current_line_time = datetime.now()
82
+ line_history[current_line_after_regex] = current_line_time
83
+ util.use_previous_audio = False
84
+ multi_mine_event_bus(current_line_after_regex, current_line_time)
85
+ logger.debug(f"New Line: {current_clipboard}")
86
+
78
87
 
79
88
  def reset_line_hotkey_pressed():
80
- global previous_line_time
89
+ global current_line_time
81
90
  logger.info("LINE RESET HOTKEY PRESSED")
82
- previous_line_time = datetime.now()
83
- line_history[previous_line] = previous_line_time
91
+ current_line_time = datetime.now()
92
+ line_history[current_line_after_regex] = current_line_time
84
93
  util.use_previous_audio = False
85
94
 
86
95
 
@@ -88,7 +97,9 @@ def run_websocket_listener():
88
97
  asyncio.run(listen_websocket())
89
98
 
90
99
 
91
- def start_text_monitor():
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
92
103
  if get_config().general.use_websocket:
93
104
  text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
94
105
  else:
@@ -101,9 +112,9 @@ def get_line_timing(last_note):
101
112
  return SequenceMatcher(None, a, b).ratio()
102
113
 
103
114
  if not last_note:
104
- return previous_line_time, 0
115
+ return current_line_time, 0
105
116
 
106
- line_time = previous_line_time
117
+ line_time = current_line_time
107
118
  next_line = 0
108
119
  prev_clip_time = 0
109
120
 
@@ -112,7 +123,7 @@ def get_line_timing(last_note):
112
123
  if sentence:
113
124
  for i, (line, clip_time) in enumerate(reversed(line_history.items())):
114
125
  similarity = similar(remove_html_tags(sentence), line)
115
- if similarity >= 0.60: # 80% similarity threshold
126
+ if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
116
127
  line_time = clip_time
117
128
  next_line = prev_clip_time
118
129
  break
@@ -123,6 +134,58 @@ def get_line_timing(last_note):
123
134
  return line_time, next_line
124
135
 
125
136
 
126
- def get_last_two_sentences():
137
+ def get_last_two_sentences(last_note):
138
+ def similar(a, b):
139
+ return SequenceMatcher(None, a, b).ratio()
127
140
  lines = list(line_history.items())
128
- return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
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_line = ""
146
+ prev_line = ""
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
+ prev_line = line
156
+ break
157
+ if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
158
+ found = True
159
+ current_line = line
160
+
161
+ logger.debug(f"Current Line: {current_line}")
162
+ logger.debug(f"Previous Line: {prev_line}")
163
+
164
+ if not current_line or not prev_line:
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_line, prev_line
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
GameSentenceMiner/gsm.py CHANGED
@@ -2,6 +2,7 @@ import signal
2
2
  import subprocess
3
3
  import sys
4
4
  import time
5
+ import ttkbootstrap as ttk
5
6
  from subprocess import Popen
6
7
 
7
8
  import keyboard
@@ -11,6 +12,7 @@ from pystray import Icon, Menu, MenuItem
11
12
  from watchdog.events import FileSystemEventHandler
12
13
  from watchdog.observers import Observer
13
14
 
15
+ from GameSentenceMiner import utility_gui
14
16
  from GameSentenceMiner import anki
15
17
  from GameSentenceMiner import config_gui
16
18
  from GameSentenceMiner import configuration
@@ -29,12 +31,14 @@ from GameSentenceMiner.util import *
29
31
  if is_windows():
30
32
  import win32api
31
33
 
32
- obs_process: Popen
34
+ obs_process: Popen = None
33
35
  procs_to_close = []
34
36
  settings_window: config_gui.ConfigApp = None
37
+ utility_window: utility_gui.UtilityApp = None
35
38
  obs_paused = False
36
39
  icon: Icon
37
40
  menu: Menu
41
+ root = None
38
42
 
39
43
 
40
44
  class VideoToAudioHandler(FileSystemEventHandler):
@@ -66,15 +70,17 @@ class VideoToAudioHandler(FileSystemEventHandler):
66
70
  if get_config().anki.update_anki:
67
71
  last_note = anki.get_last_anki_card()
68
72
  if get_config().features.backfill_audio:
69
- last_note = anki.get_cards_by_sentence(gametext.previous_line)
73
+ last_note = anki.get_cards_by_sentence(gametext.current_line)
70
74
  line_time, next_line_time = get_line_timing(last_note)
75
+ if utility_window.lines_selected():
76
+ line_time, next_line_time = utility_window.get_selected_times()
71
77
  ss_timing = 0
72
78
  if line_time and next_line_time:
73
79
  ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
74
80
  if last_note:
75
81
  logger.debug(json.dumps(last_note))
76
82
 
77
- note = anki.get_initial_card_info(last_note)
83
+ note = anki.get_initial_card_info(last_note, utility_window.get_selected_lines())
78
84
 
79
85
  tango = last_note['fields'][get_config().anki.word_field]['value'] if last_note else ''
80
86
 
@@ -106,6 +112,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
106
112
  os.remove(video_path) # Optionally remove the video after conversion
107
113
  if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
108
114
  os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
115
+ utility_window.reset_checkboxes()
109
116
 
110
117
  @staticmethod
111
118
  def get_audio(line_time, next_line_time, video_path):
@@ -153,7 +160,7 @@ def initialize(reloading=False):
153
160
  obs_process = obs.start_obs()
154
161
  obs.connect_to_obs(start_replay=True)
155
162
  anki.start_monitoring_anki()
156
- gametext.start_text_monitor()
163
+ gametext.start_text_monitor(utility_window.add_text)
157
164
  os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
158
165
  os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
159
166
  os.makedirs(get_config().paths.audio_destination, exist_ok=True)
@@ -175,6 +182,7 @@ def initial_checks():
175
182
  def register_hotkeys():
176
183
  keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
177
184
  keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
185
+ keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
178
186
 
179
187
 
180
188
  def get_screenshot():
@@ -189,7 +197,7 @@ def get_screenshot():
189
197
  if last_note:
190
198
  logger.debug(json.dumps(last_note))
191
199
  if get_config().features.backfill_audio:
192
- last_note = anki.get_cards_by_sentence(gametext.previous_line)
200
+ last_note = anki.get_cards_by_sentence(gametext.current_line)
193
201
  if last_note:
194
202
  anki.add_image_to_card(last_note, encoded_image)
195
203
  notification.send_screenshot_updated(last_note['fields'][get_config().anki.word_field]['value'])
@@ -219,11 +227,14 @@ def create_image():
219
227
 
220
228
  return image
221
229
 
222
-
223
230
  def open_settings():
224
231
  obs.update_current_game()
225
232
  settings_window.show()
226
233
 
234
+ def open_multimine():
235
+ obs.update_current_game()
236
+ utility_window.show()
237
+
227
238
 
228
239
  def open_log():
229
240
  """Function to handle opening log."""
@@ -267,6 +278,7 @@ def update_icon():
267
278
 
268
279
  menu = Menu(
269
280
  MenuItem("Open Settings", open_settings),
281
+ MenuItem("Open Multi-Mine GUI", open_multimine),
270
282
  MenuItem("Open Log", open_log),
271
283
  MenuItem("Toggle Replay Buffer", play_pause),
272
284
  MenuItem("Restart OBS", restart_obs),
@@ -299,6 +311,7 @@ def run_tray():
299
311
 
300
312
  menu = Menu(
301
313
  MenuItem("Open Settings", open_settings),
314
+ MenuItem("Open Multi-Mine GUI", open_multimine),
302
315
  MenuItem("Open Log", open_log),
303
316
  MenuItem("Toggle Replay Buffer", play_pause),
304
317
  MenuItem("Restart OBS", restart_obs),
@@ -317,10 +330,11 @@ def close_obs():
317
330
 
318
331
  def restart_obs():
319
332
  global obs_process
320
- close_obs()
321
- time.sleep(2)
322
- obs_process = obs.start_obs()
323
- obs.connect_to_obs(start_replay=True)
333
+ if obs_process:
334
+ close_obs()
335
+ time.sleep(2)
336
+ obs_process = obs.start_obs()
337
+ obs.connect_to_obs(start_replay=True)
324
338
 
325
339
  def cleanup():
326
340
  logger.info("Performing cleanup...")
@@ -362,8 +376,11 @@ def handle_exit():
362
376
 
363
377
 
364
378
  def main(reloading=False, do_config_input=True):
365
- global settings_window
379
+ global root, settings_window, utility_window
366
380
  logger.info("Script started.")
381
+ root = ttk.Window(themename='darkly')
382
+ settings_window = config_gui.ConfigApp(root)
383
+ utility_window = utility_gui.UtilityApp(root)
367
384
  initialize(reloading)
368
385
  initial_checks()
369
386
  event_handler = VideoToAudioHandler()
@@ -384,13 +401,13 @@ def main(reloading=False, do_config_input=True):
384
401
  util.run_new_thread(run_tray)
385
402
 
386
403
  try:
387
- settings_window = config_gui.ConfigApp()
388
404
  if get_config().general.check_for_update_on_startup:
389
- settings_window.window.after(0, settings_window.check_update)
405
+ root.after(0, settings_window.check_update)
390
406
  if get_config().general.open_config_on_startup:
391
- settings_window.window.after(0, settings_window.show)
407
+ root.after(0, settings_window.show)
392
408
  settings_window.add_save_hook(update_icon)
393
- settings_window.window.mainloop()
409
+ settings_window.on_exit = exit_program
410
+ root.mainloop()
394
411
  except KeyboardInterrupt:
395
412
  cleanup()
396
413
 
@@ -39,11 +39,3 @@ def check_for_updates(force=False):
39
39
  return False, latest_version
40
40
  except Exception as e:
41
41
  logger.error(f"Error checking for updates: {e}")
42
-
43
- def update():
44
- try:
45
- pyperclip.copy("pip install --upgrade GameSentenceMiner")
46
- exit()
47
- except Exception as e:
48
- logger.error(f"Error updating {PACKAGE_NAME}: {e}")
49
- return False
@@ -0,0 +1,127 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk
3
+
4
+ from GameSentenceMiner.configuration import logger
5
+
6
+
7
+ class UtilityApp:
8
+ def __init__(self, root):
9
+ self.root = root
10
+
11
+ self.items = []
12
+ self.checkboxes = []
13
+ self.multi_mine_window = None # Store the multi-mine window reference
14
+ self.checkbox_frame = None
15
+
16
+ style = ttk.Style()
17
+ style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
18
+
19
+ # def show(self):
20
+ # if self.multi_mine_window is None or not tk.Toplevel.winfo_exists(self.multi_mine_window):
21
+ # self.multi_mine_window = tk.Toplevel(self.root)
22
+ # self.multi_mine_window.title("Multi-Mine Window")
23
+ # self.update_multi_mine_window()
24
+ #
25
+ def show(self):
26
+ """ Open the multi-mine window only if it doesn't exist. """
27
+ if not self.multi_mine_window or not tk.Toplevel.winfo_exists(self.multi_mine_window):
28
+ logger.info("opening multi-mine_window")
29
+ self.multi_mine_window = tk.Toplevel(self.root)
30
+ self.multi_mine_window.title("Multi Mine Window")
31
+
32
+ self.multi_mine_window.minsize(800, 400) # Set a minimum size to prevent shrinking too
33
+
34
+ self.checkbox_frame = ttk.Frame(self.multi_mine_window)
35
+ self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
36
+
37
+ # Add existing items
38
+ for text, var, time in self.items:
39
+ self.add_checkbox_to_gui(text, var, time)
40
+ else:
41
+ self.multi_mine_window.deiconify()
42
+ self.multi_mine_window.lift()
43
+
44
+ def add_text(self, text, time):
45
+ if text:
46
+ var = tk.BooleanVar()
47
+ self.items.append((text, var, time))
48
+
49
+ # Remove the first checkbox if there are more than 10
50
+ if len(self.items) > 10:
51
+ self.checkboxes[0].destroy()
52
+ self.checkboxes.pop(0)
53
+ self.items.pop(0)
54
+
55
+ if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
56
+ self.add_checkbox_to_gui(text, var, time)
57
+
58
+ def add_checkbox_to_gui(self, text, var, time):
59
+ """ Add a single checkbox without repainting everything. """
60
+ if self.checkbox_frame:
61
+ chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
62
+ chk.pack(anchor='w')
63
+ self.checkboxes.append(chk)
64
+
65
+
66
+ # def update_multi_mine_window(self):
67
+ # for widget in self.multi_mine_window.winfo_children():
68
+ # widget.destroy()
69
+ #
70
+ # for i, (text, var, time) in enumerate(self.items):
71
+ # time: datetime
72
+ # chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
73
+ # chk.pack(anchor='w')
74
+
75
+ def get_selected_lines(self):
76
+ filtered_items = [text for text, var, _ in self.items if var.get()]
77
+ return filtered_items if len(filtered_items) >= 2 else []
78
+
79
+ def get_selected_times(self):
80
+ filtered_times = [time for _, var, time in self.items if var.get()]
81
+
82
+ if len(filtered_times) >= 2:
83
+ logger.info(filtered_times)
84
+ # Find the index of the last checked checkbox
85
+ last_checked_index = max(i for i, (_, var, _) in enumerate(self.items) if var.get())
86
+
87
+ # Get the time AFTER the last checked checkbox, if it exists
88
+ if last_checked_index + 1 < len(self.items):
89
+ next_time = self.items[last_checked_index + 1][2]
90
+ else:
91
+ next_time = 0
92
+
93
+ return filtered_times[0], next_time
94
+
95
+ return None
96
+
97
+ def lines_selected(self):
98
+ filter_times = [time for _, var, time in self.items if var.get()]
99
+ if len(filter_times) >= 2:
100
+ return True
101
+ return False
102
+
103
+ # def validate_checkboxes(self, *args):
104
+ # logger.debug("Validating checkboxes")
105
+ # found_checked = False
106
+ # found_unchecked = False
107
+ # for _, var in self.items:
108
+ # if var.get():
109
+ # if found_unchecked:
110
+ # messagebox.showinfo("Invalid", "Can only select neighboring checkboxes.")
111
+ # break
112
+ # found_checked = True
113
+ # if found_checked and not var.get():
114
+ # found_unchecked = True
115
+
116
+ def reset_checkboxes(self):
117
+ for _, var, _ in self.items:
118
+ var.set(False)
119
+ # if self.multi_mine_window:
120
+ # for checkbox in self.checkboxes:
121
+ # checkbox.set(False)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ root = tk.Tk()
126
+ app = UtilityApp(root)
127
+ root.mainloop()
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: GameSentenceMiner
3
- Version: 2.2.3
4
- Summary: A tool for mining sentences from games. Update: Use ffprobe from GSM Directory
3
+ Version: 2.3.0
4
+ Summary: A tool for mining sentences from games. Update: Multi-Line Mining!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
7
7
  Project-URL: Homepage, https://github.com/bpwhelan/GameSentenceMiner
@@ -1,15 +1,16 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=_yaHQZwOYooQssfJ5hbST5cC6vsffkSEvUIeMouwQZw,9208
3
- GameSentenceMiner/config_gui.py,sha256=PMJ-Ghyp_Vgt2w9CwDpN5rc41QHlLtnmiYR50Hy9p_s,49011
4
- GameSentenceMiner/configuration.py,sha256=tkbZfVf-XxmVdJzfjS6GUvy878qnnHa_KxXGAN0UEqI,14089
5
- GameSentenceMiner/ffmpeg.py,sha256=txTpco-IGWtfF8vIiWUzrtgI5TA1xPVIK-WJWxU02mM,10878
6
- GameSentenceMiner/gametext.py,sha256=pAovclbBSLigoyJMcdhNrieFDDPLJY3htHBGhjQ2Xk0,4081
7
- GameSentenceMiner/gsm.py,sha256=Z_e29j-RYsEC6JGLP-wONocv1wfojDa06FIzxUz8pE8,16285
2
+ GameSentenceMiner/anki.py,sha256=895FTnqZCn2AmfUqxWKsSOkzumFoPRNhwPRf00mB7OI,9755
3
+ GameSentenceMiner/config_gui.py,sha256=IYM_qZV22Hn_eGbXIqrQx2r7UGlTFDLBa0bqu3HqFl0,52491
4
+ GameSentenceMiner/configuration.py,sha256=X61ks7iuzf0Wu7U4QuhggUGB9-1KLka-V4cduoEOddA,14294
5
+ GameSentenceMiner/ffmpeg.py,sha256=VExJYWSFhYuWukIXgOiHufsoSROEDA8LnVQFG8srRGc,10924
6
+ GameSentenceMiner/gametext.py,sha256=ci1ZTwObi-5pxFWHcXp1cDWMrCz_zaPtUsWvTO_jwRU,6570
7
+ GameSentenceMiner/gsm.py,sha256=GY9T3h2dwY-XofXqESiyZpezRvlfaZrhzo0PDuuE87g,17065
8
8
  GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
9
9
  GameSentenceMiner/notification.py,sha256=WBaQWoPNhW4XqdPBUmxPBgjk0ngzH_4v9zMQ-XQAKC8,2010
10
10
  GameSentenceMiner/obs.py,sha256=3Flcjxy812VpF78EPI7sxlGx6yyM3GfqzlinW17SK20,6231
11
- GameSentenceMiner/package_updater.py,sha256=KBJwLrlcAyoGHNNmDSVdPgWawqfWiIWOodcZhYheoms,1486
11
+ GameSentenceMiner/package_updater.py,sha256=0uaLAp0WrWqostNTBWRS0laITjI9aN9Yt_6GXosS4NQ,1278
12
12
  GameSentenceMiner/util.py,sha256=cgKpPfRpouWI6tjE_35MWp8nXqRzXs3LvsYXWm5_DOg,4584
13
+ GameSentenceMiner/utility_gui.py,sha256=-T5b14Nx6KvNnBEmdVz0mWkXoYi-bZzHIce6ADwfVEQ,4701
13
14
  GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
14
15
  GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
16
  GameSentenceMiner/downloader/download_tools.py,sha256=M7vLo6_0QMuk1Ji4CsZqk1C2g7Bq6PyM29r7XNoP6Rw,6406
@@ -17,8 +18,8 @@ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
17
18
  GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
18
19
  GameSentenceMiner/vad/vosk_helper.py,sha256=-AAwK0cgOC5rK3_gL0sQgrPJ75E49g_PxZR4d5ckwc4,5826
19
20
  GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
20
- GameSentenceMiner-2.2.3.dist-info/METADATA,sha256=8tTYMSToJ11bkM8XMjZLEihZh_VunSVXm1WqI4uqusY,10125
21
- GameSentenceMiner-2.2.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
- GameSentenceMiner-2.2.3.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
23
- GameSentenceMiner-2.2.3.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
24
- GameSentenceMiner-2.2.3.dist-info/RECORD,,
21
+ GameSentenceMiner-2.3.0.dist-info/METADATA,sha256=isZtUWBqPSC2Q-MG3HbL9aUT4uAbjHIiXhJFSYSR4mg,10113
22
+ GameSentenceMiner-2.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
+ GameSentenceMiner-2.3.0.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
24
+ GameSentenceMiner-2.3.0.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
25
+ GameSentenceMiner-2.3.0.dist-info/RECORD,,