GameSentenceMiner 2.9.14__py3-none-any.whl → 2.9.16__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,3 +1,4 @@
1
+ import copy
1
2
  import queue
2
3
  import time
3
4
 
@@ -19,10 +20,6 @@ from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_m
19
20
  from GameSentenceMiner.obs import get_current_game
20
21
  from GameSentenceMiner.web import texthooking_page
21
22
 
22
- audio_in_anki = None
23
- screenshot_in_anki = None
24
- prev_screenshot_in_anki = None
25
-
26
23
  # Global variables to track state
27
24
  previous_note_ids = set()
28
25
  first_run = True
@@ -31,14 +28,21 @@ card_queue = []
31
28
 
32
29
  def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
33
30
  should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0):
34
- global audio_in_anki, screenshot_in_anki, prev_screenshot_in_anki
35
31
  update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
36
32
  last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
37
33
  update_picture = (get_config().anki.picture_field and get_config().screenshot.enabled
38
34
  and (get_config().anki.overwrite_picture or not last_note.get_field(get_config().anki.picture_field)))
39
35
 
40
-
41
- if not reuse_audio:
36
+ audio_in_anki = ''
37
+ screenshot_in_anki = ''
38
+ prev_screenshot_in_anki = ''
39
+ if reuse_audio:
40
+ logger.info("Reusing Audio from last note")
41
+ anki_result = anki_results[game_line.id]
42
+ audio_in_anki = anki_result.audio_in_anki
43
+ screenshot_in_anki = anki_result.screenshot_in_anki
44
+ prev_screenshot_in_anki = anki_result.prev_screenshot_in_anki
45
+ else:
42
46
  if update_audio:
43
47
  audio_in_anki = store_media_file(audio_path)
44
48
  if get_config().audio.external_tool and get_config().audio.external_tool_enabled:
@@ -96,6 +100,16 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
96
100
  tag_string = " ".join(tags)
97
101
  invoke("addTags", tags=tag_string, notes=[last_note.noteId])
98
102
 
103
+ if not reuse_audio:
104
+ anki_results[game_line.id] = AnkiUpdateResult(
105
+ success=True,
106
+ audio_in_anki=audio_in_anki,
107
+ screenshot_in_anki=screenshot_in_anki,
108
+ prev_screenshot_in_anki=prev_screenshot_in_anki,
109
+ sentence_in_anki=game_line.text if game_line else '',
110
+ multi_line=bool(selected_lines and len(selected_lines) > 1)
111
+ )
112
+
99
113
  run_new_thread(lambda: check_and_update_note(last_note, note, tags))
100
114
 
101
115
  def check_and_update_note(last_note, note, tags=[]):
@@ -111,6 +125,7 @@ def check_and_update_note(last_note, note, tags=[]):
111
125
  notification.open_anki_card(last_note.noteId)
112
126
  if get_config().features.notify_on_update:
113
127
  notification.send_note_updated(last_note.noteId)
128
+ gsm_status.remove_word_being_processed(last_note.get_field(get_config().anki.word_field))
114
129
 
115
130
 
116
131
  def add_image_to_card(last_note: AnkiCard, image_path):
@@ -262,7 +277,9 @@ def check_for_new_cards():
262
277
  current_note_ids = set()
263
278
  try:
264
279
  current_note_ids = get_note_ids()
280
+ gsm_status.anki_connected = True
265
281
  except Exception as e:
282
+ gsm_status.anki_connected = False
266
283
  if datetime.now() - last_connection_error > timedelta(seconds=10):
267
284
  logger.error(f"Error fetching Anki notes, Make sure Anki is running, ankiconnect add-on is installed, and url/port is configured correctly in GSM Settings")
268
285
  last_connection_error = datetime.now()
@@ -270,10 +287,7 @@ def check_for_new_cards():
270
287
  new_card_ids = current_note_ids - previous_note_ids
271
288
  if new_card_ids and not first_run:
272
289
  try:
273
- if gsm_state.lock.locked():
274
- run_new_thread(update_new_card)
275
- else:
276
- update_new_card()
290
+ update_new_card()
277
291
  except Exception as e:
278
292
  logger.error("Error updating new card, Reason:", e)
279
293
  first_run = False
@@ -283,6 +297,7 @@ def update_new_card():
283
297
  last_card = get_last_anki_card()
284
298
  if not last_card or not check_tags_for_should_update(last_card):
285
299
  return
