GameSentenceMiner 2.5.5__py3-none-any.whl → 2.5.7__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
@@ -120,10 +120,9 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
120
120
  if not last_note:
121
121
  return note
122
122
  game_line = get_text_event(last_note)
123
-
124
- if selected_lines and get_config().anki.multi_overwrites_sentence:
125
- sentences = []
126
- sentences_text = ''
123
+ sentences = []
124
+ sentences_text = ''
125
+ if selected_lines:
127
126
  try:
128
127
  sentence_in_anki = last_note.get_field(get_config().anki.sentence_field)
129
128
  logger.info(f"Attempting Preserve HTML for multi-line")
@@ -134,11 +133,11 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
134
133
  else:
135
134
  sentences.append(line.text)
136
135
 
137
- logger.info(f"Attempting to Fix Character Dialogue Format")
138
- logger.info([f"{line.text}" for line in sentences])
136
+ logger.debug(f"Attempting to Fix Character Dialogue Format")
137
+ logger.debug([f"{line}" for line in sentences])
139
138
  try:
140
139
  combined_lines = combine_dialogue(sentences)
141
-
140
+ logger.debug(combined_lines)
142
141
  if combined_lines:
143
142
  sentences_text = "".join(combined_lines)
144
143
  except Exception as e:
@@ -147,7 +146,13 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
147
146
  except Exception as e:
148
147
  logger.debug(f"Error preserving HTML for multi-line: {e}")
149
148
  pass
150
- note['fields'][get_config().anki.sentence_field] = sentences_text if sentences_text else "<br>".join(sentences)
149
+ multi_line_sentence = sentences_text if sentences_text else get_config().advanced.multi_line_line_break.join(sentences)
150
+ if get_config().anki.multi_overwrites_sentence:
151
+ note['fields'][get_config().anki.sentence_field] = multi_line_sentence
152
+ else:
153
+ logger.info(f"Configured to not overwrite sentence field, Multi-line Sentence If you want it, Note you need to do ctrl+shift+x in anki to paste properly:\n\n" + (sentences_text if sentences_text else get_config().advanced.multi_line_line_break.join(sentences)) + "\n")
154
+ if get_config().advanced.multi_line_sentence_storage_field:
155
+ note['fields'][get_config().advanced.multi_line_sentence_storage_field] = multi_line_sentence
151
156
 
152
157
  if get_config().anki.previous_sentence_field and game_line.prev and not \
153
158
  last_note.get_field(get_config().anki.previous_sentence_field):
@@ -259,6 +264,7 @@ def update_new_card():
259
264
  lines = get_utility_window().get_selected_lines()
260
265
  with util.lock:
261
266
  update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
267
+ get_utility_window().reset_checkboxes()
262
268
  else:
263
269
  logger.info("New card(s) detected! Added to Processing Queue!")
264
270
  card_queue.append(last_card)
@@ -0,0 +1,22 @@
1
+ import os.path
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict, Optional, Any
4
+
5
+ from dataclasses_json import dataclass_json, Undefined
6
+ from websocket import WebSocket
7
+
8
+ from GameSentenceMiner.configuration import get_app_directory
9
+
10
+ CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
11
+ websocket: WebSocket = None
12
+
13
+ @dataclass_json(undefined=Undefined.RAISE)
14
+ @dataclass
15
+ class Message:
16
+ """
17
+ Represents a message for inter-process communication.
18
+ Mimics the structure of IPC or HTTP calls.
19
+ """
20
+ function: str
21
+ data: Dict[str, Any] = field(default_factory=dict)
22
+ id: Optional[str] = None
@@ -0,0 +1,7 @@
1
+ import json
2
+
3
+ from GameSentenceMiner.communication.websocket import websocket, Message
4
+
5
+ def send_restart_signal():
6
+ if websocket and websocket.connected:
7
+ websocket.send(json.dumps(Message(function="restart").to_json()))
@@ -0,0 +1,77 @@
1
+ import asyncio
2
+ import os.path
3
+
4
+ import websockets
5
+ import json
6
+
7
+ from websocket import WebSocket
8
+
9
+ from GameSentenceMiner.communication import Message
10
+ from GameSentenceMiner.configuration import get_app_directory, logger
11
+
12
+ CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
13
+ websocket: WebSocket = None
14
+ handle_websocket_message = None
15
+
16
+ async def do_websocket_connection(port):
17
+ """
18
+ Connects to the WebSocket server running in the Electron app.
19
+ """
20
+ global websocket
21
+
22
+ uri = f"ws://localhost:{port}" # Use the port from Electron
23
+ logger.debug(f"Electron Communication : Connecting to server at {uri}...")
24
+ try:
25
+ async with websockets.connect(uri) as websocket:
26
+ logger.debug(f"Connected to websocket server at {uri}")
27
+
28
+ # Send an initial message
29
+ message = Message(function="on_connect", data={"message": "Hello from Python!"})
30
+ await websocket.send(message.to_json())
31
+ logger.debug(f"> Sent: {message}")
32
+
33
+ # Receive messages from the server
34
+ while True:
35
+ try:
36
+ response = await websocket.recv()
37
+ if response is None:
38
+ break
39
+ logger.debug(f"Electron Communication : < Received: {response}")
40
+ handle_websocket_message(Message.from_json(response))
41
+ await asyncio.sleep(1) # keep the connection alive
42
+ except websockets.ConnectionClosedOK:
43
+ logger.debug("Electron Communication : Connection closed by server")
44
+ break
45
+ except websockets.ConnectionClosedError as e:
46
+ logger.debug(f"Electron Communication : Connection closed with error: {e}")
47
+ break
48
+ except ConnectionRefusedError:
49
+ logger.debug(f"Electron Communication : Error: Could not connect to server at {uri}. Electron App not running..")
50
+ except Exception as e:
51
+ logger.debug(f"Electron Communication : An error occurred: {e}")
52
+
53
+ def connect_websocket():
54
+ """
55
+ Main function to run the WebSocket client.
56
+ """
57
+ # Load the port from the same config.json the Electron app uses
58
+ try:
59
+ with open(CONFIG_FILE, "r") as f:
60
+ config = json.load(f)
61
+ port = config["port"]
62
+ except FileNotFoundError:
63
+ print("Error: shared_config.json not found. Using default port 8766. Ensure Electron app creates this file.")
64
+ port = 8766 # Default port, same as in Electron
65
+ except json.JSONDecodeError:
66
+ print("Error: shared_config.json was not valid JSON. Using default port 8765.")
67
+ port = 8766
68
+
69
+ asyncio.run(do_websocket_connection(port))
70
+
71
+ def register_websocket_message_handler(handler):
72
+ global handle_websocket_message
73
+ handle_websocket_message = handler
74
+
75
+
76
+ if __name__ == "__main__":
77
+ connect_websocket()
@@ -1,13 +1,12 @@
1
1
  import tkinter as tk
