GameSentenceMiner 2.9.4__py3-none-any.whl → 2.9.5__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.
Files changed (42) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +3 -3
  2. GameSentenceMiner/anki.py +16 -14
  3. GameSentenceMiner/config_gui.py +22 -7
  4. GameSentenceMiner/gametext.py +5 -5
  5. GameSentenceMiner/gsm.py +26 -67
  6. GameSentenceMiner/obs.py +7 -8
  7. GameSentenceMiner/ocr/owocr_area_selector.py +1 -1
  8. GameSentenceMiner/ocr/owocr_helper.py +30 -13
  9. GameSentenceMiner/owocr/owocr/ocr.py +0 -2
  10. GameSentenceMiner/owocr/owocr/run.py +1 -1
  11. GameSentenceMiner/{communication → util/communication}/__init__.py +1 -1
  12. GameSentenceMiner/{communication → util/communication}/send.py +1 -1
  13. GameSentenceMiner/{communication → util/communication}/websocket.py +2 -2
  14. GameSentenceMiner/{downloader → util/downloader}/download_tools.py +3 -3
  15. GameSentenceMiner/vad.py +344 -0
  16. GameSentenceMiner/web/texthooking_page.py +15 -10
  17. {gamesentenceminer-2.9.4.dist-info → gamesentenceminer-2.9.5.dist-info}/METADATA +2 -3
  18. gamesentenceminer-2.9.5.dist-info/RECORD +57 -0
  19. GameSentenceMiner/configuration.py +0 -647
  20. GameSentenceMiner/electron_config.py +0 -315
  21. GameSentenceMiner/ffmpeg.py +0 -441
  22. GameSentenceMiner/model.py +0 -177
  23. GameSentenceMiner/notification.py +0 -126
  24. GameSentenceMiner/package.py +0 -38
  25. GameSentenceMiner/ss_selector.py +0 -121
  26. GameSentenceMiner/text_log.py +0 -186
  27. GameSentenceMiner/util.py +0 -262
  28. GameSentenceMiner/vad/__init__.py +0 -0
  29. GameSentenceMiner/vad/groq_trim.py +0 -82
  30. GameSentenceMiner/vad/result.py +0 -21
  31. GameSentenceMiner/vad/silero_trim.py +0 -52
  32. GameSentenceMiner/vad/vad_utils.py +0 -13
  33. GameSentenceMiner/vad/vosk_helper.py +0 -158
  34. GameSentenceMiner/vad/whisper_helper.py +0 -105
  35. gamesentenceminer-2.9.4.dist-info/RECORD +0 -72
  36. /GameSentenceMiner/{downloader → util/downloader}/Untitled_json.py +0 -0
  37. /GameSentenceMiner/{downloader → util/downloader}/__init__.py +0 -0
  38. /GameSentenceMiner/{downloader → util/downloader}/oneocr_dl.py +0 -0
  39. {gamesentenceminer-2.9.4.dist-info → gamesentenceminer-2.9.5.dist-info}/WHEEL +0 -0
  40. {gamesentenceminer-2.9.4.dist-info → gamesentenceminer-2.9.5.dist-info}/entry_points.txt +0 -0
  41. {gamesentenceminer-2.9.4.dist-info → gamesentenceminer-2.9.5.dist-info}/licenses/LICENSE +0 -0
  42. {gamesentenceminer-2.9.4.dist-info → gamesentenceminer-2.9.5.dist-info}/top_level.txt +0 -0
@@ -3,14 +3,14 @@ import textwrap
3
3
  from abc import ABC, abstractmethod
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
- from typing import List, Any, Optional
6
+ from typing import List, Optional
7
7
 
8
8
  import google.generativeai as genai
9
9
  from google.generativeai import GenerationConfig
10
10
  from groq import Groq
11
11
 
12
- from GameSentenceMiner.configuration import get_config, Ai, logger
13
- from GameSentenceMiner.text_log import GameLine
12
+ from GameSentenceMiner.util.configuration import get_config, Ai, logger
13
+ from GameSentenceMiner.util.text_log import GameLine
14
14
 