300
+ gsm_status.add_word_being_processed(last_card.get_field(get_config().anki.word_field))
286
301
  use_prev_audio = sentence_is_same_as_previous(last_card)
287
302
  logger.debug(f"last mined line: {gsm_state.last_mined_line}, current sentence: {get_sentence(last_card)}")
288
303
  logger.info(f"New card using previous audio: {use_prev_audio}")
@@ -290,8 +305,7 @@ def update_new_card():
290
305
  obs.update_current_game()
291
306
  if use_prev_audio:
292
307
  lines = texthooking_page.get_selected_lines()
293
- with gsm_state.lock:
294
- update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=get_mined_line(last_card, lines), reuse_audio=True)
308
+ run_new_thread(lambda: update_card_from_same_sentence(last_card, lines=lines, game_line=get_mined_line(last_card, lines)))
295
309
  texthooking_page.reset_checked_lines()
296
310
  else:
297
311
  logger.info("New card(s) detected! Added to Processing Queue!")
@@ -303,8 +317,16 @@ def update_new_card():
303
317
  logger.error(f"Error saving replay buffer: {e}")
304
318
  return
305
319
 
306
-
307
-
320
+ def update_card_from_same_sentence(last_card, lines, game_line):
321
+ while game_line.id not in anki_results:
322
+ time.sleep(0.5)
323
+ anki_result = anki_results[game_line.id]
324
+ if anki_result.success:
325
+ update_anki_card(last_card, note=get_initial_card_info(last_card, lines),
326
+ game_line=get_mined_line(last_card, lines), reuse_audio=True)
327
+ else:
328
+ logger.error(f"Anki update failed for card {last_card.noteId}")
329
+ notification.send_error_no_anki_update()
308
330
 
309
331
 
310
332
  def sentence_is_same_as_previous(last_card):
@@ -25,15 +25,18 @@ async def monitor_clipboard():
25
25
  send_message_on_resume = False
26
26
  while True:
27
27
  if not get_config().general.use_clipboard:
28
+ gsm_status.clipboard_enabled = False
28
29
  await asyncio.sleep(5)
29
30
  continue
30
31
  if not get_config().general.use_both_clipboard_and_websocket and any(websocket_connected.values()):
32
+ gsm_status.clipboard_enabled = False
31
33
  await asyncio.sleep(1)
32
34
  send_message_on_resume = True
33
35
  continue
34
36
  elif send_message_on_resume:
35
37
  logger.info("No Websocket Connections, resuming Clipboard Monitoring.")
36
38
  send_message_on_resume = False
39
+ gsm_status.clipboard_enabled = True
37
40
  current_clipboard = pyperclip.paste()
38
41
 
39
42
  if current_clipboard and current_clipboard != current_line:
@@ -57,6 +60,7 @@ async def listen_websockets():
57
60
  try:
58
61
  async with websockets.connect(websocket_url, ping_interval=None) as websocket:
59
62
  logger.info(f"TextHooker Websocket {uri} Connected!")
63
+ gsm_status.websockets_connected.append(websocket_url)
60
64
  if reconnecting:
61
65
  logger.info(f"Texthooker WebSocket {uri} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
62
66
  reconnecting = False
@@ -79,6 +83,8 @@ async def listen_websockets():
79
83
  if current_clipboard != current_line:
80
84
  await handle_new_text_event(current_clipboard, line_time if line_time else None)
81
85
  except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
86
+ if websocket_url in gsm_status.websockets_connected:
87
+ gsm_status.websockets_connected.remove(websocket_url)
82
88
  if isinstance(e, InvalidStatus):
83
89
  e: InvalidStatus
84
90
  if e.response.status_code == 404:
@@ -113,6 +119,7 @@ async def handle_new_text_event(current_clipboard, line_time=None):
113
119
  current_line_after_regex = do_text_replacements(current_line, TEXT_REPLACEMENTS_FILE)
114
120
  logger.info(f"Line Received: {current_line_after_regex}")
115
121
  current_line_time = line_time if line_time else datetime.now()
122
+ gsm_status.last_line_received = current_line_time.strftime("%Y-%m-%d %H:%M:%S")
116
123
  add_line(current_line_after_regex, line_time)
117
124
  if len(get_text_log().values) > 0:
118
125
  await add_event_to_texthooker(get_text_log()[-1])
@@ -121,7 +128,7 @@ def reset_line_hotkey_pressed():
121
128
  global current_line_time
122
129
  logger.info("LINE RESET HOTKEY PRESSED")
123
130
  current_line_time = datetime.now()
124
- gsm_state.last_mined_line = ""
131
+ gsm_state.last_mined_line = None
125
132
 
126
133
 
127
134
  def run_websocket_listener():
GameSentenceMiner/gsm.py CHANGED
@@ -76,6 +76,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
76
76
 
77
77
  def process_replay(self, video_path):
78
78
  vad_trimmed_audio = ''
79
+ skip_delete = False
79
80
  if "previous.mkv" in video_path:
80
81
  os.remove(video_path)
81
82
  video_path = gsm_state.previous_replay
@@ -89,92 +90,95 @@ class VideoToAudioHandler(FileSystemEventHandler):
89
90
  last_note, anki_card_creation_time = anki.card_queue.pop(0)
90
91
  else:
91
92
  logger.info("Replay buffer initiated externally. Skipping processing.")
93
+ skip_delete = True
92
94
  return
93
- with gsm_state.lock:
94
- mined_line = get_text_event(last_note)
95
- gsm_state.last_mined_line = mined_line
96
- if os.path.exists(video_path) and os.access(video_path, os.R_OK):
97
- logger.debug(f"Video found and is readable: {video_path}")
98
- if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
99
- get_config().obs.minimum_replay_size):
100
- logger.debug("Checking if video is big enough")
101
- notification.send_check_obs_notification(reason="Video may be empty, check scene in OBS.")
102
- logger.error(
103
- f"Video was unusually small, potentially empty! Check OBS for Correct Scene Settings! Path: {video_path}")
104
- return
105
- if not last_note:
106
- logger.debug("Attempting to get last anki card")
107
- if get_config().anki.update_anki:
108
- last_note = anki.get_last_anki_card()
109
- if get_config().features.backfill_audio:
110
- last_note = anki.get_cards_by_sentence(gametext.current_line_after_regex)
111
- line_cutoff = None
112
- start_line = None
113
- if mined_line:
114
- start_line = mined_line
115
- if mined_line.next:
116
- line_cutoff = mined_line.next.time
117
-
118
- selected_lines = []
119
- if texthooking_page.are_lines_selected():
120
- selected_lines = texthooking_page.get_selected_lines()
121
- start_line = selected_lines[0]
122
- mined_line = get_mined_line(last_note, selected_lines)
123
- line_cutoff = selected_lines[-1].get_next_time()
124
-
125
- if last_note:
126
- logger.debug(last_note.to_json())
127
- note = anki.get_initial_card_info(last_note, selected_lines)
128
- tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
129
- texthooking_page.reset_checked_lines()
130
-
131
- if get_config().anki.sentence_audio_field and get_config().audio.enabled:
132
- logger.debug("Attempting to get audio from video")
133
- final_audio_output, vad_result, vad_trimmed_audio = VideoToAudioHandler.get_audio(
134
- start_line,
135
- line_cutoff,
136
- video_path,
137
- anki_card_creation_time,
138
- mined_line=mined_line)
139
- else:
140
- final_audio_output = ""
141
- vad_result = VADResult(False, 0, 0, '')
142
- vad_trimmed_audio = ""
143
- if not get_config().audio.enabled:
144
- logger.info("Audio is disabled in config, skipping audio processing!")
145
- elif not get_config().anki.sentence_audio_field:
146
- logger.info("No SentenceAudio Field in config, skipping audio processing!")
147
-
148
- ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_result=vad_result, doing_multi_line=bool(selected_lines))
149
- # prev_ss_timing = 0
150
- # if get_config().anki.previous_image_field and get_config().vad.do_vad_postprocessing:
151
- # prev_ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line.prev,
152
- # vad_result=VideoToAudioHandler.get_audio(game_line=mined_line.prev,
153
- # next_line_time=mined_line.time,
154
- # video_path=video_path,
155
- # anki_card_creation_time=anki_card_creation_time,
156
- # timing_only=True) ,doing_multi_line=bool(selected_lines), previous_line=True)
157
-
158
- if get_config().anki.update_anki and last_note:
159
- anki.update_anki_card(
160
- last_note, note, audio_path=final_audio_output, video_path=video_path,
161
- tango=tango,
162
- should_update_audio=vad_result.success,
163
- ss_time=ss_timing,
164
- game_line=start_line,
165
- selected_lines=selected_lines
166
- )
167
- elif get_config().features.notify_on_update and vad_result.success:
168
- notification.send_audio_generated_notification(vad_trimmed_audio)
95
+
96
+ mined_line = get_text_event(last_note)
97
+ gsm_state.last_mined_line = mined_line
98
+ if os.path.exists(video_path) and os.access(video_path, os.R_OK):
99
+ logger.debug(f"Video found and is readable: {video_path}")
100
+ if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
101
+ get_config().obs.minimum_replay_size):
102
+ logger.debug("Checking if video is big enough")
103
+ notification.send_check_obs_notification(reason="Video may be empty, check scene in OBS.")
104
+ logger.error(
105
+ f"Video was unusually small, potentially empty! Check OBS for Correct Scene Settings! Path: {video_path}")
106
+ return
107
+ if not last_note:
108
+ logger.debug("Attempting to get last anki card")
109
+ if get_config().anki.update_anki:
110
+ last_note = anki.get_last_anki_card()
111
+ if get_config().features.backfill_audio:
112
+ last_note = anki.get_cards_by_sentence(gametext.current_line_after_regex)
113
+ line_cutoff = None
114
+ start_line = None
115
+ if mined_line:
116
+ start_line = mined_line
117
+ if mined_line.next:
118
+ line_cutoff = mined_line.next.time
119
+
120
+ selected_lines = []
121
+ if texthooking_page.are_lines_selected():
122
+ selected_lines = texthooking_page.get_selected_lines()
123
+ start_line = selected_lines[0]
124
+ mined_line = get_mined_line(last_note, selected_lines)
125
+ line_cutoff = selected_lines[-1].get_next_time()
126
+
127
+ if last_note:
128
+ logger.debug(last_note.to_json())
129
+ note = anki.get_initial_card_info(last_note, selected_lines)
130
+ tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
131
+ texthooking_page.reset_checked_lines()
132
+
133
+ if get_config().anki.sentence_audio_field and get_config().audio.enabled:
134
+ logger.debug("Attempting to get audio from video")
135
+ final_audio_output, vad_result, vad_trimmed_audio = VideoToAudioHandler.get_audio(
136
+ start_line,
137
+ line_cutoff,
138
+ video_path,
139
+ anki_card_creation_time,
140
+ mined_line=mined_line)
141
+ else:
142
+ final_audio_output = ""
143
+ vad_result = VADResult(False, 0, 0, '')
144
+ vad_trimmed_audio = ""
145
+ if not get_config().audio.enabled:
146
+ logger.info("Audio is disabled in config, skipping audio processing!")
147
+ elif not get_config().anki.sentence_audio_field:
148
+ logger.info("No SentenceAudio Field in config, skipping audio processing!")
149
+
150
+ ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_result=vad_result, doing_multi_line=bool(selected_lines), anki_card_creation_time=anki_card_creation_time)
151
+ # prev_ss_timing = 0
152
+ # if get_config().anki.previous_image_field and get_config().vad.do_vad_postprocessing:
153
+ # prev_ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line.prev,
154
+ # vad_result=VideoToAudioHandler.get_audio(game_line=mined_line.prev,
155
+ # next_line_time=mined_line.time,
156
+ # video_path=video_path,
157
+ # anki_card_creation_time=anki_card_creation_time,
158
+ # timing_only=True) ,doing_multi_line=bool(selected_lines), previous_line=True)
159
+
160
+ if get_config().anki.update_anki and last_note:
161
+ anki.update_anki_card(
162
+ last_note, note, audio_path=final_audio_output, video_path=video_path,
163
+ tango=tango,
164
+ should_update_audio=vad_result.success,
165
+ ss_time=ss_timing,
166
+ game_line=start_line,
167
+ selected_lines=selected_lines
168
+ )
169
+ elif get_config().features.notify_on_update and vad_result.success:
170
+ notification.send_audio_generated_notification(vad_trimmed_audio)
169
171
  except Exception as e:
172
+ anki_results[mined_line.id] = AnkiUpdateResult.failure()
170
173
  logger.error(f"Failed Processing and/or adding to Anki: Reason {e}")
171
174
  logger.debug(f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
172
175
  notification.send_error_no_anki_update()
173
176
  finally:
174
- if video_path and get_config().paths.remove_video and os.path.exists(video_path):
175
- os.remove(video_path) # Optionally remove the video after conversion
176
- if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
177
- os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
177
+ if not skip_delete:
178
+ if video_path and get_config().paths.remove_video and os.path.exists(video_path):
179
+ os.remove(video_path)
180
+ if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
181
+ os.remove(vad_trimmed_audio)
178
182
 
179
183
  def handle_texthooker_button(self, video_path):
180
184
  try:
@@ -643,6 +647,7 @@ def async_loop():
643
647
  async def register_scene_switcher_callback():
644
648
  def scene_switcher_callback(scene):
645
649
  logger.info(f"Scene changed to: {scene}")
650
+ gsm_state.current_game = obs.get_current_game(sanitize=True)
646
651
  all_configured_scenes = [config.scenes for config in get_master_config().configs.values()]
647
652
  print(all_configured_scenes)
648
653
  matching_configs = [name.strip() for name, config in config_instance.configs.items() if scene.strip() in config.scenes]
@@ -691,7 +696,8 @@ async def async_main(reloading=False):
691
696
  if is_windows():
692
697
  win32api.SetConsoleCtrlHandler(handle_exit())
693
698
 
694
-
699
+ gsm_status.ready = True
700
+ gsm_status.status = "Ready"
695
701
  try:
696
702
  if get_config().general.open_config_on_startup:
697
703
  root.after(50, settings_window.show)
GameSentenceMiner/obs.py CHANGED
@@ -31,6 +31,7 @@ class OBSConnectionManager(threading.Thread):
31
31
  try:
32
32
  if not client or not client.get_version() and not connecting:
33
33
  logger.info("OBS WebSocket not connected. Attempting to reconnect...")
34
+ gsm_status.obs_connected = False
34
35
  asyncio.run(connect_to_obs())
35
36
  except Exception as e:
36
37
  logger.debug(f"Error checking OBS connection: {e}")
@@ -157,6 +158,7 @@ async def connect_to_obs(retry_count=0):
157
158
  password=get_config().obs.password,
158
159
  timeout=1,
159
160
  )
