GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.0__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.
Files changed (51) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +6 -6
  2. GameSentenceMiner/anki.py +236 -152
  3. GameSentenceMiner/gametext.py +7 -4
  4. GameSentenceMiner/gsm.py +49 -10
  5. GameSentenceMiner/locales/en_us.json +7 -3
  6. GameSentenceMiner/locales/ja_jp.json +8 -4
  7. GameSentenceMiner/locales/zh_cn.json +8 -4
  8. GameSentenceMiner/obs.py +238 -59
  9. GameSentenceMiner/ocr/owocr_helper.py +1 -1
  10. GameSentenceMiner/tools/ss_selector.py +7 -8
  11. GameSentenceMiner/ui/__init__.py +0 -0
  12. GameSentenceMiner/ui/anki_confirmation.py +187 -0
  13. GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
  14. GameSentenceMiner/ui/screenshot_selector.py +215 -0
  15. GameSentenceMiner/util/configuration.py +124 -22
  16. GameSentenceMiner/util/db.py +22 -13
  17. GameSentenceMiner/util/downloader/download_tools.py +2 -2
  18. GameSentenceMiner/util/ffmpeg.py +24 -30
  19. GameSentenceMiner/util/get_overlay_coords.py +34 -34
  20. GameSentenceMiner/util/gsm_utils.py +31 -1
  21. GameSentenceMiner/util/text_log.py +11 -9
  22. GameSentenceMiner/vad.py +31 -12
  23. GameSentenceMiner/web/database_api.py +742 -123
  24. GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
  25. GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
  26. GameSentenceMiner/web/static/css/overview.css +850 -0
  27. GameSentenceMiner/web/static/css/popups-shared.css +126 -0
  28. GameSentenceMiner/web/static/css/shared.css +97 -0
  29. GameSentenceMiner/web/static/css/stats.css +192 -597
  30. GameSentenceMiner/web/static/js/anki_stats.js +6 -4
  31. GameSentenceMiner/web/static/js/database.js +209 -5
  32. GameSentenceMiner/web/static/js/goals.js +610 -0
  33. GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
  34. GameSentenceMiner/web/static/js/overview.js +1176 -0
  35. GameSentenceMiner/web/static/js/shared.js +25 -0
  36. GameSentenceMiner/web/static/js/stats.js +154 -1459
  37. GameSentenceMiner/web/stats.py +2 -2
  38. GameSentenceMiner/web/templates/anki_stats.html +5 -0
  39. GameSentenceMiner/web/templates/components/navigation.html +3 -1
  40. GameSentenceMiner/web/templates/database.html +73 -1
  41. GameSentenceMiner/web/templates/goals.html +376 -0
  42. GameSentenceMiner/web/templates/index.html +13 -11
  43. GameSentenceMiner/web/templates/overview.html +416 -0
  44. GameSentenceMiner/web/templates/stats.html +46 -251
  45. GameSentenceMiner/web/texthooking_page.py +18 -0
  46. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/METADATA +5 -1
  47. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/RECORD +51 -41
  48. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/WHEEL +0 -0
  49. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/entry_points.txt +0 -0
  50. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/licenses/LICENSE +0 -0
  51. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.0.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gsm.py CHANGED
@@ -1,3 +1,7 @@
1
+ # There should be no imports here, as any error will crash the program.
2
+ # All imports should be done in the try/except block below.
3
+
4
+
1
5
  def handle_error_in_initialization(e):
2
6
  """Handle errors that occur during initialization."""
3
7
  logger.exception(e, exc_info=True)
@@ -24,6 +28,7 @@ try:
24
28
  import requests
25
29
  import os.path
26
30
  import signal
31
+ import datetime
27
32
  from subprocess import Popen
28
33
  os.environ.pop('TCL_LIBRARY', None)
29
34
 
@@ -36,13 +41,17 @@ try:
36
41
  import psutil
37
42
 
38
43
  start_time = time.time()
44
+ import GameSentenceMiner.util.configuration
39
45
  from GameSentenceMiner.util.configuration import logger, gsm_state, get_config, anki_results, AnkiUpdateResult, \
40
46
  get_temporary_directory, get_log_path, get_master_config, switch_profile_and_save, get_app_directory, gsm_status, \
