GameSentenceMiner 2.9.2__py3-none-any.whl → 2.9.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.
GameSentenceMiner/anki.py CHANGED
@@ -82,6 +82,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
82
82
  for key, value in get_config().anki.anki_custom_fields.items():
83
83
  note['fields'][key] = str(value)
84
84
 
85
+
86
+ notification.open_browser_window(1)
85
87
  invoke("updateNoteFields", note=note)
86
88
  tags = []
87
89
  if get_config().anki.custom_tags:
@@ -94,9 +96,11 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
94
96
  logger.info(f"UPDATED ANKI CARD FOR {last_note.noteId}")
95
97
  if get_config().features.notify_on_update:
96
98
  notification.send_note_updated(tango)
99
+ notification.open_browser_window(last_note.noteId)
97
100
  if get_config().features.open_anki_edit:
98
101
  notification.open_anki_card(last_note.noteId)
99
102
 
103
+
100
104
  if get_config().audio.external_tool and get_config().audio.external_tool_enabled and update_audio:
101
105
  open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
102
106
 
File without changes
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -39,6 +39,13 @@ DEFAULT_CONFIG = 'Default'
39
39
 
40
40
  current_game = ''
41
41
 
42
+ def is_linux():
43
+ return platform == 'linux'
44
+
45
+
46
+ def is_windows():
47
+ return platform == 'win32'
48
+
42
49
 
43
50
  class Language(Enum):
44
51
  JAPANESE = "ja"
@@ -153,7 +160,7 @@ class Audio:
153
160
  extension: str = 'opus'
154
161
  beginning_offset: float = 0.0
155
162
  end_offset: float = 0.5
156
- ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"'
163
+ ffmpeg_reencode_options: str = '-c:a libopus -f opus -af \"afade=t=in:d=0.10\"' if is_windows() else ''
157
164
  external_tool: str = ""
158
165
  anki_media_collection: str = ""
159
166
  external_tool_enabled: bool = True
@@ -619,7 +626,7 @@ console_handler.setFormatter(formatter)
619
626
  logger.addHandler(console_handler)
620
627
 
621
628
  # Create rotating file handler with level DEBUG
622
- if 'gsm' in sys.argv[0]:
629
+ if 'gsm' in sys.argv[0].lower() or 'gamesentenceminer' in sys.argv[0].lower():
623
630
  file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
624
631
  file_handler.setLevel(logging.DEBUG)
625
632
  file_handler.setFormatter(formatter)
@@ -637,5 +644,4 @@ class GsmAppState:
637
644
  self.previous_screenshot = None
638
645
  self.previous_replay = None
639
646
 
640
- gsm_state = GsmAppState()
641
-
647
+ gsm_state = GsmAppState()
@@ -1,6 +1,7 @@
1
1
  import shutil
2
2
  import tempfile
3
3
 
4
+ import GameSentenceMiner.configuration
4
5
  from GameSentenceMiner import obs, util, configuration
5
6
  from GameSentenceMiner.configuration import *
6
7
  from GameSentenceMiner.text_log import initial_time
@@ -8,10 +9,10 @@ from GameSentenceMiner.util import *
8
9
 
9
10
 
10
11
  def get_ffmpeg_path():
11
- return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if util.is_windows() else "ffmpeg"
12
+ return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
12
13
 
13
14
  def get_ffprobe_path():
14
- return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if util.is_windows() else "ffprobe"
15
+ return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
15
16
 
16
17
  ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
17
18
 
@@ -398,7 +399,7 @@ def convert_audio_to_wav(input_audio, output_wav):
398
399
  "-i", input_audio,
399
400
  "-ar", "16000", # Resample to 16kHz
400
401
  "-ac", "1", # Convert to mono
401
- "-af", "afftdn,dialoguenhance" if not util.is_linux() else "afftdn",
402
+ "-af", "afftdn,dialoguenhance" if not is_linux() else "afftdn",
402
403
  output_wav
403
404
  ]
404
405
  logger.debug(" ".join(command))
GameSentenceMiner/gsm.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import shutil
3
2
  import sys
4
3
 
5
4
  from GameSentenceMiner.vad.result import VADResult
@@ -39,7 +38,7 @@ try:
39
38
  from GameSentenceMiner.web import texthooking_page
40
39
  from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
41
40
  except Exception as e:
42
- from GameSentenceMiner.configuration import logger
41
+ from GameSentenceMiner.configuration import logger, is_linux, is_windows
43
42
  import time