161
+ gsm_status.obs_connected = True
160
162
  logger.info("Connected to OBS WebSocket.")
161
163
  if not obs_connection_manager:
162
164
  obs_connection_manager = OBSConnectionManager()
@@ -357,15 +359,15 @@ def get_screenshot_base64():
357
359
  return None
358
360
 
359
361
  def update_current_game():
360
- configuration.current_game = get_current_scene()
362
+ gsm_state.current_game = get_current_scene()
361
363
 
362
364
  def get_current_game(sanitize=False):
363
- if not configuration.current_game:
365
+ if not gsm_state.current_game:
364
366
  update_current_game()
365
367
 
366
368
  if sanitize:
367
- return sanitize_filename(configuration.current_game)
368
- return configuration.current_game
369
+ return sanitize_filename(gsm_state.current_game)
370
+ return gsm_state.current_game
369
371
 
370
372
 
371
373
  def main():
@@ -27,6 +27,7 @@ class FunctionName(Enum):
27
27
  TOGGLE_REPLAY_BUFFER = "toggle_replay_buffer"
28
28
  RESTART_OBS = "restart_obs"
29
29
  EXIT = "exit"
30
+ GET_STATUS = "get_status"
30
31
 
31
32
 
32
33
  async def do_websocket_connection(port):
@@ -232,7 +232,7 @@ class Advanced:
232
232
  ocr_sends_to_clipboard: bool = True
233
233
  ocr_websocket_port: int = 9002
234
234
  texthooker_communication_websocket_port: int = 55001
235
- use_anki_note_creation_time: bool = False
235
+ use_anki_note_creation_time: bool = True
236
236
 
237
237
  @dataclass_json
238
238
  @dataclass
@@ -633,8 +633,11 @@ logger.addHandler(console_handler)
633
633
 
634
634
  # Create rotating file handler with level DEBUG
635
635
  file_path = get_log_path()
636
- if os.path.exists(file_path) and os.path.getsize(file_path) > 10 * 1024 * 1024:
637
- shutil.move(os.path.join(file_path, "gamesentenceminer.log"), os.path.join(file_path, "gamesentenceminer_old.log"))
636
+ try:
637
+ if os.path.exists(file_path) and os.path.getsize(file_path) > 10 * 1024 * 1024 and os.access(file_path, os.W_OK):
638
+ shutil.move(file_path, os.path.join(os.path.dirname(file_path), "gamesentenceminer_old.log"))
639
+ except Exception as e:
640
+ logger.debug("Error rotating log, probably because the file is being written to by another process.")
638
641
 