2
2
  from tkinter import filedialog, messagebox, simpledialog
3
3
 
4
- import pyperclip
5
4
  import ttkbootstrap as ttk
6
5
 
7
6
  from GameSentenceMiner import obs, configuration
7
+ from GameSentenceMiner.communication.send import send_restart_signal
8
8
  from GameSentenceMiner.configuration import *
9
9
  from GameSentenceMiner.downloader.download_tools import download_ocenaudio_if_needed
10
- from GameSentenceMiner.electron_messaging import signal_restart_settings_change
11
10
  from GameSentenceMiner.package import get_current_version, get_latest_version
12
11
 
13
12
  settings_saved = False
@@ -77,6 +76,7 @@ class ConfigApp:
77
76
  self.create_obs_tab()
78
77
  self.create_hotkeys_tab()
79
78
  self.create_profiles_tab()
79
+ self.create_advanced_tab()
80
80
 
81
81
  ttk.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
82
82
 
@@ -178,7 +178,8 @@ class ConfigApp:
178
178
  hotkeys=Hotkeys(
179
179
  reset_line=self.reset_line_hotkey.get(),
180
180
  take_screenshot=self.take_screenshot_hotkey.get(),
181
- open_utility=self.open_utility_hotkey.get()
181
+ open_utility=self.open_utility_hotkey.get(),
182
+ play_latest_audio=self.play_latest_audio_hotkey.get()
182
183
  ),
183
184
  vad=VAD(
184
185
  whisper_model=self.whisper_model.get(),
@@ -189,6 +190,13 @@ class ConfigApp:
189
190
  trim_beginning=self.vad_trim_beginning.get(),
190
191
  beginning_offset=float(self.vad_beginning_offset.get()),
191
192
  add_audio_on_no_results=self.add_audio_on_no_results.get(),
193
+ ),
194
+ advanced=Advanced(
195
+ audio_player_path=self.audio_player_path.get(),
196
+ video_player_path=self.video_player_path.get(),
197
+ show_screenshot_buttons=self.show_screenshot_button.get(),
198
+ multi_line_line_break=self.multi_line_line_break.get(),
199
+ multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
192
200
  )
193
201
  )
194
202
 
@@ -216,7 +224,7 @@ class ConfigApp:
216
224
 
217
225
  if self.master_config.get_config().restart_required(prev_config):
218
226
  logger.info("Restart Required for some settings to take affect!")
219
- signal_restart_settings_change()
227
+ send_restart_signal()
220
228
  settings_saved = True
221
229
  configuration.reload_config()
222
230
  self.settings = get_config()
@@ -248,6 +256,7 @@ class ConfigApp:
248
256
  self.create_obs_tab()
249
257
  self.create_hotkeys_tab()
250
258
  self.create_profiles_tab()
259
+ self.create_advanced_tab()
251
260
 
252
261
 
253
262
  def increment_row(self):
@@ -442,6 +451,12 @@ class ConfigApp:
442
451
 
443
452
  return paths_frame
444
453
 
454
+ def browse_file(self, entry_widget):
455
+ file_selected = filedialog.askopenfilename()
456
+ if file_selected:
457
+ entry_widget.delete(0, tk.END)
458
+ entry_widget.insert(0, file_selected)
459
+
445
460
  def browse_folder(self, entry_widget):
446
461
  folder_selected = filedialog.askdirectory()
447
462
  if folder_selected:
@@ -880,6 +895,51 @@ class ConfigApp:
880
895
  if self.master_config.current_profile != DEFAULT_CONFIG:
881
896
  ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile).grid(row=self.current_row, column=2, pady=5)
882
897
 
898
+ @new_tab
899
+ def create_advanced_tab(self):
900
+ advanced_frame = ttk.Frame(self.notebook)
901
+ self.notebook.add(advanced_frame, text='Advanced')
902
+
903
+ ttk.Label(advanced_frame, text="Note: Only one of these will take effect, prioritizing audio.", foreground="red").grid(row=self.current_row, column=0, columnspan=3, sticky='W')
904
+ self.current_row += 1
905
+
906
+ ttk.Label(advanced_frame, text="Audio Player Path:").grid(row=self.current_row, column=0, sticky='W')
907
+ self.audio_player_path = ttk.Entry(advanced_frame, width=50)
908
+ self.audio_player_path.insert(0, self.settings.advanced.audio_player_path)
909
+ self.audio_player_path.grid(row=self.current_row, column=1)
910
+ ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.audio_player_path)).grid(row=self.current_row, column=2)
911
+ self.add_label_and_increment_row(advanced_frame, "Path to the audio player executable. Will open the trimmed Audio", row=self.current_row, column=3)
912
+
913
+ ttk.Label(advanced_frame, text="Video Player Path:").grid(row=self.current_row, column=0, sticky='W')
914
+ self.video_player_path = ttk.Entry(advanced_frame, width=50)
915
+ self.video_player_path.insert(0, self.settings.advanced.video_player_path)
916
+ self.video_player_path.grid(row=self.current_row, column=1)
917
+ ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.video_player_path)).grid(row=self.current_row, column=2)
918
+ self.add_label_and_increment_row(advanced_frame, "Path to the video player executable. Will seek to the location of the line in the replay", row=self.current_row, column=3)
919
+
920
+
921
+ ttk.Label(advanced_frame, text="Play Latest Video/Audio Hotkey:").grid(row=self.current_row, column=0, sticky='W')
922
+ self.play_latest_audio_hotkey = ttk.Entry(advanced_frame)
923
+ self.play_latest_audio_hotkey.insert(0, self.settings.hotkeys.play_latest_audio)
924
+ self.play_latest_audio_hotkey.grid(row=self.current_row, column=1)
925
+ self.add_label_and_increment_row(advanced_frame, "Hotkey to trim and play the latest audio.", row=self.current_row, column=2)
926
+
927
+ ttk.Label(advanced_frame, text="Show Screenshot Button:").grid(row=self.current_row, column=0, sticky='W')
928
+ self.show_screenshot_button = tk.BooleanVar(value=self.settings.advanced.show_screenshot_buttons)
929
+ ttk.Checkbutton(advanced_frame, variable=self.show_screenshot_button).grid(row=self.current_row, column=1, sticky='W')
930
+ self.add_label_and_increment_row(advanced_frame, "Show the screenshot button in the utility gui.", row=self.current_row, column=2)
931
+
932
+ ttk.Label(advanced_frame, text="Multi-line Line-Break:").grid(row=self.current_row, column=0, sticky='W')
933
+ self.multi_line_line_break = ttk.Entry(advanced_frame)
934
+ self.multi_line_line_break.insert(0, self.settings.advanced.multi_line_line_break)
935
+ self.multi_line_line_break.grid(row=self.current_row, column=1)
936
+ self.add_label_and_increment_row(advanced_frame, "Line break for multi-line mining. This goes between each sentence", row=self.current_row, column=2)
937
+
938
+ ttk.Label(advanced_frame, text="Multi-Line Sentence Storage Field:").grid(row=self.current_row, column=0, sticky='W')
939
+ self.multi_line_sentence_storage_field = ttk.Entry(advanced_frame)
940
+ self.multi_line_sentence_storage_field.insert(0, self.settings.advanced.multi_line_sentence_storage_field)
941
+ self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1)
942
+ self.add_label_and_increment_row(advanced_frame, "Field in Anki for storing the multi-line sentence temporarily.", row=self.current_row, column=2)
883
943
 
