GameSentenceMiner 2.15.3__py3-none-any.whl → 2.15.4__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.
@@ -379,6 +379,7 @@ class ConfigApp:
379
379
  self.ai_enabled_value = tk.BooleanVar(value=self.settings.ai.enabled)
380
380
  self.ai_provider_value = tk.StringVar(value=self.settings.ai.provider)
381
381
  self.gemini_model_value = tk.StringVar(value=self.settings.ai.gemini_model)
382
+ self.use_cpu_for_inference_value = tk.BooleanVar(value=self.settings.vad.use_cpu_for_inference)
382
383
  self.groq_model_value = tk.StringVar(value=self.settings.ai.groq_model)
383
384
  self.gemini_api_key_value = tk.StringVar(value=self.settings.ai.gemini_api_key)
384
385
  self.groq_api_key_value = tk.StringVar(value=self.settings.ai.groq_api_key)
@@ -592,6 +593,7 @@ class ConfigApp:
592
593
  language=self.language_value.get(),
593
594
  cut_and_splice_segments=self.cut_and_splice_segments_value.get(),
594
595
  splice_padding=float(self.splice_padding_value.get()) if self.splice_padding_value.get() else 0.0,
596
+ use_cpu_for_inference=self.use_cpu_for_inference_value.get(),
595
597
  ),
596
598
  advanced=Advanced(
597
599
  audio_player_path=self.audio_player_path_value.get(),
@@ -1137,12 +1139,11 @@ class ConfigApp:
1137
1139
  ttk.Entry(vad_frame, textvariable=self.splice_padding_value).grid(row=self.current_row, column=3, sticky='EW', pady=2)
1138
1140
  self.current_row += 1
1139
1141
 
1140
- self.add_reset_button(vad_frame, "vad", self.current_row, 0, self.create_vad_tab)
1141
-
1142
- for col in range(5): vad_frame.grid_columnconfigure(col, weight=0)
1143
- for row in range(self.current_row): vad_frame.grid_rowconfigure(row, minsize=30)
1144
-
1145
- return vad_frame
1142
+ # Force CPU for Whisper
1143
+ use_cpu_i18n = vad_i18n.get('use_cpu_for_inference', {})
1144
+ HoverInfoLabelWidget(vad_frame, text=use_cpu_i18n.get('label', 'Force CPU'), tooltip=use_cpu_i18n.get('tooltip', 'Even if CUDA is installed, use CPU for Whisper'), row=self.current_row, column=0)
1145
+ ttk.Checkbutton(vad_frame, variable=self.use_cpu_for_inference_value, bootstyle="round-toggle").grid(row=self.current_row, column=1, sticky='W', pady=2)
1146
+ self.current_row += 1
1146
1147
 
1147
1148
  @new_tab
1148
1149
  def create_paths_tab(self):
@@ -1224,8 +1225,10 @@ class ConfigApp:
1224
1225
 
1225
1226
  self.add_reset_button(paths_frame, "paths", self.current_row, 0, self.create_paths_tab)
1226
1227
 
1227
- for col in range(3): paths_frame.grid_columnconfigure(col, weight=0)
1228
- for row in range(self.current_row): paths_frame.grid_rowconfigure(row, minsize=30)
1228
+ for col in range(3):
1229
+ paths_frame.grid_columnconfigure(col, weight=0)
1230
+ for row in range(self.current_row):
1231
+ paths_frame.grid_rowconfigure(row, minsize=30)
1229
1232
 
1230
1233
  return paths_frame
1231
1234
 
@@ -1368,8 +1371,10 @@ class ConfigApp:
1368
1371
 
1369
1372
  self.add_reset_button(anki_frame, "anki", self.current_row, 0, self.create_anki_tab)
1370
1373
 
1371
- for col in range(2): anki_frame.grid_columnconfigure(col, weight=0)
1372
- for row in range(self.current_row): anki_frame.grid_rowconfigure(row, minsize=30)
1374
+ for col in range(2):
1375
+ anki_frame.grid_columnconfigure(col, weight=0)
1376
+ for row in range(self.current_row):
1377
+ anki_frame.grid_rowconfigure(row, minsize=30)
1373
1378
 
1374
1379
  return anki_frame
1375
1380
 
@@ -1431,8 +1436,10 @@ class ConfigApp:
1431
1436
 
1432
1437
  self.add_reset_button(features_frame, "features", self.current_row, 0, self.create_features_tab)
1433
1438
 
1434
- for col in range(3): features_frame.grid_columnconfigure(col, weight=0)
1435
- for row in range(self.current_row): features_frame.grid_rowconfigure(row, minsize=30)
1439
+ for col in range(3):
1440
+ features_frame.grid_columnconfigure(col, weight=0)
1441
+ for row in range(self.current_row):
1442
+ features_frame.grid_rowconfigure(row, minsize=30)
1436
1443
 
1437
1444
  return features_frame
1438
1445
 
@@ -1533,8 +1540,10 @@ class ConfigApp:
1533
1540
 
1534
1541
  self.add_reset_button(screenshot_frame, "screenshot", self.current_row, 0, self.create_screenshot_tab)
1535
1542
 
1536
- for col in range(3): screenshot_frame.grid_columnconfigure(col, weight=0)
1537
- for row in range(self.current_row): screenshot_frame.grid_rowconfigure(row, minsize=30)
1543
+ for col in range(3):
1544
+ screenshot_frame.grid_columnconfigure(col, weight=0)
1545
+ for row in range(self.current_row):
1546
+ screenshot_frame.grid_rowconfigure(row, minsize=30)
1538
1547
 
1539
1548
  return screenshot_frame
1540
1549
 
@@ -1647,8 +1656,10 @@ class ConfigApp:
1647
1656
 
1648
1657
  self.add_reset_button(audio_frame, "audio", self.current_row, 0, self.create_audio_tab)
1649
1658
 
1650
- for col in range(5): audio_frame.grid_columnconfigure(col, weight=0)
1651
- for row in range(self.current_row): audio_frame.grid_rowconfigure(row, minsize=30)
1659
+ for col in range(5):
1660
+ audio_frame.grid_columnconfigure(col, weight=0)
1661
+ for row in range(self.current_row):
1662
+ audio_frame.grid_rowconfigure(row, minsize=30)
1652
1663
 
1653
1664
  return audio_frame
1654
1665
 
@@ -1752,8 +1763,10 @@ class ConfigApp:
1752
1763
 
1753
1764
  self.add_reset_button(obs_frame, "obs", self.current_row, 0, self.create_obs_tab)
1754
1765
 
1755
- for col in range(3): obs_frame.grid_columnconfigure(col, weight=0)
1756
- for row in range(self.current_row): obs_frame.grid_rowconfigure(row, minsize=30)
1766
+ for col in range(3):
1767
+ obs_frame.grid_columnconfigure(col, weight=0)
1768
+ for row in range(self.current_row):
1769
+ obs_frame.grid_rowconfigure(row, minsize=30)
1757
1770
 
1758
1771
  return obs_frame
1759
1772
 
@@ -1814,8 +1827,10 @@ class ConfigApp:
1814
1827
  row=self.current_row, column=1, sticky='W', pady=2)
