GameSentenceMiner 2.8.41__tar.gz → 2.8.43__tar.gz

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.
Files changed (67) hide show
  1. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ai/ai_prompting.py +4 -1
  2. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/anki.py +8 -2
  3. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ffmpeg.py +89 -7
  4. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/gsm.py +4 -3
  5. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/obs.py +2 -2
  6. gamesentenceminer-2.8.43/GameSentenceMiner/obs_back.py +309 -0
  7. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/owocr_helper.py +48 -67
  8. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/run.py +51 -17
  9. gamesentenceminer-2.8.43/GameSentenceMiner/ss_selector.py +121 -0
  10. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/util.py +6 -1
  11. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/PKG-INFO +2 -2
  12. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/SOURCES.txt +2 -0
  13. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/PKG-INFO +2 -2
  14. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/pyproject.toml +2 -2
  15. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/__init__.py +0 -0
  16. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ai/__init__.py +0 -0
  17. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/__init__.py +0 -0
  18. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/send.py +0 -0
  19. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/websocket.py +0 -0
  20. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/config_gui.py +0 -0
  21. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/configuration.py +0 -0
  22. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
  23. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/__init__.py +0 -0
  24. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/download_tools.py +0 -0
  25. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/oneocr_dl.py +0 -0
  26. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/electron_config.py +0 -0
  27. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/gametext.py +0 -0
  28. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/model.py +0 -0
  29. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/notification.py +0 -0
  30. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/__init__.py +0 -0
  31. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  32. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  33. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  34. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  35. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  36. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  37. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  38. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  39. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  40. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/package.py +0 -0
  41. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/text_log.py +0 -0
  42. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/__init__.py +0 -0
  43. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/result.py +0 -0
  44. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/silero_trim.py +0 -0
  45. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/vosk_helper.py +0 -0
  46. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/whisper_helper.py +0 -0
  47. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/__init__.py +0 -0
  48. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/__init__.py +0 -0
  49. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  50. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  51. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon.ico +0 -0
  52. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon.svg +0 -0
  53. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  54. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/style.css +0 -0
  55. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  56. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  57. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/__init__.py +0 -0
  58. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  59. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/utility.html +0 -0
  60. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/texthooking_page.py +0 -0
  61. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  62. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  63. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/requires.txt +1 -1
  64. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  65. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/LICENSE +0 -0
  66. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/README.md +0 -0
  67. {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/setup.cfg +0 -0
@@ -119,7 +119,10 @@ class GeminiAI(AIManager):
119
119
  try:
120
120
  prompt = self._build_prompt(lines, sentence, current_line, game_title)
121
121
  self.logger.debug(f"Generated prompt:\n{prompt}")
122
- response = self.model.generate_content(prompt)
122
+ response = self.model.generate_content(
123
+ prompt,
124
+ temperature=0.5,
125
+ )
123
126
  result = response.text.strip()
124
127
  self.logger.debug(f"Received response:\n{result}")
125
128
  return result
@@ -14,7 +14,7 @@ from GameSentenceMiner.configuration import get_config
14
14
  from GameSentenceMiner.model import AnkiCard
15
15
  from GameSentenceMiner.text_log import get_all_lines, get_text_event, get_mined_line
16
16
  from GameSentenceMiner.obs import get_current_game
17
- from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue
17
+ from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue, wait_for_stable_file
18
18
  from GameSentenceMiner.web import texthooking_page
19
19
 
20
20
  audio_in_anki = None
@@ -41,11 +41,13 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
41
41
  audio_in_anki = store_media_file(audio_path)
42
42
  if update_picture:
43
43
  screenshot = ffmpeg.get_screenshot(video_path, ss_time)
44
+ wait_for_stable_file(screenshot)
44
45
  screenshot_in_anki = store_media_file(screenshot)
45
46
  if get_config().paths.remove_screenshot:
46
47
  os.remove(screenshot)
47
48
  if get_config().anki.previous_image_field:
48
49
  prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev)