884
944
  def on_profile_change(self, event):
885
945
  print("profile Changed!")
@@ -139,6 +139,7 @@ class Hotkeys:
139
139
  reset_line: str = 'f5'
140
140
  take_screenshot: str = 'f6'
141
141
  open_utility: str = 'ctrl+m'
142
+ play_latest_audio: str = 'f7'
142
143
 
143
144
 
144
145
  @dataclass_json
@@ -153,6 +154,26 @@ class VAD:
153
154
  beginning_offset: float = -0.25
154
155
  add_audio_on_no_results: bool = False
155
156
 
157
+ def is_silero(self):
158
+ return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
159
+
160
+ def is_whisper(self):
161
+ return self.selected_vad_model == WHISPER or self.backup_vad_model == WHISPER
162
+
163
+ def is_vosk(self):
164
+ return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
165
+
166
+
167
+ @dataclass_json
168
+ @dataclass
169
+ class Advanced:
170
+ audio_player_path: str = ''
171
+ video_player_path: str = ''
172
+ show_screenshot_buttons: bool = False
173
+ multi_line_line_break: str = '<br>'
174
+ multi_line_sentence_storage_field: str = ''
175
+
176
+
156
177
 
157
178
  @dataclass_json
158
179
  @dataclass
@@ -167,6 +188,7 @@ class ProfileConfig:
167
188
  obs: OBS = field(default_factory=OBS)
168
189
  hotkeys: Hotkeys = field(default_factory=Hotkeys)
169
190
  vad: VAD = field(default_factory=VAD)
191
+ advanced: Advanced = field(default_factory=Advanced)
170
192
 
171
193
 
172
194
  # This is just for legacy support
@@ -421,7 +443,7 @@ console_handler = logging.StreamHandler(sys.stdout)
421
443
  console_handler.setLevel(logging.INFO)
422
444
 
423
445
  # Create rotating file handler with level DEBUG
424
- file_handler = RotatingFileHandler(get_log_path(), maxBytes=10 * 1024 * 1024, backupCount=0, encoding='utf-8')
446
+ file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
425
447
  file_handler.setLevel(logging.DEBUG)
426
448
 
427
449
  # Create a formatter
@@ -48,6 +48,10 @@ def get_screenshot(video_file, time_from_end):
48
48
 
49
49
  return output_image
50
50
 
51
+ def get_screenshot_for_line(video_file, game_line):
52
+ return get_screenshot(video_file, get_screenshot_time(video_file, game_line))
53
+
54
+
51
55
 
52
56
  def get_screenshot_time(video_path, game_line, default_beginning=False):
53
57
  if game_line:
@@ -183,19 +187,7 @@ def get_video_duration(file_path):
183
187
  def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line):
184
188
  trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
185
189
  suffix=f".{get_config().audio.extension}").name
186
- file_mod_time = get_file_modification_time(video_path)
187
- file_length = get_video_duration(video_path)
188
- time_delta = file_mod_time - game_line.time
189
- # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
190
- total_seconds = file_length - time_delta.total_seconds()
191
- total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
192
- if total_seconds < 0 or total_seconds >= file_length:
193
- logger.info(f"0 seconds trimmed off of beginning")
194
- return untrimmed_audio
195
-
196
- hours, remainder = divmod(total_seconds_after_offset, 3600)
197
- minutes, seconds = divmod(remainder, 60)
198
- start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
190
+ start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line)
199
191
 