44
43
  logger.info("Something bad happened during import/initialization, closing in 5 seconds")
45
44
  logger.exception(e)
@@ -74,7 +73,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
74
73
 
75
74
  def process_replay(self, video_path):
76
75
  vad_trimmed_audio = ''
77
- print(video_path)
78
76
  if "previous.mkv" in video_path:
79
77
  os.remove(video_path)
80
78
  video_path = gsm_state.previous_replay
@@ -682,7 +680,7 @@ async def register_scene_switcher_callback():
682
680
 
683
681
  await obs.register_scene_change_callback(scene_switcher_callback)
684
682
 
685
- async def main(reloading=False):
683
+ async def async_main(reloading=False):
686
684
  global root, settings_window
687
685
  initialize(reloading)
688
686
  logger.info("Script started.")
@@ -721,11 +719,14 @@ async def main(reloading=False):
721
719
  except Exception as e:
722
720
  logger.error(f"Error stopping observer: {e}")
723
721
 
722
+ def main():
723
+ asyncio.run(async_main())
724
+
724
725
 
725
726
  if __name__ == "__main__":
726
727
  logger.info("Starting GSM")
727
728
  try:
728
- asyncio.run(main())
729
+ asyncio.run(async_main())
729
730
  except Exception as e:
730
731
  logger.exception(e)
731
732
  time.sleep(5)
@@ -2,14 +2,11 @@ import platform
2
2
 
3
3
  import requests
4
4
  from plyer import notification
5
-
6
- from GameSentenceMiner.util import is_windows
5
+ from GameSentenceMiner.configuration import logger, is_windows
7
6
 
8
7
  if is_windows():
9
8
  from win10toast import ToastNotifier
10
9
 
11
- from GameSentenceMiner.configuration import logger
12
-
13
10
  if is_windows():
14
11
  class MyToastNotifier(ToastNotifier):
15
12
  def __init__(self):
@@ -24,6 +21,27 @@ if is_windows():
24
21
  else:
25
22
  notifier = notification
26
23
 
24
+ def open_browser_window(note_id):
25
+ url = "http://localhost:8765"
26
+ headers = {'Content-Type': 'application/json'}
27
+
28
+ data = {
29
+ "action": "guiBrowse",
30
+ "version": 6,
31
+ "params": {
32
+ "query": f"nid:{note_id}"
33
+ }
34
+ }
35
+
36
+ try:
37
+ response = requests.post(url, json=data, headers=headers)
38
+ if response.status_code == 200:
39
+ logger.info(f"Opened Anki note with ID {note_id}")
40
+ else:
41
+ logger.error(f"Failed to open Anki note with ID {note_id}")
42
+ except Exception as e:
43
+ logger.info(f"Error connecting to AnkiConnect: {e}")
44
+
27
45
 
28
46
  def open_anki_card(note_id):
29
47
  url = "http://localhost:8765"
GameSentenceMiner/obs.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import logging
3
2
  import os.path
4
3
  import subprocess
5
4
  import threading
@@ -8,6 +7,7 @@ import psutil
8
7
 
9
8
  import obsws_python as obs
10
9
 
10
+ import GameSentenceMiner.configuration
11
11
  from GameSentenceMiner import util, configuration
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.model import *
@@ -140,7 +140,7 @@ async def connect_to_obs(retry_count=0):
140
140
  if not get_config().obs.enabled:
141
141
  return
142
142
 
143
- if util.is_windows():
143
+ if GameSentenceMiner.configuration.is_windows():
144
144
  get_obs_websocket_config_values()
145
145
 
146
146
  while True:
@@ -174,7 +174,7 @@ def connect_to_obs_sync(retry_count=0):
174
174
  if not get_config().obs.enabled or client:
175
175
  return
176
176
 
177
- if util.is_windows():
177
+ if GameSentenceMiner.configuration.is_windows():
178
178
  get_obs_websocket_config_values()
179
179
 
180
180
  while True:
@@ -6,7 +6,6 @@ import requests
6
6
  from GameSentenceMiner.configuration import logger, get_app_directory
7
7
 
8
8
  PACKAGE_NAME = "GameSentenceMiner"
9
- VERSION_FILE_PATH = os.path.join(get_app_directory(), 'version.txt')
10
9
 
11
10
  def get_current_version():
12
11
  try:
GameSentenceMiner/util.py CHANGED
@@ -7,7 +7,6 @@ import subprocess
7
7
  import threading
8
8
  import time
9
9
  from datetime import datetime
10
- from sys import platform
11
10
 
12
11
  from rapidfuzz import process
13
12
 
@@ -131,12 +130,6 @@ def run_agent_and_hook(pname, agent_script):
131
130
  keep_running = False
132
131
 
133
132
 
134
- def is_linux():
135
- return platform == 'linux'
136
-
137
- def is_windows():
138
- return platform == 'win32'
139
-
140
133
  # def run_command(command, shell=False, input=None, capture_output=False, timeout=None, check=False, **kwargs):
141
134
  # # Use shell=True if the OS is Linux, otherwise shell=False
142
135
  # if is_linux():
@@ -258,8 +251,6 @@ TEXT_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'text_repla
258
251
  OCR_REPLACEMENTS_FILE = os.path.join(get_app_directory(), 'config', 'ocr_replacements.json')
259
252
  os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
260
253
 
261
- import urllib.request
262
-
263
254
  # if not os.path.exists(OCR_REPLACEMENTS_FILE):
264
255
  # url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/refs/heads/main/electron-src/assets/ocr_replacements.json"
265
256
  # try:
@@ -247,7 +247,7 @@ def clear_history():
247
247
 
248
248
  async def add_event_to_texthooker(line: GameLine):
249
249
  new_event = event_manager.add_gameline(line)
