GameSentenceMiner 2.18.21__py3-none-any.whl → 2.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of GameSentenceMiner might be problematic. Click here for more details.

@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  import json
3
3
  import re
4
- from datetime import datetime
4
+ from datetime import datetime, timedelta
5
+ from collections import defaultdict, deque
5
6
 
6
7
  import pyperclip
7
8
  import requests
@@ -17,7 +18,7 @@ from GameSentenceMiner.util.gsm_utils import add_srt_line
17
18
  from GameSentenceMiner.util.text_log import add_line, get_text_log
18
19
  from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker, overlay_server_thread
19
20
 
20
- from GameSentenceMiner.util.get_overlay_coords import overlay_processor
21
+ from GameSentenceMiner.util.get_overlay_coords import get_overlay_processor
21
22
 
22
23
 
23
24
  current_line = ''
@@ -34,6 +35,61 @@ last_clipboard = ''
34
35
  reconnecting = False
35
36
  websocket_connected = {}
36
37
 
38
+ # Rate-based spam detection globals
39
+ message_timestamps = defaultdict(lambda: deque(maxlen=60)) # Store last 60 message timestamps per source
40
+ rate_limit_active = defaultdict(bool) # Track if rate limiting is active per source
41
+
42
+
43
+ def is_message_rate_limited(source="clipboard"):
44
+ """
45
+ Aggressive rate-based spam detection optimized for game texthookers.
46
+ Uses multiple time windows for faster detection and recovery.
47
+
48
+ Args:
49
+ source (str): The source of the message (clipboard, websocket, etc.)
50
+
51
+ Returns:
52
+ bool: True if message should be dropped due to rate limiting
53
+ """
54
+ current_time = datetime.now()
55
+ timestamps = message_timestamps[source]
56
+
57
+ # Add current message timestamp
58
+ timestamps.append(current_time)
59
+
60
+ # Check multiple time windows for aggressive detection
61
+ half_second_ago = current_time - timedelta(milliseconds=500)
62
+ one_second_ago = current_time - timedelta(seconds=1)
63
+
64
+ # Count messages in different time windows
65
+ last_500ms = sum(1 for ts in timestamps if ts > half_second_ago)
66
+ last_1s = sum(1 for ts in timestamps if ts > one_second_ago)
67
+
68
+ # Very aggressive thresholds for game texthookers:
69
+ # - 5+ messages in 500ms = instant spam detection
70
+ # - 8+ messages in 1 second = spam detection
71
+ spam_detected = last_500ms >= 5 or last_1s >= 8
72
+
73
+ if spam_detected:
74
+ if not rate_limit_active[source]:
75
+ logger.warning(f"Rate limiting activated for {source}: {last_500ms} msgs/500ms, {last_1s} msgs/1s")
76
+ rate_limit_active[source] = True
77
+ return True
78
+
79
+ # If rate limiting is active, check if we can deactivate it immediately
80
+ if rate_limit_active[source]:
81
+ # Very fast recovery: allow if current 500ms window has <= 2 messages
82
+ if last_500ms <= 2:
83
+ logger.info(f"Rate limiting deactivated for {source}: rate normalized ({last_500ms} msgs/500ms)")
84
+ rate_limit_active[source] = False
85
+ return False # Allow this message through
86
+ else:
87
+ # Still too fast, keep dropping
88
+ return True
89
+
90
+ return False
91
+
92
+
37
93
  async def monitor_clipboard():
38
94
  global current_line, last_clipboard
39
95
  current_line = pyperclip.paste()
@@ -55,6 +111,9 @@ async def monitor_clipboard():
55
111
  current_clipboard = pyperclip.paste()
56
112
 
57
113
  if current_clipboard and current_clipboard != current_line and current_clipboard != last_clipboard:
114
+ # Check for rate limiting before processing
115
+ if is_message_rate_limited("clipboard"):
116
+ continue # Drop message due to rate limiting
58
117
  last_clipboard = current_clipboard
59
118
  await handle_new_text_event(current_clipboard)
60
119
 