200
192
  ffmpeg_command = ffmpeg_base_command_list + [
201
193
  "-i", untrimmed_audio,
@@ -222,6 +214,22 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
222
214
  logger.info(f"Audio trimmed and saved to {trimmed_audio}")
223
215
  return trimmed_audio
224
216
 
217
+ def get_video_timings(video_path, game_line):
218
+ file_mod_time = get_file_modification_time(video_path)
219
+ file_length = get_video_duration(video_path)
220
+ time_delta = file_mod_time - game_line.time
221
+ # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
222
+ total_seconds = file_length - time_delta.total_seconds()
223
+ total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
224
+ if total_seconds < 0 or total_seconds >= file_length:
225
+ logger.info(f"0 seconds trimmed off of beginning")
226
+ return 0, 0, 0
227
+
228
+ hours, remainder = divmod(total_seconds_after_offset, 3600)
229
+ minutes, seconds = divmod(remainder, 60)
230
+ start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
231
+ return start_trim_time, total_seconds, total_seconds_after_offset
232
+
225
233
 
226
234
  def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
227
235
  logger.info(f"Re-encode running with settings: {user_ffmpeg_options}")
@@ -251,7 +259,7 @@ def create_temp_file_with_same_name(input_file: str):
251
259
  def replace_file_with_retry(temp_file, input_file, retries=5, delay=1):
252
260
  for attempt in range(retries):
253
261
  try:
254
- os.replace(temp_file, input_file)
262
+ shutil.move(temp_file, input_file)
255
263
  logger.info(f'Re-encode Finished!')
256
264
  return
257
265
  except OSError as e:
GameSentenceMiner/gsm.py CHANGED
@@ -18,19 +18,20 @@ from GameSentenceMiner import gametext
18
18
  from GameSentenceMiner import notification
19
19
  from GameSentenceMiner import obs
20
20
  from GameSentenceMiner import util
21
+ from GameSentenceMiner.communication import Message
22
+ from GameSentenceMiner.communication.send import send_restart_signal
23
+ from GameSentenceMiner.communication.websocket import connect_websocket, register_websocket_message_handler
21
24
  from GameSentenceMiner.configuration import *
22
25
  from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
23
- from GameSentenceMiner.electron_messaging import signal_restart_settings_change
24
- from GameSentenceMiner.ffmpeg import get_audio_and_trim
25
- from GameSentenceMiner.gametext import get_text_event, get_mined_line
26
+ from GameSentenceMiner.ffmpeg import get_audio_and_trim, get_video_timings
27
+ from GameSentenceMiner.gametext import get_text_event, get_mined_line, GameLine
26
28
  from GameSentenceMiner.util import *
27
29
  from GameSentenceMiner.utility_gui import init_utility_window, get_utility_window
28
- from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
30
+ from GameSentenceMiner.vad import silero_trim, whisper_helper, vosk_helper
29
31
 
30
32
  if is_windows():
31
33
  import win32api
32
34
 
33
- obs_process = None
34
35
  procs_to_close = []
35
36
  settings_window: config_gui.ConfigApp = None
36
37
  obs_paused = False
@@ -69,6 +70,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
69
70
 
70
71
  @staticmethod
71
72
  def convert_to_audio(video_path):
73
+ if get_utility_window().line_for_audio:
74
+ line: GameLine = get_utility_window().line_for_audio
75
+ get_utility_window().line_for_audio = None
76
+ if get_config().advanced.audio_player_path:
77
+ audio = VideoToAudioHandler.get_audio(line, line.next.time if line.next else None, video_path, temporary=True)
78
+ play_audio_in_external(audio)
79
+ os.remove(video_path)
80
+ elif get_config().advanced.video_player_path:
81
+ play_video_in_external(line, video_path)
82
+ return
83
+ if get_utility_window().line_for_screenshot:
84
+ line: GameLine = get_utility_window().line_for_screenshot
85
+ get_utility_window().line_for_screenshot = None
86
+ screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
87
+ os.startfile(screenshot)
88
+ os.remove(video_path)
89
+ return
72
90
  try:
73
91
  last_note = None
74
92
  if anki.card_queue and len(anki.card_queue) > 0:
@@ -112,8 +130,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
112
130
  logger.debug(last_note.to_json())
113
131
 
114
132
  note = anki.get_initial_card_info(last_note, get_utility_window().get_selected_lines())
115
-
116
133
  tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
134
+ get_utility_window().reset_checkboxes()
117
135
 
118
136
  if get_config().anki.sentence_audio_field:
119
137
  logger.debug("Attempting to get audio from video")
@@ -142,11 +160,13 @@ class VideoToAudioHandler(FileSystemEventHandler):
142
160
  os.remove(video_path) # Optionally remove the video after conversion
143
161
  if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
144
162
  os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
145
- get_utility_window().reset_checkboxes()
163
+
146
164
 
147
165
  @staticmethod
148
- def get_audio(game_line, next_line_time, video_path):
166
+ def get_audio(game_line, next_line_time, video_path, temporary=False):
149
167
  trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
168
+ if temporary:
169
+ return trimmed_audio
150
170
  vad_trimmed_audio = make_unique_file_name(
151
171
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
152
172
  final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
@@ -181,31 +201,54 @@ class VideoToAudioHandler(FileSystemEventHandler):
181
201
  ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
182
202
  get_config().audio.ffmpeg_reencode_options)
183
203
  elif os.path.exists(vad_trimmed_audio):
184
- os.replace(vad_trimmed_audio, final_audio_output)
204
+ shutil.move(vad_trimmed_audio, final_audio_output)
185
205
  return final_audio_output, should_update_audio, vad_trimmed_audio
186
206
 
207
+ def play_audio_in_external(filepath):
208
+ exe = get_config().advanced.audio_player_path
187
209
 
188
- def initialize(reloading=False):
189
- global obs_process
190
- if not reloading:
191
- if is_windows():
192
- download_obs_if_needed()
193
- download_ffmpeg_if_needed()
194
- if get_config().obs.enabled:
195
- if get_config().obs.open_obs:
196
- obs_process = obs.start_obs()
197
- obs.connect_to_obs(start_replay=True)
198
- anki.start_monitoring_anki()
199
- gametext.start_text_monitor()
200
- os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
201
- os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
202
- os.makedirs(get_config().paths.audio_destination, exist_ok=True)
203
- if get_config().vad.do_vad_postprocessing:
204
- if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
205
- vosk_helper.get_vosk_model()
206
- if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
207
- whisper_helper.initialize_whisper_model()
210
+ filepath = os.path.normpath(filepath)
211
+
212
+ command = [exe, filepath]
213
+
214
+ try:
215
+ subprocess.Popen(command)
216
+ print(f"Opened {filepath} in {exe}.")
217
+ except Exception as e:
218
+ print(f"An error occurred: {e}")
219
+
220
+ def play_video_in_external(line, filepath):
221
+ def remove_video_when_closed(p, fp):
222
+ p.wait()
223
+ os.remove(fp)
224
+
225
+ command = [get_config().advanced.video_player_path, os.path.normpath(filepath)]
226
+
227
+ start, _, _ = get_video_timings(filepath, line)
228
+
229
+ print(start)
230
+
231
+ if start:
232
+ command.extend(["--start-time", convert_to_vlc_seconds(start)])
233
+
234
+ try:
235
+ proc = subprocess.Popen(command)
236
+ print(f"Opened {filepath} in VLC.")
237
+ threading.Thread(target=remove_video_when_closed, args=(proc, filepath)).start()
238
+ except FileNotFoundError:
239
+ print("VLC not found. Make sure it's installed and in your PATH.")
240
+ except Exception as e:
241
+ print(f"An error occurred: {e}")
208
242
 
243
+ def convert_to_vlc_seconds(time_str):
244
+ """Converts HH:MM:SS.milliseconds to VLC-compatible seconds."""
245
+ try:
246
+ hours, minutes, seconds_ms = time_str.split(":")
247
+ seconds, milliseconds = seconds_ms.split(".")
248
+ total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + int(seconds) + (int(milliseconds) / 1000.0)
249
+ return str(total_seconds)
250
+ except ValueError:
251
+ return "Invalid time format"
209
252
 
210
253
  def initial_checks():
211
254
  try:
@@ -220,6 +263,7 @@ def register_hotkeys():
220
263
  keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
221
264
  keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
222
265
  keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
266
+ keyboard.add_hotkey(get_config().hotkeys.play_latest_audio, play_most_recent_audio)
223
267
 
224
268
 
225
269
  def get_screenshot():
@@ -274,6 +318,13 @@ def open_multimine():
274
318
  obs.update_current_game()
275
319
  get_utility_window().show()
276
320
 
321
+ def play_most_recent_audio():
322
+ if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(gametext.line_history) > 0:
323
+ get_utility_window().line_for_audio = gametext.line_history.values[-1]
324
+ obs.save_replay_buffer()
325
+ else:
326
+ logger.error("Feature Disabled. No audio or video player path set in config!")
327
+
277
328
 
278
329
  def open_log():
279
330
  """Function to handle opening log."""
@@ -343,7 +394,7 @@ def switch_profile(icon, item):
343
394
  settings_window.reload_settings()
344
395
  update_icon()
345
396
  if get_config().restart_required(prev_config):
346
- signal_restart_settings_change()
397
+ send_restart_signal()
347
398
 
348
399
 
349
400
  def run_tray():
@@ -388,10 +439,10 @@ def run_tray():
388
439
  # proc.wait()
389
440
 
390
441
  def close_obs():
391
- if obs_process:
442
+ if obs.obs_process:
392
443
  try:
393
- subprocess.run(["taskkill", "/PID", str(obs_process), "/F"], check=True, capture_output=True, text=True)
394
- print(f"OBS (PID {obs_process}) has been terminated.")
444
+ subprocess.run(["taskkill", "/PID", str(obs.obs_process.pid), "/F"], check=True, capture_output=True, text=True)
445
+ print(f"OBS (PID {obs.obs_process.pid}) has been terminated.")
395
446
  except subprocess.CalledProcessError as e:
396
447
  print(f"Error terminating OBS: {e.stderr}")
397
448
  else:
@@ -399,12 +450,11 @@ def close_obs():
399
450
 
400
451
 
401
452
  def restart_obs():
402
- global obs_process
403
- if obs_process:
453
+ if obs.obs_process:
404
454
  close_obs()
405
455
  time.sleep(2)
406
- obs_process = obs.start_obs()
407
- obs.connect_to_obs(start_replay=True)
456
+ obs.start_obs()
457
+ obs.connect_to_obs()
408
458
 
409
459
 
410
460
  def cleanup():
@@ -438,20 +488,6 @@ def cleanup():
438
488
  logger.info("Cleanup complete.")
439
489
 
440
490
 
441
- def check_for_stdin():
442
- while True:
443
- for line in sys.stdin:
444
- logger.info(f"Got stdin: {line}")
445
- if "exit" in line:
446
- cleanup()
447
- sys.exit(0)
448
- elif "restart_obs" in line:
449
- restart_obs()
450
- elif "update" in line:
451
- update_icon()
452
- sys.stdin.flush()
453
-
454
-
455
491
  def handle_exit():
456
492
  """Signal handler for graceful termination."""
457
493
 
@@ -462,23 +498,74 @@ def handle_exit():
462
498
 
463
499
  return _handle_exit
464
500
 
501
+ def initialize(reloading=False):
502
+ global obs_process
503
+ if not reloading:
504
+ if is_windows():
505
+ download_obs_if_needed()
506
+ download_ffmpeg_if_needed()
507
+ # if get_config().obs.enabled:
508
+ # if get_config().obs.open_obs:
509
+ # obs_process = obs.start_obs()
510
+ # obs.connect_to_obs(start_replay=True)
511
+ # anki.start_monitoring_anki()
512
+ # gametext.start_text_monitor()
513
+ os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
514
+ os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
515
+ os.makedirs(get_config().paths.audio_destination, exist_ok=True)
516
+ initial_checks()
517
+ register_websocket_message_handler(handle_websocket_message)
518
+ # if get_config().vad.do_vad_postprocessing:
519
+ # if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
520
+ # vosk_helper.get_vosk_model()
521
+ # if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
522
+ # whisper_helper.initialize_whisper_model()
523
+
524
+ def initialize_async():
525
+ tasks = [gametext.start_text_monitor, connect_websocket, run_tray]
526
+ threads = []
527
+ if get_config().obs.enabled:
528
+ if get_config().obs.open_obs:
529
+ tasks.append(obs.start_obs)
530
+ tasks.append(obs.connect_to_obs)
531
+ tasks.append(anki.start_monitoring_anki)
532
+ if get_config().vad.do_vad_postprocessing:
533
+ if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
534
+ tasks.append(vosk_helper.get_vosk_model)
535
+ if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
536
+ tasks.append(whisper_helper.initialize_whisper_model)
537
+ for task in tasks:
538
+ threads.append(util.run_new_thread(task))
539
+ return threads
540
+
541
+ def check_async_init_done(threads):
542
+ while True:
543
+ if all(not thread.is_alive() for thread in threads):
544
+ break
545
+ time.sleep(0.5)
546
+ logger.info("Script Fully Initialized. Happy Mining!")
547
+
548
+
549
+ def handle_websocket_message(message: Message):
550
+ match message.function:
551
+ case "quit":
552
+ cleanup()
553
+ sys.exit(0)
554
+ case _:
555
+ logger.debug(f"unknown message from electron websocket: {message.to_json()}")
465
556
 
466
- def main(reloading=False, do_config_input=True):
557
+
558
+ def main(reloading=False):
467
559
  global root, settings_window
468
560
  logger.info("Script started.")
469
- util.run_new_thread(check_for_stdin)
470
561
  root = ttk.Window(themename='darkly')
471
562
  settings_window = config_gui.ConfigApp(root)
472
563
  init_utility_window(root)
473
564
  initialize(reloading)
474
- util.run_new_thread(run_tray)
475
- initial_checks()
476
- event_handler = VideoToAudioHandler()
565
+ initialize_async()
477
566
  observer = Observer()
478
- observer.schedule(event_handler, get_config().paths.folder_to_watch, recursive=False)
567
+ observer.schedule(VideoToAudioHandler(), get_config().paths.folder_to_watch, recursive=False)
479
568
  observer.start()
480
-
481
- logger.info("Script Initialized. Happy Mining!")
482
569
  if not is_linux():
483
570
  register_hotkeys()
484
571
 
@@ -40,7 +40,7 @@ def open_anki_card(note_id):
40
40
 
41
41
  def send_notification(title, message, timeout):
42
42
  if windows:
43
- notifier.show_toast(title, message, duration=timeout)
43
+ notifier.show_toast(title, message, duration=timeout, threaded=True)
44
44
  else:
45
45
  notification.notify(
46
46
  title=title,
GameSentenceMiner/obs.py CHANGED
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import subprocess
2
3
  import time
3
4
 
@@ -9,6 +10,8 @@ from GameSentenceMiner.configuration import *
9
10
  from GameSentenceMiner.model import *
10
11
 
11
12
  client: obsws = None
13
+ obs_process = None
14
+ logging.getLogger('obswebsocket').setLevel(logging.CRITICAL)
12
15
 
13
16
  # REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
14
17
 
@@ -17,6 +20,7 @@ def get_obs_path():
17
20
  return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
18
21
 
19
22
  def start_obs():
23
+ global obs_process
20
24
  obs_path = get_obs_path()
21
25
  if not os.path.exists(obs_path):
22
26
  logger.error(f"OBS not found at {obs_path}. Please install OBS.")
@@ -26,9 +30,9 @@ def start_obs():
26
30
  obs_pid = is_obs_running(obs_path)
27
31
  if obs_pid:
28
32
  return obs_pid
29
- process = subprocess.Popen([obs_path, '--disable-shutdown-check', '--portable'], cwd=os.path.dirname(obs_path))
33
+ obs_process = subprocess.Popen([obs_path, '--disable-shutdown-check', '--portable'], cwd=os.path.dirname(obs_path))
30
34
  logger.info("OBS launched")
31
- return process.pid
35
+ return obs_process.pid
32
36
  except Exception as e:
33
37
  logger.error(f"Error launching OBS: {e}")
34
38
  return None
@@ -87,7 +91,7 @@ def on_disconnect(obs):
87
91
  logger.error("OBS Connection Lost!")
88
92
 
89
93
 
90
- def connect_to_obs(start_replay=False):
94
+ def connect_to_obs():
91
95
  global client
92
96
  if get_config().obs.enabled:
93
97
  if util.is_windows():
@@ -97,8 +101,7 @@ def connect_to_obs(start_replay=False):
97
101
  on_disconnect=on_disconnect)
98
102
  client.connect()
99
103
 
100
- time.sleep(1)
101
- if start_replay and get_config().obs.start_buffer:
104
+ if get_config().obs.start_buffer:
102
105
  start_replay_buffer()
103
106
  update_current_game()
104
107
 
@@ -111,22 +114,45 @@ def disconnect_from_obs():
111
114
  client = None
112
115
  logger.info("Disconnected from OBS WebSocket.")
113
116
 
117
+ def do_obs_call(request, from_dict = None, retry=5):
118
+ try:
119
+ response = client.call(request)
120
+ if not response.status and retry > 0:
121
+ time.sleep(1)
122
+ return do_obs_call(request, from_dict, retry - 1)
123
+ if from_dict:
124
+ return from_dict(response.datain)
125
+ return None
126
+ except Exception as e:
127
+ if "socket is already closed" in str(e) or "object has no attribute" in str(e):
128
+ if retry > 0:
129
+ time.sleep(1)
130
+ return do_obs_call(request, from_dict, retry - 1)
131
+ else:
132
+ logger.error(f"Error doing obs call: {e}")
133
+ raise e
134
+ return None
114
135
 
115
136
  def toggle_replay_buffer():
116
137
  try:
117
- client.call(requests.ToggleReplayBuffer())
138
+ do_obs_call(requests.ToggleReplayBuffer())
118
139
  logger.info("Replay buffer Toggled.")
119
140
  except Exception as e:
120
141
  logger.error(f"Error toggling buffer: {e}")
121
142
 
122
143
 
123
144
  # Start replay buffer
124
- def start_replay_buffer():
145
+ def start_replay_buffer(retry=5):
125
146
  try:
126
147
  client.call(requests.GetVersion())
127
148
  client.call(requests.StartReplayBuffer())
128
149
  except Exception as e:
129
- logger.error(f"Error starting replay buffer: {e}")
150
+ if "socket is already closed" in str(e):
151
+ if retry > 0:
152
+ time.sleep(1)
153
+ start_replay_buffer(retry - 1)
154
+ else:
155
+ logger.error(f"Error starting replay buffer: {e}")
130
156
 
131
157
 
132
158
  # Stop replay buffer
@@ -153,9 +179,7 @@ def save_replay_buffer():
153
179
 
154
180
  def get_current_scene():
155
181
  try:
156
- response = client.call(requests.GetCurrentProgramScene())
157
- scene_info = SceneInfo.from_dict(response.datain)
158
- return scene_info.sceneName
182
+ return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict).sceneName
159
183
  except Exception as e:
160
184
  logger.error(f"Couldn't get scene: {e}")
161
185
  return ''
@@ -163,9 +187,7 @@ def get_current_scene():
163
187
 
164
188
  def get_source_from_scene(scene_name):
165
189
  try:
166
- response = client.call(requests.GetSceneItemList(sceneName=scene_name))
167
- scene_list = SceneItemsResponse.from_dict(response.datain)
168
- return scene_list.sceneItems[0]
190
+ return do_obs_call(requests.GetSceneItemList(sceneName=scene_name), SceneItemsResponse.from_dict).sceneItems[0]
169
191
  except Exception as e:
170
192
  logger.error(f"Error getting source from scene: {e}")
171
193
  return ''
GameSentenceMiner/util.py CHANGED
@@ -1,15 +1,17 @@
1
+ import importlib
1
2
  import os
2
3
  import random
3
4
  import re
4
5
  import string
5
6
  import subprocess
7
+ import sys
6
8
  import threading
7
9
  from datetime import datetime
8
10
  from sys import platform
9
11
 
10
12
  from rapidfuzz import process
11
13
 
12
- from GameSentenceMiner.configuration import logger
14
+ from GameSentenceMiner.configuration import logger, get_config
13
15
 
14
16
  SCRIPTS_DIR = r"E:\Japanese Stuff\agent-v0.1.4-win32-x64\data\scripts"