15
15
  # Suppress debug logs from httpcore
16
16
  logging.getLogger("httpcore").setLevel(logging.WARNING)
GameSentenceMiner/anki.py CHANGED
@@ -2,19 +2,19 @@ import time
2
2
 
3
3
  import base64
4
4
  import subprocess
5
- import threading
6
5
  import urllib.request
7
6
  from datetime import datetime, timedelta
8
7
  from requests import post
9
8
 
10
- from GameSentenceMiner import obs, util, notification, ffmpeg
9
+ from GameSentenceMiner import obs
11
10
  from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
12
- from GameSentenceMiner.configuration import *
13
- from GameSentenceMiner.configuration import get_config
14
- from GameSentenceMiner.model import AnkiCard
15
- from GameSentenceMiner.text_log import get_all_lines, get_text_event, get_mined_line
11
+ from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue
12
+ from GameSentenceMiner.util import ffmpeg, notification
13
+ from GameSentenceMiner.util.configuration import *
14
+ from GameSentenceMiner.util.configuration import get_config
15
+ from GameSentenceMiner.util.model import AnkiCard
16
+ from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_mined_line
16
17
  from GameSentenceMiner.obs import get_current_game
17
- from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue, wait_for_stable_file
18
18
  from GameSentenceMiner.web import texthooking_page
19
19
 
20
20
  audio_in_anki = None
@@ -82,8 +82,9 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
82
82
  for key, value in get_config().anki.anki_custom_fields.items():
83
83
  note['fields'][key] = str(value)
84
84
 
85
-
86
- notification.open_browser_window(1)
85
+ selected_notes = invoke("guiSelectedNotes")
86
+ if last_note.noteId in selected_notes:
87
+ notification.open_browser_window(1)
87
88
  invoke("updateNoteFields", note=note)
88
89
  tags = []
89
90
  if get_config().anki.custom_tags:
@@ -96,7 +97,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
96
97
  logger.info(f"UPDATED ANKI CARD FOR {last_note.noteId}")
97
98
  if get_config().features.notify_on_update:
98
99
  notification.send_note_updated(tango)
99
- notification.open_browser_window(last_note.noteId)
100
+ if last_note.noteId in selected_notes or get_config().features.open_anki_in_browser:
101
+ notification.open_browser_window(last_note.noteId, get_config().features.browser_query)
100
102
  if get_config().features.open_anki_edit:
101
103
  notification.open_anki_card(last_note.noteId)
102
104
 
@@ -282,13 +284,13 @@ def update_new_card():
282
284
  if not last_card or not check_tags_for_should_update(last_card):
283
285
  return
284
286
  use_prev_audio = sentence_is_same_as_previous(last_card)
285
- logger.debug(f"last mined line: {util.get_last_mined_line()}, current sentence: {get_sentence(last_card)}")
287
+ logger.debug(f"last mined line: {gsm_state.last_mined_line}, current sentence: {get_sentence(last_card)}")
286
288
  logger.info(f"New card using previous audio: {use_prev_audio}")
287
289
  if get_config().obs.get_game_from_scene:
288
290
  obs.update_current_game()
289
291
  if use_prev_audio:
290
292
  lines = texthooking_page.get_selected_lines()
291
- with util.lock:
293
+ with gsm_state.lock:
292
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)
293
295
  texthooking_page.reset_checked_lines()
294
296
  else:
@@ -303,9 +305,9 @@ def update_new_card():
303
305
 
304
306
 
305
307
  def sentence_is_same_as_previous(last_card):
306
- if not util.get_last_mined_line():
308
+ if not gsm_state.last_mined_line:
307
309
  return False
308
- return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(util.get_last_mined_line())
310
+ return remove_html_and_cloze_tags(get_sentence(last_card)) == remove_html_and_cloze_tags(gsm_state.last_mined_line)
309
311
 
310
312
  def get_sentence(card):
311
313
  return card.get_field(get_config().anki.sentence_field)
@@ -4,12 +4,12 @@ from tkinter import filedialog, messagebox, simpledialog, scrolledtext
4
4
 