41
- is_windows, is_linux
42
- from GameSentenceMiner.util.get_overlay_coords import OverlayThread
43
- from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
47
+ is_windows, is_linux, get_ffmpeg_path
44
48
 
45
49
  logger.debug(f"[Import] configuration: {time.time() - start_time:.3f}s")
50
+
51
+ start_time = time.time()
52
+ from GameSentenceMiner.util.get_overlay_coords import OverlayThread
53
+ from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags, add_srt_line
54
+ logger.debug(f"[Import] get_overlay_coords (OverlayThread, remove_html_and_cloze_tags): {time.time() - start_time:.3f}s")
46
55
 
47
56
  start_time = time.time()
48
57
  from GameSentenceMiner.util.model import VADResult
@@ -72,7 +81,7 @@ try:
72
81
  logger.debug(f"[Import] anki: {time.time() - start_time:.3f}s")
73
82
 
74
83
  start_time = time.time()
75
- from GameSentenceMiner import config_gui
84
+ from GameSentenceMiner.ui import config_gui
76
85
  logger.debug(f"[Import] config_gui: {time.time() - start_time:.3f}s")
77
86
 
78
87
  start_time = time.time()
@@ -98,7 +107,7 @@ try:
98
107
  f"[Import] websocket (connect_websocket, register_websocket_message_handler, FunctionName): {time.time() - start_time:.3f}s")
99
108
 
100
109
  start_time = time.time()
101
- from GameSentenceMiner.util.ffmpeg import get_audio_and_trim, get_video_timings, get_ffmpeg_path
110
+ from GameSentenceMiner.util.ffmpeg import get_audio_and_trim, get_video_timings
102
111
  logger.debug(
103
112
  f"[Import] util.ffmpeg (get_audio_and_trim, get_video_timings, get_ffmpeg_path): {time.time() - start_time:.3f}s")
104
113
 
@@ -145,15 +154,28 @@ class VideoToAudioHandler(FileSystemEventHandler):
145
154
  super().__init__()
146
155
 
147
156
  def on_created(self, event):
148
- if event.is_directory or ("Replay" not in event.src_path and "GSM" not in event.src_path):
157
+ file_name = os.path.basename(event.src_path)
158
+ if event.is_directory or ("Replay" not in file_name and "GSM" not in file_name):
159
+ # This shows up as soon as recording starts, so it's kinda hard to use...
160
+ # if get_config().features.generate_longplay and event.src_path.endswith(".mkv") or event.src_path.endswith(".mp4"):
161
+ # add_srt_line(datetime.datetime.now(), get_all_lines()[-1])
162
+ # logger.info(f"Recording {event.src_path} FOUND, RUNNING LOGIC")
163
+ # wait_for_stable_file(event.src_path)
164
+ # current_srt = gsm_state.current_srt
165
+ # srt_name = os.path.splitext(os.path.basename(event.src_path))[0] + ".srt"
166
+ # srt_path = os.path.join(os.path.dirname(event.src_path), srt_name)
167
+ # shutil.move(current_srt, srt_path)
168
+ # gsm_state.current_srt = None
169
+ # # self.process_replay(event.src_path)
149
170
  return
150
171
  # Adjust based on your OBS output format
151
- if event.src_path.endswith(".mkv") or event.src_path.endswith(".mp4"):
172
+ if file_name.endswith(".mkv") or file_name.endswith(".mp4"):
152
173
  logger.info(f"MKV {event.src_path} FOUND, RUNNING LOGIC")
153
174
  wait_for_stable_file(event.src_path)
154
175
  self.process_replay(event.src_path)
155
176
 
156
177
  def process_replay(self, video_path):
178
+ gsm_state.current_replay = video_path
157
179
  vad_trimmed_audio = ''
158
180
  final_audio_output = ''
159
181
  skip_delete = False
@@ -194,6 +216,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
194
216
  # Get Info of line mined
195
217
  line_cutoff = None
196
218
  start_line = None
219
+ full_text = ''
197
220
  if selected_lines:
198
221
  start_line = selected_lines[0]
199
222
  mined_line = get_mined_line(last_note, selected_lines)
@@ -300,7 +323,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
300
323
  f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
301
324
 
302
325
  vad_result = vad_processor.trim_audio_with_vad(
303
- trimmed_audio, vad_trimmed_audio, game_line)
326
+ trimmed_audio, vad_trimmed_audio, game_line, full_text)
304
327
  if timing_only:
305
328
  return vad_result
306
329
 
@@ -337,7 +360,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
337
360
 
338
361
  def initial_checks():
339
362
  try:
340
- subprocess.run(ffmpeg.ffmpeg_base_command_list)
363
+ subprocess.run(GameSentenceMiner.util.configuration.ffmpeg_base_command_list)
341
364
  logger.debug("FFMPEG is installed and accessible.")
342
365
  except FileNotFoundError:
343
366
  logger.error(
@@ -465,6 +488,13 @@ class GSMTray(threading.Thread):
465
488
  self.run_tray()
466
489
 
467
490
  def run_tray(self):
491
+ def run_anki_confirmation_window():
492
+ settings_window.show_anki_confirmation_dialog(expression="こんにちは",
493
+ sentence="こんにちは、世界!元気ですか?",
494
+ screenshot_path="test_image.png",
495
+ audio_path="C:/path/to/my/audio.mp3",
496
+ translation="Hello world! How are you?")
497
+
468
498
  self.profile_menu = Menu(
469
499
  *[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, self.switch_profile) for
470
500
  profile in
@@ -478,9 +508,10 @@ class GSMTray(threading.Thread):
478
508
  MenuItem("Toggle Replay Buffer", self.play_pause),
479
509
  MenuItem("Restart OBS", restart_obs),
480
510
  MenuItem("Switch Profile", self.profile_menu),
511
+ MenuItem("Run Test Code", run_anki_confirmation_window),
481
512
  MenuItem("Exit", exit_program)
482
513
  )
483
-
514
+
484
515
  self.icon = Icon("TrayApp", create_image(), "GameSentenceMiner", menu)
485
516
  self.icon.run()
486
517
 
@@ -574,6 +605,8 @@ def restart_obs():
574
605
 
575
606
  def cleanup():
576
607
  try:
608
+ if gsm_state.current_srt and len(get_all_lines()) > 0:
609
+ add_srt_line(datetime.datetime.now(), get_all_lines()[-1])
577
610
  logger.info("Performing cleanup...")
578
611
  gsm_state.keep_running = False
579
612
 
@@ -794,6 +827,7 @@ async def async_main(reloading=False):
794
827
  root = ttk.Window(themename='darkly')
795
828
  start_time = time.time()
796
829
  settings_window = config_gui.ConfigApp(root)
830
+ gsm_state.config_app = settings_window
797
831
  initialize_async()
798
832
  observer = Observer()
799
833
  observer.schedule(VideoToAudioHandler(),
@@ -823,6 +857,11 @@ async def async_main(reloading=False):
823
857
  if get_config().general.open_config_on_startup:
824
858
  root.after(50, settings_window.show)
825
859
  root.after(50, gsm_tray.start)
860
+ # root.after(100, settings_window.show_anki_confirmation_dialog(expression="こんにちは",
861
+ # sentence="こんにちは、世界!元気ですか?",
862
+ # screenshot_path="test_image.png",
863
+ # audio_path="C:/path/to/my/audio.mp3",
864
+ # translation="Hello world! How are you?"))
826
865
  settings_window.add_save_hook(gsm_tray.update_icon)
827
866
  settings_window.on_exit = exit_program
828
867
  root.mainloop()
@@ -154,6 +154,10 @@
154
154
  "label": "Update Anki:",
155
155
  "tooltip": "Automatically update Anki with new data."
156
156
  },
157
+ "show_update_confirmation_dialog": {
158
+ "label": "Show Update Confirmation Dialog:",
159
+ "tooltip": "Show a confirmation dialog before updating Anki cards."
160
+ },
157
161
  "url": {
158
162
  "label": "Anki URL:",
159
163
  "tooltip": "The URL to connect to your Anki instance."
@@ -440,9 +444,9 @@
440
444
  "label": "Minimum Replay Size (KB):",
441
445
  "tooltip": "Minimum Replay Size for OBS Replays in KB. If Replay is Under this, Audio/Screenshot Will not be grabbed."
442
446
  },
443
- "turn_off_output_check": {
444
- "label": "Turn Off Output Check:",
445
- "tooltip": "Disable the video output Replay Buffer Check."
447
+ "turn_off_replay_buffer_management": {
448
+ "label": "Turn off Replay Buffer Management:",
449
+ "tooltip": "Disable OBS Replay Buffer Management. This is useful if you want to manually manage the replay buffer in OBS."
446
450
  }
447
451
  },
448
452
  "profiles": {
@@ -153,6 +153,10 @@
153
153
  "label": "Ankiを更新:",
154
154
  "tooltip": "自動的にAnkiを更新します。"
155
155
  },
156
+ "show_update_confirmation_dialog": {
157
+ "label": "更新確認ダイアログを表示:",
158
+ "tooltip": "Ankiカードを更新する前に確認ダイアログを表示します。"
159
+ },
156
160
  "url": {
157
161
  "label": "Anki URL:",
158
162
  "tooltip": "AnkiConnectの接続URL。"
@@ -439,10 +443,10 @@
439
443
  "label": "最小リプレイサイズ (KB):",
440
444
  "tooltip": "このサイズ未満のリプレイは処理しません。"
441
445
  },
442
- "turn_off_output_check": {
443
- "label": "出力チェックを無効化:",
444
- "tooltip": "動画出力のリプレイバッファチェックを無効化します。"
445
- }
446
+ "turn_off_replay_buffer_management": {
447
+ "label": "リプレイバッファ管理を無効化:",
448
+ "tooltip": "OBSリプレイバッファの管理を無効化します。OBSでリプレイバッファを手動で管理したい場合に便利です。"
449
+ }
446
450
  },