15
17
 
@@ -156,25 +158,58 @@ def combine_dialogue(dialogue_lines, new_lines=None):
156
158
  new_lines = []
157
159
 
158
160
  if len(dialogue_lines) == 1 and '「' not in dialogue_lines[0]:
159
- new_lines.append(dialogue_lines[0] + "<br>")
161
+ new_lines.append(dialogue_lines[0])
160
162
  return new_lines
161
163
 
162
164
  character_name = dialogue_lines[0].split("「")[0]
163
165
  text = character_name + "「"
164
- next_character = ''
165
166
 
166
167
  for i, line in enumerate(dialogue_lines):
167
168
  if not line.startswith(character_name + "「"):
168
- text = text + "」<br>"
169
- next_character = line.split("「")[0]
169
+ text = text + "" + get_config().advanced.multi_line_line_break
170
170
  new_lines.append(text)
171
171
  new_lines.extend(combine_dialogue(dialogue_lines[i:]))
172
172
  break
173
173
  else:
174
- text += ("<br>" if i > 0 else "") + line.split("「")[1].rstrip("」") + ""
174
+ text += (get_config().advanced.multi_line_line_break if i > 0 else "") + line.split("「")[1].rstrip("」") + ""
175
175
  else:
176
176
  text = text + "」"
177
177
  new_lines.append(text)