5
5
  import ttkbootstrap as ttk
6
6
 
7
- from GameSentenceMiner import obs, configuration
8
- from GameSentenceMiner.communication.send import send_restart_signal
9
- from GameSentenceMiner.configuration import *
10
- from GameSentenceMiner.downloader.download_tools import download_ocenaudio_if_needed
11
- from GameSentenceMiner.model import SceneItem, SceneInfo
12
- from GameSentenceMiner.package import get_current_version, get_latest_version
7
+ from GameSentenceMiner import obs
8
+ from GameSentenceMiner.util import configuration
9
+ from GameSentenceMiner.util.communication.send import send_restart_signal
10
+ from GameSentenceMiner.util.configuration import *
11
+ from GameSentenceMiner.util.downloader.download_tools import download_ocenaudio_if_needed
12
+ from GameSentenceMiner.util.package import get_current_version, get_latest_version
13
13
 
14
14
  settings_saved = False
15
15
  on_save = []
@@ -172,7 +172,9 @@ class ConfigApp:
172
172
  full_auto=self.full_auto.get(),
173
173
  notify_on_update=self.notify_on_update.get(),
174
174
  open_anki_edit=self.open_anki_edit.get(),
175
- backfill_audio=self.backfill_audio.get()
175
+ open_anki_in_browser=self.open_anki_browser.get(),
176
+ backfill_audio=self.backfill_audio.get(),
177
+ browser_query=self.browser_query.get(),
176
178
  ),