447
451
  "profiles": {
448
452
  "title": "プロファイル",
@@ -154,6 +154,10 @@
154
154
  "label": "更新 Anki:",
155
155
  "tooltip": "使用新数据自动更新 Anki。"
156
156
  },
157
+ "show_update_confirmation_dialog": {
158
+ "label": "显示更新确认对话框:",
159
+ "tooltip": "在更新 Anki 卡片之前显示确认对话框。"
160
+ },
157
161
  "url": {
158
162
  "label": "Anki URL:",
159
163
  "tooltip": "连接到您的 Anki 实例的 URL。"
@@ -440,10 +444,10 @@
440
444
  "label": "最小回放大小 (KB):",
441
445
  "tooltip": "OBS 录像回放的最小大小(KB)。如果回放小于此大小,将不抓取音频/截图。"
442
446
  },
443
- "turn_off_output_check": {
444
- "label": "关闭输出检查:",
445
- "tooltip": "禁用视频输出的回放缓冲区检查。"
446
- }
447
+ "turn_off_replay_buffer_management": {
448
+ "label": "关闭回放缓冲区管理:",
449
+ "tooltip": "禁用 OBS 回放缓冲区管理。如果您想在 OBS 中手动管理回放缓冲区,这将很有用。"
450
+ }
447
451
  },