50
+ wait_for_stable_file(prev_screenshot)
49
51
  prev_screenshot_in_anki = store_media_file(prev_screenshot)
50
52
  if get_config().paths.remove_screenshot:
51
53
  os.remove(prev_screenshot)
@@ -178,7 +180,11 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
178
180
 
179
181
 
180
182
  def store_media_file(path):
181
- return invoke('storeMediaFile', filename=path, data=convert_to_base64(path))
183
+ try:
184
+ return invoke('storeMediaFile', filename=path, data=convert_to_base64(path))
185
+ except Exception as e:
186
+ logger.error(f"Error storing media file, check anki card for blank media fields: {e}")
187
+ return "None"
182
188
 
183
189
 
184
190
  def convert_to_base64(file_path):
@@ -1,3 +1,4 @@
1
+ import shutil
1
2
  import tempfile
2
3
 
3
4
  from GameSentenceMiner import obs, util, configuration
@@ -15,13 +16,65 @@ def get_ffprobe_path():
15
16
  ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
16
17
 
17
18
 
18
- def get_screenshot(video_file, screenshot_timing):
19
+ def call_frame_extractor(video_path, timestamp):
20
+ """
21
+ Calls the video frame extractor script and captures the output.
22
+
23
+ Args:
24
+ video_path (str): Path to the video file.
25
+ timestamp (str): Timestamp string (HH:MM:SS).
26
+
27
+ Returns:
28
+ str: The path of the selected image, or None on error.
29
+ """
30
+ try:
31
+ # Get the directory of the current script
32
+ current_dir = os.path.dirname(os.path.abspath(__file__))
33
+ # Construct the path to the frame extractor script
34
+ script_path = os.path.join(current_dir, "ss_selector.py") # Replace with the actual script name if different
35
+
36
+ logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp)]))
37
+
38
+ # Run the script using subprocess.run()
39
+ result = subprocess.run(
40
+ [sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
41
+ capture_output=True,
42
+ text=True, # Get output as text
43
+ check=False # Raise an exception for non-zero exit codes
44
+ )
45
+ if result.returncode != 0:
46
+ logger.error(f"Script failed with return code: {result.returncode}")
47
+ return None
48
+ logger.info(result)
49
+ # Print the standard output
50
+ logger.info(f"Frame extractor script output: {result.stdout.strip()}")
51
+ return result.stdout.strip() # Return the output
52
+
53
+ except subprocess.CalledProcessError as e:
54
+ logger.error(f"Error calling script: {e}")
55
+ logger.error(f"Script output (stderr): {e.stderr.strip()}")
56
+ return None
57
+ except FileNotFoundError:
58
+ logger.error(f"Error: Script not found at {script_path}. Make sure the script name is correct.")
59
+ return None
60
+ except Exception as e:
61
+ logger.error(f"An unexpected error occurred: {e}")
62
+ return None
63
+
64
+ def get_screenshot(video_file, screenshot_timing, try_selector=False):
19
65
  screenshot_timing = screenshot_timing if screenshot_timing else 1
66
+ if try_selector:
67
+ filepath = call_frame_extractor(video_path=video_file, timestamp=screenshot_timing)
68
+ output = process_image(filepath)
69
+ if output:
70
+ return output
71
+ else:
72
+ logger.error("Frame extractor script failed to run or returned no output, defaulting")
20
73
  output_image = make_unique_file_name(os.path.join(
21
74
  get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
22
75
  # FFmpeg command to extract the last frame of the video
23
76
  ffmpeg_command = ffmpeg_base_command_list + [
24
- "-ss", f"{screenshot_timing}", # Seek to 1 second after the beginning
77
+ "-ss", f"{screenshot_timing}",
25
78
  "-i", f"{video_file}",
26
79
  "-vframes", "1" # Extract only one frame
27
80
  ]
@@ -39,15 +92,33 @@ def get_screenshot(video_file, screenshot_timing):
39
92
 
40
93
  logger.debug(f"FFMPEG SS Command: {ffmpeg_command}")
41
94
 
42
- # Run the command
43
- subprocess.run(ffmpeg_command)
95
+ try:
96
+ for i in range(3):
97
+ logger.debug(" ".join(ffmpeg_command))
98
+ result = subprocess.run(ffmpeg_command)
99
+ if result.returncode != 0 and i < 2:
100
+ raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
101
+ else:
102
+ break
103
+ except Exception as e:
104
+ logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
105
+ output_image = make_unique_file_name(os.path.join(
106
+ get_config().paths.screenshot_destination,
107
+ f"{obs.get_current_game(sanitize=True)}.png"))
108
+ ffmpeg_command = ffmpeg_base_command_list + [
109
+ "-ss", f"{screenshot_timing}", # Default to 1 second
110
+ "-i", video_file,
111
+ "-vframes", "1",
112
+ output_image
113
+ ]
114
+ subprocess.run(ffmpeg_command)
44
115
 
45
116
  logger.debug(f"Screenshot saved to: {output_image}")
46
117
 
47
118
  return output_image
48
119
 
49
- def get_screenshot_for_line(video_file, game_line):
50
- return get_screenshot(video_file, get_screenshot_time(video_file, game_line))
120
+ def get_screenshot_for_line(video_file, game_line, try_selector=False):
121
+ return get_screenshot(video_file, get_screenshot_time(video_file, game_line), try_selector)
51
122
 
52
123
 
53
124
  def get_screenshot_time(video_path, game_line, default_beginning=False, vad_result=None, doing_multi_line=False, previous_line=False):
@@ -127,7 +198,18 @@ def process_image(image_file):
127
198
  logger.debug(ffmpeg_command)
128
199
  logger.debug(" ".join(ffmpeg_command))
129
200
  # Run the command
130
- subprocess.run(ffmpeg_command)
201
+ try:
202
+ for i in range(3):
203
+ logger.debug(" ".join(ffmpeg_command))
204
+ result = subprocess.run(ffmpeg_command)
205
+ if result.returncode != 0 and i < 2:
206
+ raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
207
+ else:
208
+ break
209
+ except Exception as e:
210
+ logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
211
+ output_image = make_unique_file_name(os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.png"))
212
+ shutil.move(image_file, output_image)
131
213
 
132
214
  logger.info(f"Processed image saved to: {output_image}")
133
215
 
@@ -82,7 +82,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
82
82
  if texthooking_page.event_manager.line_for_screenshot:
83
83
  line: GameLine = texthooking_page.event_manager.line_for_screenshot
84
84
  texthooking_page.event_manager.line_for_screenshot = None
85
- screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
85
+ screenshot = ffmpeg.get_screenshot_for_line(video_path, line, True)
86
86
  os.startfile(screenshot)
87
87
  os.remove(video_path)
88
88
  return
@@ -92,10 +92,11 @@ class VideoToAudioHandler(FileSystemEventHandler):
92
92
  os.remove(video_path)
93
93
  return
94
94
  try:
95
- last_note = None
96
- anki_card_creation_time = None
97
95
  if anki.card_queue and len(anki.card_queue) > 0:
98
96
  last_note, anki_card_creation_time = anki.card_queue.pop(0)
97
+ else:
98
+ logger.info("Replay buffer initiated externally. Skipping processing.")
99
+ return
99
100
  with util.lock:
100
101
  util.set_last_mined_line(anki.get_sentence(last_note))
101
102
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
@@ -27,8 +27,8 @@ class OBSConnectionManager(threading.Thread):
27
27
 
28
28
  def run(self):
29
29
  while self.running:
30
- time.sleep(5)
31
- if not client.get_version():
30
+ time.sleep(1)
31
+ if not client or not client.get_version():
32
32
  logger.info("OBS WebSocket not connected. Attempting to reconnect...")
33
33
  connect_to_obs()
34
34
 
@@ -0,0 +1,309 @@
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