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.
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ai/ai_prompting.py +4 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/anki.py +8 -2
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ffmpeg.py +89 -7
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/gsm.py +4 -3
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/obs.py +2 -2
- gamesentenceminer-2.8.43/GameSentenceMiner/obs_back.py +309 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/owocr_helper.py +48 -67
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/run.py +51 -17
- gamesentenceminer-2.8.43/GameSentenceMiner/ss_selector.py +121 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/util.py +6 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/PKG-INFO +2 -2
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/SOURCES.txt +2 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/PKG-INFO +2 -2
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/pyproject.toml +2 -2
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/send.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/communication/websocket.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/config_gui.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/configuration.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/electron_config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/gametext.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/model.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/notification.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/package.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/text_log.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/result.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/silero_trim.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/vosk_helper.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/vad/whisper_helper.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/templates/utility.html +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner/web/texthooking_page.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/requires.txt +1 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/LICENSE +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.43}/README.md +0 -0
- {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(
|
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
|
-
|
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
|
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}",
|
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
|
-
|
43
|
-
|
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
|
-
|
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(
|
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
|