250
- await broadcast_message({
250
+ await websocket_server_thread.send_text({
251
251
  'event': 'text_received',
252
252
  'sentence': line.text,
253
253
  'data': new_event.to_serializable()
@@ -294,37 +294,6 @@ def play_audio():
294
294
  return jsonify({}), 200
295
295
 
296
296
 
297
- connected_clients = set()
298
-
299
- async def websocket_handler(websocket):
300
- logger.debug(f"Client connected: {websocket.remote_address}")
301
- connected_clients.add(websocket)
302
- try:
303
- async for message in websocket:
304
- try:
305
- data = json.loads(message)
306
- if 'type' in data and data['type'] == 'get_events':
307
- initial_events = [{'id': 1, 'text': 'Initial event from WebSocket'}, {'id': 2, 'text': 'Another initial event'}]
308
- await websocket.send(json.dumps({'event': 'initial_events', 'payload': initial_events}))
309
- elif 'update_checkbox' in data:
310
- print(f"Received checkbox update: {data}")
311
- # Handle checkbox update logic
312
- pass
313
- await websocket.send(json.dumps({'response': f'Server received: {message}'}))
314
- except json.JSONDecodeError:
315
- await websocket.send(json.dumps({'error': 'Invalid JSON format'}))
316
- except websockets.exceptions.ConnectionClosedError:
317
- print(f"Client disconnected abruptly: {websocket.remote_address}")
318
- except websockets.exceptions.ConnectionClosedOK:
319
- print(f"Client disconnected gracefully: {websocket.remote_address}")
320
- finally:
321
- connected_clients.discard(websocket)
322
-
323
- async def broadcast_message(message):
324
- if connected_clients:
325
- for client in connected_clients:
326
- await client.send(json.dumps(message))
327
-
328
297
  # async def main():
329
298
  # async with websockets.serve(websocket_handler, "localhost", 8765): # Choose a port for WebSocket
330
299
  # print("WebSocket server started on ws://localhost:8765/ws (adjust as needed)")
@@ -360,7 +329,7 @@ def are_lines_selected():
360
329
 
361
330
  def reset_checked_lines():
362
331
  async def send_reset_message():
363
- await broadcast_message({
332
+ await websocket_server_thread.send_text({
364
333
  'event': 'reset_checkboxes',
365
334
  })
366
335
  event_manager.reset_checked_lines()
@@ -383,23 +352,69 @@ def start_web_server():
383
352
 
384
353
  import signal
385
354
 
386
- async def run_websocket_server(host="0.0.0.0"):
387
- global websocket_port
388
- websocket = None
389
- try:
390
- websocket_port = get_config().advanced.texthooker_communication_websocket_port
391
- websocket = await websockets.serve(websocket_handler, host, websocket_port)
392
- logger.debug(f"WebSocket server started at ws://{host}:{websocket_port}/")
393
- await asyncio.Future() # Keep the server running
394
- except asyncio.CancelledError:
395
- logger.info("WebSocket server shutting down...")
396
- except OSError as e:
397
- logger.error(f"TextHooker WebSocket server failed to start on port {websocket_port}: {e}")
398
- logger.info("You may need to try a different port in GSM's advanced config, and then update that in the Texthooker's settings.")
399
- finally:
400
- if websocket:
401
- websocket.close()
402
- await asyncio.sleep(1) # Wait before retrying
355
+ websocket_server_thread = None
356
+ websocket_queue = queue.Queue()
357
+ paused = False
358
+
359
+
360
+ class WebsocketServerThread(threading.Thread):
361
+ def __init__(self, read):
362
+ super().__init__(daemon=True)
363
+ self._loop = None
364
+ self.read = read
365
+ self.clients = set()
366
+ self._event = threading.Event()
367
+
368
+ @property
369
+ def loop(self):
370
+ self._event.wait()
371
+ return self._loop
372
+
373
+ async def send_text_coroutine(self, message):
374
+ for client in self.clients:
375
+ await client.send(message)
376
+
377
+ async def server_handler(self, websocket):
378
+ self.clients.add(websocket)
379
+ try:
380
+ async for message in websocket:
381
+ if self.read and not paused:
382
+ websocket_queue.put(message)
383
+ try:
384
+ await websocket.send('True')
385
+ except websockets.exceptions.ConnectionClosedOK:
386
+ pass
387
+ else:
388
+ try:
389
+ await websocket.send('False')
390
+ except websockets.exceptions.ConnectionClosedOK:
391
+ pass
392
+ except websockets.exceptions.ConnectionClosedError:
393
+ pass
394
+ finally:
395
+ self.clients.remove(websocket)
396
+
397
+ async def send_text(self, text):
398
+ if text:
399
+ return asyncio.run_coroutine_threadsafe(
400
+ self.send_text_coroutine(json.dumps(text)), self.loop)
401
+
402
+ def stop_server(self):
403
+ self.loop.call_soon_threadsafe(self._stop_event.set)
404
+
405
+ def run(self):
406
+ async def main():
407
+ self._loop = asyncio.get_running_loop()
408
+ self._stop_event = stop_event = asyncio.Event()
409
+ self._event.set()
410
+ self.server = start_server = websockets.serve(self.server_handler,
411
+ "0.0.0.0",
412
+ get_config().advanced.texthooker_communication_websocket_port,
413
+ max_size=1000000000)
414
+ async with start_server:
415
+ await stop_event.wait()
416
+
417
+ asyncio.run(main())
403
418
 
404
419
  def handle_exit_signal(loop):
405
420
  logger.info("Received exit signal. Shutting down...")
@@ -407,13 +422,16 @@ def handle_exit_signal(loop):
407
422
  task.cancel()
408
423
 
409
424
  async def texthooker_page_coro():
425
+ global websocket_server_thread
410
426
  # Run the WebSocket server in the asyncio event loop
411
427
  flask_thread = threading.Thread(target=start_web_server)
412
428
  flask_thread.daemon = True
413
429
  flask_thread.start()
414
430
 
431
+ websocket_server_thread = WebsocketServerThread(read=True)
432
+ websocket_server_thread.start()
433
+
415
434
  # Keep the main asyncio event loop running (for the WebSocket server)
416
- await run_websocket_server()
417
435
 
418
436
  def run_text_hooker_page():
419
437
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.9.2
3
+ Version: 2.9.4
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,21 +1,28 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=JnVfFkLpEfWaPfOLngU0PSQq4vrgWuhd_VLYZEBqNTY,14608
2
+ GameSentenceMiner/anki.py,sha256=HAYblBjkth5p0tx9Z0LNQn6zuQC8_54RIB2BMzN8-LE,14705
3
3
  GameSentenceMiner/config_gui.py,sha256=h4zz85gfhxSphaJ-IZSu9D4jR70mDlKecZ9JRCO5Noc,80927
4
- GameSentenceMiner/configuration.py,sha256=8CfdTJ0ROJrxyzNg3NaElAVS1bwchg1ih6XfhfDZy1g,25492
4
+ GameSentenceMiner/configuration.py,sha256=KKW6fmpxya4dmXx9cERFVrzsKCTw0vmZrF2HAJDURBU,25667
5
5
  GameSentenceMiner/electron_config.py,sha256=dGcPYCISPehXubYSzsDuI2Gl092MYK0u3bTnkL9Jh1Y,9787
6
- GameSentenceMiner/ffmpeg.py,sha256=zVmLJOsXpy71zKb0cLBPrXJ6YpjPVRJmH0uRfd5O30k,18299
6
+ GameSentenceMiner/ffmpeg.py,sha256=APa2vNdAgxYdG96_Z3Xdh1WqOiWaK6gTLJqzEvCMMeU,18323
7
7
  GameSentenceMiner/gametext.py,sha256=sll-6Pficd4ZXYy8yL8hBrEOSpfa53TOye7vtHHKFN4,6218
8
- GameSentenceMiner/gsm.py,sha256=_Mp_gZFomeFz9FTZqYEXIgqxbICYcAB06KUlFkClX5Q,29831
8
+ GameSentenceMiner/gsm.py,sha256=olG3BIWjbVHWTsRKmeDVE5X8XrgppWke73Fy1J15dxA,29868
9
9
  GameSentenceMiner/model.py,sha256=1lRyJFf_LND_4O16h8CWVqDfosLgr0ZS6ufBZ3qJHpY,5699
10
- GameSentenceMiner/notification.py,sha256=pXKoLfmRQLH55IQ5G6uxdMuczqX7D6l3ubVEY1e6hXg,2859
11
- GameSentenceMiner/obs.py,sha256=DoUJk00Gk0Idley7CEldfIobqJ9na2UBlCv7nclZO4s,14793
12
- GameSentenceMiner/obs_back.py,sha256=_N_UV7Nh5cyy3mnH5lOUOzhgZwHMACeFEuBo1Z-bNzg,10894
13
- GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
10
+ GameSentenceMiner/notification.py,sha256=e6TOzZJD7RTvMgxaY-V01r5OiocHhdqEIVdAnj4MGSw,3437
11
+ GameSentenceMiner/obs.py,sha256=JiydRMpfSpNZ0nDAzH8OOECbYbsxMNSGz46lO8lZbvA,14871
12
+ GameSentenceMiner/package.py,sha256=uu3Yb3pqu8vN5ISzP87YCHlFNR9wxMMf5hPRncTr7ws,1181
14
13
  GameSentenceMiner/ss_selector.py,sha256=csey9H3561-guRJcT6gQN6hXxvylP0CBI0dp2-kwo2Q,4446
15
14
  GameSentenceMiner/text_log.py,sha256=U2_g8THAYeexRiE2bLk_bCt_2ShiA8SQ9VdJsi4riHs,5181
16
- GameSentenceMiner/util.py,sha256=PrDNnxWiJZh1lGuwnp3DjWIlwbkVxweRTYWLtQk94Ao,9122
15
+ GameSentenceMiner/util.py,sha256=ZbK7i1UeOzKyc5WtCcttiGljR_stfu7qpnEpgqFBwro,8976
17
16
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
17
  GameSentenceMiner/ai/ai_prompting.py,sha256=xw8et6XNwQiDXOXZnw8iIntVSg8lni4YYZbgWsK7qDE,10013
18
+ GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ GameSentenceMiner/assets/icon.png,sha256=9GRL8uXUAgkUSlvbm9Pv9o2poFVRGdW6s2ub_DeUD9M,937624
20
+ GameSentenceMiner/assets/icon128.png,sha256=l90j7biwdz5ahwOd5wZ-406ryEV9Pan93dquJQ3e1CI,18395
21
+ GameSentenceMiner/assets/icon256.png,sha256=JEW46wOrG1KR-907rvFaEdNbPtj5gu0HJmG7qUnIHxQ,51874
22
+ GameSentenceMiner/assets/icon32.png,sha256=Kww0hU_qke9_22wBuO_Nq0Dv2SfnOLwMhCyGgbgXdg8,6089
23
+ GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB_7W4sqs,192487
24
+ GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
25
+ GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
19
26
  GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
20
27
  GameSentenceMiner/communication/send.py,sha256=X0MytGv5hY-uUvkfvdCqQA_ljZFmV6UkJ6in1TA1bUE,217
21
28
  GameSentenceMiner/communication/websocket.py,sha256=8eFZaTtoFggEPdqw2Jl4zqHC2I7J3-Gk27CxVX7SyBo,3277
@@ -43,7 +50,7 @@ GameSentenceMiner/vad/vad_utils.py,sha256=_YC6rW2eXSBeLnYbVl_F3na1KCRL90VrnOzKYJ
43
50
  GameSentenceMiner/vad/vosk_helper.py,sha256=h7yNHrzrzT-J74UniA0T2ZX8cHqhflCzwyDjoIdKLO4,6479
44
51
  GameSentenceMiner/vad/whisper_helper.py,sha256=B64-Eq_ZMCIyQX_A8uvYz-c48hSXJAyz6tSXNRaLjtA,4020
45
52
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
- GameSentenceMiner/web/texthooking_page.py,sha256=alXNkpm2Kl-ewYIm_aw24_8RdcYedLB59y6YTkp_mrE,14789
53
+ GameSentenceMiner/web/texthooking_page.py,sha256=7Z7TGDcnj-94Y9ws7bQph4oIXrqf8Q9qVKowKimHWxM,14749
47
54
  GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
55
  GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
49
56
  GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
@@ -57,9 +64,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
57
64
  GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
58
65
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
59
66
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
60
- gamesentenceminer-2.9.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
61
- gamesentenceminer-2.9.2.dist-info/METADATA,sha256=XVxZdTUhTDvmNprnr75Eqs1Cd-6QjFDQOjjn-CeLI0Y,7280
62
- gamesentenceminer-2.9.2.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
63
- gamesentenceminer-2.9.2.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
64
- gamesentenceminer-2.9.2.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
65
- gamesentenceminer-2.9.2.dist-info/RECORD,,
67
+ gamesentenceminer-2.9.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
68
+ gamesentenceminer-2.9.4.dist-info/METADATA,sha256=jNhOj4IaiTEJqGlm4-VCtviW5hmN8J5XnvPBGWgcY0Y,7280
69
+ gamesentenceminer-2.9.4.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
70
+ gamesentenceminer-2.9.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
71
+ gamesentenceminer-2.9.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
72
+ gamesentenceminer-2.9.4.dist-info/RECORD,,
@@ -1,309 +0,0 @@
1
- import os.path
2
- import subprocess
3
- import threading
4
- import time
5
- import psutil
6
-
7
- from obswebsocket import obsws, requests, events
8
- from obswebsocket.exceptions import ConnectionFailure
9
-
10
- from GameSentenceMiner import util, configuration
11
- from GameSentenceMiner.configuration import *
12
- from GameSentenceMiner.model import *
13
-
14
- client: obsws = None
15
- obs_process_pid = None
16
- # logging.getLogger('obswebsocket').setLevel(logging.CRITICAL)
17
- OBS_PID_FILE = os.path.join(configuration.get_app_directory(), 'obs-studio', 'obs_pid.txt')
18
-
19
- # REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
20
-
21
-
22
- def get_obs_path():
23
- return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
24
-
25
- def is_process_running(pid):
26
- try:
27
- process = psutil.Process(pid)
28
- return 'obs' in process.exe()
29
- except (psutil.NoSuchProcess, psutil.AccessDenied, OSError):
30
- if os.path.exists(OBS_PID_FILE):
31
- os.remove(OBS_PID_FILE)
32
- return False
33
-
34
- def start_obs():
35
- global obs_process_pid
36
- if os.path.exists(OBS_PID_FILE):
37
- with open(OBS_PID_FILE, "r") as f:
38
- try:
39
- obs_process_pid = int(f.read().strip())
40
- if is_process_running(obs_process_pid):
41
- print(f"OBS is already running with PID: {obs_process_pid}")
42
- connect_to_obs()
43
- return obs_process_pid
44
- except ValueError:
45
- print("Invalid PID found in file. Launching new OBS instance.")
46
- except OSError:
47
- print("No process found with the stored PID. Launching new OBS instance.")
48
-
49
- obs_path = get_obs_path()
50
- if not os.path.exists(obs_path):
51
- print(f"OBS not found at {obs_path}. Please install OBS.")
52
- return None
53
- try:
54
- obs_process = subprocess.Popen([obs_path, '--disable-shutdown-check', '--portable', '--startreplaybuffer', ], cwd=os.path.dirname(obs_path))
55
- obs_process_pid = obs_process.pid
56
- connect_to_obs()
57
- with open(OBS_PID_FILE, "w") as f:
58
- f.write(str(obs_process_pid))
59
- print(f"OBS launched with PID: {obs_process_pid}")
60
- return obs_process_pid
61
- except Exception as e:
62
- print(f"Error launching OBS: {e}")
63
- return None
64
-
65
- def check_obs_folder_is_correct():
66
- obs_record_directory = get_record_directory()
67
- if obs_record_directory and os.path.normpath(obs_record_directory) != os.path.normpath(
68
- get_config().paths.folder_to_watch):
69
- logger.info("OBS Path Setting wrong, OBS Recording folder in GSM Config")
70
- get_config().paths.folder_to_watch = os.path.normpath(obs_record_directory)
71
- get_master_config().sync_shared_fields()
72
- save_full_config(get_master_config())
73
-
74
-
75
- def get_obs_websocket_config_values():
76
- config_path = os.path.join(get_app_directory(), 'obs-studio', 'config', 'obs-studio', 'plugin_config', 'obs-websocket', 'config.json')
77
-
78
- # Check if config file exists
79
- if not os.path.isfile(config_path):
80
- raise FileNotFoundError(f"OBS WebSocket config not found at {config_path}")
81
-
82
- # Read the JSON configuration
83
- with open(config_path, 'r') as file:
84
- config = json.load(file)
85
-
86
- # Extract values
87
- server_enabled = config.get("server_enabled", False)
88
- server_port = config.get("server_port", 7274) # Default to 4455 if not set
89
- server_password = config.get("server_password", None)
90
-
91
- if not server_enabled:
92
- logger.info("OBS WebSocket server is not enabled. Enabling it now... Restart OBS for changes to take effect.")
93
- config["server_enabled"] = True
94
-
95
- with open(config_path, 'w') as file:
96
- json.dump(config, file, indent=4)
97
-
98
- if get_config().obs.password == 'your_password':
99
- logger.info("OBS WebSocket password is not set. Setting it now...")
100
- full_config = get_master_config()
101
- full_config.get_config().obs.port = server_port
102
- full_config.get_config().obs.password = server_password
103
- full_config.sync_shared_fields()
104
- full_config.save()
105
- reload_config()
106
-
107
-
108
- connected = False
109
-
110
- def on_connect(obs):
111
- global connected
112
- logger.info("Reconnected to OBS WebSocket.")
113
- start_replay_buffer()
114
- connected = True
115
-
116
-
117
- def on_disconnect(obs):
118
- global connected
119
- logger.error("OBS Connection Lost!")
120
- connected = False
121
-
122
-
123
- def connect_to_obs(retry_count=0):
124
- global client
125
- if not get_config().obs.enabled or client:
126
- return
127
-
128
- if util.is_windows():
129
- get_obs_websocket_config_values()
130
-
131
- try:
132
- client = obsws(
133
- host=get_config().obs.host,
134
- port=get_config().obs.port,
135
- password=get_config().obs.password,
136
- authreconnect=1,
137
- on_connect=on_connect,
138
- on_disconnect=on_disconnect
139
- )
140
- client.connect()
141
- update_current_game()
142
- except ConnectionFailure as e:
143
- if retry_count % 5 == 0:
144
- logger.error(f"Failed to connect to OBS WebSocket: {e}. Retrying...")
145
- time.sleep(1)
146
- connect_to_obs(retry_count=retry_count + 1)
147
-
148
-
149
- # Disconnect from OBS WebSocket
150
- def disconnect_from_obs():
151
- global client
152
- if client:
153
- client.disconnect()
154
- client = None
155
- logger.info("Disconnected from OBS WebSocket.")
156
-
157
- def do_obs_call(request, from_dict=None, retry=3):
158
- connect_to_obs()
159
- for _ in range(retry + 1):
160
- try:
161
- response = client.call(request)
162
- if response and response.status:
163
- return from_dict(response.datain) if from_dict else response.datain
164
- time.sleep(0.3)
165
- except Exception as e:
166
- logger.error(f"Error calling OBS: {e}")
167
- if "socket is already closed" in str(e) or "object has no attribute" in str(e):
168
- time.sleep(0.3)
169
- else:
170
- return None
171
- return None
172
-
173
- def toggle_replay_buffer():
174
- try:
175
- do_obs_call(requests.ToggleReplayBuffer())
176
- logger.info("Replay buffer Toggled.")
177
- except Exception as e:
178
- logger.error(f"Error toggling buffer: {e}")
179
-
180
-
181
- # Start replay buffer
182
- def start_replay_buffer(retry=5):
183
- try:
184
- if not get_replay_buffer_status()['outputActive']:
185
- do_obs_call(requests.StartReplayBuffer(), retry=0)
186
- except Exception as e:
187
- if "socket is already closed" in str(e):
188
- if retry > 0:
189
- time.sleep(1)
190
- start_replay_buffer(retry - 1)
191
- else:
192
- logger.error(f"Error starting replay buffer: {e}")
193
-
194
- def get_replay_buffer_status():
195
- try:
196
- return do_obs_call(requests.GetReplayBufferStatus())
197
- except Exception as e:
198
- logger.error(f"Error getting replay buffer status: {e}")
199
-
200
-
201
- # Stop replay buffer
202
- def stop_replay_buffer():
203
- try:
204
- client.call(requests.StopReplayBuffer())
205
- logger.error("Replay buffer stopped.")
206
- except Exception as e:
207
- logger.error(f"Error stopping replay buffer: {e}")
208
-
209
- # Save the current replay buffer
210
- def save_replay_buffer():
211
- replay_buffer_started = do_obs_call(requests.GetReplayBufferStatus())['outputActive']
212
- if replay_buffer_started:
213
- client.call(requests.SaveReplayBuffer())
214
- logger.info("Replay buffer saved. If your log stops bere, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
215
- else:
216
- logger.error("Replay Buffer is not active, could not save Replay Buffer!")
217
-
218
-
219
- def get_current_scene():
220
- try:
221
- return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict, retry=0).sceneName
222
- except Exception as e:
223
- logger.debug(f"Couldn't get scene: {e}")
224
- return ''
225
-
226
-
227
- def get_source_from_scene(scene_name):
228
- try:
229
- return do_obs_call(requests.GetSceneItemList(sceneName=scene_name), SceneItemsResponse.from_dict).sceneItems[0]
230
- except Exception as e:
231
- logger.error(f"Error getting source from scene: {e}")
232
- return ''
233
-
234
- def get_record_directory():
235
- try:
236
- return do_obs_call(requests.GetRecordDirectory(), RecordDirectory.from_dict).recordDirectory
237
- except Exception as e:
238
- logger.error(f"Error getting recording folder: {e}")
239
- return ''
240
-
241
- def get_obs_scenes():
242
- try:
243
- response: SceneListResponse = do_obs_call(requests.GetSceneList(), SceneListResponse.from_dict, retry=0)
244
- return response.scenes
245
- except Exception as e:
246
- logger.error(f"Error getting scenes: {e}")
247
- return None
248
-
249
- def register_scene_change_callback(callback):
250
- global client
251
- if not client:
252
- logger.error("OBS client is not connected.")
253
- return
254
-
255
- def on_scene_change(data):
256
- logger.info("Scene changed: " + str(data))
257
- scene_name = data.getSceneName()
258
- if scene_name:
259
- callback(scene_name)
260
-
261
- client.register(on_scene_change, events.CurrentProgramSceneChanged)
262
- logger.info("Scene change callback registered.")
263
-
264
-
265
- def get_screenshot(compression=-1):
266
- try:
267
- screenshot = util.make_unique_file_name(os.path.abspath(
268
- configuration.get_temporary_directory()) + '/screenshot.png')
269
- update_current_game()
270
- current_source = get_source_from_scene(get_current_game())
271
- current_source_name = current_source.sourceName
272
- if not current_source_name:
273
- logger.error("No active scene found.")
274
- return
275
- start = time.time()
276
- logger.debug(f"Current source name: {current_source_name}")
277
- response = client.call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot, imageCompressionQuality=compression))
278
- logger.debug(f"Screenshot response: {response}")
279
- logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
280
- return screenshot
281
- except Exception as e:
282
- logger.error(f"Error getting screenshot: {e}")
283
-
284
- def get_screenshot_base64():
285
- try:
286
- update_current_game()
287
- current_source = get_source_from_scene(get_current_game())
288
- current_source_name = current_source.sourceName
289
- if not current_source_name:
290
- logger.error("No active scene found.")
291
- return
292
- response = do_obs_call(requests.GetSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageCompressionQuality=0))
293
- with open('screenshot_response.txt', 'wb') as f:
294
- f.write(str(response).encode())
295
- return response['imageData']
296
- except Exception as e:
297
- logger.error(f"Error getting screenshot: {e}")
298
-
299
- def update_current_game():
300
- configuration.current_game = get_current_scene()
301
-
302
-
303
- def get_current_game(sanitize=False):
304
- if not configuration.current_game:
305
- update_current_game()
306
-
307
- if sanitize:
308
- return util.sanitize_filename(configuration.current_game)
309
- return configuration.current_game