GameSentenceMiner 2.17.6__py3-none-any.whl → 2.18.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/ai/ai_prompting.py +51 -51
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +102 -37
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/METADATA +5 -1
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/RECORD +51 -41
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.6.dist-info → gamesentenceminer-2.18.0.dist-info}/top_level.txt +0 -0
GameSentenceMiner/obs.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import datetime
|
|
2
3
|
import json
|
|
3
4
|
import os.path
|
|
4
5
|
import subprocess
|
|
@@ -11,12 +12,11 @@ import shutil
|
|
|
11
12
|
import psutil
|
|
12
13
|
|
|
13
14
|
import obsws_python as obs
|
|
15
|
+
import numpy as np
|
|
14
16
|
|
|
15
17
|
from GameSentenceMiner.util import configuration
|
|
16
18
|
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
|
|
17
|
-
from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name
|
|
18
|
-
import tkinter as tk
|
|
19
|
-
from tkinter import messagebox
|
|
19
|
+
from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name, make_unique_temp_file
|
|
20
20
|
|
|
21
21
|
connection_pool: 'OBSConnectionPool' = None
|
|
22
22
|
event_client: obs.EventClient = None
|
|
@@ -75,7 +75,7 @@ class OBSConnectionPool:
|
|
|
75
75
|
self._clients[index] = obs.ReqClient(**self.connection_kwargs)
|
|
76
76
|
|
|
77
77
|
@contextlib.contextmanager
|
|
78
|
-
def get_client(self):
|
|
78
|
+
def get_client(self) -> obs.ReqClient:
|
|
79
79
|
"""A context manager to safely get a client from the pool."""
|
|
80
80
|
with self._idx_lock:
|
|
81
81
|
idx = self._next_idx
|
|
@@ -106,37 +106,114 @@ class OBSConnectionManager(threading.Thread):
|
|
|
106
106
|
super().__init__()
|
|
107
107
|
self.daemon = True
|
|
108
108
|
self.running = True
|
|
109
|
+
self.should_check_output = check_output
|
|
109
110
|
self.check_connection_interval = 1
|
|
110
|
-
self.said_no_to_replay_buffer = False
|
|
111
111
|
self.counter = 0
|
|
112
|
-
self.
|
|
112
|
+
self.last_replay_buffer_status = None
|
|
113
|
+
self.no_output_timestamp = None
|
|
114
|
+
self.NO_OUTPUT_SHUTDOWN_SECONDS = 300
|
|
115
|
+
self.last_errors = []
|
|
116
|
+
self.previous_image = None
|
|
117
|
+
|
|
118
|
+
def _check_obs_connection(self):
|
|
119
|
+
try:
|
|
120
|
+
client = connection_pool.get_healthcheck_client() if connection_pool else None
|
|
121
|
+
if client and not connecting:
|
|
122
|
+
client.get_version()
|
|
123
|
+
gsm_status.obs_connected = True
|
|
124
|
+
return True
|
|
125
|
+
else:
|
|
126
|
+
raise ConnectionError("Healthcheck client not available or connection in progress")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.debug(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
|
|
129
|
+
gsm_status.obs_connected = False
|
|
130
|
+
asyncio.run(connect_to_obs())
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def check_replay_buffer_enabled(self):
|
|
134
|
+
if not self.should_check_output:
|
|
135
|
+
return True, ""
|
|
136
|
+
output = get_replay_buffer_output()
|
|
137
|
+
if not output:
|
|
138
|
+
return False, "Replay Buffer output not found in OBS. Please enable Replay Buffer In OBS Settings -> Output -> Replay Buffer. I recommend 300 seconds (5 minutes) or higher."
|
|
139
|
+
return True, ""
|
|
140
|
+
|
|
141
|
+
def _manage_replay_buffer_and_utils(self):
|
|
142
|
+
errors = []
|
|
143
|
+
|
|
144
|
+
if not self.should_check_output:
|
|
145
|
+
return errors
|
|
146
|
+
|
|
147
|
+
set_fit_to_screen_for_scene_items(get_current_scene())
|
|
148
|
+
|
|
149
|
+
if not get_config().obs.automatically_manage_replay_buffer:
|
|
150
|
+
errors.append("Automatic Replay Buffer management is disabled in GSM settings.")
|
|
151
|
+
return errors
|
|
152
|
+
|
|
153
|
+
replay_buffer_enabled, error_message = self.check_replay_buffer_enabled()
|
|
154
|
+
|
|
155
|
+
if not replay_buffer_enabled:
|
|
156
|
+
errors.append(error_message)
|
|
157
|
+
return errors
|
|
158
|
+
|
|
159
|
+
current_status = get_replay_buffer_status()
|
|
160
|
+
|
|
161
|
+
if self.last_replay_buffer_status is None:
|
|
162
|
+
self.last_replay_buffer_status = current_status
|
|
163
|
+
return errors
|
|
164
|
+
|
|
165
|
+
if current_status != self.last_replay_buffer_status:
|
|
166
|
+
self.last_replay_buffer_status = current_status
|
|
167
|
+
self.no_output_timestamp = None
|
|
168
|
+
return errors
|
|
169
|
+
|
|
170
|
+
img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
|
|
171
|
+
has_changed = self.has_image_changed(img) if img else True
|
|
172
|
+
|
|
173
|
+
if not has_changed:
|
|
174
|
+
self.no_output_timestamp = None
|
|
175
|
+
if not current_status:
|
|
176
|
+
start_replay_buffer()
|
|
177
|
+
self.last_replay_buffer_status = True
|
|
178
|
+
else: # is_empty
|
|
179
|
+
if current_status:
|
|
180
|
+
if self.no_output_timestamp is None:
|
|
181
|
+
self.no_output_timestamp = time.time()
|
|
182
|
+
elif time.time() - self.no_output_timestamp >= self.NO_OUTPUT_SHUTDOWN_SECONDS:
|
|
183
|
+
stop_replay_buffer()
|
|
184
|
+
self.last_replay_buffer_status = False
|
|
185
|
+
self.no_output_timestamp = None
|
|
186
|
+
|
|
187
|
+
def has_image_changed(self, img):
|
|
188
|
+
if self.previous_image is None:
|
|
189
|
+
self.previous_image = np.array(img)
|
|
190
|
+
return True
|
|
191
|
+
try:
|
|
192
|
+
img1_np = np.array(img) if not isinstance(img, np.ndarray) else img
|
|
193
|
+
img2_np = self.previous_image
|
|
194
|
+
self.previous_image = img1_np
|
|
195
|
+
except Exception:
|
|
196
|
+
logger.warning("Failed to convert images to numpy arrays for comparison.")
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
return (img1_np.shape == img2_np.shape) and np.array_equal(img1_np, img2_np)
|
|
113
200
|
|
|
114
201
|
def run(self):
|
|
202
|
+
time.sleep(5) # Initial delay to allow OBS to start
|
|
115
203
|
while self.running:
|
|
116
204
|
time.sleep(self.check_connection_interval)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else:
|
|
122
|
-
raise ConnectionError("Healthcheck client not healthy or not initialized")
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
|
|
125
|
-
gsm_status.obs_connected = False
|
|
126
|
-
asyncio.run(connect_to_obs())
|
|
205
|
+
|
|
206
|
+
if not self._check_obs_connection():
|
|
207
|
+
continue
|
|
208
|
+
|
|
127
209
|
if self.counter % 5 == 0:
|
|
128
210
|
try:
|
|
129
|
-
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
|
|
136
|
-
try:
|
|
137
|
-
self.check_output()
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
211
|
+
errors = self._manage_replay_buffer_and_utils()
|
|
212
|
+
if errors != self.last_errors:
|
|
213
|
+
if errors:
|
|
214
|
+
for error in errors:
|
|
215
|
+
logger.error(f"OBS Health Check: {error}")
|
|
216
|
+
self.last_errors = errors
|
|
140
217
|
except Exception as e:
|
|
141
218
|
logger.error(f"Error when running Extra Utils in OBS Health Check, Keeping ConnectionManager Alive: {e}")
|
|
142
219
|
self.counter += 1
|
|
@@ -144,28 +221,6 @@ class OBSConnectionManager(threading.Thread):
|
|
|
144
221
|
def stop(self):
|
|
145
222
|
self.running = False
|
|
146
223
|
|
|
147
|
-
def check_output(self):
|
|
148
|
-
img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
|
|
149
|
-
extrema = img.getextrema()
|
|
150
|
-
if isinstance(extrema[0], tuple):
|
|
151
|
-
is_empty = all(e[0] == e[1] for e in extrema)
|
|
152
|
-
else:
|
|
153
|
-
is_empty = extrema[0] == extrema[1]
|
|
154
|
-
if is_empty:
|
|
155
|
-
return
|
|
156
|
-
else:
|
|
157
|
-
root = tk.Tk()
|
|
158
|
-
root.attributes('-topmost', True)
|
|
159
|
-
root.withdraw()
|
|
160
|
-
root.deiconify()
|
|
161
|
-
result = messagebox.askyesno("GSM - Replay Buffer", "The replay buffer is not running, but there seems to be output in OBS. Do you want to start it? (If you click 'No', you won't be asked until you either restart GSM or start/stop replay buffer manually.)")
|
|
162
|
-
root.destroy()
|
|
163
|
-
if not result:
|
|
164
|
-
self.said_no_to_replay_buffer = True
|
|
165
|
-
self.counter = 0
|
|
166
|
-
return
|
|
167
|
-
start_replay_buffer()
|
|
168
|
-
|
|
169
224
|
def get_obs_path():
|
|
170
225
|
return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
|
|
171
226
|
|
|
@@ -232,6 +287,7 @@ async def wait_for_obs_connected():
|
|
|
232
287
|
for _ in range(10):
|
|
233
288
|
try:
|
|
234
289
|
with connection_pool.get_client() as client:
|
|
290
|
+
client: obs.ReqClient
|
|
235
291
|
response = client.get_version()
|
|
236
292
|
if response:
|
|
237
293
|
return True
|
|
@@ -313,6 +369,8 @@ async def connect_to_obs(retry=5, connections=2, check_output=False):
|
|
|
313
369
|
obs_connection_manager = OBSConnectionManager(check_output=check_output)
|
|
314
370
|
obs_connection_manager.start()
|
|
315
371
|
update_current_game()
|
|
372
|
+
if get_config().features.generate_longplay and check_output:
|
|
373
|
+
start_recording(True)
|
|
316
374
|
break # Exit the loop once connected
|
|
317
375
|
except Exception as e:
|
|
318
376
|
if retry <= 0:
|
|
@@ -368,8 +426,8 @@ def do_obs_call(method_name: str, from_dict=None, retry=3, **kwargs):
|
|
|
368
426
|
def toggle_replay_buffer():
|
|
369
427
|
try:
|
|
370
428
|
with connection_pool.get_client() as client:
|
|
371
|
-
|
|
372
|
-
|
|
429
|
+
client: obs.ReqClient
|
|
430
|
+
client.toggle_replay_buffer()
|
|
373
431
|
logger.info("Replay buffer Toggled.")
|
|
374
432
|
except Exception as e:
|
|
375
433
|
logger.error(f"Error toggling buffer: {e}")
|
|
@@ -377,8 +435,10 @@ def toggle_replay_buffer():
|
|
|
377
435
|
def start_replay_buffer():
|
|
378
436
|
try:
|
|
379
437
|
with connection_pool.get_client() as client:
|
|
380
|
-
|
|
381
|
-
|
|
438
|
+
client: obs.ReqClient
|
|
439
|
+
client.start_replay_buffer()
|
|
440
|
+
if get_config().features.generate_longplay:
|
|
441
|
+
start_recording(True)
|
|
382
442
|
logger.info("Replay buffer started.")
|
|
383
443
|
except Exception as e:
|
|
384
444
|
logger.error(f"Error starting replay buffer: {e}")
|
|
@@ -394,8 +454,10 @@ def get_replay_buffer_status():
|
|
|
394
454
|
def stop_replay_buffer():
|
|
395
455
|
try:
|
|
396
456
|
with connection_pool.get_client() as client:
|
|
397
|
-
|
|
398
|
-
|
|
457
|
+
client: obs.ReqClient
|
|
458
|
+
client.stop_replay_buffer()
|
|
459
|
+
if get_config().features.generate_longplay:
|
|
460
|
+
stop_recording()
|
|
399
461
|
logger.info("Replay buffer stopped.")
|
|
400
462
|
except Exception as e:
|
|
401
463
|
logger.warning(f"Error stopping replay buffer: {e}")
|
|
@@ -403,15 +465,49 @@ def stop_replay_buffer():
|
|
|
403
465
|
def save_replay_buffer():
|
|
404
466
|
try:
|
|
405
467
|
with connection_pool.get_client() as client:
|
|
406
|
-
|
|
407
|
-
|
|
468
|
+
client: obs.ReqClient
|
|
469
|
+
client.save_replay_buffer()
|
|
408
470
|
logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
|
|
409
471
|
except Exception as e:
|
|
410
472
|
raise Exception(f"Error saving replay buffer: {e}")
|
|
411
473
|
|
|
474
|
+
def start_recording(longplay=False):
|
|
475
|
+
try:
|
|
476
|
+
with connection_pool.get_client() as client:
|
|
477
|
+
client: obs.ReqClient
|
|
478
|
+
if longplay:
|
|
479
|
+
gsm_state.recording_started_time = datetime.datetime.now()
|
|
480
|
+
gsm_state.current_srt = make_unique_temp_file(f"{get_current_game(sanitize=True)}.srt")
|
|
481
|
+
gsm_state.srt_index = 1
|
|
482
|
+
client.start_record()
|
|
483
|
+
logger.info("Recording started.")
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.error(f"Error starting recording: {e}")
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
def stop_recording():
|
|
489
|
+
try:
|
|
490
|
+
with connection_pool.get_client() as client:
|
|
491
|
+
client: obs.ReqClient
|
|
492
|
+
client.stop_record()
|
|
493
|
+
logger.info("Recording stopped.")
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.error(f"Error stopping recording: {e}")
|
|
496
|
+
|
|
497
|
+
def get_last_recording_filename():
|
|
498
|
+
try:
|
|
499
|
+
with connection_pool.get_client() as client:
|
|
500
|
+
client: obs.ReqClient
|
|
501
|
+
response = client.get_record_status()
|
|
502
|
+
return response.recording_filename if response else ''
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Error getting last recording filename: {e}")
|
|
505
|
+
return ''
|
|
506
|
+
|
|
412
507
|
def get_current_scene():
|
|
413
508
|
try:
|
|
414
509
|
with connection_pool.get_client() as client:
|
|
510
|
+
client: obs.ReqClient
|
|
415
511
|
response = client.get_current_program_scene()
|
|
416
512
|
return response.scene_name if response else ''
|
|
417
513
|
except Exception as e:
|
|
@@ -421,6 +517,7 @@ def get_current_scene():
|
|
|
421
517
|
def get_source_from_scene(scene_name):
|
|
422
518
|
try:
|
|
423
519
|
with connection_pool.get_client() as client:
|
|
520
|
+
client: obs.ReqClient
|
|
424
521
|
response = client.get_scene_item_list(name=scene_name)
|
|
425
522
|
return response.scene_items[0] if response and response.scene_items else ''
|
|
426
523
|
except Exception as e:
|
|
@@ -436,15 +533,80 @@ def get_active_source():
|
|
|
436
533
|
def get_record_directory():
|
|
437
534
|
try:
|
|
438
535
|
with connection_pool.get_client() as client:
|
|
536
|
+
client: obs.ReqClient
|
|
439
537
|
response = client.get_record_directory()
|
|
440
538
|
return response.record_directory if response else ''
|
|
441
539
|
except Exception as e:
|
|
442
540
|
logger.error(f"Error getting recording folder: {e}")
|
|
443
541
|
return ''
|
|
542
|
+
|
|
543
|
+
def get_replay_buffer_max_time_seconds():
|
|
544
|
+
"""
|
|
545
|
+
Gets the configured maximum replay buffer time in seconds using the v5 protocol.
|
|
546
|
+
"""
|
|
547
|
+
try:
|
|
548
|
+
# Assumes a connection_pool object that provides a connected client
|
|
549
|
+
with connection_pool.get_client() as client:
|
|
550
|
+
client: obs.ReqClient
|
|
551
|
+
# For v5, we get settings for the 'replay_buffer' output
|
|
552
|
+
response = client.get_output_settings(name='Replay Buffer')
|
|
553
|
+
|
|
554
|
+
print(response.output_settings)
|
|
555
|
+
|
|
556
|
+
# The response object contains a dict of the actual settings
|
|
557
|
+
if response:
|
|
558
|
+
# The key for replay buffer length in seconds is 'max_time_sec'
|
|
559
|
+
settings = response.output_settings
|
|
560
|
+
if settings and 'max_time_sec' in settings:
|
|
561
|
+
return settings['max_time_sec']
|
|
562
|
+
else:
|
|
563
|
+
logger.warning("Replay buffer settings received, but 'max_time_sec' key was not found.")
|
|
564
|
+
return 0
|
|
565
|
+
else:
|
|
566
|
+
logger.warning(f"get_output_settings for replay_buffer failed: {response.status}")
|
|
567
|
+
return 0
|
|
568
|
+
except Exception as e:
|
|
569
|
+
logger.error(f"Exception while fetching replay buffer settings: {e}")
|
|
570
|
+
return 0
|
|
571
|
+
|
|
572
|
+
def enable_replay_buffer():
|
|
573
|
+
try:
|
|
574
|
+
with connection_pool.get_client() as client:
|
|
575
|
+
client: obs.ReqClient
|
|
576
|
+
response = client.set_output_settings(name='Replay Buffer', settings={'outputFlags': {'OBS_OUTPUT_AUDIO': True, 'OBS_OUTPUT_ENCODED': True, 'OBS_OUTPUT_MULTI_TRACK': True, 'OBS_OUTPUT_SERVICE': False, 'OBS_OUTPUT_VIDEO': True}})
|
|
577
|
+
if response and response.ok:
|
|
578
|
+
logger.info("Replay buffer enabled.")
|
|
579
|
+
return True
|
|
580
|
+
else:
|
|
581
|
+
logger.error(f"Failed to enable replay buffer: {response.status if response else 'No response'}")
|
|
582
|
+
return False
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.error(f"Error enabling replay buffer: {e}")
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
def get_output_list():
|
|
588
|
+
try:
|
|
589
|
+
with connection_pool.get_client() as client:
|
|
590
|
+
client: obs.ReqClient
|
|
591
|
+
response = client.get_output_list()
|
|
592
|
+
return response.outputs if response else None
|
|
593
|
+
except Exception as e:
|
|
594
|
+
logger.error(f"Error getting output list: {e}")
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
def get_replay_buffer_output():
|
|
598
|
+
outputs = get_output_list()
|
|
599
|
+
if not outputs:
|
|
600
|
+
return None
|
|
601
|
+
for output in outputs:
|
|
602
|
+
if output.get('outputKind') == 'replay_buffer':
|
|
603
|
+
return output
|
|
604
|
+
return None
|
|
444
605
|
|
|
445
606
|
def get_obs_scenes():
|
|
446
607
|
try:
|
|
447
608
|
with connection_pool.get_client() as client:
|
|
609
|
+
client: obs.ReqClient
|
|
448
610
|
response = client.get_scene_list()
|
|
449
611
|
return response.scenes if response else None
|
|
450
612
|
except Exception as e:
|
|
@@ -526,6 +688,7 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
|
|
|
526
688
|
return None
|
|
527
689
|
while True:
|
|
528
690
|
with connection_pool.get_client() as client:
|
|
691
|
+
client: obs.ReqClient
|
|
529
692
|
response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
|
|
530
693
|
try:
|
|
531
694
|
response.image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
|
|
@@ -567,6 +730,7 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
|
|
|
567
730
|
|
|
568
731
|
try:
|
|
569
732
|
with connection_pool.get_client() as client:
|
|
733
|
+
client: obs.ReqClient
|
|
570
734
|
# 1. Get the canvas (base) resolution from OBS video settings
|
|
571
735
|
video_settings = client.get_video_settings()
|
|
572
736
|
if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
|
|
@@ -686,6 +850,7 @@ def main():
|
|
|
686
850
|
|
|
687
851
|
def create_scene():
|
|
688
852
|
with connection_pool.get_client() as client:
|
|
853
|
+
client: obs.ReqClient
|
|
689
854
|
# Extract fields from request_json
|
|
690
855
|
request_json = r'{"sceneName":"SILENT HILL f","inputName":"SILENT HILL f - Capture","inputKind":"window_capture","inputSettings":{"mode":"window","window":"SILENT HILL f :UnrealWindow:SHf-Win64-Shipping.exe","capture_audio":true,"cursor":false,"method":"2"}}'
|
|
691
856
|
request_dict = json.loads(request_json)
|
|
@@ -701,7 +866,21 @@ def create_scene():
|
|
|
701
866
|
if __name__ == '__main__':
|
|
702
867
|
logging.basicConfig(level=logging.INFO)
|
|
703
868
|
connect_to_obs_sync()
|
|
704
|
-
|
|
705
|
-
|
|
869
|
+
|
|
870
|
+
save_replay_buffer()
|
|
871
|
+
# img = get_screenshot_PIL(source_name='Display Capture 2', compression=100, img_format='jpg', width=2560, height=1440)
|
|
872
|
+
# img.show()
|
|
873
|
+
# output_list = get_output_list()
|
|
874
|
+
# print(output_list)
|
|
875
|
+
|
|
876
|
+
# response = enable_replay_buffer()
|
|
877
|
+
# print(response)
|
|
878
|
+
|
|
879
|
+
# response = get_replay_buffer_max_time_seconds()
|
|
880
|
+
# # response is dataclass with attributes, print attributes
|
|
881
|
+
# print(response)
|
|
882
|
+
|
|
883
|
+
# response = enable_replay_buffer()
|
|
884
|
+
# print(response)
|
|
706
885
|
# # set_fit_to_screen_for_scene_items(get_current_scene())
|
|
707
886
|
# create_scene()
|
|
@@ -38,7 +38,7 @@ logger.setLevel(logging.DEBUG)
|
|
|
38
38
|
# Create a file handler for logging
|
|
39
39
|
log_file = os.path.join(get_app_directory(), "logs", "ocr_log.txt")
|
|
40
40
|
os.makedirs(os.path.join(get_app_directory(), "logs"), exist_ok=True)
|
|
41
|
-
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024, backupCount=
|
|
41
|
+
file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024, backupCount=2, encoding='utf-8')
|
|
42
42
|
file_handler.setLevel(logging.DEBUG)
|
|
43
43
|
# Create a formatter and set it for the handler
|
|
44
44
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
# TODO REMOVE THIS, DEPRECATED
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
import subprocess
|
|
6
6
|
|
|
7
|
+
import tkinter as tk
|
|
8
|
+
from PIL import Image, ImageTk
|
|
9
|
+
from GameSentenceMiner.util.gsm_utils import sanitize_filename
|
|
10
|
+
from GameSentenceMiner.util.configuration import get_temporary_directory, logger, ffmpeg_base_command_list
|
|
11
|
+
from GameSentenceMiner.util import ffmpeg
|
|
12
|
+
|
|
7
13
|
# Suppress stdout and stderr during imports
|
|
8
14
|
sys_stdout = sys.stdout
|
|
9
15
|
sys_stderr = sys.stderr
|
|
10
16
|
sys.stdout = open(os.devnull, 'w')
|
|
11
17
|
sys.stderr = open(os.devnull, 'w')
|
|
12
18
|
|
|
13
|
-
import tkinter as tk
|
|
14
|
-
from PIL import Image, ImageTk
|
|
15
|
-
from GameSentenceMiner.util.gsm_utils import sanitize_filename
|
|
16
|
-
from GameSentenceMiner.util.configuration import get_temporary_directory, logger
|
|
17
|
-
from GameSentenceMiner.util.ffmpeg import ffmpeg_base_command_list
|
|
18
|
-
from GameSentenceMiner.util import ffmpeg
|
|
19
|
-
|
|
20
19
|
def extract_frames(video_path, timestamp, temp_dir, mode):
|
|
21
20
|
frame_paths = []
|
|
22
21
|
timestamp_number = float(timestamp)
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import scrolledtext
|
|
3
|
+
from PIL import Image, ImageTk
|
|
4
|
+
|
|
5
|
+
import ttkbootstrap as ttk
|
|
6
|
+
from GameSentenceMiner.util.configuration import get_config, logger, gsm_state
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
class AnkiConfirmationDialog(tk.Toplevel):
|
|
13
|
+
"""
|
|
14
|
+
A modal dialog to confirm Anki card details and choose an audio option.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, parent, config_app, expression, sentence, screenshot_path, audio_path, translation, screenshot_timestamp):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self.config_app = config_app
|
|
19
|
+
self.screenshot_timestamp = screenshot_timestamp
|
|
20
|
+
|
|
21
|
+
# Initialize screenshot_path here, will be updated by button if needed
|
|
22
|
+
self.screenshot_path = screenshot_path
|
|
23
|
+
|
|
24
|
+
self.title("Confirm Anki Card Details")
|
|
25
|
+
self.result = None # This will store the user's choice
|
|
26
|
+
|
|
27
|
+
# This makes the dialog block interaction with other windows.
|
|
28
|
+
self.grab_set()
|
|
29
|
+
|
|
30
|
+
# --- Create and lay out widgets ---
|
|
31
|
+
self._create_widgets(expression, sentence, screenshot_path, audio_path, translation)
|
|
32
|
+
|
|
33
|
+
# --- Smarter Centering Logic ---
|
|
34
|
+
self.update_idletasks()
|
|
35
|
+
|
|
36
|
+
if parent.state() == 'withdrawn':
|
|
37
|
+
screen_width = self.winfo_screenwidth()
|
|
38
|
+
screen_height = self.winfo_screenheight()
|
|
39
|
+
dialog_width = self.winfo_width()
|
|
40
|
+
dialog_height = self.winfo_height()
|
|
41
|
+
x = (screen_width // 2) - (dialog_width // 2)
|
|
42
|
+
y = (screen_height // 2) - (dialog_height // 2)
|
|
43
|
+
self.geometry(f'+{x}+{y}')
|
|
44
|
+
else:
|
|
45
|
+
self.transient(parent)
|
|
46
|
+
parent_x = parent.winfo_x()
|
|
47
|
+
parent_y = parent.winfo_y()
|
|
48
|
+
parent_width = parent.winfo_width()
|
|
49
|
+
parent_height = parent.winfo_height()
|
|
50
|
+
dialog_width = self.winfo_width()
|
|
51
|
+
dialog_height = self.winfo_height()
|
|
52
|
+
x = parent_x + (parent_width // 2) - (dialog_width // 2)
|
|
53
|
+
y = parent_y + (parent_height // 2) - (dialog_height // 2)
|
|
54
|
+
self.geometry(f'+{x}+{y}')
|
|
55
|
+
|
|
56
|
+
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
|
57
|
+
self.attributes('-topmost', True)
|
|
58
|
+
self.wait_window(self)
|
|
59
|
+
|
|
60
|
+
def _create_widgets(self, expression, sentence, screenshot_path, audio_path, translation):
|
|
61
|
+
main_frame = ttk.Frame(self, padding=20)
|
|
62
|
+
main_frame.pack(expand=True, fill="both")
|
|
63
|
+
|
|
64
|
+
row = 0
|
|
65
|
+
|
|
66
|
+
# Expression
|
|
67
|
+
ttk.Label(main_frame, text=f"{get_config().anki.word_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
68
|
+
ttk.Label(main_frame, text=expression, wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
69
|
+
row += 1
|
|
70
|
+
|
|
71
|
+
# Sentence
|
|
72
|
+
ttk.Label(main_frame, text=f"{get_config().anki.sentence_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
73
|
+
sentence_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
|
|
74
|
+
sentence_text.insert(tk.END, sentence)
|
|
75
|
+
sentence_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
76
|
+
self.sentence_text = sentence_text
|
|
77
|
+
row += 1
|
|
78
|
+
|
|
79
|
+
# Translation
|
|
80
|
+
ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
81
|
+
translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
|
|
82
|
+
translation_text.insert(tk.END, translation)
|
|
83
|
+
translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
84
|
+
self.translation_text = translation_text
|
|
85
|
+
row += 1
|
|
86
|
+
|
|
87
|
+
# Screenshot
|
|
88
|
+
ttk.Label(main_frame, text=f"{get_config().anki.picture_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
89
|
+
|
|
90
|
+
# <<< CHANGED: Step 1 - Create and store the label for the image
|
|
91
|
+
self.image_label = ttk.Label(main_frame)
|
|
92
|
+
self.image_label.grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
img = Image.open(screenshot_path)
|
|
96
|
+
img.thumbnail((400, 300))
|
|
97
|
+
self.photo_image = ImageTk.PhotoImage(img)
|
|
98
|
+
# Configure the label we just created
|
|
99
|
+
self.image_label.config(image=self.photo_image)
|
|
100
|
+
# Keep a reference on the widget itself to prevent garbage collection!
|
|
101
|
+
self.image_label.image = self.photo_image
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Configure the label to show an error message
|
|
104
|
+
self.image_label.config(text=f"Could not load image:\n{screenshot_path}\n{e}", foreground="red")
|
|
105
|
+
|
|
106
|
+
# Open Screenshot Selector button
|
|
107
|
+
ttk.Button(main_frame, text="Open Screenshot Selector", command=self._get_different_screenshot).grid(row=row, column=2, sticky="w", padx=5, pady=2)
|
|
108
|
+
|
|
109
|
+
row += 1
|
|
110
|
+
|
|
111
|
+
# Audio Path
|
|
112
|
+
ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
113
|
+
ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
114
|
+
if audio_path and os.path.isfile(audio_path):
|
|
115
|
+
ttk.Button(main_frame, text="Play Audio", command=lambda: self._play_audio(audio_path)).grid(row=row, column=2, sticky="w", padx=5, pady=2)
|
|
116
|
+
|
|
117
|
+
row += 1
|
|
118
|
+
|
|
119
|
+
# Action Buttons
|
|
120
|
+
button_frame = ttk.Frame(main_frame)
|
|
121
|
+
button_frame.grid(row=row, column=0, columnspan=2, pady=15)
|
|
122
|
+
if audio_path and os.path.isfile(audio_path):
|
|
123
|
+
ttk.Button(button_frame, text="Voice", command=self._on_voice, bootstyle="success").pack(side="left", padx=10)
|
|
124
|
+
ttk.Button(button_frame, text="NO Voice", command=self._on_no_voice, bootstyle="danger").pack(side="left", padx=10)
|
|
125
|
+
else:
|
|
126
|
+
ttk.Button(button_frame, text="Confirm", command=self._on_no_voice, bootstyle="primary").pack(side="left", padx=10)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_different_screenshot(self):
|
|
130
|
+
video_path = gsm_state.current_replay
|
|
131
|
+
new_screenshot_path = self.config_app.show_screenshot_selector(
|
|
132
|
+
video_path, self.screenshot_timestamp, mode=get_config().screenshot.screenshot_timing_setting
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# If the user cancels the selector, it might return None or an empty string
|
|
136
|
+
if not new_screenshot_path:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self.screenshot_path = new_screenshot_path # Update the path to be returned later
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
img = Image.open(self.screenshot_path)
|
|
143
|
+
img.thumbnail((400, 300))
|
|
144
|
+
# Create the new image object
|
|
145
|
+
self.photo_image = ImageTk.PhotoImage(img)
|
|
146
|
+
|
|
147
|
+
# <<< CHANGED: Step 2 - Update the label with the new image
|
|
148
|
+
self.image_label.config(image=self.photo_image, text="") # Clear any previous error text
|
|
149
|
+
|
|
150
|
+
# This is crucial! Keep a reference to the new image object on the widget
|
|
151
|
+
# itself, so it doesn't get garbage-collected.
|
|
152
|
+
self.image_label.image = self.photo_image
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
# Handle cases where the newly selected file is invalid
|
|
156
|
+
self.image_label.config(image=None, text=f"Could not load new image:\n{e}", foreground="red")
|
|
157
|
+
self.image_label.image = None # Clear old image reference
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _play_audio(self, audio_path):
|
|
161
|
+
if not os.path.isfile(audio_path):
|
|
162
|
+
print(f"Audio file does not exist: {audio_path}")
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
if platform.system() == "Windows":
|
|
166
|
+
os.startfile(audio_path)
|
|
167
|
+
elif platform.system() == "Darwin":
|
|
168
|
+
subprocess.run(["open", audio_path])
|
|
169
|
+
else:
|
|
170
|
+
subprocess.run(["xdg-open", audio_path])
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"Failed to play audio: {e}")
|
|
173
|
+
|
|
174
|
+
def _on_voice(self):
|
|
175
|
+
# The screenshot_path is now correctly updated if the user chose a new one
|
|
176
|
+
self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
|
|
177
|
+
self.destroy()
|
|
178
|
+
|
|
179
|
+
def _on_no_voice(self):
|
|
180
|
+
self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
|
|
181
|
+
self.destroy()
|
|
182
|
+
|
|
183
|
+
def _on_cancel(self):
|
|
184
|
+
# We block the cancel button, but if you wanted to enable it:
|
|
185
|
+
# self.result = None
|
|
186
|
+
# self.destroy()
|
|
187
|
+
pass
|