GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.1__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.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/ai/ai_prompting.py +6 -6
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
- GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
- GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
- GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
- GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
- GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
- gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
- gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
444
|
-
"label": "Turn
|
|
445
|
-
"tooltip": "Disable
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
705
|
-
|
|
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()
|