177
179
  screenshot=Screenshot(
178
180
  enabled=self.screenshot_enabled.get(),
@@ -753,6 +755,19 @@ class ConfigApp:
753
755
  self.add_label_and_increment_row(features_frame, "Automatically open Anki for editing after updating.",
754
756
  row=self.current_row, column=2)
755
757
 
758
+ ttk.Label(features_frame, text="Open Anki Note in Browser:").grid(row=self.current_row, column=0, sticky='W')
759
+ self.open_anki_browser = tk.BooleanVar(value=self.settings.features.open_anki_in_browser)
760
+ ttk.Checkbutton(features_frame, variable=self.open_anki_browser).grid(row=self.current_row, column=1, sticky='W')
761
+ self.add_label_and_increment_row(features_frame, "Open Anki note in browser after updating.", row=self.current_row,
762
+ column=2)
763
+
764
+ ttk.Label(features_frame, text="Browser Query:").grid(row=self.current_row, column=0, sticky='W')
765
+ self.browser_query = ttk.Entry(features_frame, width=50)
766
+ self.browser_query.insert(0, self.settings.features.browser_query)
767
+ self.browser_query.grid(row=self.current_row, column=1)
768
+ self.add_label_and_increment_row(features_frame, "Query to use when opening Anki notes in the browser. Ex: 'Added:1'", row=self.current_row,
769
+ column=2)
770
+
756
771
  ttk.Label(features_frame, text="Backfill Audio:").grid(row=self.current_row, column=0, sticky='W')
757
772
  self.backfill_audio = tk.BooleanVar(value=self.settings.features.backfill_audio)
758
773
  ttk.Checkbutton(features_frame, variable=self.backfill_audio).grid(row=self.current_row, column=1, sticky='W')
@@ -6,9 +6,9 @@ import websockets
6
6
  from websockets import InvalidStatus
7
7
 
8
8
  from GameSentenceMiner import util
9
- from GameSentenceMiner.configuration import *
10
- from GameSentenceMiner.text_log import *
11
- from GameSentenceMiner.util import do_text_replacements, TEXT_REPLACEMENTS_FILE
9
+ from GameSentenceMiner.util.gsm_utils import do_text_replacements, TEXT_REPLACEMENTS_FILE, run_new_thread
10
+ from GameSentenceMiner.util.configuration import *
11
+ from GameSentenceMiner.util.text_log import *
12
12
 
13
13
  from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker
14
14
 
@@ -121,7 +121,7 @@ def reset_line_hotkey_pressed():
121
121
  global current_line_time
122
122
  logger.info("LINE RESET HOTKEY PRESSED")
123
123
  current_line_time = datetime.now()
124
- util.set_last_mined_line("")
124
+ gsm_state.last_mined_line = ""
125
125
 
126
126
 
127
127
  def run_websocket_listener():
@@ -129,7 +129,7 @@ def run_websocket_listener():
129
129
 
130
130
 
131
131
  async def start_text_monitor():
132
- util.run_new_thread(run_websocket_listener)
132
+ run_new_thread(run_websocket_listener)
133
133
  if get_config().general.use_websocket:
134
134
  if get_config().general.use_both_clipboard_and_websocket:
135
135
  logger.info("Listening for Text on both WebSocket and Clipboard.")
GameSentenceMiner/gsm.py CHANGED
@@ -1,7 +1,12 @@
1
1
  import asyncio
2
+ import subprocess
2
3
  import sys
4
+ import threading
3
5
 
4
- from GameSentenceMiner.vad.result import VADResult
6
+ from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
7
+ from GameSentenceMiner.util.communication.send import send_restart_signal
8
+ from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
9
+ from GameSentenceMiner.vad import vad_processor, VADResult
5
10
 
6
11
  try:
7
12
  import os.path
@@ -19,26 +24,21 @@ try:
19
24
 
20
25
  from GameSentenceMiner import anki
21
26
  from GameSentenceMiner import config_gui
22
- from GameSentenceMiner import configuration
23
- from GameSentenceMiner import ffmpeg
27
+ from GameSentenceMiner.util import configuration, notification, ffmpeg
24
28
  from GameSentenceMiner import gametext
25
- from GameSentenceMiner import notification
26
29
  from GameSentenceMiner import obs
27
- from GameSentenceMiner import util
28
- from GameSentenceMiner.communication import Message
29
- from GameSentenceMiner.communication.send import send_restart_signal
30
- from GameSentenceMiner.communication.websocket import connect_websocket, register_websocket_message_handler, \
30
+ from GameSentenceMiner.util.communication import Message
31
+ from GameSentenceMiner.util.communication.websocket import connect_websocket, register_websocket_message_handler, \
31
32
  FunctionName
32
- from GameSentenceMiner.configuration import *
33
- from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
34
- from GameSentenceMiner.ffmpeg import get_audio_and_trim, get_video_timings
33
+ from GameSentenceMiner.util.configuration import *
34
+ from GameSentenceMiner.util.ffmpeg import get_audio_and_trim, get_video_timings
35
35
  from GameSentenceMiner.obs import check_obs_folder_is_correct
36
- from GameSentenceMiner.text_log import GameLine, get_text_event, get_mined_line, get_all_lines
36
+ from GameSentenceMiner.util.text_log import GameLine, get_text_event, get_mined_line, get_all_lines
37
37
  from GameSentenceMiner.util import *
38
38
  from GameSentenceMiner.web import texthooking_page
39
39
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
40
40
  except Exception as e:
41
- from GameSentenceMiner.configuration import logger, is_linux, is_windows
41
+ from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
42
42
  import time
43
43
  logger.info("Something bad happened during import/initialization, closing in 5 seconds")
44
44
  logger.exception(e)
@@ -48,7 +48,6 @@ except Exception as e:
48
48
  if is_windows():
49
49
  import win32api
50
50
 
51
- silero_trim, whisper_helper, vosk_helper = None, None, None
52
51
  procs_to_close = []
53
52
  settings_window: config_gui.ConfigApp = None
54
53
  obs_paused = False
@@ -87,11 +86,10 @@ class VideoToAudioHandler(FileSystemEventHandler):
87
86
  else:
88
87
  logger.info("Replay buffer initiated externally. Skipping processing.")
89
88
  return
90
- with util.lock:
91
- util.set_last_mined_line(anki.get_sentence(last_note))
89
+ with gsm_state.lock:
90
+ gsm_state.last_mined_line = anki.get_sentence(last_note)
92
91
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
93
92
  logger.debug(f"Video found and is readable: {video_path}")
94
-
95
93
  if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
96
94
  get_config().obs.minimum_replay_size):