448
452
  "profiles": {
449
453
  "title": "配置文件",
GameSentenceMiner/obs.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import datetime
2
3
  import json
3
4
  import os.path
4
5
  import subprocess
@@ -11,12 +12,11 @@ import shutil
11
12
  import psutil
12
13
 
13
14
  import obsws_python as obs
15
+ import numpy as np
14
16
 
15
17
  from GameSentenceMiner.util import configuration
16
18
  from GameSentenceMiner.util.configuration import get_app_directory, get_config, get_master_config, is_windows, save_full_config, reload_config, logger, gsm_status, gsm_state
17
- from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name
18
- import tkinter as tk
19
- from tkinter import messagebox
19
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename, make_unique_file_name, make_unique_temp_file
20
20
 
21
21
  connection_pool: 'OBSConnectionPool' = None
22
22
  event_client: obs.EventClient = None
@@ -75,7 +75,7 @@ class OBSConnectionPool:
75
75
  self._clients[index] = obs.ReqClient(**self.connection_kwargs)
76
76
 
77
77
  @contextlib.contextmanager
78
- def get_client(self):
78
+ def get_client(self) -> obs.ReqClient:
79
79
  """A context manager to safely get a client from the pool."""
80
80
  with self._idx_lock:
81
81
  idx = self._next_idx
@@ -106,37 +106,114 @@ class OBSConnectionManager(threading.Thread):
106
106
  super().__init__()
107
107
  self.daemon = True
108
108
  self.running = True
109
+ self.should_check_output = check_output
109
110
  self.check_connection_interval = 1
110
- self.said_no_to_replay_buffer = False
111
111
  self.counter = 0
112
- self.check_output = check_output
112
+ self.last_replay_buffer_status = None
113
+ self.no_output_timestamp = None
114
+ self.NO_OUTPUT_SHUTDOWN_SECONDS = 300
115
+ self.last_errors = []
116
+ self.previous_image = None
117
+
118
+ def _check_obs_connection(self):
119
+ try:
120
+ client = connection_pool.get_healthcheck_client() if connection_pool else None
121
+ if client and not connecting:
122
+ client.get_version()
123
+ gsm_status.obs_connected = True
124
+ return True
125
+ else:
126
+ raise ConnectionError("Healthcheck client not available or connection in progress")
127
+ except Exception as e:
128
+ logger.debug(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
129
+ gsm_status.obs_connected = False
130
+ asyncio.run(connect_to_obs())
131
+ return False
132
+
133
+ def check_replay_buffer_enabled(self):
134
+ if not self.should_check_output:
135
+ return True, ""
136
+ output = get_replay_buffer_output()
137
+ if not output:
138
+ return False, "Replay Buffer output not found in OBS. Please enable Replay Buffer In OBS Settings -> Output -> Replay Buffer. I recommend 300 seconds (5 minutes) or higher."
139
+ return True, ""
140
+
141
+ def _manage_replay_buffer_and_utils(self):
142
+ errors = []
143
+
144
+ if not self.should_check_output:
145
+ return errors
146
+
147
+ set_fit_to_screen_for_scene_items(get_current_scene())
148
+
149
+ if not get_config().obs.automatically_manage_replay_buffer:
150
+ errors.append("Automatic Replay Buffer management is disabled in GSM settings.")
151
+ return errors
152
+
153
+ replay_buffer_enabled, error_message = self.check_replay_buffer_enabled()
154
+
155
+ if not replay_buffer_enabled:
156
+ errors.append(error_message)
157
+ return errors
158
+
159
+ current_status = get_replay_buffer_status()
160
+
161
+ if self.last_replay_buffer_status is None:
162
+ self.last_replay_buffer_status = current_status
163
+ return errors
164
+
165
+ if current_status != self.last_replay_buffer_status:
166
+ self.last_replay_buffer_status = current_status
167
+ self.no_output_timestamp = None
168
+ return errors
169
+
170
+ img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
171
+ has_changed = self.has_image_changed(img) if img else True
172
+
173
+ if not has_changed:
174
+ self.no_output_timestamp = None
175
+ if not current_status:
176
+ start_replay_buffer()
177
+ self.last_replay_buffer_status = True
178
+ else: # is_empty
179
+ if current_status:
180
+ if self.no_output_timestamp is None:
181
+ self.no_output_timestamp = time.time()
182
+ elif time.time() - self.no_output_timestamp >= self.NO_OUTPUT_SHUTDOWN_SECONDS:
183
+ stop_replay_buffer()
184
+ self.last_replay_buffer_status = False
185
+ self.no_output_timestamp = None
186
+
187
+ def has_image_changed(self, img):
188
+ if self.previous_image is None:
189
+ self.previous_image = np.array(img)
190
+ return True
191
+ try:
192
+ img1_np = np.array(img) if not isinstance(img, np.ndarray) else img
193
+ img2_np = self.previous_image
194
+ self.previous_image = img1_np
195
+ except Exception:
196
+ logger.warning("Failed to convert images to numpy arrays for comparison.")
197
+ return False
198
+
199
+ return (img1_np.shape == img2_np.shape) and np.array_equal(img1_np, img2_np)
113
200
 
114
201
  def run(self):
202
+ time.sleep(5) # Initial delay to allow OBS to start
115
203
  while self.running:
116
204
  time.sleep(self.check_connection_interval)
117
- try:
118
- client = connection_pool.get_healthcheck_client() if connection_pool else None
119
- if client and not connecting:
120
- client.get_version()
121
- else:
122
- raise ConnectionError("Healthcheck client not healthy or not initialized")
123
- except Exception as e:
124
- logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
125
- gsm_status.obs_connected = False
126
- asyncio.run(connect_to_obs())
205
+
206
+ if not self._check_obs_connection():
207
+ continue
208
+
127
209
  if self.counter % 5 == 0:
128
210
  try:
129
- set_fit_to_screen_for_scene_items(get_current_scene())
130
- if get_config().obs.turn_off_output_check and self.check_output:
131
- replay_buffer_status = get_replay_buffer_status()
132
- if replay_buffer_status and self.said_no_to_replay_buffer:
133
- self.said_no_to_replay_buffer = False
134
- self.counter = 0
135
- if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
136
- try:
137
- self.check_output()
138
- except Exception:
139
- pass
211
+ errors = self._manage_replay_buffer_and_utils()
212
+ if errors != self.last_errors:
213
+ if errors:
214
+ for error in errors:
215
+ logger.error(f"OBS Health Check: {error}")
216
+ self.last_errors = errors
140
217
  except Exception as e:
141
218
  logger.error(f"Error when running Extra Utils in OBS Health Check, Keeping ConnectionManager Alive: {e}")
142
219
  self.counter += 1
@@ -144,28 +221,6 @@ class OBSConnectionManager(threading.Thread):
144
221
  def stop(self):
145
222
  self.running = False
146
223
 
147
- def check_output(self):
148
- img = get_screenshot_PIL(compression=100, img_format='jpg', width=1280, height=720)
149
- extrema = img.getextrema()
150
- if isinstance(extrema[0], tuple):
151
- is_empty = all(e[0] == e[1] for e in extrema)
152
- else:
153
- is_empty = extrema[0] == extrema[1]
154
- if is_empty:
155
- return
156
- else:
157
- root = tk.Tk()
158
- root.attributes('-topmost', True)
159
- root.withdraw()
160
- root.deiconify()
161
- result = messagebox.askyesno("GSM - Replay Buffer", "The replay buffer is not running, but there seems to be output in OBS. Do you want to start it? (If you click 'No', you won't be asked until you either restart GSM or start/stop replay buffer manually.)")
162
- root.destroy()
163
- if not result:
164
- self.said_no_to_replay_buffer = True
165
- self.counter = 0
166
- return
167
- start_replay_buffer()
168
-
169
224
  def get_obs_path():
170
225
  return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
171
226
 
@@ -232,6 +287,7 @@ async def wait_for_obs_connected():
232
287
  for _ in range(10):
233
288
  try:
234
289
  with connection_pool.get_client() as client:
290
+ client: obs.ReqClient
235
291
  response = client.get_version()
236
292
  if response:
237
293
  return True
@@ -313,6 +369,8 @@ async def connect_to_obs(retry=5, connections=2, check_output=False):
313
369
  obs_connection_manager = OBSConnectionManager(check_output=check_output)
314
370
  obs_connection_manager.start()
315
371
  update_current_game()
372
+ if get_config().features.generate_longplay and check_output:
373
+ start_recording(True)
316
374
  break # Exit the loop once connected
317
375
  except Exception as e:
318
376
  if retry <= 0:
@@ -368,8 +426,8 @@ def do_obs_call(method_name: str, from_dict=None, retry=3, **kwargs):
368
426
  def toggle_replay_buffer():
369
427
  try:
370
428
  with connection_pool.get_client() as client:
371
- response = client.toggle_replay_buffer()
372
- if response:
429
+ client: obs.ReqClient
430
+ client.toggle_replay_buffer()
373
431
  logger.info("Replay buffer Toggled.")
374
432
  except Exception as e:
375
433
  logger.error(f"Error toggling buffer: {e}")
@@ -377,8 +435,10 @@ def toggle_replay_buffer():
377
435
  def start_replay_buffer():
378
436
  try:
379
437
  with connection_pool.get_client() as client:
380
- response = client.start_replay_buffer()
381
- if response and response.ok:
438
+ client: obs.ReqClient
439
+ client.start_replay_buffer()
440
+ if get_config().features.generate_longplay:
441
+ start_recording(True)
382
442
  logger.info("Replay buffer started.")
383
443
  except Exception as e:
384
444
  logger.error(f"Error starting replay buffer: {e}")
@@ -394,8 +454,10 @@ def get_replay_buffer_status():
394
454
  def stop_replay_buffer():
395
455
  try:
396
456
  with connection_pool.get_client() as client:
397
- response = client.stop_replay_buffer()
398
- if response and response.ok:
457
+ client: obs.ReqClient
458
+ client.stop_replay_buffer()
459
+ if get_config().features.generate_longplay:
460
+ stop_recording()
399
461
  logger.info("Replay buffer stopped.")
400
462
  except Exception as e:
401
463
  logger.warning(f"Error stopping replay buffer: {e}")
@@ -403,15 +465,49 @@ def stop_replay_buffer():
403
465
  def save_replay_buffer():
404
466
  try:
405
467
  with connection_pool.get_client() as client:
406
- response = client.save_replay_buffer()
407
- if response and response.ok:
468
+ client: obs.ReqClient
469
+ client.save_replay_buffer()
408
470
  logger.info("Replay buffer saved. If your log stops here, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
409
471
  except Exception as e:
410
472
  raise Exception(f"Error saving replay buffer: {e}")
411
473
 
474
+ def start_recording(longplay=False):
475
+ try:
476
+ with connection_pool.get_client() as client:
477
+ client: obs.ReqClient
478
+ if longplay:
479
+ gsm_state.recording_started_time = datetime.datetime.now()
480
+ gsm_state.current_srt = make_unique_temp_file(f"{get_current_game(sanitize=True)}.srt")
481
+ gsm_state.srt_index = 1
482
+ client.start_record()
483
+ logger.info("Recording started.")
484
+ except Exception as e:
485
+ logger.error(f"Error starting recording: {e}")
486
+ return None
487
+
488
+ def stop_recording():
489
+ try:
490
+ with connection_pool.get_client() as client:
491
+ client: obs.ReqClient
492
+ client.stop_record()
493
+ logger.info("Recording stopped.")
494
+ except Exception as e:
495
+ logger.error(f"Error stopping recording: {e}")
496
+
497
+ def get_last_recording_filename():
498
+ try:
499
+ with connection_pool.get_client() as client:
500
+ client: obs.ReqClient
501
+ response = client.get_record_status()
502
+ return response.recording_filename if response else ''
503
+ except Exception as e:
504
+ logger.error(f"Error getting last recording filename: {e}")
505
+ return ''
506
+
412
507
  def get_current_scene():
413
508
  try:
414
509
  with connection_pool.get_client() as client:
510
+ client: obs.ReqClient
415
511
  response = client.get_current_program_scene()
416
512
  return response.scene_name if response else ''
417
513
  except Exception as e:
@@ -421,6 +517,7 @@ def get_current_scene():
421
517
  def get_source_from_scene(scene_name):
422
518
  try:
423
519
  with connection_pool.get_client() as client:
520
+ client: obs.ReqClient
424
521
  response = client.get_scene_item_list(name=scene_name)
425
522
  return response.scene_items[0] if response and response.scene_items else ''
426
523
  except Exception as e:
@@ -436,15 +533,80 @@ def get_active_source():
436
533
  def get_record_directory():
437
534
  try:
438
535
  with connection_pool.get_client() as client:
536
+ client: obs.ReqClient
439
537
  response = client.get_record_directory()
440
538
  return response.record_directory if response else ''
441
539
  except Exception as e:
442
540
  logger.error(f"Error getting recording folder: {e}")
443
541
  return ''
542
+
543
+ def get_replay_buffer_max_time_seconds():
544
+ """
545
+ Gets the configured maximum replay buffer time in seconds using the v5 protocol.
546
+ """
547
+ try:
548
+ # Assumes a connection_pool object that provides a connected client
549
+ with connection_pool.get_client() as client:
550
+ client: obs.ReqClient
551
+ # For v5, we get settings for the 'replay_buffer' output
552
+ response = client.get_output_settings(name='Replay Buffer')
553
+
554
+ print(response.output_settings)
555
+
556
+ # The response object contains a dict of the actual settings
557
+ if response:
558
+ # The key for replay buffer length in seconds is 'max_time_sec'
559
+ settings = response.output_settings
560
+ if settings and 'max_time_sec' in settings:
561
+ return settings['max_time_sec']
562
+ else:
563
+ logger.warning("Replay buffer settings received, but 'max_time_sec' key was not found.")
564
+ return 0
565
+ else:
566
+ logger.warning(f"get_output_settings for replay_buffer failed: {response.status}")
567
+ return 0
568
+ except Exception as e:
569
+ logger.error(f"Exception while fetching replay buffer settings: {e}")
570
+ return 0
571
+
572
+ def enable_replay_buffer():
573
+ try:
574
+ with connection_pool.get_client() as client:
575
+ client: obs.ReqClient
576
+ response = client.set_output_settings(name='Replay Buffer', settings={'outputFlags': {'OBS_OUTPUT_AUDIO': True, 'OBS_OUTPUT_ENCODED': True, 'OBS_OUTPUT_MULTI_TRACK': True, 'OBS_OUTPUT_SERVICE': False, 'OBS_OUTPUT_VIDEO': True}})
577
+ if response and response.ok:
578
+ logger.info("Replay buffer enabled.")
579
+ return True
580
+ else:
581
+ logger.error(f"Failed to enable replay buffer: {response.status if response else 'No response'}")
582
+ return False
583
+ except Exception as e:
584
+ logger.error(f"Error enabling replay buffer: {e}")
585
+ return False
586
+
587
+ def get_output_list():
588
+ try:
589
+ with connection_pool.get_client() as client:
590
+ client: obs.ReqClient
591
+ response = client.get_output_list()
592
+ return response.outputs if response else None
593
+ except Exception as e:
594
+ logger.error(f"Error getting output list: {e}")
595
+ return None
596
+
597
+ def get_replay_buffer_output():
598
+ outputs = get_output_list()
599
+ if not outputs:
600
+ return None
601
+ for output in outputs:
602
+ if output.get('outputKind') == 'replay_buffer':
603
+ return output
604
+ return None
444
605
 
445
606
  def get_obs_scenes():
446
607
  try:
447
608
  with connection_pool.get_client() as client:
609
+ client: obs.ReqClient
448
610
  response = client.get_scene_list()
449
611
  return response.scenes if response else None
450
612
  except Exception as e:
@@ -526,6 +688,7 @@ def get_screenshot_PIL(source_name=None, compression=75, img_format='png', width
526
688
  return None
527
689
  while True:
528
690
  with connection_pool.get_client() as client:
691
+ client: obs.ReqClient
529
692
  response = client.get_source_screenshot(name=source_name, img_format=img_format, quality=compression, width=width, height=height)
530
693
  try:
531
694
  response.image_data = response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
@@ -567,6 +730,7 @@ def set_fit_to_screen_for_scene_items(scene_name: str):
567
730
 
568
731
  try:
569
732
  with connection_pool.get_client() as client:
733
+ client: obs.ReqClient
570
734
  # 1. Get the canvas (base) resolution from OBS video settings
571
735
  video_settings = client.get_video_settings()
572
736
  if not hasattr(video_settings, 'base_width') or not hasattr(video_settings, 'base_height'):
@@ -686,6 +850,7 @@ def main():
686
850
 
687
851
  def create_scene():
688
852
  with connection_pool.get_client() as client:
853
+ client: obs.ReqClient
689
854
  # Extract fields from request_json
690
855
  request_json = r'{"sceneName":"SILENT HILL f","inputName":"SILENT HILL f - Capture","inputKind":"window_capture","inputSettings":{"mode":"window","window":"SILENT HILL f :UnrealWindow:SHf-Win64-Shipping.exe","capture_audio":true,"cursor":false,"method":"2"}}'
691
856
  request_dict = json.loads(request_json)
@@ -701,7 +866,21 @@ def create_scene():
701
866
  if __name__ == '__main__':
702
867
  logging.basicConfig(level=logging.INFO)
703
868
  connect_to_obs_sync()
704
- img = get_screenshot_PIL(source_name='Display Capture 2', compression=100, img_format='jpg', width=2560, height=1440)
705
- img.show()
869
+
870
+ save_replay_buffer()
871
+ # img = get_screenshot_PIL(source_name='Display Capture 2', compression=100, img_format='jpg', width=2560, height=1440)
872
+ # img.show()
873
+ # output_list = get_output_list()
874
+ # print(output_list)
875
+
876
+ # response = enable_replay_buffer()
877
+ # print(response)
878
+
879
+ # response = get_replay_buffer_max_time_seconds()
880
+ # # response is dataclass with attributes, print attributes
881
+ # print(response)
882
+
883
+ # response = enable_replay_buffer()
884
+ # print(response)
706
885
  # # set_fit_to_screen_for_scene_items(get_current_scene())
707
886
  # create_scene()