1815
1828
  self.current_row += 1
1816
1829
 
1817
- for col in range(4): profiles_frame.grid_columnconfigure(col, weight=0)
1818
- for row in range(self.current_row): profiles_frame.grid_rowconfigure(row, minsize=30)
1830
+ for col in range(4):
1831
+ profiles_frame.grid_columnconfigure(col, weight=0)
1832
+ for row in range(self.current_row):
1833
+ profiles_frame.grid_rowconfigure(row, minsize=30)
1819
1834
 
1820
1835
  return profiles_frame
1821
1836
 
@@ -1945,8 +1960,10 @@ class ConfigApp:
1945
1960
 
1946
1961
  self.add_reset_button(advanced_frame, "advanced", self.current_row, 0, self.create_advanced_tab)
1947
1962
 
1948
- for col in range(4): advanced_frame.grid_columnconfigure(col, weight=0)
1949
- for row in range(self.current_row): advanced_frame.grid_rowconfigure(row, minsize=30)
1963
+ for col in range(4):
1964
+ advanced_frame.grid_columnconfigure(col, weight=0)
1965
+ for row in range(self.current_row):
1966
+ advanced_frame.grid_rowconfigure(row, minsize=30)
1950
1967
 
1951
1968
  return advanced_frame
1952
1969
 
@@ -1997,6 +2014,12 @@ class ConfigApp:
1997
2014
  self.groq_models_combobox = ttk.Combobox(ai_frame, textvariable=self.groq_model_value, values=RECOMMENDED_GROQ_MODELS, state="readonly")
1998
2015
  self.groq_models_combobox.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1999
2016
  self.current_row += 1
2017
+
2018
+ # Force CPU for Whisper
2019
+ use_cpu_i18n = ai_i18n.get('use_cpu_for_inference', {})
2020
+ HoverInfoLabelWidget(ai_frame, text=use_cpu_i18n.get('label', 'Force CPU'), tooltip=use_cpu_i18n.get('tooltip', 'Even if CUDA is installed, use CPU for Whisper'), row=self.current_row, column=0)
2021
+ ttk.Checkbutton(ai_frame, variable=self.use_cpu_for_inference_value, bootstyle="round-toggle").grid(row=self.current_row, column=1, sticky='W', pady=2)
2022
+ self.current_row += 1
2000
2023
 
2001
2024
  groq_key_i18n = ai_i18n.get('groq_api_key', {})