97
95
  logger.debug("Checking if video is big enough")
@@ -212,7 +210,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
212
210
 
213
211
  @staticmethod
214
212
  def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
215
- logger.info("Getting audio from video...")
216
213
  trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time)
217
214
  if temporary:
218
215
  return trimmed_audio
@@ -220,47 +217,16 @@ class VideoToAudioHandler(FileSystemEventHandler):
220
217
  f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
221
218
  final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
222
219
  f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
223
- result = VADResult(False, 0, 0, "")
224
- if get_config().vad.do_vad_postprocessing:
225
- result = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio, vad_trimmed_audio, game_line=mined_line)
226
- if not result.success:
227
- result = do_vad_processing(get_config().vad.selected_vad_model, trimmed_audio,
228
- vad_trimmed_audio, game_line=mined_line)
229
- if not result.success:
230
- if get_config().vad.add_audio_on_no_results:
231
- logger.info("No voice activity detected, using full audio.")
232
- vad_trimmed_audio = trimmed_audio
233
- else:
234
- logger.info("No voice activity detected.")
235
- return None, result, None
236
- else:
237
- logger.info(result.trim_successful_string())
220
+
221
+ vad_result = vad_processor.trim_audio_with_vad(trimmed_audio, vad_trimmed_audio, game_line)
238
222
  if timing_only:
239
- return result
223
+ return vad_result
240
224
  if get_config().audio.ffmpeg_reencode_options and os.path.exists(vad_trimmed_audio):
241
225
  ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
242
226
  get_config().audio.ffmpeg_reencode_options)
243
227
  elif os.path.exists(vad_trimmed_audio):
244
228
  shutil.move(vad_trimmed_audio, final_audio_output)
245
- return final_audio_output, result, vad_trimmed_audio
246
-
247
-
248
- def do_vad_processing(model, trimmed_audio, vad_trimmed_audio, game_line=None, second_pass=False):
249
- match model:
250
- case configuration.OFF:
251
- pass
252
- case configuration.GROQ:
253
- from GameSentenceMiner.vad import groq_trim
254
- return groq_trim.process_audio_with_groq(trimmed_audio, vad_trimmed_audio, game_line)
255
- case configuration.SILERO:
256
- from GameSentenceMiner.vad import silero_trim
257
- return silero_trim.process_audio_with_silero(trimmed_audio, vad_trimmed_audio, game_line)
258
- case configuration.VOSK:
259
- from GameSentenceMiner.vad import vosk_helper
260
- return vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio, game_line)
261
- case configuration.WHISPER:
262
- from GameSentenceMiner.vad import whisper_helper
263
- return whisper_helper.process_audio_with_whisper(trimmed_audio, vad_trimmed_audio, game_line)
229
+ return final_audio_output, vad_result, vad_trimmed_audio
264
230
 
265
231
 
266
232
  def play_audio_in_external(filepath):
@@ -535,7 +501,7 @@ def restart_obs():
535
501
 
536
502
  def cleanup():
537
503
  logger.info("Performing cleanup...")
538
- util.keep_running = False
504
+ gsm_state.keep_running = False
539
505
 
540
506
  if get_config().obs.enabled:
541
507
  obs.stop_replay_buffer()
@@ -567,7 +533,7 @@ def cleanup():
567
533
  def handle_exit():
568
534
  """Signal handler for graceful termination."""
569
535
 
570
- def _handle_exit(signum):
536
+ def _handle_exit(signum, *args):
571
537
  logger.info(f"Received signal {signum}. Exiting gracefully...")
572
538
  cleanup()
573
539
  sys.exit(0)
@@ -602,7 +568,7 @@ def initialize_async():
602
568
  threads = []
603
569
  tasks.append(anki.start_monitoring_anki)
604
570
  for task in tasks:
605
- threads.append(util.run_new_thread(task))
571
+ threads.append(run_new_thread(task))
606
572
  return threads
607
573
 
608
574
  def handle_websocket_message(message: Message):