639
642
  file_handler = logging.FileHandler(file_path, encoding='utf-8')
640
643
  file_handler.setLevel(logging.DEBUG)
@@ -656,5 +659,45 @@ class GsmAppState:
656
659
  self.lock = threading.Lock()
657
660
  self.last_mined_line = None
658
661
  self.keep_running = True
662
+ self.current_game = ''
659
663
 
664
+ @dataclass_json
665
+ @dataclass
666
+ class AnkiUpdateResult:
667
+ success: bool = False
668
+ audio_in_anki: str = ''
669
+ screenshot_in_anki: str = ''
670
+ prev_screenshot_in_anki: str = ''
671
+ sentence_in_anki: str = ''
672
+ multi_line: bool = False
673
+
674
+ @staticmethod
675
+ def failure():
676
+ return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False)
677
+
678
+
679
+ @dataclass_json
680
+ @dataclass
681
+ class GsmStatus:
682
+ ready: bool = False
683
+ status: bool = "Initializing"
684
+ cards_created: int = 0
685
+ websockets_connected: List[str] = field(default_factory=list)
686
+ obs_connected: bool = False
687
+ anki_connected: bool = False
688
+ last_line_received: str = None
689
+ words_being_processed: List[str] = field(default_factory=list)
690
+ clipboard_enabled: bool = True
691
+
692
+ def add_word_being_processed(self, word: str):
693
+ if word not in self.words_being_processed:
694
+ self.words_being_processed.append(word)
695
+
696
+ def remove_word_being_processed(self, word: str):
697
+ if word in self.words_being_processed:
698
+ self.words_being_processed.remove(word)
699
+
700
+
701
+ gsm_status = GsmStatus()
702
+ anki_results = {}
660
703
  gsm_state = GsmAppState()
@@ -37,7 +37,7 @@ def open_browser_window(note_id, query=None):
37
37
  "action": "guiBrowse",
38
38
  "version": 6,
39
39
  "params": {
40
- "query": "refreshing...",
40
+ "query": "nid:1",
41
41
  }
42
42
  }
43
43
  requests.post(url, json=blank_req_data, headers=headers)
@@ -4,8 +4,9 @@ from datetime import datetime
4
4
  from difflib import SequenceMatcher
5
5
  from typing import Optional
6
6
 
7
+ from GameSentenceMiner.obs import get_current_game
7
8
  from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
8
- from GameSentenceMiner.util.configuration import logger, get_config
9
+ from GameSentenceMiner.util.configuration import logger, get_config, gsm_state
9
10
  from GameSentenceMiner.util.model import AnkiCard
10
11
 
11
12
  initial_time = datetime.now()
@@ -19,6 +20,7 @@ class GameLine:
19
20
  prev: 'GameLine | None'
20
21
  next: 'GameLine | None'
21
22
  index: int = 0
23
+ scene: str = ""
22
24
 
23
25
  def get_previous_time(self):
24
26
  if self.prev:
@@ -71,10 +73,11 @@ class GameText:
71
73
  new_line = GameLine(
72
74
  id=line_id, # Time-based UUID as an integer
73
75
  text=line_text,
74
- time=line_time if line_time else datetime.now(),
76
+ time=line_time or datetime.now(),
75
77
  prev=self.values[-1] if self.values else None,
76
78
  next=None,
77
- index=self.game_line_index
79
+ index=self.game_line_index,
80
+ scene=gsm_state.current_game or ""
78
81
  )
79
82
  self.values_dict[line_id] = new_line
80
83
  logger.debug(f"Adding line: {new_line}")
GameSentenceMiner/vad.py CHANGED
@@ -7,16 +7,6 @@ from GameSentenceMiner.util import configuration, ffmpeg
7
7
  from GameSentenceMiner.util.configuration import *
8
8
  from GameSentenceMiner.util.ffmpeg import get_ffprobe_path
9
9
 
10
-
11
- def get_audio_length(path):
12
- result = subprocess.run(
13
- [get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path],
14
- stdout=subprocess.PIPE,
15
- stderr=subprocess.PIPE,
16
- text=True
17
- )
18
- return float(result.stdout.strip())
19
-
20
10
  class VADResult:
21
11
  def __init__(self, success: bool, start: float, end: float, model: str, output_audio: str = None):
22
12
  self.success = success