2002
2025
  HoverInfoLabelWidget(ai_frame, text=groq_key_i18n.get('label', '...'), tooltip=groq_key_i18n.get('tooltip', '...'),
@@ -2067,8 +2090,10 @@ class ConfigApp:
2067
2090
 
2068
2091
  self.add_reset_button(ai_frame, "ai", self.current_row, 0, self.create_ai_tab)
2069
2092
 
2070
- for col in range(3): ai_frame.grid_columnconfigure(col, weight=0)
2071
- for row in range(self.current_row): ai_frame.grid_rowconfigure(row, minsize=30)
2093
+ for col in range(3):
2094
+ ai_frame.grid_columnconfigure(col, weight=0)
2095
+ for row in range(self.current_row):
2096
+ ai_frame.grid_rowconfigure(row, minsize=30)
2072
2097
 
2073
2098
  return ai_frame
2074
2099
 
@@ -2245,8 +2270,10 @@ class ConfigApp:
2245
2270
 
2246
2271
  self.add_reset_button(wip_frame, "wip", self.current_row, 0, self.create_wip_tab)
2247
2272
 
2248
- for col in range(2): wip_frame.grid_columnconfigure(col, weight=0)
2249
- for row in range(self.current_row): wip_frame.grid_rowconfigure(row, minsize=30)
2273
+ for col in range(2):
2274
+ wip_frame.grid_columnconfigure(col, weight=0)
2275
+ for row in range(self.current_row):
2276
+ wip_frame.grid_rowconfigure(row, minsize=30)
2250
2277
 
2251
2278
  return wip_frame
2252
2279
 
@@ -260,6 +260,10 @@
260
260
  "splice_padding": {
261
261
  "label": "Padding:",
262
262
  "tooltip": "Cut Detected Voice Segments and Paste them back together. More Padding = More Space between voicelines."
263
+ },
264
+ "use_cpu_for_inference": {
265
+ "label": "Force CPU:",
266
+ "tooltip": "Even if CUDA is installed, use CPU for Whisper"
263
267
  }
264
268
  },
265
269
  "features": {
@@ -259,6 +259,10 @@
259
259
  "splice_padding": {
260
260
  "label": "パディング:",
261
261
  "tooltip": "結合する音声セグメント間の間隔(秒)。"
262
+ },
263
+ "use_cpu_for_inference": {
264
+ "label": "CPU強制使用:",
265
+ "tooltip": "CUDAがインストールされていてもWhisperでCPUを使用します"
262
266
  }
263
267
  },
264
268
  "features": {
@@ -260,6 +260,10 @@
260
260
  "splice_padding": {
261
261
  "label": "填充:",
262
262
  "tooltip": "剪切检测到的语音片段并将其重新拼接。更多填充 = 语音间距更大。"
263
+ },
264
+ "use_cpu_for_inference": {
265
+ "label": "强制使用 CPU:",
266
+ "tooltip": "即使已安装 CUDA,也强制使用 CPU 运行 Whisper"
263
267
  }
264
268
  },