@@ -640,14 +606,7 @@ def async_loop():
640
606
  await register_scene_switcher_callback()
641
607
  await check_obs_folder_is_correct()
642
608
  logger.info("Post-Initialization started.")
643
- if get_config().vad.is_vosk():
644
- from GameSentenceMiner.vad import vosk_helper
645
- vosk_helper.get_vosk_model()
646
- if get_config().vad.is_whisper():
647
- from GameSentenceMiner.vad import whisper_helper
648
- whisper_helper.initialize_whisper_model()
649
- if get_config().vad.is_silero():
650
- from GameSentenceMiner.vad import silero_trim
609
+ vad_processor.init()
651
610
 
652
611
  asyncio.run(loop())
653
612
 
@@ -693,9 +652,9 @@ async def async_main(reloading=False):
693
652
  if not is_linux():
694
653
  register_hotkeys()
695
654
 
696
- util.run_new_thread(post_init2)
697
- util.run_new_thread(run_text_hooker_page)
698
- util.run_new_thread(async_loop)
655
+ run_new_thread(post_init2)
656
+ run_new_thread(run_text_hooker_page)
657
+ run_new_thread(async_loop)
699
658
 
700
659
  # Register signal handlers for graceful shutdown
701
660
  signal.signal(signal.SIGTERM, handle_exit()) # Handle `kill` commands
GameSentenceMiner/obs.py CHANGED
@@ -7,10 +7,9 @@ import psutil
7
7
 
8
8
  import obsws_python as obs
9
9
 
10
- import GameSentenceMiner.configuration
11
- from GameSentenceMiner import util, configuration
12
- from GameSentenceMiner.configuration import *
13
- from GameSentenceMiner.model import *
10
+ from GameSentenceMiner.util import configuration
11
+ from GameSentenceMiner.util.configuration import *
12
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name
14
13
 
15
14
  client: obs.ReqClient = None
16
15
  event_client: obs.EventClient = None
@@ -140,7 +139,7 @@ async def connect_to_obs(retry_count=0):
140
139
  if not get_config().obs.enabled:
141
140
  return
142
141
 
143
- if GameSentenceMiner.configuration.is_windows():
142
+ if is_windows():
144
143
  get_obs_websocket_config_values()
145
144
 
146
145
  while True:
@@ -174,7 +173,7 @@ def connect_to_obs_sync(retry_count=0):
174
173
  if not get_config().obs.enabled or client:
175
174
  return
176
175
 
177
- if GameSentenceMiner.configuration.is_windows():
176
+ if is_windows():
178
177
  get_obs_websocket_config_values()
179
178
 
180
179
  while True:
@@ -314,7 +313,7 @@ async def register_scene_change_callback(callback):
314
313
 
315
314
  def get_screenshot(compression=-1):
316
315
  try:
317
- screenshot = util.make_unique_file_name(os.path.abspath(
316
+ screenshot = make_unique_file_name(os.path.abspath(
318
317
  configuration.get_temporary_directory()) + '/screenshot.png')
319
318
  update_current_game()
320
319
  if not configuration.current_game:
@@ -367,7 +366,7 @@ def get_current_game(sanitize=False):
367
366
  update_current_game()
368
367
 
369
368
  if sanitize:
370
- return util.sanitize_filename(configuration.current_game)
369
+ return sanitize_filename(configuration.current_game)
371
370
  return configuration.current_game
372
371
 
373
372
 
@@ -9,7 +9,7 @@ from PIL import Image, ImageTk, ImageDraw
9
9
 
10
10
  from GameSentenceMiner import obs # Import your actual obs module
11
11
  from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness
12
- from GameSentenceMiner.util import sanitize_filename # Import your actual util module
12
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
13
13
 
14
14
  try:
15
15
  import pygetwindow as gw
@@ -1,10 +1,8 @@
1
1
  import asyncio
2
- import ctypes
3
2
  import json
4
3
  import logging
5
4
  import os
6
5
  import queue
7
- import re
8
6
  import threading
9
7
  import time
10
8
  from datetime import datetime
@@ -16,13 +14,12 @@ import mss
16
14
  import websockets
17
15
  from rapidfuzz import fuzz
18
16
 
19
- from GameSentenceMiner import obs, util
20
- from GameSentenceMiner.configuration import get_config, get_app_directory, get_temporary_directory
21
- from GameSentenceMiner.electron_config import get_ocr_scan_rate, get_requires_open_window
22
- from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, Rectangle, set_dpi_awareness
17
+ from GameSentenceMiner import obs
18
+ from GameSentenceMiner.util.configuration import get_config, get_app_directory, get_temporary_directory
19
+ from GameSentenceMiner.util.electron_config import get_ocr_scan_rate, get_requires_open_window
20
+ from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, set_dpi_awareness
23
21
  from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
24
- from GameSentenceMiner.owocr.owocr.run import TextFiltering
25
- from GameSentenceMiner.util import do_text_replacements, OCR_REPLACEMENTS_FILE
22
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename, do_text_replacements, OCR_REPLACEMENTS_FILE
26
23
 
27
24
  CONFIG_FILE = Path("ocr_config.json")
28
25
  DEFAULT_IMAGE_PATH = r"C:\Users\Beangate\Pictures\msedge_acbl8GL7Ax.jpg" # CHANGE THIS
@@ -66,7 +63,7 @@ def get_new_game_cords():
66
63
  ocr_config_dir = app_dir / "ocr_config"
67
64
  ocr_config_dir.mkdir(parents=True, exist_ok=True)
68
65
  obs.connect_to_obs_sync()
69
- scene = util.sanitize_filename(obs.get_current_scene())
66
+ scene = sanitize_filename(obs.get_current_scene())
70
67
  config_path = ocr_config_dir / f"{scene}.json"
71
68
  with open(config_path, 'w') as f:
72
69
  json.dump({"scene": scene, "window": None, "rectangles": coords_list}, f, indent=4)
@@ -80,7 +77,7 @@ def get_ocr_config() -> OCRConfig:
80
77
  ocr_config_dir = app_dir / "ocr_config"
81
78
  os.makedirs(ocr_config_dir, exist_ok=True)
82
79
  obs.connect_to_obs_sync()
83
- scene = util.sanitize_filename(obs.get_current_scene())
80
+ scene = sanitize_filename(obs.get_current_scene())
84
81
  config_path = ocr_config_dir / f"{scene}.json"
85
82
  if not config_path.exists():
86
83
  config_path.touch()
@@ -214,11 +211,11 @@ def do_second_ocr(ocr1_text, time, img, filtering, scrolling=False):
214
211
  engine=ocr2)
215
212
  if scrolling:
216
213
  return text
217
- if fuzz.ratio(last_ocr2_result, orig_text) >= 80:
214
+ if fuzz.ratio(last_ocr2_result, text) >= 80:
218
215
  logger.info("Seems like the same text from previous ocr2 result, not sending")
219
216
  return
220
217
  save_result_image(img)
221
- last_ocr2_result = orig_text
218
+ last_ocr2_result = text
222
219
  asyncio.run(send_result(text, time))
223
220
  except json.JSONDecodeError:
224
221
  print("Invalid JSON received.")
@@ -289,13 +286,15 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
289
286
  previous_img_local = previous_img
290
287
  if previous_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 90:
291
288
  logger.info("Seems like Text we already sent, not doing anything.")
289
+ previous_text = None
292
290
  return
293
291
  previous_orig_text = orig_text_string
294
292
  previous_ocr1_result = previous_text
295
293
  if crop_coords:
296
294
  previous_img_local.save(os.path.join(get_temporary_directory(), "pre_oneocrcrop.png"))
297
295
  previous_img_local = previous_img_local.crop(crop_coords)
298
- do_second_ocr(previous_text, stable_time, previous_img_local, filtering)
296
+ second_ocr_queue.put((previous_text, stable_time, previous_img_local, filtering))
297
+ # threading.Thread(target=do_second_ocr, args=(previous_text, stable_time, previous_img_local, filtering), daemon=True).start()
299
298
  previous_img = None
300
299
  previous_text = None
301
300
  text_stable_start_time = None
@@ -312,6 +311,22 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
312
311
 
313
312
  done = False
314
313
 
314
+ # Create a queue for tasks
315
+ second_ocr_queue = queue.Queue()
316
+
317
+ def process_task_queue():
318
+ while True:
319
+ try:
320
+ task = second_ocr_queue.get()
321
+ if task is None: # Exit signal
322
+ break
323
+ ocr1_text, stable_time, previous_img_local, filtering = task
324
+ do_second_ocr(ocr1_text, stable_time, previous_img_local, filtering)
325
+ except Exception as e:
326
+ logger.exception(f"Error processing task: {e}")
327
+ finally:
328
+ second_ocr_queue.task_done()
329
+
315
330
 
316
331
  def run_oneocr(ocr_config: OCRConfig, rectangles):
317
332
  global done
@@ -420,6 +435,8 @@ if __name__ == "__main__":
420
435
  ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config,rectangles ), daemon=True)