@@ -107,6 +97,17 @@ class VADProcessor(ABC):
107
97
  def _detect_voice_activity(self, input_audio):
108
98
  pass
109
99
 
100
+ @staticmethod
101
+ def get_audio_length(path):
102
+ result = subprocess.run(
103
+ [get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of",
104
+ "default=noprint_wrappers=1:nokey=1", path],
105
+ stdout=subprocess.PIPE,
106
+ stderr=subprocess.PIPE,
107
+ text=True
108
+ )
109
+ return float(result.stdout.strip())
110
+
110
111
  def process_audio(self, input_audio, output_audio, game_line):
111
112
  voice_activity = self._detect_voice_activity(input_audio)
112
113
 
@@ -119,7 +120,7 @@ class VADProcessor(ABC):
119
120
 
120
121
  # Attempt to fix the end time if the last segment is too short
121
122
  if game_line and game_line.next and len(voice_activity) > 1:
122
- audio_length = get_audio_length(input_audio)
123
+ audio_length = self.get_audio_length(input_audio)
123
124
  if 0 > audio_length - voice_activity[-1]['start'] + get_config().audio.beginning_offset:
124
125
  end_time = voice_activity[-2]['end']
125
126
 
@@ -15,7 +15,7 @@ from GameSentenceMiner.util.text_log import GameLine, get_line_by_id, initial_ti
15
15
  from flask import request, jsonify, send_from_directory
16
16
  import webbrowser
17
17
  from GameSentenceMiner import obs
18
- from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state
18
+ from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state, gsm_status
19
19
 
20
20
  port = get_config().general.texthooker_port
21
21
  url = f"http://localhost:{port}"
@@ -294,6 +294,12 @@ def play_audio():
294
294
  return jsonify({}), 200
295
295
 
296
296
 
297
+ @app.route('/get_status', methods=['GET'])
298
+ def get_status():
299
+ gsm_status.clipboard_enabled
300
+ return jsonify(gsm_status.to_dict()), 200
301
+
302
+
297
303
  # async def main():
298
304
  # async with websockets.serve(websocket_handler, "localhost", 8765): # Choose a port for WebSocket
299
305
  # print("WebSocket server started on ws://localhost:8765/ws (adjust as needed)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.9.14
3
+ Version: 2.9.16
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,10 +1,10 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=tMX6nTVO2PKgXXHM6uh1XaWBYEvZq-6VPhuTSbBrE-Y,15075
2
+ GameSentenceMiner/anki.py,sha256=hNHBIoJRrsWIhLe0sehOYPXTWzPREeXl4gYCPHUCaiE,16331
3
3
  GameSentenceMiner/config_gui.py,sha256=iAOLD47sQW67kzBcZKSQ0Dwctc1ngZK1lwSVIaLpQPI,83559
4
- GameSentenceMiner/gametext.py,sha256=mM-gw1d7c2EEvMUznaAevTQFLswNZavCuxMXhA9pV4g,6251
5
- GameSentenceMiner/gsm.py,sha256=hmdgvad7k6mei2M0TywNfdaWt6I_1U6o3MNHJaGTMvQ,29307
6
- GameSentenceMiner/obs.py,sha256=O9NYOGu7kwp4flq8LLXp8YJQg0JTZ8qBqiQNQ6u4ku4,14724
7
- GameSentenceMiner/vad.py,sha256=Gk_VthD7mDp3-wM_S6bEv8ykGmqzCDbbcRiaEBzAE_o,14835
4
+ GameSentenceMiner/gametext.py,sha256=iO1o2980XBzBc2nsgBVr_ZaKHRLotebVpDhwGqBCK9k,6696
5
+ GameSentenceMiner/gsm.py,sha256=j6Y4Nd79Qi09r-Pgt2y3THxeP-yBAfZSFbbsISE9Ah8,29251
6
+ GameSentenceMiner/obs.py,sha256=21GQ4DzjccLc-m4rU5Tfm4E3n1R1U4vRwmIWfKqcjmw,14805
7
+ GameSentenceMiner/vad.py,sha256=TbP3NVdjfB1TFJeB0QpOXZysgo_UHHKLdx95pYmM0JI,14902
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  GameSentenceMiner/ai/ai_prompting.py,sha256=0jBAnngNwmc3dqJiVWe_QRy4Syr-muV-ML2rq0FiUtU,10215
10
10
  GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,24 +28,24 @@ GameSentenceMiner/owocr/owocr/ocr.py,sha256=vZR3du1fGg5-3cmPvYKTO4PFk7Lxyf6-BrIy
