GameSentenceMiner 2.18.20__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.
- GameSentenceMiner/gametext.py +73 -15
- GameSentenceMiner/gsm.py +7 -49
- GameSentenceMiner/obs.py +25 -2
- GameSentenceMiner/ui/config_gui.py +29 -17
- GameSentenceMiner/util/communication/send.py +13 -1
- GameSentenceMiner/util/communication/websocket.py +2 -2
- GameSentenceMiner/util/configuration.py +3 -15
- GameSentenceMiner/util/db.py +1 -1
- GameSentenceMiner/util/downloader/download_tools.py +53 -27
- GameSentenceMiner/util/get_overlay_coords.py +24 -4
- GameSentenceMiner/web/events.py +31 -107
- GameSentenceMiner/web/templates/anki_stats.html +1 -0
- GameSentenceMiner/web/templates/index.html +12 -12
- GameSentenceMiner/web/texthooking_page.py +15 -2
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/METADATA +15 -5
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/RECORD +20 -20
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.20.dist-info → gamesentenceminer-2.19.0.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gametext.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
166
|
+
except (json.JSONDecodeError, TypeError):
|
|
105
167
|
current_clipboard = message
|
|
106
|
-
logger.info
|
|
107
168
|
if current_clipboard != current_line:
|
|
108
|
-
|
|
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
|
|
205
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
GameSentenceMiner/util/db.py
CHANGED
|
@@ -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
|
-
|
|
114
|
-
|
|
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":
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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()
|