GameSentenceMiner 2.15.3__py3-none-any.whl → 2.15.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/config_gui.py +53 -26
- GameSentenceMiner/gsm.py +2 -2
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +234 -226
- GameSentenceMiner/ocr/owocr_helper.py +80 -15
- GameSentenceMiner/owocr/owocr/ocr.py +15 -3
- GameSentenceMiner/owocr/owocr/run.py +82 -60
- GameSentenceMiner/util/configuration.py +1 -0
- GameSentenceMiner/util/get_overlay_coords.py +0 -1
- GameSentenceMiner/vad.py +7 -3
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/METADATA +3 -2
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/RECORD +18 -18
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.15.3.dist-info → gamesentenceminer-2.15.5.dist-info}/top_level.txt +0 -0
GameSentenceMiner/obs.py
CHANGED
@@ -5,6 +5,7 @@ import subprocess
|
|
5
5
|
import threading
|
6
6
|
import time
|
7
7
|
import logging
|
8
|
+
import contextlib
|
8
9
|
|
9
10
|
import psutil
|
10
11
|
|
@@ -16,7 +17,7 @@ from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file
|
|
16
17
|
import tkinter as tk
|
17
18
|
from tkinter import messagebox
|
18
19
|
|
19
|
-
|
20
|
+
connection_pool: 'OBSConnectionPool' = None
|
20
21
|
event_client: obs.EventClient = None
|
21
22
|
obs_process_pid = None
|
22
23
|
OBS_PID_FILE = os.path.join(configuration.get_app_directory(), 'obs-studio', 'obs_pid.txt')
|
@@ -24,8 +25,83 @@ obs_connection_manager = None
|
|
24
25
|
logging.getLogger("obsws_python").setLevel(logging.CRITICAL)
|
25
26
|
connecting = False
|
26
27
|
|
28
|
+
class OBSConnectionPool:
|
29
|
+
"""Manages a pool of thread-safe connections to the OBS WebSocket."""
|
30
|
+
def __init__(self, size=3, **kwargs):
|
31
|
+
self.size = size
|
32
|
+
self.connection_kwargs = kwargs
|
33
|
+
self._clients = [None] * self.size
|
34
|
+
self._locks = [threading.Lock() for _ in range(self.size)]
|
35
|
+
self._next_idx = 0
|
36
|
+
self._idx_lock = threading.Lock()
|
37
|
+
logger.info(f"Initialized OBSConnectionPool with size {self.size}")
|
38
|
+
|
39
|
+
def connect_all(self):
|
40
|
+
"""Initializes all client objects in the pool."""
|
41
|
+
for i in range(self.size):
|
42
|
+
try:
|
43
|
+
self._clients[i] = obs.ReqClient(**self.connection_kwargs)
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"Failed to create client {i} in pool: {e}")
|
46
|
+
return True
|
47
|
+
|
48
|
+
def disconnect_all(self):
|
49
|
+
"""Disconnects all clients in the pool."""
|
50
|
+
for client in self._clients:
|
51
|
+
if client:
|
52
|
+
try:
|
53
|
+
client.disconnect()
|
54
|
+
except Exception:
|
55
|
+
pass
|
56
|
+
self._clients = [None] * self.size
|
57
|
+
logger.info("Disconnected all clients in OBSConnectionPool.")
|
58
|
+
|
59
|
+
def _check_and_reconnect(self, index):
|
60
|
+
"""Checks a specific client and reconnects if necessary."""
|
61
|
+
client = self._clients[index]
|
62
|
+
if not client:
|
63
|
+
self._clients[index] = obs.ReqClient(**self.connection_kwargs)
|
64
|
+
logger.info(f"Re-initialized client {index} in pool.")
|
65
|
+
return
|
66
|
+
try:
|
67
|
+
client.get_version()
|
68
|
+
except Exception:
|
69
|
+
logger.info(f"Reconnecting client {index} in pool.")
|
70
|
+
try:
|
71
|
+
client.disconnect()
|
72
|
+
except Exception:
|
73
|
+
pass
|
74
|
+
self._clients[index] = obs.ReqClient(**self.connection_kwargs)
|
75
|
+
|
76
|
+
@contextlib.contextmanager
|
77
|
+
def get_client(self):
|
78
|
+
"""A context manager to safely get a client from the pool."""
|
79
|
+
with self._idx_lock:
|
80
|
+
idx = self._next_idx
|
81
|
+
self._next_idx = (self._next_idx + 1) % self.size
|
82
|
+
|
83
|
+
lock = self._locks[idx]
|
84
|
+
lock.acquire()
|
85
|
+
try:
|
86
|
+
self._check_and_reconnect(idx)
|
87
|
+
yield self._clients[idx]
|
88
|
+
finally:
|
89
|
+
lock.release()
|
90
|
+
|
91
|
+
def get_healthcheck_client(self):
|
92
|
+
"""Returns a dedicated client for health checks, separate from the main pool."""
|
93
|
+
if not hasattr(self, '_healthcheck_client') or self._healthcheck_client is None:
|
94
|
+
try:
|
95
|
+
self._healthcheck_client = obs.ReqClient(**self.connection_kwargs)
|
96
|
+
logger.info("Initialized dedicated healthcheck client.")
|
97
|
+
except Exception as e:
|
98
|
+
logger.error(f"Failed to create healthcheck client: {e}")
|
99
|
+
self._healthcheck_client = None
|
100
|
+
return self._healthcheck_client
|
101
|
+
|
102
|
+
|
27
103
|
class OBSConnectionManager(threading.Thread):
|
28
|
-
def __init__(self, check_output=
|
104
|
+
def __init__(self, check_output=False):
|
29
105
|
super().__init__()
|
30
106
|
self.daemon = True
|
31
107
|
self.running = True
|
@@ -38,8 +114,11 @@ class OBSConnectionManager(threading.Thread):
|
|
38
114
|
while self.running:
|
39
115
|
time.sleep(self.check_connection_interval)
|
40
116
|
try:
|
41
|
-
if
|
117
|
+
client = connection_pool.get_healthcheck_client() if connection_pool else None
|
118
|
+
if client and not connecting:
|
42
119
|
client.get_version()
|
120
|
+
else:
|
121
|
+
raise ConnectionError("Healthcheck client not healthy or not initialized")
|
43
122
|
except Exception as e:
|
44
123
|
logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
|
45
124
|
gsm_status.obs_connected = False
|
@@ -128,12 +207,12 @@ def start_obs():
|
|
128
207
|
return None
|
129
208
|
|
130
209
|
async def wait_for_obs_connected():
|
131
|
-
|
132
|
-
if not client:
|
210
|
+
if not connection_pool:
|
133
211
|
return False
|
134
212
|
for _ in range(10):
|
135
213
|
try:
|
136
|
-
|
214
|
+
with connection_pool.get_client() as client:
|
215
|
+
response = client.get_version()
|
137
216
|
if response:
|
138
217
|
return True
|
139
218
|
except Exception as e:
|
@@ -182,20 +261,26 @@ def get_obs_websocket_config_values():
|
|
182
261
|
full_config.save()
|
183
262
|
reload_config()
|
184
263
|
|
185
|
-
async def connect_to_obs(retry=5, check_output=
|
186
|
-
global
|
264
|
+
async def connect_to_obs(retry=5, connections=2, check_output=False):
|
265
|
+
global connection_pool, obs_connection_manager, event_client, connecting
|
187
266
|
if is_windows():
|
188
267
|
get_obs_websocket_config_values()
|
189
268
|
|
190
269
|
while True:
|
191
270
|
connecting = True
|
192
271
|
try:
|
193
|
-
|
194
|
-
host
|
195
|
-
port
|
196
|
-
password
|
197
|
-
timeout
|
198
|
-
|
272
|
+
pool_kwargs = {
|
273
|
+
'host': get_config().obs.host,
|
274
|
+
'port': get_config().obs.port,
|
275
|
+
'password': get_config().obs.password,
|
276
|
+
'timeout': 3,
|
277
|
+
}
|
278
|
+
connection_pool = OBSConnectionPool(size=connections, **pool_kwargs)
|
279
|
+
connection_pool.connect_all()
|
280
|
+
|
281
|
+
with connection_pool.get_client() as client:
|
282
|
+
client.get_version() # Test one connection to confirm it works
|
283
|
+
|
199
284
|
event_client = obs.EventClient(
|
200
285
|
host=get_config().obs.host,
|
201
286
|
port=get_config().obs.port,
|
@@ -213,7 +298,7 @@ async def connect_to_obs(retry=5, check_output=True):
|
|
213
298
|
if retry <= 0:
|
214
299
|
gsm_status.obs_connected = False
|
215
300
|
logger.error(f"Failed to connect to OBS WebSocket: {e}")
|
216
|
-
|
301
|
+
connection_pool = None
|
217
302
|
event_client = None
|
218
303
|
connecting = False
|
219
304
|
break
|
@@ -221,70 +306,49 @@ async def connect_to_obs(retry=5, check_output=True):
|
|
221
306
|
retry -= 1
|
222
307
|
connecting = False
|
223
308
|
|
224
|
-
def connect_to_obs_sync(retry=2, check_output=
|
225
|
-
|
226
|
-
if is_windows():
|
227
|
-
get_obs_websocket_config_values()
|
228
|
-
|
229
|
-
while True:
|
230
|
-
try:
|
231
|
-
client = obs.ReqClient(
|
232
|
-
host=get_config().obs.host,
|
233
|
-
port=get_config().obs.port,
|
234
|
-
password=get_config().obs.password,
|
235
|
-
timeout=1,
|
236
|
-
)
|
237
|
-
event_client = obs.EventClient(
|
238
|
-
host=get_config().obs.host,
|
239
|
-
port=get_config().obs.port,
|
240
|
-
password=get_config().obs.password,
|
241
|
-
timeout=1,
|
242
|
-
)
|
243
|
-
if not obs_connection_manager:
|
244
|
-
obs_connection_manager = OBSConnectionManager(check_output=check_output)
|
245
|
-
obs_connection_manager.start()
|
246
|
-
update_current_game()
|
247
|
-
logger.info("Connected to OBS WebSocket.")
|
248
|
-
break # Exit the loop once connected
|
249
|
-
except Exception as e:
|
250
|
-
if retry <= 0:
|
251
|
-
gsm_status.obs_connected = False
|
252
|
-
logger.error(f"Failed to connect to OBS WebSocket: {e}")
|
253
|
-
client = None
|
254
|
-
event_client = None
|
255
|
-
break
|
256
|
-
time.sleep(1)
|
257
|
-
retry -= 1
|
309
|
+
def connect_to_obs_sync(retry=2, connections=2, check_output=False):
|
310
|
+
asyncio.run(connect_to_obs(retry=retry, connections=connections, check_output=check_output))
|
258
311
|
|
259
312
|
|
260
313
|
def disconnect_from_obs():
|
261
|
-
global
|
262
|
-
if
|
263
|
-
|
264
|
-
|
314
|
+
global connection_pool
|
315
|
+
if connection_pool:
|
316
|
+
connection_pool.disconnect_all()
|
317
|
+
connection_pool = None
|
265
318
|
logger.info("Disconnected from OBS WebSocket.")
|
266
319
|
|
267
|
-
def do_obs_call(
|
268
|
-
|
269
|
-
|
320
|
+
def do_obs_call(method_name: str, from_dict=None, retry=3, **kwargs):
|
321
|
+
if not connection_pool:
|
322
|
+
connect_to_obs_sync(retry=1)
|
323
|
+
if not connection_pool:
|
270
324
|
return None
|
325
|
+
|
326
|
+
last_exception = None
|
271
327
|
for _ in range(retry + 1):
|
272
328
|
try:
|
273
|
-
|
274
|
-
|
275
|
-
|
329
|
+
with connection_pool.get_client() as client:
|
330
|
+
method_to_call = getattr(client, method_name)
|
331
|
+
response = method_to_call(**kwargs)
|
332
|
+
if response and response.ok:
|
333
|
+
return from_dict(response.datain) if from_dict else response.datain
|
276
334
|
time.sleep(0.3)
|
335
|
+
except AttributeError:
|
336
|
+
logger.error(f"OBS client has no method '{method_name}'")
|
337
|
+
return None
|
277
338
|
except Exception as e:
|
278
|
-
|
339
|
+
last_exception = e
|
340
|
+
logger.error(f"Error calling OBS ('{method_name}'): {e}")
|
279
341
|
if "socket is already closed" in str(e) or "object has no attribute" in str(e):
|
280
342
|
time.sleep(0.3)
|
281
343
|
else:
|
282
344
|
return None
|
345
|
+
logger.error(f"OBS call '{method_name}' failed after retries. Last error: {last_exception}")
|
283
346
|
return None
|
284
347
|
|
285
348
|
def toggle_replay_buffer():
|
286
349
|
try:
|
287
|
-
|
350
|
+
with connection_pool.get_client() as client:
|
351
|
+
response = client.toggle_replay_buffer()
|
288
352
|
if response:
|
289
353
|
logger.info("Replay buffer Toggled.")
|
290
354
|
except Exception as e:
|
@@ -292,7 +356,8 @@ def toggle_replay_buffer():
|
|
292
356
|
|
293
357
|
def start_replay_buffer():
|
294
358
|
try:
|
295
|
-
|
359
|
+
with connection_pool.get_client() as client:
|
360
|
+
response = client.start_replay_buffer()
|
296
361
|
if response and response.ok:
|
297
362
|
logger.info("Replay buffer started.")
|
298
363
|
except Exception as e:
|
@@ -300,31 +365,34 @@ def start_replay_buffer():
|
|
300
365
|
|
301
366
|
def get_replay_buffer_status():
|
302
367
|
try:
|
303
|
-
|
368
|
+
with connection_pool.get_client() as client:
|
369
|
+
return client.get_replay_buffer_status().output_active
|
304
370
|
except Exception as e:
|
305
371
|
logger.debug(f"Error getting replay buffer status: {e}")
|
306
372
|
return None
|
307
373
|
|
308
374
|
def stop_replay_buffer():
|
309
375
|
try:
|
310
|
-
|
376
|
+
with connection_pool.get_client() as client:
|
377
|
+
response = client.stop_replay_buffer()
|
311
378
|
if response and response.ok:
|
312
379
|
logger.info("Replay buffer stopped.")
|
313
380
|
except Exception as e:
|
314
381
|
logger.warning(f"Error stopping replay buffer: {e}")
|
315
382
|
|
316
383
|
def save_replay_buffer():
|
317
|
-
|
318
|
-
|
319
|
-
|
384
|
+
try:
|
385
|
+
with connection_pool.get_client() as client:
|
386
|
+
response = client.save_replay_buffer()
|
320
387
|
if response and response.ok:
|
321
388
|
logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
|
322
|
-
|
323
|
-
raise Exception("
|
389
|
+
except Exception as e:
|
390
|
+
raise Exception(f"Error saving replay buffer: {e}")
|
324
391
|
|
325
392
|
def get_current_scene():
|
326
393
|
try:
|
327
|
-
|
394
|
+
with connection_pool.get_client() as client:
|
395
|
+
response = client.get_current_program_scene()
|
328
396
|
return response.scene_name if response else ''
|
329
397
|
except Exception as e:
|
330
398
|
logger.debug(f"Couldn't get scene: {e}")
|
@@ -332,7 +400,8 @@ def get_current_scene():
|
|
332
400
|
|
333
401
|
def get_source_from_scene(scene_name):
|
334
402
|
try:
|
335
|
-
|
403
|
+
with connection_pool.get_client() as client:
|
404
|
+
response = client.get_scene_item_list(name=scene_name)
|
336
405
|
return response.scene_items[0] if response and response.scene_items else ''
|
337
406
|
except Exception as e:
|
338
407
|
logger.error(f"Error getting source from scene: {e}")
|
@@ -346,7 +415,8 @@ def get_active_source():
|
|
346
415
|
|
347
416
|
def get_record_directory():
|
348
417
|
try:
|
349
|
-
|
418
|
+
with connection_pool.get_client() as client:
|
419
|
+
response = client.get_record_directory()
|
350
420
|
return response.record_directory if response else ''
|
351
421
|
except Exception as e:
|
352
422
|
logger.error(f"Error getting recording folder: {e}")
|
@@ -354,17 +424,17 @@ def get_record_directory():
|
|
354
424
|
|
355
425
|
def get_obs_scenes():
|
356
426
|
try:
|
357
|
-
|
427
|
+
with connection_pool.get_client() as client:
|
428
|
+
response = client.get_scene_list()
|
358
429
|
return response.scenes if response else None
|
359
430
|
except Exception as e:
|
360
431
|
logger.error(f"Error getting scenes: {e}")
|
361
432
|
return None
|
362
433
|
|
363
434
|
async def register_scene_change_callback(callback):
|
364
|
-
global client
|
365
435
|
if await wait_for_obs_connected():
|
366
|
-
if not
|
367
|
-
logger.error("OBS
|
436
|
+
if not connection_pool:
|
437
|
+
logger.error("OBS connection pool is not connected.")
|
368
438
|
return
|
369
439
|
|
370
440
|
def on_current_program_scene_changed(data):
|
@@ -391,7 +461,8 @@ def get_screenshot(compression=-1):
|
|
391
461
|
return None
|
392
462
|
start = time.time()
|
393
463
|
logger.debug(f"Current source name: {current_source_name}")
|
394
|
-
|
464
|
+
with connection_pool.get_client() as client:
|
465
|
+
client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
|
395
466
|
logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
|
396
467
|
return screenshot
|
397
468
|
except Exception as e:
|
@@ -410,11 +481,10 @@ def get_screenshot_base64(compression=75, width=None, height=None):
|
|
410
481
|
if not current_source_name:
|
411
482
|
logger.error("No active source found in the current scene.")
|
412
483
|
return None
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
# print(responseraw)
|
484
|
+
|
485
|
+
with connection_pool.get_client() as client:
|
486
|
+
response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
|
487
|
+
|
418
488
|
if response and response.image_data:
|
419
489
|
return response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
|
420
490
|
else:
|
@@ -435,7 +505,8 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
|
|
435
505
|
logger.error("No active source found in the current scene.")
|
436
506
|
return None
|
437
507
|
while True:
|
438
|
-
|
508
|
+
with connection_pool.get_client() as client:
|
509
|
+
response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
|
439
510
|
try:
|
440
511
|
response.image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
|
441
512
|
except AttributeError:
|
@@ -448,8 +519,6 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
|
|
448
519
|
image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
|
449
520
|
image_data = base64.b64decode(image_data)
|
450
521
|
img = Image.open(io.BytesIO(image_data)).convert("RGBA")
|
451
|
-
# if width and height:
|
452
|
-
# img = img.resize((width, height), Image.Resampling.LANCZOS)
|
453
522
|
return img
|
454
523
|
return None
|
455
524
|
|
@@ -472,103 +541,84 @@ def get_current_game(sanitize=False, update=True):
|
|
472
541
|
def set_fit_to_screen_for_scene_items(scene_name: str):
|
473
542
|
"""
|
474
543
|
Sets all sources in a given scene to "Fit to Screen" (like Ctrl+F in OBS).
|
475
|
-
|
476
|
-
This function fetches the canvas dimensions, then iterates through all scene
|
477
|
-
items in the specified scene and applies a transform that scales them to
|
478
|
-
fit within the canvas while maintaining aspect ratio and centering them.
|
479
|
-
|
480
|
-
Args:
|
481
|
-
scene_name: The name of the scene to modify.
|
482
544
|
"""
|
545
|
+
if not scene_name:
|
546
|
+
return
|
547
|
+
|
483
548
|
try:
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
549
|
+
with connection_pool.get_client() as client:
|
550
|
+
# 1. Get the canvas (base) resolution from OBS video settings
|
551
|
+
video_settings = client.get_video_settings()
|
552
|
+
if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
|
553
|
+
logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
|
554
|
+
return
|
555
|
+
canvas_width = video_settings.base_width
|
556
|
+
canvas_height = video_settings.base_height
|
491
557
|
|
492
|
-
|
493
|
-
|
494
|
-
|
558
|
+
# 2. Get the list of items in the specified scene
|
559
|
+
scene_items_response = client.get_scene_item_list(scene_name)
|
560
|
+
items = scene_items_response.scene_items if scene_items_response.scene_items else []
|
495
561
|
|
496
|
-
|
497
|
-
|
498
|
-
|
562
|
+
if not items:
|
563
|
+
logger.warning(f"No items found in scene '{scene_name}'.")
|
564
|
+
return
|
499
565
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
aspect_ratio_different
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
'boundsType': 'OBS_BOUNDS_SCALE_INNER',
|
546
|
-
'alignment': 5, # 5 = Center alignment (horizontal and vertical)
|
547
|
-
'boundsWidth': canvas_width,
|
548
|
-
'boundsHeight': canvas_height,
|
549
|
-
'positionX': 0,
|
550
|
-
'positionY': 0,
|
551
|
-
}
|
552
|
-
|
553
|
-
if not already_cropped:
|
554
|
-
fit_to_screen_transform.update({
|
555
|
-
'cropLeft': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
|
556
|
-
'cropRight': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
|
557
|
-
'cropTop': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
|
558
|
-
'cropBottom': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
|
559
|
-
})
|
566
|
+
# 3. Loop through each item and apply the "Fit to Screen" transform
|
567
|
+
for item in items:
|
568
|
+
item_id = item['sceneItemId']
|
569
|
+
source_name = item['sourceName']
|
570
|
+
|
571
|
+
scene_item_transform = item.get('sceneItemTransform', {})
|
572
|
+
|
573
|
+
source_width = scene_item_transform.get('sourceWidth', None)
|
574
|
+
source_height = scene_item_transform.get('sourceHeight', None)
|
575
|
+
|
576
|
+
aspect_ratio_different = False
|
577
|
+
already_cropped = any([
|
578
|
+
scene_item_transform.get('cropLeft', 0) != 0,
|
579
|
+
scene_item_transform.get('cropRight', 0) != 0,
|
580
|
+
scene_item_transform.get('cropTop', 0) != 0,
|
581
|
+
scene_item_transform.get('cropBottom', 0) != 0,
|
582
|
+
])
|
583
|
+
|
584
|
+
if source_width and source_height and not already_cropped:
|
585
|
+
source_aspect_ratio = source_width / source_height
|
586
|
+
canvas_aspect_ratio = canvas_width / canvas_height
|
587
|
+
aspect_ratio_different = abs(source_aspect_ratio - canvas_aspect_ratio) > 0.01
|
588
|
+
|
589
|
+
standard_ratios = [4 / 3, 16 / 9, 16 / 10, 21 / 9, 32 / 9, 5 / 4, 3 / 2]
|
590
|
+
|
591
|
+
def is_standard_ratio(ratio):
|
592
|
+
return any(abs(ratio - std) < 0.02 for std in standard_ratios)
|
593
|
+
|
594
|
+
if aspect_ratio_different:
|
595
|
+
if not (is_standard_ratio(source_aspect_ratio) and is_standard_ratio(canvas_aspect_ratio)):
|
596
|
+
aspect_ratio_different = False
|
597
|
+
|
598
|
+
fit_to_screen_transform = {
|
599
|
+
'boundsType': 'OBS_BOUNDS_SCALE_INNER', 'alignment': 5,
|
600
|
+
'boundsWidth': canvas_width, 'boundsHeight': canvas_height,
|
601
|
+
'positionX': 0, 'positionY': 0,
|
602
|
+
}
|
603
|
+
|
604
|
+
if not already_cropped:
|
605
|
+
fit_to_screen_transform.update({
|
606
|
+
'cropLeft': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
|
607
|
+
'cropRight': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
|
608
|
+
'cropTop': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
|
609
|
+
'cropBottom': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
|
610
|
+
})
|
560
611
|
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
612
|
+
try:
|
613
|
+
client.set_scene_item_transform(
|
614
|
+
scene_name=scene_name,
|
615
|
+
item_id=item_id,
|
616
|
+
transform=fit_to_screen_transform
|
617
|
+
)
|
618
|
+
except obs.error.OBSSDKError as e:
|
619
|
+
logger.error(f"Failed to set transform for source '{source_name}': {e}")
|
569
620
|
|
570
621
|
except obs.error.OBSSDKError as e:
|
571
|
-
# This will catch errors like "scene not found"
|
572
622
|
logger.error(f"An OBS error occurred: {e}")
|
573
623
|
except Exception as e:
|
574
624
|
logger.error(f"An unexpected error occurred: {e}")
|
@@ -576,12 +626,14 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
|
|
576
626
|
|
577
627
|
def main():
|
578
628
|
start_obs()
|
579
|
-
connect_to_obs()
|
629
|
+
# connect_to_obs() is async, main is not. Use the sync version.
|
630
|
+
connect_to_obs_sync()
|
580
631
|
# Test each method
|
581
632
|
print("Testing `get_obs_path`:", get_obs_path())
|
582
633
|
print("Testing `is_process_running` with PID 1:", is_process_running(1))
|
583
634
|
print("Testing `check_obs_folder_is_correct`:")
|
584
|
-
|
635
|
+
# This is async, need to run it in an event loop if testing from main
|
636
|
+
asyncio.run(check_obs_folder_is_correct())
|
585
637
|
print("Testing `get_obs_websocket_config_values`:")
|
586
638
|
try:
|
587
639
|
get_obs_websocket_config_values()
|
@@ -595,10 +647,13 @@ def main():
|
|
595
647
|
print("Testing `stop_replay_buffer`:")
|
596
648
|
stop_replay_buffer()
|
597
649
|
print("Testing `save_replay_buffer`:")
|
598
|
-
|
650
|
+
try:
|
651
|
+
save_replay_buffer()
|
652
|
+
except Exception as e:
|
653
|
+
print(f"Could not save replay buffer: {e}")
|
599
654
|
current_scene = get_current_scene()
|
600
655
|
print("Testing `get_current_scene`:", current_scene)
|
601
|
-
print("Testing `get_source_from_scene` with
|
656
|
+
print("Testing `get_source_from_scene` with current scene:", get_source_from_scene(current_scene))
|
602
657
|
print("Testing `get_record_directory`:", get_record_directory())
|
603
658
|
print("Testing `get_obs_scenes`:", get_obs_scenes())
|
604
659
|
print("Testing `get_screenshot`:", get_screenshot())
|
@@ -612,51 +667,4 @@ def main():
|
|
612
667
|
if __name__ == '__main__':
|
613
668
|
logging.basicConfig(level=logging.INFO)
|
614
669
|
connect_to_obs_sync()
|
615
|
-
set_fit_to_screen_for_scene_items(get_current_scene())
|
616
|
-
# main()
|
617
|
-
# connect_to_obs_sync()
|
618
|
-
# img = get_screenshot_PIL(source_name="Display Capture 2", compression=75, img_format='png', width=1280, height=720)
|
619
|
-
# img.show()
|
620
|
-
# while True:
|
621
|
-
# print(get_active_source())
|
622
|
-
# time.sleep(3)
|
623
|
-
# i = 100
|
624
|
-
# for i in range(1, 100):
|
625
|
-
# print(f"Getting screenshot {i}")
|
626
|
-
# start = time.time()
|
627
|
-
# # get_screenshot(compression=95)
|
628
|
-
# # get_screenshot_base64(compression=95, width=1280, height=720)
|
629
|
-
|
630
|
-
# img = get_screenshot_PIL(compression=i, img_format='jpg', width=1280, height=720)
|
631
|
-
# end = time.time()
|
632
|
-
# print(f"Time taken to get screenshot with compression {i}: {end - start} seconds")
|
633
|
-
|
634
|
-
# for i in range(1, 100):
|
635
|
-
# print(f"Getting screenshot {i}")
|
636
|
-
# start = time.time()
|
637
|
-
# # get_screenshot(compression=95)
|
638
|
-
# # get_screenshot_base64(compression=95, width=1280, height=720)
|
639
|
-
|
640
|
-
# img = get_screenshot_PIL(compression=i, img_format='jpg', width=2560, height=1440)
|
641
|
-
# end = time.time()
|
642
|
-
# print(f"Time taken to get screenshot full sized jpg with compression {i}: {end - start} seconds")
|
643
|
-
|
644
|
-
# png_img = get_screenshot_PIL(compression=75, img_format='png', width=1280, height=720)
|
645
|
-
|
646
|
-
# jpg_img = get_screenshot_PIL(compression=100, img_format='jpg', width=2560, height=1440)
|
647
|
-
|
648
|
-
# png_img.show()
|
649
|
-
# jpg_img.show()
|
650
|
-
|
651
|
-
# start = time.time()
|
652
|
-
# with mss() as sct:
|
653
|
-
# monitor = sct.monitors[1]
|
654
|
-
# sct_img = sct.grab(monitor)
|
655
|
-
# img = Image.frombytes('RGB', sct_img.size, sct_img.bgra, 'raw', 'BGRX')
|
656
|
-
# img.show()
|
657
|
-
# end = time.time()
|
658
|
-
# print(f"Time taken to get screenshot with mss: {end - start} seconds")
|
659
|
-
|
660
|
-
|
661
|
-
# print(get_screenshot_base64(compression=75, width=1280, height=720))
|
662
|
-
|
670
|
+
set_fit_to_screen_for_scene_items(get_current_scene())
|