@@ -65,6 +124,7 @@ async def listen_websockets():
65
124
  async def listen_on_websocket(uri):
66
125
  global current_line, current_line_time, reconnecting, websocket_connected
67
126
  try_other = False
127
+ rated_limited = False
68
128
  websocket_connected[uri] = False
69
129
  websocket_names = {
70
130
  "9002": "GSM OCR",
@@ -82,17 +142,19 @@ async def listen_websockets():
82
142
  websocket_url = f'ws://{uri}/api/ws/text/origin'
83
143
  try:
84
144
  async with websockets.connect(websocket_url, ping_interval=None) as websocket:
145
+ websocket_source = f"websocket_{uri}"
85
146
  gsm_status.websockets_connected.append(websocket_url)
86
- if reconnecting:
87
- logger.info(f"Texthooker WebSocket {uri}{likely_websocket_name} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
88
- reconnecting = False
147
+ logger.info(f"Texthooker WebSocket {uri}{likely_websocket_name} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
89
148
  websocket_connected[uri] = True
90
149
  line_time = None
91
- while True:
92
- message = await websocket.recv()
150
+ async for message in websocket:
93
151
  message_received_time = datetime.now()
94
152
  if not message:
95
153
  continue
154
+ # Check for rate limiting before processing
155
+ if is_message_rate_limited(websocket_source):
156
+ rated_limited = True
157
+ continue # Drop message due to rate limiting
96
158
  if is_dev:
97
159
  logger.debug(message)
98
160
  try:
@@ -101,14 +163,10 @@ async def listen_websockets():
101
163
  current_clipboard = data["sentence"]
102
164
  if "time" in data:
103
165
  line_time = datetime.fromisoformat(data["time"])
104
- except json.JSONDecodeError or TypeError:
166
+ except (json.JSONDecodeError, TypeError):
105
167
  current_clipboard = message
106
- logger.info
107
168
  if current_clipboard != current_line:
108
- try:
109
- await handle_new_text_event(current_clipboard, line_time if line_time else message_received_time)
110
- except Exception as e:
111
- logger.error(f"Error handling new text event: {e}", exc_info=True)
169
+ await handle_new_text_event(current_clipboard, line_time if line_time else message_received_time)
112
170
  except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
113
171
  if websocket_url in gsm_status.websockets_connected:
114
172
  gsm_status.websockets_connected.remove(websocket_url)
@@ -201,8 +259,8 @@ async def add_line_to_text_log(line, line_time=None):
201
259
  if len(get_text_log().values) > 0:
202
260
  await add_event_to_texthooker(get_text_log()[-1])
203
261
  if get_config().overlay.websocket_port and overlay_server_thread.has_clients():
204
- if overlay_processor.ready:
205
- await overlay_processor.find_box_and_send_to_overlay(current_line_after_regex)
262
+ if get_overlay_processor().ready:
263
+ await get_overlay_processor().find_box_and_send_to_overlay(current_line_after_regex)
206
264
  add_srt_line(line_time, new_line)
207
265
  if 'nostatspls' not in new_line.scene.lower():
208
266
  GameLinesTable.add_line(new_line)
GameSentenceMiner/gsm.py CHANGED
@@ -1,7 +1,5 @@
1
1
  # There should be no imports here, as any error will crash the program.
2
2
  # All imports should be done in the try/except block below.
3
-
4
-
5
3
  def handle_error_in_initialization(e):
6
4
  """Handle errors that occur during initialization."""
7
5
  logger.exception(e, exc_info=True)
@@ -49,7 +47,7 @@ try:
49
47
  logger.debug(f"[Import] configuration: {time.time() - start_time:.3f}s")
50
48
 
51
49
  start_time = time.time()
52
- from GameSentenceMiner.util.get_overlay_coords import OverlayThread
50
+ from GameSentenceMiner.util.get_overlay_coords import init_overlay_processor
53
51
  from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags, add_srt_line
54
52
  logger.debug(f"[Import] get_overlay_coords (OverlayThread, remove_html_and_cloze_tags): {time.time() - start_time:.3f}s")
55
53
 
@@ -62,7 +60,7 @@ try:
62
60
  logger.debug(f"[Import] vad_processor: {time.time() - start_time:.3f}s")
63
61
 
64
62
  start_time = time.time()
65
- from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
63
+ from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed, write_obs_configs, download_oneocr_dlls_if_needed
66
64
  logger.debug(
67
65
  f"[Import] download_tools (download_obs_if_needed, download_ffmpeg_if_needed): {time.time() - start_time:.3f}s")
68
66
 
@@ -191,9 +189,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
191
189
  if anki.card_queue and len(anki.card_queue) > 0:
192
190
  last_note, anki_card_creation_time, selected_lines = anki.card_queue.pop(
193
191
  0)
194
- elif get_config().features.backfill_audio:
195
- last_note = anki.get_cards_by_sentence(
196
- gametext.current_line_after_regex)
197
192
  else:
198
193
  logger.info(
199
194
  "Replay buffer initiated externally. Skipping processing.")
@@ -204,9 +199,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
204
199
  if not last_note:
205
200
  if get_config().anki.update_anki:
206
201
  last_note = anki.get_last_anki_card()
207
- if get_config().features.backfill_audio:
208
- last_note = anki.get_cards_by_sentence(
209
- gametext.current_line_after_regex)
210
202
 
211
203
  note, last_note = anki.get_initial_card_info(
212
204
  last_note, selected_lines)
@@ -362,6 +354,7 @@ def initial_checks():
362
354
  try:
363
355
  subprocess.run(GameSentenceMiner.util.configuration.ffmpeg_base_command_list)
364
356
  logger.debug("FFMPEG is installed and accessible.")
357
+
365
358
  except FileNotFoundError:
366
359
  logger.error(
367
360
  "FFmpeg not found, please install it and add it to your PATH.")
@@ -381,48 +374,11 @@ def register_hotkeys():
381
374
 
382
375
 
383
376
  def get_screenshot():
384
- # try:
385
377
  last_note = anki.get_last_anki_card()
386
378
  gsm_state.anki_note_for_screenshot = last_note
387
379
  gsm_state.line_for_screenshot = get_mined_line(last_note, get_all_lines())
388
380
  obs.save_replay_buffer()
389
- # image = obs.get_screenshot()
390
- # wait_for_stable_file(image, timeout=3)
391
- # if not image:
392
- # raise Exception("Failed to get Screenshot from OBS")
393
- # encoded_image = ffmpeg.process_image(image)
394
- # if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
395
- # last_note = anki.get_last_anki_card()
396
- # if get_config().features.backfill_audio:
397
- # last_note = anki.get_cards_by_sentence(gametext.current_line)
398
- # if last_note:
399
- # anki.add_image_to_card(last_note, encoded_image)
400
- # notification.send_screenshot_updated(last_note.get_field(get_config().anki.word_field))
401
- # if get_config().features.open_anki_edit:
402
- # notification.open_anki_card(last_note.noteId)
403
- # else:
404
- # notification.send_screenshot_saved(encoded_image)
405
- # else:
406
- # notification.send_screenshot_saved(encoded_image)
407
- # except Exception as e:
408
- # logger.error(f"Failed to get Screenshot: {e}")
409
-
410
-
411
- # def create_image():
412
- # """Create a simple pickaxe icon."""
413
- # width, height = 64, 64
414
- # image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # Transparent background
415
- # draw = ImageDraw.Draw(image)
416
- #
417
- # # Handle (rectangle)
418
- # handle_color = (139, 69, 19) # Brown color
419
- # draw.rectangle([(30, 15), (34, 50)], fill=handle_color)
420
- #
421
- # # Blade (triangle-like shape)
422
- # blade_color = (192, 192, 192) # Silver color
423
- # draw.polygon([(15, 15), (49, 15), (32, 5)], fill=blade_color)
424
- #
425
- # return image
381
+
426
382
 
427
383
  def create_image():
428
384
  image_path = os.path.join(os.path.dirname(
@@ -673,6 +629,8 @@ def initialize(reloading=False):
673
629
  if is_windows():
674
630
  download_obs_if_needed()
675
631
  download_ffmpeg_if_needed()
632
+ download_oneocr_dlls_if_needed()
633
+ write_obs_configs(obs.get_base_obs_dir())
676
634
  if shutil.which("ffmpeg") is None:
677
635
  os.environ["PATH"] += os.pathsep + \
678
636
  os.path.dirname(get_ffmpeg_path())
@@ -746,7 +704,7 @@ def async_loop():
746
704
  await register_scene_switcher_callback()
747
705
  await check_obs_folder_is_correct()
748
706
  vad_processor.init()
749
- OverlayThread().start()
707
+ await init_overlay_processor()
750
708
 
751
709
  # Keep loop alive
752
710
  # if is_beangate:
GameSentenceMiner/obs.py CHANGED
@@ -8,6 +8,7 @@ import time
8
8
  import logging
9
9
  import contextlib
10
10
  import shutil
11
+ import queue
11
12
 
12
13
  import psutil
13
14
 
@@ -18,6 +19,21 @@ from GameSentenceMiner.util import configuration
18
19
  from GameSentenceMiner.util.configuration import get_app_directory, get_config, get_master_config, is_windows, save_full_config, reload_config, logger, gsm_status, gsm_state
19
20
  from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name, make_unique_temp_file
20
21
 
22
+ # Thread-safe queue for GUI error messages
23
+ _gui_error_queue = queue.Queue()
24
+
25
+ def _queue_error_for_gui(title, message, recheck_function=None):
26
+ _gui_error_queue.put((title, message, recheck_function))
27
+
28
+ def get_queued_gui_errors():
29
+ errors = []
30
+ try:
31
+ while True:
32
+ errors.append(_gui_error_queue.get_nowait())
33
+ except queue.Empty:
34
+ pass
35
+ return errors
36
+
21
37
  connection_pool: 'OBSConnectionPool' = None
22
38
  event_client: obs.EventClient = None
23
39
  obs_process_pid = None
@@ -100,7 +116,6 @@ class OBSConnectionPool:
100
116
  if not hasattr(self, '_healthcheck_client') or self._healthcheck_client is None:
101
117
  try:
102
118
  self._healthcheck_client = obs.ReqClient(**self.connection_kwargs)
103
- logger.info("Initialized dedicated healthcheck client.")
104
119
  except Exception as e:
105
120
  logger.error(f"Failed to create healthcheck client: {e}")
106
121
  self._healthcheck_client = None
@@ -161,6 +176,11 @@ class OBSConnectionManager(threading.Thread):
161
176
 
162
177
  buffer_seconds, error_message = self.check_replay_buffer_enabled()
163
178
 
179
+ if not buffer_seconds:
180
+ # Queue the error message to be shown safely in the main thread
181
+ _queue_error_for_gui("OBS Replay Buffer Error", error_message + "\n\nTo disable this message, turn off 'Automatically Manage Replay Buffer' in GSM settings.", recheck_function=get_replay_buffer_output)
182
+ return errors
183
+
164
184
  gsm_state.replay_buffer_length = buffer_seconds or 300
165
185
 
166
186
  if not buffer_seconds:
@@ -176,7 +196,7 @@ class OBSConnectionManager(threading.Thread):
176
196
  return errors
177
197
 
178
198
  if current_status != self.last_replay_buffer_status:
179
- self.last_replay_buffer_status = current_status
199
+ errors.append("Replay Buffer Changed Externally, Not Managing Automatically.")
180
200
  self.no_output_timestamp = None
181
201
  return errors
182
202
 
@@ -231,6 +251,9 @@ class OBSConnectionManager(threading.Thread):
231
251
 
232
252
  def stop(self):
233
253
  self.running = False
254
+
255
+ def get_base_obs_dir():
256
+ return os.path.join(configuration.get_app_directory(), 'obs-studio')
234
257
 
235
258
  def get_obs_path():
236
259
  return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
@@ -251,6 +251,26 @@ class ConfigApp:
251
251
  self.window.update_idletasks()
252
252
  self.window.geometry("")
253
253
  self.window.withdraw()
254
+
255
+ # Start checking for OBS error messages
256
+ self.check_obs_errors()
257
+
258
+ def check_obs_errors(self):
259
+ """Check for queued error messages from OBS and display them."""
260
+ try:
261
+ from GameSentenceMiner import obs
262
+ errors = obs.get_queued_gui_errors()
263
+ for title, message, recheck_func in errors:
264
+ if recheck_func is not None:
265
+ if recheck_func():
266
+ continue # Issue resolved, don't show error
267
+ messagebox.showerror(title, message)
268
+ except Exception as e:
269
+ # Don't let error checking crash the GUI
270
+ logger.debug(f"Error checking OBS error queue: {e}")
271
+
272
+ # Schedule the next check in 1 second
273
+ self.window.after(1000, self.check_obs_errors)
254
274
 
255
275
  def change_locale(self):
256
276
  """Change the locale of the application."""
@@ -414,7 +434,6 @@ class ConfigApp:
414
434
  self.notify_on_update_value = tk.BooleanVar(value=self.settings.features.notify_on_update)
415
435
  self.open_anki_edit_value = tk.BooleanVar(value=self.settings.features.open_anki_edit)
416
436
  self.open_anki_browser_value = tk.BooleanVar(value=self.settings.features.open_anki_in_browser)
417
- self.backfill_audio_value = tk.BooleanVar(value=self.settings.features.backfill_audio)
418
437
  self.browser_query_value = tk.StringVar(value=self.settings.features.browser_query)
419
438
  self.generate_longplay_value = tk.BooleanVar(value=self.settings.features.generate_longplay)
420
439
 
@@ -544,7 +563,7 @@ class ConfigApp:
544
563
  self.create_vars() # Recreate variables to reflect default values
545
564
  recreate_tab()
546
565
  self.save_settings(profile_change=False)
547
- self.reload_settings()
566
+ self.reload_settings(force_refresh=True)
548
567
 
549
568
  def show_scene_selection(self, matched_configs):
550
569
  selected_scene = None
@@ -645,7 +664,6 @@ class ConfigApp:
645
664
  notify_on_update=self.notify_on_update_value.get(),
646
665
  open_anki_edit=self.open_anki_edit_value.get(),
647
666
  open_anki_in_browser=self.open_anki_browser_value.get(),
648
- backfill_audio=self.backfill_audio_value.get(),
649
667
  browser_query=self.browser_query_value.get(),
650
668
  generate_longplay=self.generate_longplay_value.get(),
651
669
  ),
@@ -1536,6 +1554,14 @@ class ConfigApp:
1536
1554
  ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence_value, bootstyle="round-toggle").grid(
1537
1555
  row=self.current_row, column=1, sticky='W', pady=2)
1538
1556
  self.current_row += 1
1557
+
1558
+ advanced_i18n = anki_i18n.get('advanced_settings', {})
1559
+ linebreak_i18n = advanced_i18n.get('multiline_linebreak', {})
1560
+ HoverInfoLabelWidget(anki_frame, text=linebreak_i18n.get('label', '...'),
1561
+ tooltip=linebreak_i18n.get('tooltip', '...'),
1562
+ row=self.current_row, column=0)
1563
+ ttk.Entry(anki_frame, textvariable=self.multi_line_line_break_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1564
+ self.current_row += 1
1539
1565
 
1540
1566
  self.add_reset_button(anki_frame, "anki", self.current_row, 0, self.create_anki_tab)
1541
1567
 
@@ -1595,13 +1621,6 @@ class ConfigApp:
1595
1621
  ttk.Entry(features_frame, width=50, textvariable=self.browser_query_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1596
1622
  self.current_row += 1
1597
1623
 
1598
- backfill_i18n = features_i18n.get('backfill_audio', {})
1599
- HoverInfoLabelWidget(features_frame, text=backfill_i18n.get('label', '...'), tooltip=backfill_i18n.get('tooltip', '...'),
1600
- row=self.current_row, column=0)
1601
- ttk.Checkbutton(features_frame, variable=self.backfill_audio_value, bootstyle="round-toggle").grid(
1602
- row=self.current_row, column=1, sticky='W', pady=2)
1603
- self.current_row += 1
1604
-
1605
1624
  HoverInfoLabelWidget(features_frame, text="Generate LongPlay", tooltip="Generate a LongPlay video using OBS recording, and write to a .srt file with all the text coming into gsm. RESTART REQUIRED FOR SETTING TO TAKE EFFECT.",
1606
1625
  row=self.current_row, column=0)
1607
1626
  ttk.Checkbutton(features_frame, variable=self.generate_longplay_value, bootstyle="round-toggle").grid(
@@ -2063,13 +2082,6 @@ class ConfigApp:
2063
2082
  ttk.Entry(advanced_frame, textvariable=self.play_latest_audio_hotkey_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2064
2083
  self.current_row += 1
2065
2084
 
2066
- linebreak_i18n = advanced_i18n.get('multiline_linebreak', {})
2067
- HoverInfoLabelWidget(advanced_frame, text=linebreak_i18n.get('label', '...'),
2068
- tooltip=linebreak_i18n.get('tooltip', '...'),
2069
- row=self.current_row, column=0)
2070
- ttk.Entry(advanced_frame, textvariable=self.multi_line_line_break_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2071
- self.current_row += 1
2072
-
2073
2085
  storage_field_i18n = advanced_i18n.get('multiline_storage_field', {})
2074
2086
  HoverInfoLabelWidget(advanced_frame, text=storage_field_i18n.get('label', '...'),
2075
2087
  tooltip=storage_field_i18n.get('tooltip', '...'),
@@ -4,4 +4,16 @@ from GameSentenceMiner.util.communication.websocket import websocket, Message
4
4
 
5
5
  async def send_restart_signal():
6
6
  if websocket:
7
- await websocket.send(json.dumps(Message(function="restart").to_json()))
7
+ await websocket.send(json.dumps(Message(function="restart").to_json()))
8
+
9
+
10
+ async def send_notification_signal(title: str, message: str, timeout: int):
11
+ if websocket:
12
+ await websocket.send(json.dumps(Message(
13
+ function="notification",
14
+ data={
15
+ "title": title,
16
+ "message": message,
17
+ "timeout": timeout
18
+ }
19
+ ).to_json()))
@@ -7,11 +7,11 @@ from enum import Enum
7
7
 
8
8
  from websocket import WebSocket
9
9
 
10
- from GameSentenceMiner.util.communication import Message
10
+ from GameSentenceMiner.util.communication import Message, websocket
11
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
- websocket: WebSocket = None
14
+ # websocket: WebSocket = None
15
15
  handle_websocket_message = None
16
16
 
17
17
 
@@ -470,7 +470,6 @@ class Features:
470
470
  open_anki_edit: bool = False
471
471
  open_anki_in_browser: bool = True
472
472
  browser_query: str = ''
473
- backfill_audio: bool = False
474
473
  generate_longplay: bool = False
475
474
 
476
475
 
@@ -761,9 +760,7 @@ class ProfileConfig:
761
760
  'notify_on_update', self.features.notify_on_update)
762
761
  self.features.open_anki_edit = config_data['features'].get(
763
762
  'open_anki_edit', self.features.open_anki_edit)
764
- self.features.backfill_audio = config_data['features'].get(
765
- 'backfill_audio', self.features.backfill_audio)
766
-
763
+
767
764
  self.screenshot.width = config_data['screenshot'].get(
768
765
  'width', self.screenshot.width)
769
766
  self.screenshot.height = config_data['screenshot'].get(
@@ -1176,31 +1173,22 @@ def get_config():
1176
1173
  global config_instance
1177
1174
  if config_instance is None:
1178
1175
  config_instance = load_config()
1179
- config = config_instance.get_config()
1180
-
1181
- if config.features.backfill_audio and config.features.full_auto:
1182
- logger.warning(
1183
- "Backfill audio is enabled, but full auto is also enabled. Disabling backfill...")
1184
- config.features.backfill_audio = False
1185
1176
 
1186
1177
  # print(config_instance.get_config())
1187
1178
  return config_instance.get_config()
1188
1179
 
1180
+
1189
1181
  def get_overlay_config():
1190
1182
  global config_instance
1191
1183
  if config_instance is None:
1192
1184
  config_instance = load_config()
1193
1185
  return config_instance.overlay
1194
1186
 
1187
+
1195
1188
  def reload_config():
1196
1189
  global config_instance
1197
1190
  config_instance = load_config()
1198
- config = config_instance.get_config()
1199
1191
 
1200
- if config.features.backfill_audio and config.features.full_auto:
1201
- logger.warning(
1202
- "Backfill is enabled, but full auto is also enabled. Disabling backfill...")
1203
- config.features.backfill_audio = False
1204
1192
 
1205
1193
  def get_stats_config():
1206
1194
  global config_instance
@@ -512,7 +512,7 @@ class GameLinesTable(SQLiteDBTable):
512
512
  def update(cls, line_id: str, audio_in_anki: Optional[str] = None, screenshot_in_anki: Optional[str] = None, audio_path: Optional[str] = None, screenshot_path: Optional[str] = None, replay_path: Optional[str] = None, translation: Optional[str] = None):
513
513
  line = cls.get(line_id)
514
514
  if not line:
515
- logger.warning(f"GameLine with id {line_id} not found for update.")
515
+ logger.warning(f"GameLine with id {line_id} not found for update, maybe testing?")
516
516
  return
517
517
  if screenshot_path is not None:
518
518
  line.screenshot_path = screenshot_path
@@ -7,9 +7,10 @@ import platform
7
7
  import zipfile
8
8
 
9
9
  from GameSentenceMiner.util.downloader.Untitled_json import scenes
10
- from GameSentenceMiner.util.configuration import get_app_directory, get_ffmpeg_path, logger
10
+ from GameSentenceMiner.util.configuration import get_app_directory, get_config, get_ffmpeg_path, logger
11
11
  from GameSentenceMiner.util.configuration import get_ffprobe_path
12
12
  from GameSentenceMiner.obs import get_obs_path
13
+ from GameSentenceMiner.util.downloader.oneocr_dl import Downloader
13
14
  import tempfile
14
15
 
15
16
  script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -110,41 +111,61 @@ def download_obs_if_needed():
110
111
  open(os.path.join(obs_path, "portable_mode"), 'a').close()
111
112
  # websocket_config_path = os.path.join(obs_path, 'config', 'obs-studio')
112
113
  # if not copy_obs_settings(os.path.join(os.getenv('APPDATA'), 'obs-studio'), websocket_config_path):
113
- websocket_config_path = os.path.join(obs_path, 'config', 'obs-studio', 'plugin_config', 'obs-websocket')
114
- os.makedirs(websocket_config_path, exist_ok=True)
115
-
114
+ write_obs_configs(obs_path)
115
+ logger.info(f"OBS extracted to {obs_path}.")
116
+
117
+ # remove zip
118
+ os.unlink(obs_installer)
119
+ else:
120
+ logger.error(f"Please install OBS manually from {obs_installer}")
121
+
122
+ def write_websocket_configs(obs_path):
123
+ websocket_config_path = os.path.join(obs_path, 'config', 'obs-studio', 'plugin_config', 'obs-websocket')
124
+ os.makedirs(websocket_config_path, exist_ok=True)
125
+ obs_config = get_config().obs
126
+
127
+ if os.path.exists(os.path.join(websocket_config_path, 'config.json')):
128
+ with open(os.path.join(websocket_config_path, 'config.json'), 'r') as existing_config_file:
129
+ existing_config = json.load(existing_config_file)
130
+ if obs_config.port != existing_config.get('server_port', 7274):
131
+ logger.info(f"OBS WebSocket port changed from {existing_config.get('server_port', 7274)} to {obs_config.port}. Updating config.")
132
+ existing_config['server_port'] = obs_config.port
133
+ existing_config['server_password'] = obs_config.password
134
+ existing_config['auth_required'] = False
135
+ existing_config['server_enabled'] = True
136
+ with open(os.path.join(websocket_config_path, 'config.json'), 'w') as config_file:
137
+ json.dump(existing_config, config_file, indent=4)
138
+ else:
116
139
  websocket_config = {
117
140
  "alerts_enabled": False,
118
141
  "auth_required": False,
119
142
  "first_load": False,
120
143
  "server_enabled": True,
121
144
  "server_password": secrets.token_urlsafe(16),
122
- "server_port": 7274
145
+ "server_port": obs_config.port
123
146
  }
124
147
  with open(os.path.join(websocket_config_path, 'config.json'), 'w') as config_file:
125
148
  json.dump(websocket_config, config_file, indent=4)
126
- basic_ini_path = os.path.join(obs_path, 'config', 'obs-studio', 'basic', 'profiles', 'Untitled')
127
- os.makedirs(basic_ini_path, exist_ok=True)
128
- with open(os.path.join(basic_ini_path, 'basic.ini'), 'w') as basic_ini_file:
129
- basic_ini_file.write(
130
- "[SimpleOutput]\n"
131
- f"FilePath={os.path.expanduser('~')}/Videos/GSM\n"
132
- "RecRB=true\n"
133
- "RecRBTime=300\n"
134
- "RecRBSize=512\n"
135
- "RecRBPrefix=GSM\n"
136
- "RecAudioEncoder=opus\n"
137
- )
138
- scene_json_path = os.path.join(obs_path, 'config', 'obs-studio', 'basic', 'scenes')
139
- os.makedirs(scene_json_path, exist_ok=True)
140
- with open(os.path.join(scene_json_path, 'Untitled.json'), 'w') as scene_file:
141
- scene_file.write(scenes)
142
- logger.info(f"OBS extracted to {obs_path}.")
143
-
144
- # remove zip
145
- os.unlink(obs_installer)
146
- else:
147
- logger.error(f"Please install OBS manually from {obs_installer}")
149
+
150
+ def write_replay_buffer_configs(obs_path):
151
+ basic_ini_path = os.path.join(obs_path, 'config', 'obs-studio', 'basic', 'profiles', 'GSM')
152
+ if os.path.exists(os.path.join(basic_ini_path, 'basic.ini')):
153
+ return
154
+ os.makedirs(basic_ini_path, exist_ok=True)
155
+ with open(os.path.join(basic_ini_path, 'basic.ini'), 'w') as basic_ini_file:
156
+ basic_ini_file.write(
157
+ "[SimpleOutput]\n"
158
+ f"FilePath={os.path.expanduser('~')}/Videos/GSM\n"
159
+ "RecRB=true\n"
160
+ "RecRBTime=300\n"
161
+ "RecRBSize=512\n"
162
+ "RecAudioEncoder=opus\n"
163
+ "RecRBPrefix=GSM\n"
164
+ )
165
+
166
+ def write_obs_configs(obs_path):
167
+ write_websocket_configs(obs_path)
168
+ write_replay_buffer_configs(obs_path)
148
169
 
149
170
  def download_ffmpeg_if_needed():
150
171
  ffmpeg_dir = os.path.join(get_app_directory(), 'ffmpeg')
@@ -261,10 +282,15 @@ def download_ocenaudio_if_needed():
261
282
  logger.info(f"Ocenaudio extracted to {ocenaudio_dir}.")
262
283
  return ocenaudio_exe_path
263
284
 
285
+ def download_oneocr_dlls_if_needed():
286
+ downloader = Downloader()
287
+ downloader.download_and_extract()
288
+
264
289
  def main():
265
290
  download_obs_if_needed()
266
291
  download_ffmpeg_if_needed()
267
292
  download_ocenaudio_if_needed()
293
+ download_oneocr_dlls_if_needed()
268
294
 
269
295
  if __name__ == "__main__":
270
296
  main()