178
178
 
179
179
  return new_lines
180
180
 
181
+ # def import_vad_models():
182
+ # silero_trim, whisper_helper, vosk_helper = None, None, None
183
+ #
184
+ # def check_and_install(package_name):
185
+ # try:
186
+ # importlib.import_module(package_name)
187
+ # return True
188
+ # except ImportError:
189
+ # logger.warning(f"{package_name} is not installed. Attempting to install...")
190
+ # try:
191
+ # python_executable = sys.executable
192
+ # subprocess.check_call([python_executable, "-m", "pip", "install", package_name])
193
+ # logger.info(f"{package_name} installed successfully.")
194
+ # return True
195
+ # except subprocess.CalledProcessError as e:
196
+ # logger.error(f"Failed to install {package_name}: {e}")
197
+ # return False
198
+ #
199
+ # if get_config().vad.is_silero():
200
+ # if check_and_install("silero_vad"):
201
+ # from GameSentenceMiner.vad import silero_trim
202
+ # else:
203
+ # logger.error("Silero VAD is enabled and silero_vad package could not be installed.")
204
+ # if get_config().vad.is_whisper():
205
+ # if check_and_install("stable-ts"):
206
+ # from GameSentenceMiner.vad import whisper_helper
207
+ # else:
208
+ # logger.error("Whisper is enabled and whisper package could not be installed.")
209
+ # if get_config().vad.is_vosk():
210
+ # if check_and_install("vosk"):
211
+ # from GameSentenceMiner.vad import vosk_helper
212
+ # else:
213
+ # logger.error("Vosk is enabled and vosk package could not be installed.")
214
+ #
215
+ # return silero_trim, whisper_helper, vosk_helper
@@ -3,16 +3,22 @@ import os
3
3
  import tkinter as tk
4
4
  from tkinter import ttk
5
5
 
6
- from GameSentenceMiner.configuration import logger, get_app_directory
6
+ from GameSentenceMiner import obs
7
+ from GameSentenceMiner.configuration import logger, get_app_directory, get_config
7
8
 
8
9
 
9
10
  class UtilityApp:
10
11
  def __init__(self, root):
11
12
  self.root = root
12
13
  self.items = []
14
+ self.play_audio_buttons = []
15
+ self.get_screenshot_buttons = []
13
16
  self.checkboxes = []
14
17
  self.multi_mine_window = None # Store the multi-mine window reference
15
18
  self.checkbox_frame = None
19
+ self.line_for_audio = None
20
+ self.line_for_screenshot = None
21
+ self.line_counter = 0
16
22
 
17
23
  style = ttk.Style()
18
24
  style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
@@ -80,17 +86,49 @@ class UtilityApp:
80
86
  self.checkboxes[0].destroy()
81
87
  self.checkboxes.pop(0)
82
88
  self.items.pop(0)
89
+ if self.play_audio_buttons:
90
+ self.play_audio_buttons[0].destroy()
91
+ self.play_audio_buttons.pop(0)
92
+ if self.get_screenshot_buttons:
93
+ self.get_screenshot_buttons[0].destroy()
94
+ self.get_screenshot_buttons.pop(0)
83
95
 
84
96
  if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
85
97
  self.add_checkbox_to_gui(line, var)
98
+ self.line_for_audio = None
99
+ self.line_for_screenshot = None
86
100
 
87
101
  def add_checkbox_to_gui(self, line, var):
88
102
  """ Add a single checkbox without repainting everything. """
89
103
  if self.checkbox_frame:
104
+ column = 0
105
+ if get_config().advanced.show_screenshot_buttons:
106
+ get_screenshot_button = ttk.Button(self.checkbox_frame, text="📸", command=lambda: self.take_screenshot(line))
107
+ get_screenshot_button.grid(row=self.line_counter, column=column, sticky='w', padx=5)
108
+ self.get_screenshot_buttons.append(get_screenshot_button)
109
+ column += 1
110
+
111
+ if get_config().advanced.video_player_path or get_config().advanced.audio_player_path:
112
+ play_audio_button = ttk.Button(self.checkbox_frame, text="🔊", command=lambda: self.play_audio(line))
113
+ play_audio_button.grid(row=self.line_counter, column=column, sticky='w', padx=5)
114
+ self.play_audio_buttons.append(play_audio_button)
115
+ column += 1
116
+
90
117
  chk = ttk.Checkbutton(self.checkbox_frame, text=f"{line.time.strftime('%H:%M:%S')} - {line.text}", variable=var)
91
- chk.pack(anchor='w')
118
+ chk.grid(row=self.line_counter, column=column, sticky='w', padx=5)
92
119
  self.checkboxes.append(chk)
93
120
 
121
+ self.line_counter += 1
122
+
123
+
124
+ def play_audio(self, line):
125
+ self.line_for_audio = line
126
+ obs.save_replay_buffer()
127
+
128
+ def take_screenshot(self, line):
129
+ self.line_for_screenshot = line
130
+ obs.save_replay_buffer()
131
+
94
132
 
95
133
  # def update_multi_mine_window(self):
96
134
  # for widget in self.multi_mine_window.winfo_children():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.5.5
3
+ Version: 2.5.7
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
@@ -0,0 +1,29 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=YcdNDpUskjdxi8upZ5SLGTyRbO8NlOPcqsV_8akTwuM,12877
3
+ GameSentenceMiner/config_gui.py,sha256=cLKVliB0X61WNduNisOmaEtqSr1mvTO6ZAiv-t2jW-8,61194
4
+ GameSentenceMiner/configuration.py,sha256=-b1wW6EkkOFwF1No1uZLD2nPUV5gfBIzG5a8_ykqyUI,16691
5
+ GameSentenceMiner/ffmpeg.py,sha256=Bvkk0TMHtoQkpEYQls48CbC4TB0-FzrnqQhRNg36hVk,11831
6
+ GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
7
+ GameSentenceMiner/gsm.py,sha256=UeGSoFWV1lq3YS0lcDL4zXmvOEEMvSjeS1KKQpnG3co,24169
8
+ GameSentenceMiner/model.py,sha256=bZm-2vkIw4gQCGLB02eDoTtO1Ymb_dnHk0VDJDFO3y8,5228
9
+ GameSentenceMiner/notification.py,sha256=2d8_8DUxImeC1zY-V4c_PZw1zbvvGZ6KiAnPaSmH9G0,2592
10
+ GameSentenceMiner/obs.py,sha256=N7XoPSzLk9rHi4sgsG_LS2dN06MrqwN2mpSHfjONTjE,7368
11
+ GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
12
+ GameSentenceMiner/util.py,sha256=tkaoU1bj8iPMTNwUCWUzFLAnT44Ot92D1tYwQMEnARw,7336
13
+ GameSentenceMiner/utility_gui.py,sha256=aVdI9zVXADS53g7QtTgmkVK1LumBsXF4Lou3qJzgHN8,7487
14
+ GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
15
+ GameSentenceMiner/communication/send.py,sha256=oOJdCS6-LNX90amkRn5FL2xqx6THGm56zHR2ntVIFTE,229
16
+ GameSentenceMiner/communication/websocket.py,sha256=vrZ9KwRUyZOepjayJkxZZTsNIbHGcDLgDRO9dNDwizM,2914
17
+ GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
18
+ GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
20
+ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
22
+ GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
23
+ GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
24
+ gamesentenceminer-2.5.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ gamesentenceminer-2.5.7.dist-info/METADATA,sha256=TVU0qDawh6UwnB9CD2y_zKATbvuVSVhTDKGjgPCRBLI,5435
26
+ gamesentenceminer-2.5.7.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
27
+ gamesentenceminer-2.5.7.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
28
+ gamesentenceminer-2.5.7.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
29
+ gamesentenceminer-2.5.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.0.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- from GameSentenceMiner.configuration import logger
2
-
3
-
4
- def signal_restart_settings_change():
5
- logger.info("restart_for_settings_change")
6
-
@@ -1,27 +0,0 @@
1
- GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=zjZ3oE0iTMk1BQFuEhLWk8qgqJMYihO6-jTkKaC3eUk,12229
3
- GameSentenceMiner/config_gui.py,sha256=PzzLX-OwK71U5JSFaxup0ec0oWBWaF6AeCXs0m13HK0,56762
4
- GameSentenceMiner/configuration.py,sha256=kyvNCkZSZcqXbGjak8lb_GyhhjN8tIfb7eEfusz_M8A,16038
5
- GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
6
- GameSentenceMiner/ffmpeg.py,sha256=2dwKbKxw_9sUXud67pPAx_6dGd1j-D99CdqLFVtbhSk,11479
7
- GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
8
- GameSentenceMiner/gsm.py,sha256=MkRaty1ATLLJKZs9rv9Rdlitx3C9fwz66iD2AJS5LQQ,20343
9
- GameSentenceMiner/model.py,sha256=bZm-2vkIw4gQCGLB02eDoTtO1Ymb_dnHk0VDJDFO3y8,5228
10
- GameSentenceMiner/notification.py,sha256=p24TPw1iF8vDVL56NRgRi3oZ-5oQa9DkqqVjr1gZRJk,2577
11
- GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
12
- GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
13
- GameSentenceMiner/util.py,sha256=lAFwAQNeHpVZs_Aeb2K2ShVdcmfrQzEwxUnbf7e6fOc,5699
14
- GameSentenceMiner/utility_gui.py,sha256=Ox63XZnj08MezcEG8X5EYJlbHmw-bZitN3CsWgP8lLo,5743
15
- GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
16
- GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
18
- GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
20
- GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
21
- GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
22
- gamesentenceminer-2.5.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
- gamesentenceminer-2.5.5.dist-info/METADATA,sha256=sHUHiKel1xt8b3oAys3w0In5FNvH0KGxlxsyxEMJWgs,5435
24
- gamesentenceminer-2.5.5.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
25
- gamesentenceminer-2.5.5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
26
- gamesentenceminer-2.5.5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
27
- gamesentenceminer-2.5.5.dist-info/RECORD,,