28
28
  GameSentenceMiner/owocr/owocr/run.py,sha256=U6VIfCvsNPADG3twhp4SQVX1xhihSAGGrBnQj2x0C2c,54964
29
29
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
30
30
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- GameSentenceMiner/util/configuration.py,sha256=5YJSKelIpBTK7ggpGUERf17xVwH8Hy9GzvJ12eP8kHs,26095
31
+ GameSentenceMiner/util/configuration.py,sha256=Wgr1UAf_JoXBlp9h_f3-d2DAmIWnR1FtCmMX6mCfMNM,27461
32
32
  GameSentenceMiner/util/electron_config.py,sha256=3VmIrcXhC-wIMMc4uqV85NrNenRl4ZUbnQfSjWEwuig,9852
33
33
  GameSentenceMiner/util/ffmpeg.py,sha256=daItJprEqi5PQe-aFb836rls3tBHNqIQKz61vlJK07M,19276
34
34
  GameSentenceMiner/util/gsm_utils.py,sha256=Z_Lu4jSIfUaM2VljIJXQkSJD0UsyJ5hMB46H2NS0gZo,8819
35
35
  GameSentenceMiner/util/model.py,sha256=iDtLTfR6D-ZC0gCiDqYno6-gA6Z07PZTM4B5MAA6xZI,5704
36
- GameSentenceMiner/util/notification.py,sha256=hAUKWWDB4F_9exQhgiajfP5DT8u464RowsJGmBVRN_E,3882
36
+ GameSentenceMiner/util/notification.py,sha256=0OnEYjn3DUEZ6c6OtPjdVZe-DG-QSoMAl9fetjjCvNU,3874
37
37
  GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
38
38
  GameSentenceMiner/util/ss_selector.py,sha256=oCzmDbpEGvVselF-oDPIrBcQktGIZT0Zt16uDLDAHMQ,4493
39
- GameSentenceMiner/util/text_log.py,sha256=wdFOapIwHzDntR9G0YzJzDcbhO6b1taFkhdgsTnRGJ4,5356
39
+ GameSentenceMiner/util/text_log.py,sha256=VhdiF2UIlGYW42CFurkaeKa6W8x7Z5klXN9GhY1JLIA,5471
40
40
  GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
41
41
  GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
42
- GameSentenceMiner/util/communication/websocket.py,sha256=gPgxA2R2U6QZJjPqbUgODC87gtacPhmuC8lCprIkSmA,3287
42
+ GameSentenceMiner/util/communication/websocket.py,sha256=TbphRGmxVrgEupS7tNdifsmQfWDfIp0Hio2cSiUKgsk,3317
43
43
  GameSentenceMiner/util/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
44
44
  GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  GameSentenceMiner/util/downloader/download_tools.py,sha256=mvnOjDHFlV1AbjHaNI7mdnC5_CH5k3N4n1ezqzzbzGA,8139
46
46
  GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
47
47
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- GameSentenceMiner/web/texthooking_page.py,sha256=RR70Vgde3wNHarQHbB-LBbEP-z95vRD5rtlW0GgdjmQ,15037
48
+ GameSentenceMiner/web/texthooking_page.py,sha256=kDRNeTz6eIzc9rqrnupAqEmdVF3GuhB1PSgv9azEaAI,15191
49
49
  GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
51
51
  GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
@@ -59,9 +59,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
59
59
  GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
60
60
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
61
61
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
62
- gamesentenceminer-2.9.14.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
- gamesentenceminer-2.9.14.dist-info/METADATA,sha256=H-FYTe-rnsAEphGMvv9OJthUnHzJPt9kSSHF924m98Y,7221
64
- gamesentenceminer-2.9.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
- gamesentenceminer-2.9.14.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
- gamesentenceminer-2.9.14.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
- gamesentenceminer-2.9.14.dist-info/RECORD,,
62
+ gamesentenceminer-2.9.16.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
63
+ gamesentenceminer-2.9.16.dist-info/METADATA,sha256=ybDsHKiVvTzpQUq5v7g8XQa8NKmf0Vl0Pcsuw18DXIo,7221
64
+ gamesentenceminer-2.9.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ gamesentenceminer-2.9.16.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
66
+ gamesentenceminer-2.9.16.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
67
+ gamesentenceminer-2.9.16.dist-info/RECORD,,