GameSentenceMiner 2.8.41__tar.gz → 2.8.42__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.42}/GameSentenceMiner/anki.py +8 -2
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ffmpeg.py +89 -7
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/gsm.py +4 -3
- gamesentenceminer-2.8.42/GameSentenceMiner/obs_back.py +309 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/owocr_helper.py +48 -67
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/run.py +51 -17
- gamesentenceminer-2.8.42/GameSentenceMiner/ss_selector.py +121 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/util.py +6 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/SOURCES.txt +2 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/PKG-INFO +1 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/pyproject.toml +1 -1
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ai/ai_prompting.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/send.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/websocket.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/config_gui.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/configuration.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/electron_config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/gametext.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/model.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/notification.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/obs.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/package.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/text_log.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/result.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/silero_trim.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/vosk_helper.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/whisper_helper.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/templates/__init__.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/templates/utility.html +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/texthooking_page.py +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/requires.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/LICENSE +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/README.md +0 -0
- {gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/setup.cfg +0 -0
@@ -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):
|
@@ -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
|
@@ -205,18 +205,18 @@ class WebsocketServerThread(threading.Thread):
|
|
205
205
|
|
206
206
|
all_cords = None
|
207
207
|
rectangles = None
|
208
|
+
last_ocr2_result = ""
|
208
209
|
|
209
|
-
def do_second_ocr(ocr1_text,
|
210
|
-
global twopassocr, ocr2,
|
210
|
+
def do_second_ocr(ocr1_text, time, img, filtering):
|
211
|
+
global twopassocr, ocr2, last_ocr2_result
|
211
212
|
try:
|
212
213
|
orig_text, text = run.process_and_write_results(img, None, None, None, None,
|
213
214
|
engine=ocr2)
|
214
|
-
|
215
|
-
if fuzz.ratio(previous_ocr2_text, text) >= 80:
|
215
|
+
if fuzz.ratio(last_ocr2_result, text) >= 80:
|
216
216
|
logger.info("Seems like the same text from previous ocr2 result, not sending")
|
217
217
|
return
|
218
218
|
save_result_image(img)
|
219
|
-
|
219
|
+
last_ocr2_result = text
|
220
220
|
send_result(text, time)
|
221
221
|
except json.JSONDecodeError:
|
222
222
|
print("Invalid JSON received.")
|
@@ -243,91 +243,74 @@ def send_result(text, time):
|
|
243
243
|
websocket_server_thread.send_text(text, time)
|
244
244
|
|
245
245
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
246
|
+
previous_text = "" # Store last OCR result
|
247
|
+
last_oneocr_time = None # Store last OCR time
|
248
|
+
text_stable_start_time = None # Store the start time when text becomes stable
|
249
|
+
previous_img = None
|
250
|
+
orig_text_result = "" # Store original text result
|
251
251
|
TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
|
252
252
|
|
253
|
-
def text_callback(text, orig_text,
|
254
|
-
global twopassocr, ocr2,
|
253
|
+
def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering=None):
|
254
|
+
global twopassocr, ocr2, previous_text, last_oneocr_time, text_stable_start_time, orig_text_result, previous_img
|
255
255
|
orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
|
256
|
-
# logger.debug(orig_text_string)
|
257
256
|
if came_from_ss:
|
258
257
|
save_result_image(img)
|
259
258
|
send_result(text, time)
|
260
259
|
return
|
261
260
|
|
262
|
-
|
261
|
+
line_start_time = time if time else datetime.now()
|
263
262
|
|
264
|
-
previous_text = last_oneocr_results_to_check.pop(rectangle_index, "").strip()
|
265
|
-
previous_orig_text = orig_text_results.get(rectangle_index, "").strip()
|
266
|
-
|
267
|
-
# print(previous_orig_text)
|
268
|
-
# if orig_text:
|
269
|
-
# print(orig_text_string)
|
270
263
|
if not twopassocr:
|
271
|
-
if
|
264
|
+
if previous_text and fuzz.ratio(orig_text_string, previous_text) >= 80:
|
272
265
|
logger.info("Seems like Text we already sent, not doing anything.")
|
273
266
|
return
|
274
267
|
save_result_image(img)
|
275
268
|
send_result(text, time)
|
276
|
-
|
277
|
-
|
269
|
+
orig_text_result = orig_text_string
|
270
|
+
previous_text = previous_text
|
271
|
+
previous_img = None
|
272
|
+
text_stable_start_time = None
|
273
|
+
last_oneocr_time = None
|
278
274
|
return
|
279
275
|
if not text:
|
280
276
|
if previous_text:
|
281
|
-
if
|
282
|
-
stable_time =
|
283
|
-
|
284
|
-
|
285
|
-
if previous_result and fuzz.ratio(previous_result, previous_text) >= 80:
|
286
|
-
logger.info("Seems like the same text, not " + "doing second OCR" if twopassocr else "sending")
|
287
|
-
return
|
288
|
-
if previous_orig_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 80:
|
277
|
+
if text_stable_start_time:
|
278
|
+
stable_time = text_stable_start_time
|
279
|
+
previous_img_local = previous_img
|
280
|
+
if fuzz.ratio(orig_text_string, previous_text) >= 80:
|
289
281
|
logger.info("Seems like Text we already sent, not doing anything.")
|
290
282
|
return
|
291
|
-
|
292
|
-
|
293
|
-
do_second_ocr(previous_text,
|
283
|
+
orig_text_result = orig_text_string
|
284
|
+
previous_text = previous_text
|
285
|
+
do_second_ocr(previous_text, stable_time, previous_img_local, filtering)
|
286
|
+
previous_img = None
|
287
|
+
text_stable_start_time = None
|
288
|
+
last_oneocr_time = None
|
294
289
|
return
|
295
290
|
return
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
previous_imgs[rectangle_index] = img
|
302
|
-
return
|
303
|
-
|
304
|
-
stable = text_stable_start_times.get(rectangle_index)
|
305
|
-
|
306
|
-
if stable:
|
307
|
-
time_since_stable_ms = int((current_time - stable).total_seconds() * 1000)
|
308
|
-
|
309
|
-
if time_since_stable_ms >= TEXT_APPEARENCE_DELAY:
|
310
|
-
last_oneocr_results_to_check[rectangle_index] = text
|
311
|
-
last_oneocr_times[rectangle_index] = current_time
|
312
|
-
else:
|
313
|
-
last_oneocr_results_to_check[rectangle_index] = text
|
314
|
-
last_oneocr_times[rectangle_index] = current_time
|
315
|
-
previous_imgs[rectangle_index] = img
|
291
|
+
if not text_stable_start_time:
|
292
|
+
text_stable_start_time = line_start_time
|
293
|
+
previous_text = orig_text
|
294
|
+
last_oneocr_time = line_start_time
|
295
|
+
previous_img = img
|
316
296
|
|
317
297
|
done = False
|
318
298
|
|
319
299
|
|
320
|
-
def run_oneocr(ocr_config: OCRConfig,
|
300
|
+
def run_oneocr(ocr_config: OCRConfig, area=False):
|
321
301
|
global done
|
302
|
+
print("Running OneOCR")
|
322
303
|
screen_area = None
|
323
|
-
|
324
|
-
|
304
|
+
screen_areas = []
|
305
|
+
for rect_config in ocr_config.rectangles:
|
325
306
|
coords = rect_config.coordinates
|
326
307
|
monitor_config = rect_config.monitor
|
327
308
|
screen_area = ",".join(str(c) for c in coords) if area else None
|
309
|
+
if screen_area:
|
310
|
+
screen_areas.append(screen_area)
|
328
311
|
exclusions = list(rect.coordinates for rect in list(filter(lambda x: x.is_excluded, ocr_config.rectangles)))
|
329
312
|
run.run(read_from="screencapture",
|
330
|
-
read_from_secondary="clipboard"
|
313
|
+
read_from_secondary="clipboard",
|
331
314
|
write_to="callback",
|
332
315
|
screen_capture_area=screen_area,
|
333
316
|
# screen_capture_monitor=monitor_config['index'],
|
@@ -336,10 +319,11 @@ def run_oneocr(ocr_config: OCRConfig, i, area=False):
|
|
336
319
|
screen_capture_delay_secs=get_ocr_scan_rate(), engine=ocr1,
|
337
320
|
text_callback=text_callback,
|
338
321
|
screen_capture_exclusions=exclusions,
|
339
|
-
rectangle=i,
|
340
322
|
language=language,
|
341
323
|
monitor_index=ocr_config.window,
|
342
|
-
ocr2=ocr2
|
324
|
+
ocr2=ocr2,
|
325
|
+
gsm_ocr_config=ocr_config,
|
326
|
+
screen_capture_areas=screen_areas)
|
343
327
|
done = True
|
344
328
|
|
345
329
|
|
@@ -400,17 +384,14 @@ if __name__ == "__main__":
|
|
400
384
|
logger.info(f"Starting OCR with configuration: Window: {ocr_config.window}, Rectangles: {ocr_config.rectangles}, Engine 1: {ocr1}, Engine 2: {ocr2}, Two-pass OCR: {twopassocr}")
|
401
385
|
if ocr_config:
|
402
386
|
rectangles = list(filter(lambda rect: not rect.is_excluded, ocr_config.rectangles))
|
403
|
-
last_ocr1_results = [""] * len(rectangles) if rectangles else [""]
|
404
|
-
last_ocr2_results = [""] * len(rectangles) if rectangles else [""]
|
405
387
|
oneocr_threads = []
|
406
388
|
run.init_config(False)
|
407
389
|
if rectangles:
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
thread.start()
|
390
|
+
thread = threading.Thread(target=run_oneocr, args=(ocr_config,True, ), daemon=True)
|
391
|
+
oneocr_threads.append(thread)
|
392
|
+
thread.start()
|
412
393
|
else:
|
413
|
-
single_ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config,
|
394
|
+
single_ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config,False, ), daemon=True)
|
414
395
|
oneocr_threads.append(single_ocr_thread)
|
415
396
|
single_ocr_thread.start()
|
416
397
|
websocket_server_thread = WebsocketServerThread(read=True)
|
@@ -379,7 +379,7 @@ class TextFiltering:
|
|
379
379
|
else:
|
380
380
|
orig_text_filtered.append(None)
|
381
381
|
|
382
|
-
if last_result[1] == engine_index:
|
382
|
+
if last_result and last_result[1] == engine_index:
|
383
383
|
last_text = last_result[0]
|
384
384
|
else:
|
385
385
|
last_text = []
|
@@ -399,6 +399,7 @@ class TextFiltering:
|
|
399
399
|
break
|
400
400
|
else:
|
401
401
|
for block in new_blocks:
|
402
|
+
print(block)
|
402
403
|
if lang not in ["ja", "zh"] or self.classify(block)[0] == lang:
|
403
404
|
final_blocks.append(block)
|
404
405
|
|
@@ -407,7 +408,7 @@ class TextFiltering:
|
|
407
408
|
|
408
409
|
|
409
410
|
class ScreenshotClass:
|
410
|
-
def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows):
|
411
|
+
def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas):
|
411
412
|
self.macos_window_tracker_instance = None
|
412
413
|
self.windows_window_tracker_instance = None
|
413
414
|
self.screencapture_window_active = True
|
@@ -415,6 +416,7 @@ class ScreenshotClass:
|
|
415
416
|
self.custom_left = None
|
416
417
|
self.screen_capture_exclusions = screen_capture_exclusions
|
417
418
|
self.screen_capture_window = screen_capture_window
|
419
|
+
self.areas = []
|
418
420
|
if screen_capture_area == '':
|
419
421
|
self.screencapture_mode = 0
|
420
422
|
elif screen_capture_area.startswith('screen_'):
|
@@ -463,9 +465,15 @@ class ScreenshotClass:
|
|
463
465
|
|
464
466
|
self.sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height}
|
465
467
|
logger.opt(ansi=True).info(f'Selected coordinates: {coord_left},{coord_top},{coord_width},{coord_height}')
|
466
|
-
if
|
467
|
-
|
468
|
-
|
468
|
+
if screen_capture_areas:
|
469
|
+
for area in screen_capture_areas:
|
470
|
+
if len(area.split(',')) == 4:
|
471
|
+
self.areas.append(([int(c.strip()) for c in area.split(',')]))
|
472
|
+
else:
|
473
|
+
if len(screen_capture_area.split(',')) == 4:
|
474
|
+
self.areas.append(([int(c.strip()) for c in screen_capture_area.split(',')]))
|
475
|
+
|
476
|
+
self.areas.sort(key=lambda rect: (rect[1], rect[0]))
|
469
477
|
|
470
478
|
|
471
479
|
if self.screencapture_mode == 2 or self.screen_capture_window:
|
@@ -724,8 +732,35 @@ class ScreenshotClass:
|
|
724
732
|
left, top, width, height = exclusion
|
725
733
|
draw.rectangle((left, top, left + width, top + height), fill=(0, 0, 0, 0))
|
726
734
|
|
727
|
-
|
728
|
-
|
735
|
+
cropped_sections = []
|
736
|
+
start = time.time()
|
737
|
+
for area in self.areas:
|
738
|
+
cropped_sections.append(img.crop((area[0], area[1], area[0] + area[2], area[1] + area[3])))
|
739
|
+
|
740
|
+
# if len(cropped_sections) > 1:
|
741
|
+
# combined_width = sum(section.width for section in cropped_sections)
|
742
|
+
# combined_height = max(section.height for section in cropped_sections)
|
743
|
+
# combined_img = Image.new("RGBA", (combined_width, combined_height))
|
744
|
+
#
|
745
|
+
# x_offset = 0
|
746
|
+
# for section in cropped_sections:
|
747
|
+
# combined_img.paste(section, (x_offset, 0))
|
748
|
+
# x_offset += section.width
|
749
|
+
#
|
750
|
+
# img = combined_img
|
751
|
+
if len(cropped_sections) > 1:
|
752
|
+
combined_width = max(section.width for section in cropped_sections)
|
753
|
+
combined_height = sum(section.height for section in cropped_sections) + (len(cropped_sections) - 1) * 10 # Add space for gaps
|
754
|
+
combined_img = Image.new("RGBA", (combined_width, combined_height))
|
755
|
+
|
756
|
+
y_offset = 0
|
757
|
+
for section in cropped_sections:
|
758
|
+
combined_img.paste(section, (0, y_offset))
|
759
|
+
y_offset += section.height + 50 # Add gap between sections
|
760
|
+
|
761
|
+
img = combined_img
|
762
|
+
elif cropped_sections:
|
763
|
+
img = cropped_sections[0]
|
729
764
|
|
730
765
|
if rand_int == 1:
|
731
766
|
img.save(os.path.join(get_temporary_directory(), 'after_crop.png'), 'PNG')
|
@@ -858,7 +893,7 @@ def on_window_minimized(minimized):
|
|
858
893
|
screencapture_window_visible = not minimized
|
859
894
|
|
860
895
|
|
861
|
-
def process_and_write_results(img_or_path, write_to=None, last_result=None, filtering=None, notify=None, engine=None,
|
896
|
+
def process_and_write_results(img_or_path, write_to=None, last_result=None, filtering=None, notify=None, engine=None, ocr_start_time=None):
|
862
897
|
global engine_index
|
863
898
|
if auto_pause_handler:
|
864
899
|
auto_pause_handler.stop()
|
@@ -900,7 +935,7 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
|
|
900
935
|
elif write_to == 'clipboard':
|
901
936
|
pyperclipfix.copy(text)
|
902
937
|
elif write_to == "callback":
|
903
|
-
txt_callback(text, orig_text,
|
938
|
+
txt_callback(text, orig_text, ocr_start_time, img_or_path, bool(engine), filtering)
|
904
939
|
elif write_to:
|
905
940
|
with Path(write_to).open('a', encoding='utf-8') as f:
|
906
941
|
f.write(text + '\n')
|
@@ -937,6 +972,7 @@ def run(read_from=None,
|
|
937
972
|
combo_pause=None,
|
938
973
|
combo_engine_switch=None,
|
939
974
|
screen_capture_area=None,
|
975
|
+
screen_capture_areas=None,
|
940
976
|
screen_capture_exclusions=None,
|
941
977
|
screen_capture_window=None,
|
942
978
|
screen_capture_delay_secs=None,
|
@@ -944,11 +980,11 @@ def run(read_from=None,
|
|
944
980
|
screen_capture_combo=None,
|
945
981
|
stop_running_flag=None,
|
946
982
|
screen_capture_event_bus=None,
|
947
|
-
rectangle=None,
|
948
983
|
text_callback=None,
|
949
984
|
language=None,
|
950
985
|
monitor_index=None,
|
951
986
|
ocr2=None,
|
987
|
+
gsm_ocr_config=None,
|
952
988
|
):
|
953
989
|
"""
|
954
990
|
Japanese OCR client
|
@@ -1004,9 +1040,6 @@ def run(read_from=None,
|
|
1004
1040
|
if screen_capture_event_bus is None:
|
1005
1041
|
screen_capture_event_bus = config.get_general('screen_capture_event_bus')
|
1006
1042
|
|
1007
|
-
if rectangle is None:
|
1008
|
-
rectangle = config.get_general('rectangle')
|
1009
|
-
|
1010
1043
|
if text_callback is None:
|
1011
1044
|
text_callback = config.get_general('text_callback')
|
1012
1045
|
|
@@ -1063,8 +1096,8 @@ def run(read_from=None,
|
|
1063
1096
|
global paused
|
1064
1097
|
global just_unpaused
|
1065
1098
|
global first_pressed
|
1066
|
-
global notifier
|
1067
1099
|
global auto_pause_handler
|
1100
|
+
global notifier
|
1068
1101
|
global websocket_server_thread
|
1069
1102
|
global image_queue
|
1070
1103
|
custom_left = None
|
@@ -1124,7 +1157,8 @@ def run(read_from=None,
|
|
1124
1157
|
if screen_capture_combo != '':
|
1125
1158
|
screen_capture_on_combo = True
|
1126
1159
|
key_combos[screen_capture_combo] = on_screenshot_combo
|
1127
|
-
take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows)
|
1160
|
+
take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas)
|
1161
|
+
# global_take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, rectangle)
|
1128
1162
|
filtering = TextFiltering()
|
1129
1163
|
read_from_readable.append('screen capture')
|
1130
1164
|
if 'websocket' in (read_from, read_from_secondary):
|
@@ -1201,11 +1235,11 @@ def run(read_from=None,
|
|
1201
1235
|
break
|
1202
1236
|
elif img:
|
1203
1237
|
if filter_img:
|
1204
|
-
res, _ = process_and_write_results(img, write_to, last_result, filtering, notify,
|
1238
|
+
res, _ = process_and_write_results(img, write_to, last_result, filtering, notify, ocr_start_time=ocr_start_time)
|
1205
1239
|
if res:
|
1206
1240
|
last_result = (res, engine_index)
|
1207
1241
|
else:
|
1208
|
-
process_and_write_results(img, write_to, None, notify=notify,
|
1242
|
+
process_and_write_results(img, write_to, None, notify=notify, ocr_start_time=ocr_start_time, engine=ocr2)
|
1209
1243
|
if isinstance(img, Path):
|
1210
1244
|
if delete_images:
|
1211
1245
|
Path.unlink(img)
|
@@ -0,0 +1,121 @@
|
|
1
|
+
import tkinter as tk
|
2
|
+
from PIL import Image, ImageTk
|
3
|
+
import subprocess
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
|
7
|
+
from GameSentenceMiner import ffmpeg
|
8
|
+
from GameSentenceMiner.configuration import get_temporary_directory, logger
|
9
|
+
from GameSentenceMiner.ffmpeg import ffmpeg_base_command_list
|
10
|
+
from GameSentenceMiner.util import sanitize_filename
|
11
|
+
|
12
|
+
def extract_frames(video_path, timestamp, temp_dir, mode):
|
13
|
+
frame_paths = []
|
14
|
+
timestamp_number = float(timestamp)
|
15
|
+
golden_frame_index = 1 # Default to the first frame
|
16
|
+
golden_frame = None
|
17
|
+
video_duration = ffmpeg.get_video_duration(video_path)
|
18
|
+
|
19
|
+
if mode == 'middle':
|
20
|
+
timestamp_number = max(0.0, timestamp_number - 2.5)
|
21
|
+
elif mode == 'end':
|
22
|
+
timestamp_number = max(0.0, timestamp_number - 5.0)
|
23
|
+
|
24
|
+
if video_duration is not None and timestamp_number > video_duration:
|
25
|
+
logger.debug(f"Timestamp {timestamp_number} exceeds video duration {video_duration}.")
|
26
|
+
return None
|
27
|
+
|
28
|
+
try:
|
29
|
+
command = ffmpeg_base_command_list + [
|
30
|
+
"-y",
|
31
|
+
"-ss", str(timestamp_number),
|
32
|
+
"-i", video_path,
|
33
|
+
"-vf", f"fps=1/{0.25}",
|
34
|
+
"-vframes", "20",
|
35
|
+
os.path.join(temp_dir, "frame_%02d.png")
|
36
|
+
]
|
37
|
+
subprocess.run(command, check=True, capture_output=True)
|
38
|
+
for i in range(1, 21):
|
39
|
+
if os.path.exists(os.path.join(temp_dir, f"frame_{i:02d}.png")):
|
40
|
+
frame_paths.append(os.path.join(temp_dir, f"frame_{i:02d}.png"))
|
41
|
+
|
42
|
+
if mode == "beginning":
|
43
|
+
golden_frame = frame_paths[0]
|
44
|
+
if mode == "middle":
|
45
|
+
golden_frame = frame_paths[len(frame_paths) // 2]
|
46
|
+
if mode == "end":
|
47
|
+
golden_frame = frame_paths[-1]
|
48
|
+
except subprocess.CalledProcessError as e:
|
49
|
+
logger.debug(f"Error extracting frames: {e}")
|
50
|
+
logger.debug(f"Command was: {' '.join(command)}")
|
51
|
+
logger.debug(f"FFmpeg output:\n{e.stderr.decode()}")
|
52
|
+
return None
|
53
|
+
except Exception as e:
|
54
|
+
logger.debug(f"An error occurred: {e}")
|
55
|
+
return None
|
56
|
+
return frame_paths, golden_frame
|
57
|
+
|
58
|
+
def timestamp_to_seconds(timestamp):
|
59
|
+
hours, minutes, seconds = map(int, timestamp.split(':'))
|
60
|
+
return hours * 3600 + minutes * 60 + seconds
|
61
|
+
|
62
|
+
def display_images(image_paths, golden_frame):
|
63
|
+
window = tk.Tk()
|
64
|
+
window.configure(bg="black") # Set the background color to black
|
65
|
+
window.title("Image Selector")
|
66
|
+
selected_path = tk.StringVar()
|
67
|
+
image_widgets = []
|
68
|
+
|
69
|
+
def on_image_click(event):
|
70
|
+
widget = event.widget
|
71
|
+
index = image_widgets.index(widget)
|
72
|
+
selected_path.set(image_paths[index])
|
73
|
+
window.quit()
|
74
|
+
|
75
|
+
for i, path in enumerate(image_paths):
|
76
|
+
img = Image.open(path)
|
77
|
+
img.thumbnail((450, 450))
|
78
|
+
img_tk = ImageTk.PhotoImage(img)
|
79
|
+
if golden_frame and path == golden_frame:
|
80
|
+
label = tk.Label(window, image=img_tk, borderwidth=5, relief="solid")
|
81
|
+
label.config(highlightbackground="yellow", highlightthickness=5)
|
82
|
+
else:
|
83
|
+
label = tk.Label(window, image=img_tk)
|
84
|
+
label.image = img_tk
|
85
|
+
label.grid(row=i // 5, column=i % 5, padx=5, pady=5)
|
86
|
+
label.bind("<Button-1>", on_image_click) # Bind click event to the label
|
87
|
+
image_widgets.append(label)
|
88
|
+
|
89
|
+
window.attributes("-topmost", True)
|
90
|
+
window.mainloop()
|
91
|
+
return selected_path.get()
|
92
|
+
|
93
|
+
def run_extraction_and_display(video_path, timestamp_str, mode):
|
94
|
+
temp_dir = os.path.join(get_temporary_directory(), "screenshot_frames", sanitize_filename(os.path.splitext(os.path.basename(video_path))[0]))
|
95
|
+
os.makedirs(temp_dir, exist_ok=True)
|
96
|
+
image_paths, golden_frame = extract_frames(video_path, timestamp_str, temp_dir, mode)
|
97
|
+
if image_paths:
|
98
|
+
selected_image_path = display_images(image_paths, golden_frame)
|
99
|
+
if selected_image_path:
|
100
|
+
print(selected_image_path)
|
101
|
+
else:
|
102
|
+
logger.debug("No image was selected.")
|
103
|
+
else:
|
104
|
+
logger.debug("Frame extraction failed.")
|
105
|
+
|
106
|
+
def main():
|
107
|
+
# if len(sys.argv) != 3:
|
108
|
+
# print("Usage: python script.py <video_path> <timestamp>")
|
109
|
+
# sys.exit(1)
|
110
|
+
try:
|
111
|
+
video_path = sys.argv[1]
|
112
|
+
timestamp_str = sys.argv[2]
|
113
|
+
mode = sys.argv[3] if len(sys.argv) > 3 else "beginning"
|
114
|
+
run_extraction_and_display(video_path, timestamp_str, mode)
|
115
|
+
except Exception as e:
|
116
|
+
logger.debug(e)
|
117
|
+
sys.exit(1)
|
118
|
+
|
119
|
+
|
120
|
+
if __name__ == "__main__":
|
121
|
+
main()
|
@@ -186,7 +186,12 @@ def wait_for_stable_file(file_path, timeout=10, check_interval=0.1):
|
|
186
186
|
try:
|
187
187
|
current_size = os.path.getsize(file_path)
|
188
188
|
if current_size == last_size:
|
189
|
-
|
189
|
+
try:
|
190
|
+
with open(file_path, 'rb') as f:
|
191
|
+
return True
|
192
|
+
except Exception as e:
|
193
|
+
time.sleep(check_interval)
|
194
|
+
elapsed_time += check_interval
|
190
195
|
last_size = current_size
|
191
196
|
time.sleep(check_interval)
|
192
197
|
elapsed_time += check_interval
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/SOURCES.txt
RENAMED
@@ -12,7 +12,9 @@ GameSentenceMiner/gsm.py
|
|
12
12
|
GameSentenceMiner/model.py
|
13
13
|
GameSentenceMiner/notification.py
|
14
14
|
GameSentenceMiner/obs.py
|
15
|
+
GameSentenceMiner/obs_back.py
|
15
16
|
GameSentenceMiner/package.py
|
17
|
+
GameSentenceMiner/ss_selector.py
|
16
18
|
GameSentenceMiner/text_log.py
|
17
19
|
GameSentenceMiner/util.py
|
18
20
|
GameSentenceMiner.egg-info/PKG-INFO
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
7
7
|
|
8
8
|
[project]
|
9
9
|
name = "GameSentenceMiner"
|
10
|
-
version = "2.8.
|
10
|
+
version = "2.8.42"
|
11
11
|
description = "A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!"
|
12
12
|
readme = "README.md"
|
13
13
|
requires-python = ">=3.10"
|
File without changes
|
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/__init__.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/send.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/communication/websocket.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/Untitled_json.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/__init__.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/download_tools.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/downloader/oneocr_dl.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/gsm_ocr_config.py
RENAMED
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/ocr/owocr_area_selector.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/__init__.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/__main__.py
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/owocr/owocr/config.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/vad/whisper_helper.py
RENAMED
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/__init__.py
RENAMED
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon-96x96.png
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon.ico
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/favicon.svg
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/site.webmanifest
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/static/style.css
RENAMED
File without changes
|
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/templates/__init__.py
RENAMED
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/templates/utility.html
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner/web/texthooking_page.py
RENAMED
File without changes
|
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/entry_points.txt
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/requires.txt
RENAMED
File without changes
|
{gamesentenceminer-2.8.41 → gamesentenceminer-2.8.42}/GameSentenceMiner.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|