265
269
  "features": {
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
- client: obs.ReqClient = None
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,6 +25,81 @@ 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
104
  def __init__(self, check_output=True):
29
105
  super().__init__()
@@ -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 not connecting:
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
- global client
132
- if not client:
210
+ if not connection_pool:
133
211
  return False
134
212
  for _ in range(10):
135
213
  try:
136
- response = client.get_version()
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:
@@ -183,19 +262,25 @@ def get_obs_websocket_config_values():
183
262
  reload_config()
184
263
 
185
264
  async def connect_to_obs(retry=5, check_output=True):
186
- global client, obs_connection_manager, event_client, connecting
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
- client = obs.ReqClient(
194
- host=get_config().obs.host,
195
- port=get_config().obs.port,
196
- password=get_config().obs.password,
197
- timeout=1,
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=3, **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
- client = None
301
+ connection_pool = None
217
302
  event_client = None
218
303
  connecting = False
219
304
  break
@@ -222,18 +307,24 @@ async def connect_to_obs(retry=5, check_output=True):
222
307
  connecting = False
223
308
 
224
309
  def connect_to_obs_sync(retry=2, check_output=True):
225
- global client, obs_connection_manager, event_client
310
+ global connection_pool, obs_connection_manager, event_client
226
311
  if is_windows():
227
312
  get_obs_websocket_config_values()
228
313
 
229
314
  while True:
230
315
  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
- )
316
+ pool_kwargs = {
317
+ 'host': get_config().obs.host,
318
+ 'port': get_config().obs.port,
319
+ 'password': get_config().obs.password,
320
+ 'timeout': 3,
321
+ }
322
+ connection_pool = OBSConnectionPool(size=5, **pool_kwargs)
323
+ connection_pool.connect_all()
324
+
325
+ with connection_pool.get_client() as client:
326
+ client.get_version() # Test one connection to confirm it works
327
+
237
328
  event_client = obs.EventClient(
238
329
  host=get_config().obs.host,
239
330
  port=get_config().obs.port,
@@ -250,7 +341,7 @@ def connect_to_obs_sync(retry=2, check_output=True):
250
341
  if retry <= 0:
251
342
  gsm_status.obs_connected = False
252
343
  logger.error(f"Failed to connect to OBS WebSocket: {e}")
253
- client = None
344
+ connection_pool = None
254
345
  event_client = None
255
346
  break
256
347
  time.sleep(1)
@@ -258,33 +349,44 @@ def connect_to_obs_sync(retry=2, check_output=True):
258
349
 
259
350
 
260
351
  def disconnect_from_obs():
261
- global client
262
- if client:
263
- client.disconnect()
264
- client = None
352
+ global connection_pool
353
+ if connection_pool:
354
+ connection_pool.disconnect_all()
355
+ connection_pool = None
265
356
  logger.info("Disconnected from OBS WebSocket.")
266
357
 
267
- def do_obs_call(request, *args, from_dict=None, retry=3):
268
- connect_to_obs()
269
- if not client:
358
+ def do_obs_call(method_name: str, from_dict=None, retry=3, **kwargs):
359
+ if not connection_pool:
360
+ connect_to_obs_sync(retry=1)
361
+ if not connection_pool:
270
362
  return None
363
+
364
+ last_exception = None
271
365
  for _ in range(retry + 1):
272
366
  try:
273
- response = request(*args)
274
- if response and response.ok:
275
- return from_dict(response.datain) if from_dict else response.datain
367
+ with connection_pool.get_client() as client:
368
+ method_to_call = getattr(client, method_name)
369
+ response = method_to_call(**kwargs)
370
+ if response and response.ok:
371
+ return from_dict(response.datain) if from_dict else response.datain
276
372
  time.sleep(0.3)
373
+ except AttributeError:
374
+ logger.error(f"OBS client has no method '{method_name}'")
375
+ return None
277
376
  except Exception as e:
278
- logger.error(f"Error calling OBS: {e}")
377
+ last_exception = e
378
+ logger.error(f"Error calling OBS ('{method_name}'): {e}")
279
379
  if "socket is already closed" in str(e) or "object has no attribute" in str(e):
280
380
  time.sleep(0.3)
281
381
  else:
282
382
  return None
383
+ logger.error(f"OBS call '{method_name}' failed after retries. Last error: {last_exception}")
283
384
  return None
284
385
 
285
386
  def toggle_replay_buffer():
286
387
  try:
287
- response = client.toggle_replay_buffer()
388
+ with connection_pool.get_client() as client:
389
+ response = client.toggle_replay_buffer()
288
390
  if response:
289
391
  logger.info("Replay buffer Toggled.")
290
392
  except Exception as e:
@@ -292,7 +394,8 @@ def toggle_replay_buffer():
292
394
 
293
395
  def start_replay_buffer():
294
396
  try:
295
- response = client.start_replay_buffer()
397
+ with connection_pool.get_client() as client:
398
+ response = client.start_replay_buffer()
296
399
  if response and response.ok:
297
400
  logger.info("Replay buffer started.")
298
401
  except Exception as e:
@@ -300,14 +403,16 @@ def start_replay_buffer():
300
403
 
301
404
  def get_replay_buffer_status():
302
405
  try:
303
- return client.get_replay_buffer_status().output_active
406
+ with connection_pool.get_client() as client:
407
+ return client.get_replay_buffer_status().output_active
304
408
  except Exception as e:
305
409
  logger.debug(f"Error getting replay buffer status: {e}")
306
410
  return None
307
411
 
308
412
  def stop_replay_buffer():
309
413
  try:
310
- response = client.stop_replay_buffer()
414
+ with connection_pool.get_client() as client:
415
+ response = client.stop_replay_buffer()
311
416
  if response and response.ok:
312
417
  logger.info("Replay buffer stopped.")
313
418
  except Exception as e:
@@ -316,7 +421,8 @@ def stop_replay_buffer():
316
421
  def save_replay_buffer():
317
422
  status = get_replay_buffer_status()
318
423
  if status:
319
- response = client.save_replay_buffer()
424
+ with connection_pool.get_client() as client:
425
+ response = client.save_replay_buffer()
320
426
  if response and response.ok:
321
427
  logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
322
428
  else:
@@ -324,7 +430,8 @@ def save_replay_buffer():
324
430
 
325
431
  def get_current_scene():
326
432
  try:
327
- response = client.get_current_program_scene()
433
+ with connection_pool.get_client() as client:
434
+ response = client.get_current_program_scene()
328
435
  return response.scene_name if response else ''
329
436
  except Exception as e:
330
437
  logger.debug(f"Couldn't get scene: {e}")
@@ -332,7 +439,8 @@ def get_current_scene():
332
439
 
333
440
  def get_source_from_scene(scene_name):
334
441
  try:
335
- response = client.get_scene_item_list(name=scene_name)
442
+ with connection_pool.get_client() as client:
443
+ response = client.get_scene_item_list(name=scene_name)
336
444
  return response.scene_items[0] if response and response.scene_items else ''
337
445
  except Exception as e:
338
446
  logger.error(f"Error getting source from scene: {e}")
@@ -346,7 +454,8 @@ def get_active_source():
346
454
 
347
455
  def get_record_directory():
348
456
  try:
349
- response = client.get_record_directory()
457
+ with connection_pool.get_client() as client:
458
+ response = client.get_record_directory()
350
459
  return response.record_directory if response else ''
351
460
  except Exception as e:
352
461
  logger.error(f"Error getting recording folder: {e}")
@@ -354,17 +463,17 @@ def get_record_directory():
354
463
 
355
464
  def get_obs_scenes():
356
465
  try:
357
- response = client.get_scene_list()
466
+ with connection_pool.get_client() as client:
467
+ response = client.get_scene_list()
358
468
  return response.scenes if response else None
359
469
  except Exception as e:
360
470
  logger.error(f"Error getting scenes: {e}")
361
471
  return None
362
472
 
363
473
  async def register_scene_change_callback(callback):
364
- global client
365
474
  if await wait_for_obs_connected():
366
- if not client:
367
- logger.error("OBS client is not connected.")
475
+ if not connection_pool:
476
+ logger.error("OBS connection pool is not connected.")
368
477
  return
369
478
 
370
479
  def on_current_program_scene_changed(data):
@@ -391,7 +500,8 @@ def get_screenshot(compression=-1):
391
500
  return None
392
501
  start = time.time()
393
502
  logger.debug(f"Current source name: {current_source_name}")
394
- client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
503
+ with connection_pool.get_client() as client:
504
+ client.save_source_screenshot(name=current_source_name, img_format='png', width=None, height=None, file_path=screenshot, quality=compression)
395
505
  logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
396
506
  return screenshot
397
507
  except Exception as e:
@@ -410,11 +520,10 @@ def get_screenshot_base64(compression=75, width=None, height=None):
410
520
  if not current_source_name:
411
521
  logger.error("No active source found in the current scene.")
412
522
  return None
413
- # version = client.send("GetVersion", raw=True)
414
- # pprint(version)
415
- # responseraw = client.send("GetSourceScreenshot", {"sourceName": current_source_name, "imageFormat": "png", "imageWidth": width, "imageHeight": height, "compressionQuality": compression}, raw=True)
416
- response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
417
- # print(responseraw)
523
+
524
+ with connection_pool.get_client() as client:
525
+ response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
526
+
418
527
  if response and response.image_data:
419
528
  return response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
420
529
  else:
@@ -435,7 +544,8 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
435
544
  logger.error("No active source found in the current scene.")
436
545
  return None
437
546
  while True:
438
- response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
547
+ with connection_pool.get_client() as client:
548
+ response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
439
549
  try:
440
550
  response.image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
441
551
  except AttributeError:
@@ -448,8 +558,6 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
448
558
  image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
449
559
  image_data = base64.b64decode(image_data)
450
560
  img = Image.open(io.BytesIO(image_data)).convert("RGBA")
451
- # if width and height:
452
- # img = img.resize((width, height), Image.Resampling.LANCZOS)
453
561
  return img
454
562
  return None
455
563
 
@@ -472,103 +580,84 @@ def get_current_game(sanitize=False, update=True):
472
580
  def set_fit_to_screen_for_scene_items(scene_name: str):
473
581
  """
474
582
  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
583
  """
584
+ if not scene_name:
585
+ return
586
+
483
587
  try:
484
- # 1. Get the canvas (base) resolution from OBS video settings
485
- video_settings = client.get_video_settings()
486
- if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
487
- logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
488
- return
489
- canvas_width = video_settings.base_width
490
- canvas_height = video_settings.base_height
588
+ with connection_pool.get_client() as client:
589
+ # 1. Get the canvas (base) resolution from OBS video settings
590
+ video_settings = client.get_video_settings()
591
+ if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
592
+ logger.debug("Video settings do not have base_width or base_height attributes, probably weird websocket error issue? Idk what causes it..")
593
+ return
594
+ canvas_width = video_settings.base_width
595
+ canvas_height = video_settings.base_height
491
596
 
492
- # 2. Get the list of items in the specified scene
493
- scene_items_response = client.get_scene_item_list(scene_name)
494
- items = scene_items_response.scene_items if scene_items_response.scene_items else []
597
+ # 2. Get the list of items in the specified scene
598
+ scene_items_response = client.get_scene_item_list(scene_name)
599
+ items = scene_items_response.scene_items if scene_items_response.scene_items else []
495
600
 
496
- if not items:
497
- logger.warning(f"No items found in scene '{scene_name}'.")
498
- return
601
+ if not items:
602
+ logger.warning(f"No items found in scene '{scene_name}'.")
603
+ return
499
604
 
500
- # 3. Loop through each item and apply the "Fit to Screen" transform
501
- for item in items:
502
- item_id = item['sceneItemId']
503
- source_name = item['sourceName']
504
-
505
- scene_item_transform = item.get('sceneItemTransform', {})
506
-
507
- source_width = scene_item_transform.get('sourceWidth', None)
508
- source_height = scene_item_transform.get('sourceHeight', None)
509
-
510
- aspect_ratio_different = False
511
- already_cropped = any([
512
- scene_item_transform.get('cropLeft', 0) != 0,
513
- scene_item_transform.get('cropRight', 0) != 0,
514
- scene_item_transform.get('cropTop', 0) != 0,
515
- scene_item_transform.get('cropBottom', 0) != 0,
516
- ])
517
-
518
- if source_width and source_height and not already_cropped:
519
- source_aspect_ratio = source_width / source_height
520
- canvas_aspect_ratio = canvas_width / canvas_height
521
- # Check if aspect ratio is different and if it's a standard aspect ratio
522
- aspect_ratio_different = abs(source_aspect_ratio - canvas_aspect_ratio) > 0.01
523
-
524
- # List of standard aspect ratios
525
- standard_ratios = [
526
- 4 / 3,
527
- 16 / 9,
528
- 16 / 10,
529
- 21 / 9,
530
- 32 / 9,
531
- 5 / 4,
532
- 3 / 2,
533
- ]
534
-
535
- def is_standard_ratio(ratio):
536
- return any(abs(ratio - std) < 0.02 for std in standard_ratios)
537
-
538
- # Only crop if both source and canvas are standard aspect ratios
539
- if aspect_ratio_different:
540
- if not (is_standard_ratio(source_aspect_ratio) and is_standard_ratio(canvas_aspect_ratio)):
541
- aspect_ratio_different = False
542
-
543
- # This transform object is the equivalent of "Fit to Screen"
544
- fit_to_screen_transform = {
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
- })
605
+ # 3. Loop through each item and apply the "Fit to Screen" transform
606
+ for item in items:
607
+ item_id = item['sceneItemId']
608
+ source_name = item['sourceName']
609
+
610
+ scene_item_transform = item.get('sceneItemTransform', {})
611
+
612
+ source_width = scene_item_transform.get('sourceWidth', None)
613
+ source_height = scene_item_transform.get('sourceHeight', None)
614
+
615
+ aspect_ratio_different = False
616
+ already_cropped = any([
617
+ scene_item_transform.get('cropLeft', 0) != 0,
618
+ scene_item_transform.get('cropRight', 0) != 0,
619
+ scene_item_transform.get('cropTop', 0) != 0,
620
+ scene_item_transform.get('cropBottom', 0) != 0,
621
+ ])
622
+
623
+ if source_width and source_height and not already_cropped:
624
+ source_aspect_ratio = source_width / source_height
625
+ canvas_aspect_ratio = canvas_width / canvas_height
626
+ aspect_ratio_different = abs(source_aspect_ratio - canvas_aspect_ratio) > 0.01
627
+
628
+ standard_ratios = [4 / 3, 16 / 9, 16 / 10, 21 / 9, 32 / 9, 5 / 4, 3 / 2]
629
+
630
+ def is_standard_ratio(ratio):
631
+ return any(abs(ratio - std) < 0.02 for std in standard_ratios)
632
+
633
+ if aspect_ratio_different:
634
+ if not (is_standard_ratio(source_aspect_ratio) and is_standard_ratio(canvas_aspect_ratio)):
635
+ aspect_ratio_different = False
636
+
637
+ fit_to_screen_transform = {
638
+ 'boundsType': 'OBS_BOUNDS_SCALE_INNER', 'alignment': 5,
639
+ 'boundsWidth': canvas_width, 'boundsHeight': canvas_height,
640
+ 'positionX': 0, 'positionY': 0,
641
+ }
642
+
643
+ if not already_cropped:
644
+ fit_to_screen_transform.update({
645
+ 'cropLeft': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
646
+ 'cropRight': 0 if not aspect_ratio_different or canvas_width > source_width else (source_width - canvas_width) // 2,
647
+ 'cropTop': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
648
+ 'cropBottom': 0 if not aspect_ratio_different or canvas_height > source_height else (source_height - canvas_height) // 2,
649
+ })
560
650
 
561
- try:
562
- client.set_scene_item_transform(
563
- scene_name=scene_name,
564
- item_id=item_id,
565
- transform=fit_to_screen_transform
566
- )
567
- except obs.error.OBSSDKError as e:
568
- logger.error(f"Failed to set transform for source '{source_name}': {e}")
651
+ try:
652
+ client.set_scene_item_transform(
653
+ scene_name=scene_name,
654
+ item_id=item_id,
655
+ transform=fit_to_screen_transform
656
+ )
657
+ except obs.error.OBSSDKError as e:
658
+ logger.error(f"Failed to set transform for source '{source_name}': {e}")
569
659
 
570
660
  except obs.error.OBSSDKError as e:
571
- # This will catch errors like "scene not found"
572
661
  logger.error(f"An OBS error occurred: {e}")
573
662
  except Exception as e:
574
663
  logger.error(f"An unexpected error occurred: {e}")
@@ -576,12 +665,14 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
576
665
 
577
666
  def main():
578
667
  start_obs()
579
- connect_to_obs()
668
+ # connect_to_obs() is async, main is not. Use the sync version.
669
+ connect_to_obs_sync()
580
670
  # Test each method
581
671
  print("Testing `get_obs_path`:", get_obs_path())
582
672
  print("Testing `is_process_running` with PID 1:", is_process_running(1))
583
673
  print("Testing `check_obs_folder_is_correct`:")
584
- check_obs_folder_is_correct()
674
+ # This is async, need to run it in an event loop if testing from main
675
+ asyncio.run(check_obs_folder_is_correct())
585
676
  print("Testing `get_obs_websocket_config_values`:")
586
677
  try:
587
678
  get_obs_websocket_config_values()
@@ -595,10 +686,13 @@ def main():
595
686
  print("Testing `stop_replay_buffer`:")
596
687
  stop_replay_buffer()
597
688
  print("Testing `save_replay_buffer`:")
598
- save_replay_buffer()
689
+ try:
690
+ save_replay_buffer()
691
+ except Exception as e:
692
+ print(f"Could not save replay buffer: {e}")
599
693
  current_scene = get_current_scene()
600
694
  print("Testing `get_current_scene`:", current_scene)
601
- print("Testing `get_source_from_scene` with dummy scene:", get_source_from_scene(current_scene))
695
+ print("Testing `get_source_from_scene` with current scene:", get_source_from_scene(current_scene))
602
696
  print("Testing `get_record_directory`:", get_record_directory())
603
697
  print("Testing `get_obs_scenes`:", get_obs_scenes())
604
698
  print("Testing `get_screenshot`:", get_screenshot())
@@ -612,51 +706,4 @@ def main():
612
706
  if __name__ == '__main__':
613
707
  logging.basicConfig(level=logging.INFO)
614
708
  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
-
709
+ set_fit_to_screen_for_scene_items(get_current_scene())
@@ -577,7 +577,7 @@ if __name__ == "__main__":
577
577
  keep_newline = args.keep_newline
578
578
  obs_ocr = args.obs_ocr
579
579
 
580
- obs.connect_to_obs_sync(retry=0, check_output=False)
580
+ obs.connect_to_obs_sync(check_output=False)
581
581
 
582
582
  # Start config change checker thread
583
583
  config_check_thread = ConfigChangeCheckThread()
@@ -559,6 +559,7 @@ class VAD:
559
559
  add_audio_on_no_results: bool = False
560
560
  cut_and_splice_segments: bool = False
561
561
  splice_padding: float = 0.1
562
+ use_cpu_for_inference: bool = False
562
563
 
563
564
  def is_silero(self):
564
565
  return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
@@ -104,7 +104,6 @@ class OverlayProcessor:
104
104
  """
105
105
  with mss.mss() as sct:
106
106
  monitors = sct.monitors[1:]
107
- print(monitors)
108
107
  if is_windows() and monitor_index == 0:
109
108
  from ctypes import wintypes
110
109
  import ctypes
GameSentenceMiner/vad.py CHANGED
@@ -167,7 +167,8 @@ class WhisperVADProcessor(VADProcessor):
167
167
  import stable_whisper as whisper
168
168
  if not self.vad_model:
169
169
  with warnings.catch_warnings():
170
- self.vad_model = whisper.load_model(get_config().vad.whisper_model)
170
+ warnings.simplefilter("ignore")
171
+ self.vad_model = whisper.load_model(get_config().vad.whisper_model, device="cpu" if get_config().vad.use_cpu_for_inference else None)
171
172
  logger.info(f"Whisper model '{get_config().vad.whisper_model}' loaded.")
172
173
  return self.vad_model
173
174
 
@@ -181,6 +182,7 @@ class WhisperVADProcessor(VADProcessor):
181
182
 
182
183
  # Transcribe the audio using Whisper
183
184
  with warnings.catch_warnings():
185
+ warnings.simplefilter("ignore")
184
186
  result: WhisperResult = self.vad_model.transcribe(temp_wav, vad=True, language=get_config().vad.language,
185
187
  temperature=0.0)
186
188
  voice_activity = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.15.3
3
+ Version: 2.15.4
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,10 +1,10 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  GameSentenceMiner/anki.py,sha256=4Tq6OGjfN-5tYorYRWiih7FZjSKMG6amrLv6DFKkFQc,25344
3
- GameSentenceMiner/config_gui.py,sha256=G36MLR1UcdiLB6V3T25_ggGVnt5B4Vj1pS76nQQMqpk,139669
3
+ GameSentenceMiner/config_gui.py,sha256=i79PrY2pP8_VKvIL7uoDv5cgHvCCQBIe0mS_YnX2AVg,140792
4
4
  GameSentenceMiner/gametext.py,sha256=fgBgLchezpauWELE9Y5G3kVCLfAneD0X4lJFoI3FYbs,10351
5
5
  GameSentenceMiner/gsm.py,sha256=4mJn5v4WKqKAJEtph5e0v4YPVDOpvFN1ylV2vQvf_Dg,31913
6
- GameSentenceMiner/obs.py,sha256=99V4WvBhbBTEGI1o3dlGzhqnktKFUxPc1-5vOBcV0lk,26296
7
- GameSentenceMiner/vad.py,sha256=YCn4ZIc6_Q3IGOr5QNMiheVT3Ma5nisn8-V8xD53Mw4,19236
6
+ GameSentenceMiner/obs.py,sha256=eejvd6N7_3YeC0FwED9WOZAeYmKWkA27vNT7ylh0B8k,28722
7
+ GameSentenceMiner/vad.py,sha256=EEUFLDbkJ4jYmctfVaV99BwMNRuLx7DLRTYmKUo-myw,19394
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  GameSentenceMiner/ai/ai_prompting.py,sha256=41xdBzE88Jlt12A0D-T_cMfLO5j6MSxfniOptpwNZm0,24068
10
10
  GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,14 +15,14 @@ GameSentenceMiner/assets/icon32.png,sha256=Kww0hU_qke9_22wBuO_Nq0Dv2SfnOLwMhCyGg
15
15
  GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB_7W4sqs,192487
16
16
  GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
17
17
  GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
18
- GameSentenceMiner/locales/en_us.json,sha256=FAnvsCfsFzWyxYKZKh8HKHsAahi3Oa4wGVekTTN2WGI,26587
19
- GameSentenceMiner/locales/ja_jp.json,sha256=-v0ng0psD88-C4XjYazJL0Rn0gwQU7b2VYspvdatDO4,28326
20
- GameSentenceMiner/locales/zh_cn.json,sha256=X5nw6tsu7ACaZIuSUDSUUjG8qPUwmqyG3TKcPbWSIYw,24654
18
+ GameSentenceMiner/locales/en_us.json,sha256=4lCV34FnDOe0c02qHlHnfujQedmqHSL-feN3lYCCCfs,26744
19
+ GameSentenceMiner/locales/ja_jp.json,sha256=LNLo2qIugMcDGiPbSo018zVAU8K_HG8Q4zvIcsHUzTA,28517
20
+ GameSentenceMiner/locales/zh_cn.json,sha256=lZYB3HAcxhVCSVWcnvuepuCvn6_Y2mvd0-SKJEYx_ko,24829
21
21
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=DfcR3bHTu26JJerLzqfW_KpdgUBSrRV4hqSy_LYclps,5967
23
23
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
24
24
  GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Rm1_nuZotJhfOfoJ_3mesh9udtOBjYqKhnAvSief6fo,29181
25
- GameSentenceMiner/ocr/owocr_helper.py,sha256=8aIpAHSPPByox5qcU4SX-_ECDgQZVEwrXdj4A8AQZ6U,28437
25
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=0pzeWY0L49pH_KjAg4enmg5t_Om0wFb4R2BTP6yBISI,28428
26
26
  GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
27
27
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
28
28
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
@@ -37,11 +37,11 @@ GameSentenceMiner/tools/furigana_filter_preview.py,sha256=BXv7FChPEJW_VeG5XYt6su
37
37
  GameSentenceMiner/tools/ss_selector.py,sha256=cbjMxiKOCuOfbRvLR_PCRlykBrGtm1LXd6u5czPqkmc,4793
38
38
  GameSentenceMiner/tools/window_transparency.py,sha256=GtbxbmZg0-UYPXhfHff-7IKZyY2DKe4B9GdyovfmpeM,8166
39
39
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- GameSentenceMiner/util/configuration.py,sha256=JVwaqvfrUrOUiA0kZcznDsCo9hJkJqBpVztyI6JA-YU,40201
40
+ GameSentenceMiner/util/configuration.py,sha256=cpgusOu4lItCBsZyB8QoAJaXjDovIXNlqyKzh2GMYzw,40241
41
41
  GameSentenceMiner/util/db.py,sha256=2bO0rD4i8A1hhsRBER-wgZy9IK17ibRbI8DHxdKvYsI,16598
42
42
  GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
43
43
  GameSentenceMiner/util/ffmpeg.py,sha256=jA-cFtCmdCWrUSPpdtFSLr-GSoqs4qNUzW20v4HPHf0,28715
44
- GameSentenceMiner/util/get_overlay_coords.py,sha256=bSvBSvLFmABogPp-quXcQrN-meWvl5NRB6gFEluoNNg,15142
44
+ GameSentenceMiner/util/get_overlay_coords.py,sha256=P5tI7H0cnveGs33aQdvJGy9DV6aIAGh8K8Al1XjNPzw,15114
45
45
  GameSentenceMiner/util/gsm_utils.py,sha256=Piwv88Q9av2LBeN7M6QDi0Mp0_R2lNbkcI6ekK5hd2o,11851
46
46
  GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
47
47
  GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
@@ -72,9 +72,9 @@ GameSentenceMiner/web/templates/index.html,sha256=LqXZx7-NE42pXSpHNZ3To680rD-vt9
72
72
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
73
73
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
74
74
  GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- gamesentenceminer-2.15.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
76
- gamesentenceminer-2.15.3.dist-info/METADATA,sha256=4zXzdtgHkL9Tv-ZNqDJr5mZ9ra6O8UpACup5X0zOy7M,7317
77
- gamesentenceminer-2.15.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- gamesentenceminer-2.15.3.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
79
- gamesentenceminer-2.15.3.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
80
- gamesentenceminer-2.15.3.dist-info/RECORD,,
75
+ gamesentenceminer-2.15.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
76
+ gamesentenceminer-2.15.4.dist-info/METADATA,sha256=kiXbjNcMkmEwfiVBKr3AH0Wtil4Hxe32XJWNPJjulHE,7317
77
+ gamesentenceminer-2.15.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
+ gamesentenceminer-2.15.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
79
+ gamesentenceminer-2.15.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
80
+ gamesentenceminer-2.15.4.dist-info/RECORD,,