421
436
  ocr_thread.start()
422
437
  if not ssonly:
438
+ worker_thread = threading.Thread(target=process_task_queue, daemon=True)
439
+ worker_thread.start()
423
440
  websocket_server_thread = WebsocketServerThread(read=True)
424
441
  websocket_server_thread.start()
425
442
  try:
@@ -17,8 +17,6 @@ from google.generativeai import GenerationConfig
17
17
  from loguru import logger
18
18
  import requests
19
19
 
20
- from ...configuration import get_temporary_directory
21
-
22
20
  try:
23
21
  from manga_ocr import MangaOcr as MOCR
24
22
  except ImportError:
@@ -55,7 +55,7 @@ except ImportError:
55
55
  pass
56
56
  from .config import Config
57
57
  from .screen_coordinate_picker import get_screen_selection
58
- from ...configuration import get_temporary_directory, get_app_directory
58
+ from GameSentenceMiner.util.configuration import get_temporary_directory
59
59
 
60
60
  config = None
61
61
 
@@ -5,7 +5,7 @@ from typing import Dict, Optional, Any
5
5
  from dataclasses_json import dataclass_json, Undefined
6
6
  from websocket import WebSocket
7
7
 
8
- from GameSentenceMiner.configuration import get_app_directory
8
+ from GameSentenceMiner.util.configuration import get_app_directory
9
9
 
10
10
  CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
11
11
  websocket: WebSocket = None
@@ -1,6 +1,6 @@
1
1
  import json
2
2
 
3
- from GameSentenceMiner.communication.websocket import websocket, Message
3
+ from GameSentenceMiner.util.communication.websocket import websocket, Message
4
4
 
5
5
  async def send_restart_signal():
6
6
  if websocket:
@@ -7,8 +7,8 @@ from enum import Enum
7
7
 
8
8
  from websocket import WebSocket
9
9
 
10
- from GameSentenceMiner.communication import Message
11
- from GameSentenceMiner.configuration import get_app_directory, logger
10
+ from GameSentenceMiner.util.communication import Message
11
+ from GameSentenceMiner.util.configuration import get_app_directory, logger
12
12
 
13
13
  CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
14
14
  websocket: WebSocket = None
@@ -6,9 +6,9 @@ import urllib.request
6
6
  import platform
7
7
  import zipfile
8
8
 
9
- from GameSentenceMiner.downloader.Untitled_json import scenes
10
- from GameSentenceMiner.configuration import get_app_directory, logger
11
- from GameSentenceMiner.ffmpeg import get_ffmpeg_path, get_ffprobe_path
9
+ from GameSentenceMiner.util.downloader.Untitled_json import scenes
10
+ from GameSentenceMiner.util.configuration import get_app_directory, logger
11
+ from GameSentenceMiner.util.ffmpeg import get_ffmpeg_path, get_ffprobe_path
12
12
  from GameSentenceMiner.obs import get_obs_path
13
13
 
14
14
  script_dir = os.path.dirname(os.path.abspath(__file__))